Christopher Ramey
2 years ago
25 changed files with 242 additions and 656 deletions
-
14config/alarm.go
-
14config/check.go
-
93config/config.go
-
57config/config_test.go
-
36config/group.go
-
27config/host.go
-
264config/parser.go
-
5config/testdata/comments-inline.tok
-
6config/testdata/comments.tok
-
9config/testdata/dup_api_key.yaml
-
13config/testdata/dup_group_hosts.yaml
-
12config/testdata/dup_hosts.yaml
-
8config/testdata/missing_api_keyfile.yaml
-
7config/testdata/no_api_key.yaml
-
2config/testdata/no_hosts.yaml
-
3config/testdata/quotes-empty.tok
-
6config/testdata/quotes-multiline.tok
-
3config/testdata/quotes.tok
-
6config/testdata/simple-multiline.tok
-
1config/testdata/simple-spaces.tok
-
157config/tokenizer.go
-
83config/tokenizer_test.go
-
63config/validate.go
-
5go.mod
-
4go.sum
@ -0,0 +1,14 @@ |
|||
package config |
|||
|
|||
type Alarm struct { |
|||
Name string `yaml:"name"` |
|||
Type string `yaml:"type"` |
|||
Attrs map[string]interface{} `yaml:",inline"` |
|||
} |
|||
|
|||
func (alm Alarm) NameOrType() string { |
|||
if alm.Name == "" { |
|||
return alm.Type |
|||
} |
|||
return alm.Name |
|||
} |
@ -0,0 +1,14 @@ |
|||
package config |
|||
|
|||
type Check struct { |
|||
Name string `yaml:"name"` |
|||
Type string `yaml:"type"` |
|||
Attrs map[string]interface{} `yaml:",inline"` |
|||
} |
|||
|
|||
func (chk Check) NameOrType() string { |
|||
if chk.Name == "" { |
|||
return chk.Type |
|||
} |
|||
return chk.Name |
|||
} |
@ -1,89 +1,52 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"fmt" |
|||
"git.binarythought.com/cdramey/alrm/alarm" |
|||
yaml "gopkg.in/yaml.v2" |
|||
"io" |
|||
"os" |
|||
"time" |
|||
) |
|||
|
|||
type Config struct { |
|||
Groups map[string]*Group |
|||
Alarms map[string]alarm.Alarm |
|||
Interval time.Duration |
|||
DebugLevel int |
|||
Listen string |
|||
Path string |
|||
APIKey []byte |
|||
APIKeyFile string |
|||
Interval time.Duration `yaml:"interval"` |
|||
Listen string `yaml:"listen"` |
|||
DebugLevel int `yaml:"debug_level"` |
|||
APIKey string `yaml:"api_key"` |
|||
APIKeyFile string `yaml:"api_key_file"` |
|||
WebRoot string `yaml:"web_root"` |
|||
Hosts []Host `yaml:"hosts"` |
|||
Alarms []Alarm `yaml:"alarms"` |
|||
Groups map[string][]Host `yaml:"groups"` |
|||
} |
|||
|
|||
func (c *Config) NewAlarm(name string, typename string) (alarm.Alarm, error) { |
|||
if c.Alarms == nil { |
|||
c.Alarms = make(map[string]alarm.Alarm) |
|||
} |
|||
|
|||
if _, exists := c.Alarms[name]; exists { |
|||
return nil, fmt.Errorf("alarm %s already exists", name) |
|||
func New() *Config { |
|||
return &Config{ |
|||
Interval: 30 * time.Second, |
|||
Listen: "127.0.0.1:8282", |
|||
DebugLevel: 0, |
|||
WebRoot: "/", |
|||
} |
|||
} |
|||
|
|||
a, err := alarm.NewAlarm(name, typename) |
|||
func ReadConfig(fn string, debuglvl int) (*Config, error) { |
|||
f, err := os.Open(fn) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
c.Alarms[name] = a |
|||
|
|||
return a, nil |
|||
} |
|||
|
|||
func (c *Config) NewGroup(name string) (*Group, error) { |
|||
if c.Groups == nil { |
|||
c.Groups = make(map[string]*Group) |
|||
} |
|||
|
|||
if _, exists := c.Groups[name]; exists { |
|||
return nil, fmt.Errorf("group %s already exists", name) |
|||
} |
|||
|
|||
group := &Group{Name: name} |
|||
c.Groups[name] = group |
|||
return group, nil |
|||
} |
|||
|
|||
func (c *Config) SetInterval(val string) error { |
|||
interval, err := time.ParseDuration(val) |
|||
h, err := io.ReadAll(f) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
c.Interval = interval |
|||
return nil |
|||
} |
|||
|
|||
func ReadConfig(fn string, debuglvl int) (*Config, error) { |
|||
cfg := &Config{ |
|||
// Default check interval, 30 seconds
|
|||
Interval: time.Second * 30, |
|||
// Default listen address
|
|||
Listen: "127.0.0.1:8282", |
|||
DebugLevel: debuglvl, |
|||
Path: fn, |
|||
// API keyfile defaults to alrmrc.key
|
|||
APIKeyFile: fn + ".key", |
|||
return nil, err |
|||
} |
|||
|
|||
pr := &parser{config: cfg} |
|||
if err := pr.parse(); err != nil { |
|||
cfg := New() |
|||
err = yaml.UnmarshalStrict(h, &cfg) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
if len(cfg.APIKey) == 0 { |
|||
b, err := os.ReadFile(cfg.APIKeyFile) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
cfg.APIKey = b |
|||
err = cfg.Validate() |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return cfg, nil |
|||
} |
@ -0,0 +1,57 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"strings" |
|||
"testing" |
|||
) |
|||
|
|||
// Is the absence of hosts and groups detected?
|
|||
func TestNoHosts(t *testing.T) { |
|||
_, err := ReadConfig("testdata/no_hosts.yaml", 0) |
|||
if err != ERR_NO_GROUPHOST { |
|||
t.Errorf("missing hosts/groups not detected: %s", err.Error()) |
|||
} |
|||
} |
|||
|
|||
// Are duplicate host names detected?
|
|||
func TestDupHosts(t *testing.T) { |
|||
_, err := ReadConfig("testdata/dup_hosts.yaml", 0) |
|||
if err == nil || !strings.HasPrefix(err.Error(), "duplicate host") { |
|||
t.Errorf("duplicate hosts not detected: %s", err.Error()) |
|||
} |
|||
} |
|||
|
|||
// Are duplicate host names inside groups detected?
|
|||
func TestDupGroupHosts(t *testing.T) { |
|||
_, err := ReadConfig("testdata/dup_group_hosts.yaml", 0) |
|||
if err == nil || !strings.HasPrefix(err.Error(), "duplicate host") { |
|||
t.Errorf("duplicate hosts not detected: %s", err.Error()) |
|||
} |
|||
} |
|||
|
|||
// Test if api_key and api_key_file are both defined
|
|||
func TestDupAPI(t *testing.T) { |
|||
_, err := ReadConfig("testdata/dup_api_key.yaml", 0) |
|||
if err != ERR_DUP_API { |
|||
t.Errorf("missing api key/api key file not detected: %s", err.Error()) |
|||
} |
|||
} |
|||
|
|||
// Are undefined api key / key files detected?
|
|||
func TestUndefinedAPI(t *testing.T) { |
|||
_, err := ReadConfig("testdata/no_api_key.yaml", 0) |
|||
if err != ERR_NO_API { |
|||
t.Errorf("undefined api key/api key file not detected: %s", err.Error()) |
|||
} |
|||
} |
|||
|
|||
// Are missing api key files detected?
|
|||
func TestMissingAPIKey(t *testing.T) { |
|||
_, err := ReadConfig("testdata/missing_api_keyfile.yaml", 0) |
|||
if err != ERR_MISSING_KEYFILE { |
|||
t.Errorf("missing api key file not detected: %s", err.Error()) |
|||
} |
|||
} |
|||
|
|||
// Test if referenced alarm exists (and is a valid type)
|
|||
// Test if referenced check exists (and is a valid type)
|
@ -1,36 +0,0 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"fmt" |
|||
) |
|||
|
|||
type Group struct { |
|||
Name string |
|||
Hosts map[string]*Host |
|||
} |
|||
|
|||
func (ag *Group) NewHost(name string) (*Host, error) { |
|||
if ag.Hosts == nil { |
|||
ag.Hosts = make(map[string]*Host) |
|||
} |
|||
|
|||
if _, exists := ag.Hosts[name]; exists { |
|||
return nil, fmt.Errorf("host %s already exists", name) |
|||
} |
|||
|
|||
host := &Host{Name: name} |
|||
ag.Hosts[name] = host |
|||
return host, nil |
|||
} |
|||
|
|||
func (ag *Group) Check(debuglvl int) error { |
|||
for _, host := range ag.Hosts { |
|||
for _, chk := range host.Checks { |
|||
err := chk.Check(debuglvl) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
} |
|||
} |
|||
return nil |
|||
} |
@ -1,27 +1,14 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"git.binarythought.com/cdramey/alrm/check" |
|||
) |
|||
|
|||
type Host struct { |
|||
Name string |
|||
Address string |
|||
Checks []check.Check |
|||
} |
|||
|
|||
func (ah *Host) GetAddress() string { |
|||
if ah.Address != "" { |
|||
return ah.Address |
|||
} |
|||
return ah.Name |
|||
Name string `yaml:"name"` |
|||
Address string `yaml:"address"` |
|||
Checks []Check `yaml:"checks"` |
|||
} |
|||
|
|||
func (ah *Host) NewCheck(name string) (check.Check, error) { |
|||
chk, err := check.NewCheck(name, ah.GetAddress()) |
|||
if err != nil { |
|||
return nil, err |
|||
func (hst *Host) NameOrAddress() string { |
|||
if hst.Name == "" { |
|||
return hst.Address |
|||
} |
|||
ah.Checks = append(ah.Checks, chk) |
|||
return chk, nil |
|||
return hst.Name |
|||
} |
@ -1,264 +0,0 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"fmt" |
|||
"git.binarythought.com/cdramey/alrm/alarm" |
|||
"git.binarythought.com/cdramey/alrm/check" |
|||
"strings" |
|||
) |
|||
|
|||
const ( |
|||
PR_NONE = iota |
|||
PR_SET |
|||
PR_MONITOR |
|||
PR_GROUP |
|||
PR_HOST |
|||
PR_CHECK |
|||
PR_ALARM |
|||
) |
|||
|
|||
type parser struct { |
|||
config *Config |
|||
states []int |
|||
lastHost *Host |
|||
lastGroup *Group |
|||
lastCheck check.Check |
|||
lastAlarm alarm.Alarm |
|||
lastAlarmName string |
|||
} |
|||
|
|||
func (p *parser) parse() error { |
|||
tok, err := NewTokenizer(p.config.Path) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
defer tok.Close() |
|||
|
|||
for tok.Scan() { |
|||
tk := tok.Text() |
|||
stateswitch: |
|||
switch p.state() { |
|||
case PR_NONE: |
|||
switch strings.ToLower(tk) { |
|||
case "monitor": |
|||
p.setState(PR_MONITOR) |
|||
case "set": |
|||
p.setState(PR_SET) |
|||
case "alarm": |
|||
p.setState(PR_ALARM) |
|||
default: |
|||
return fmt.Errorf("invalid token in %s, line %d: \"%s\"", |
|||
p.config.Path, tok.Line(), tk) |
|||
} |
|||
|
|||
case PR_SET: |
|||
key := strings.ToLower(tk) |
|||
if !tok.Scan() { |
|||
return fmt.Errorf("empty value name for set in %s, line %d", |
|||
p.config.Path, tok.Line()) |
|||
} |
|||
|
|||
value := tok.Text() |
|||
switch key { |
|||
case "interval": |
|||
err := p.config.SetInterval(value) |
|||
if err != nil { |
|||
return fmt.Errorf( |
|||
"invalid duration for interval in %s, line %d: \"%s\"", |
|||
p.config.Path, tok.Line(), value, |
|||
) |
|||
} |
|||
case "listen": |
|||
p.config.Listen = value |
|||
case "api.key": |
|||
p.config.APIKey = []byte(value) |
|||
case "api.keyfile": |
|||
p.config.APIKeyFile = value |
|||
default: |
|||
return fmt.Errorf("unknown key for set in %s, line %d: \"%s\"", |
|||
p.config.Path, tok.Line(), tk, |
|||
) |
|||
} |
|||
p.prevState() |
|||
|
|||
case PR_MONITOR: |
|||
switch strings.ToLower(tk) { |
|||
case "host": |
|||
p.setState(PR_HOST) |
|||
|
|||
case "group": |
|||
p.setState(PR_GROUP) |
|||
|
|||
default: |
|||
p.prevState() |
|||
goto stateswitch |
|||
} |
|||
|
|||
case PR_GROUP: |
|||
if p.lastGroup == nil { |
|||
p.lastGroup, err = p.config.NewGroup(tk) |
|||
if err != nil { |
|||
return fmt.Errorf("%s in %s, line %d", |
|||
err.Error(), p.config.Path, tok.Line(), |
|||
) |
|||
} |
|||
continue |
|||
} |
|||
|
|||
switch strings.ToLower(tk) { |
|||
case "host": |
|||
p.setState(PR_HOST) |
|||
|
|||
default: |
|||
p.prevState() |
|||
goto stateswitch |
|||
} |
|||
|
|||
case PR_HOST: |
|||
// If a host has no group, inherit the host name
|
|||
if p.lastGroup == nil { |
|||
p.lastGroup, err = p.config.NewGroup(tk) |
|||
if err != nil { |
|||
return fmt.Errorf("%s in %s, line %d", |
|||
err.Error(), p.config.Path, tok.Line(), |
|||
) |
|||
} |
|||
} |
|||
|
|||
if p.lastHost == nil { |
|||
p.lastHost, err = p.lastGroup.NewHost(tk) |
|||
if err != nil { |
|||
return fmt.Errorf("%s in %s, line %d", |
|||
err.Error(), p.config.Path, tok.Line(), |
|||
) |
|||
} |
|||
continue |
|||
} |
|||
|
|||
switch strings.ToLower(tk) { |
|||
case "address": |
|||
if !tok.Scan() { |
|||
return fmt.Errorf("empty address for host in %s, line %d", |
|||
p.config.Path, tok.Line()) |
|||
} |
|||
p.lastHost.Address = tok.Text() |
|||
|
|||
case "check": |
|||
p.setState(PR_CHECK) |
|||
|
|||
default: |
|||
p.prevState() |
|||
goto stateswitch |
|||
} |
|||
|
|||
case PR_CHECK: |
|||
if p.lastCheck == nil { |
|||
p.lastCheck, err = p.lastHost.NewCheck(tk) |
|||
if err != nil { |
|||
return fmt.Errorf("%s in %s, line %d", |
|||
err.Error(), p.config.Path, tok.Line()) |
|||
} |
|||
continue |
|||
} |
|||
cont, err := p.lastCheck.Parse(tk) |
|||
if err != nil { |
|||
return fmt.Errorf("%s in %s, line %d", |
|||
err.Error(), p.config.Path, tok.Line()) |
|||
} |
|||
if !cont { |
|||
p.lastCheck = nil |
|||
p.prevState() |
|||
goto stateswitch |
|||
} |
|||
|
|||
case PR_ALARM: |
|||
if p.lastAlarm == nil { |
|||
if p.lastAlarmName == "" { |
|||
p.lastAlarmName = tk |
|||
continue |
|||
} |
|||
|
|||
p.lastAlarm, err = p.config.NewAlarm(p.lastAlarmName, tk) |
|||
if err != nil { |
|||
return fmt.Errorf("%s in %s, line %d", |
|||
err.Error(), p.config.Path, tok.Line()) |
|||
} |
|||
p.lastAlarmName = "" |
|||
continue |
|||
} |
|||
cont, err := p.lastAlarm.Parse(tk) |
|||
if err != nil { |
|||
return fmt.Errorf("%s in %s, line %d", |
|||
err.Error(), p.config.Path, tok.Line()) |
|||
} |
|||
if !cont { |
|||
p.lastAlarm = nil |
|||
p.prevState() |
|||
goto stateswitch |
|||
} |
|||
|
|||
default: |
|||
return fmt.Errorf("unknown parser state: %d", p.state()) |
|||
} |
|||
} |
|||
if err := tok.Err(); err != nil { |
|||
return err |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
func (p *parser) state() int { |
|||
if len(p.states) < 1 { |
|||
return PR_NONE |
|||
} |
|||
return p.states[len(p.states)-1] |
|||
} |
|||
|
|||
func (p *parser) setState(state int) { |
|||
switch state { |
|||
case PR_SET, PR_MONITOR: |
|||
fallthrough |
|||
case PR_GROUP: |
|||
p.lastGroup = nil |
|||
fallthrough |
|||
case PR_HOST: |
|||
p.lastHost = nil |
|||
p.lastCheck = nil |
|||
} |
|||
|
|||
if p.config.DebugLevel > 1 { |
|||
fmt.Printf("Parser state: %s", p.stateName()) |
|||
} |
|||
p.states = append(p.states, state) |
|||
if p.config.DebugLevel > 1 { |
|||
fmt.Printf(" -> %s\n", p.stateName()) |
|||
} |
|||
} |
|||
|
|||
func (p *parser) prevState() int { |
|||
if len(p.states) > 0 { |
|||
p.states = p.states[:len(p.states)-1] |
|||
} |
|||
return p.state() |
|||
} |
|||
|
|||
func (p *parser) stateName() string { |
|||
switch p.state() { |
|||
case PR_NONE: |
|||
return "PR_NONE" |
|||
case PR_SET: |
|||
return "PR_SET" |
|||
case PR_MONITOR: |
|||
return "PR_MONITOR" |
|||
case PR_GROUP: |
|||
return "PR_GROUP" |
|||
case PR_HOST: |
|||
return "PR_HOST" |
|||
case PR_CHECK: |
|||
return "PR_CHECK" |
|||
case PR_ALARM: |
|||
return "PR_ALARM" |
|||
default: |
|||
return "UNKNOWN" |
|||
} |
|||
} |
@ -1,5 +0,0 @@ |
|||
one #one |
|||
"two#three" |
|||
# "three" |
|||
four |
|||
# EOF |
@ -1,6 +0,0 @@ |
|||
# one |
|||
one |
|||
#two |
|||
two |
|||
# three |
|||
three |
@ -0,0 +1,9 @@ |
|||
interval: 30s |
|||
api_key: bogus |
|||
api_key_file: bogus.key |
|||
|
|||
hosts: |
|||
- name: localhost |
|||
address: 127.0.0.1 |
|||
checks: |
|||
- type: ping |
@ -0,0 +1,13 @@ |
|||
interval: 30s |
|||
api_key: bogus |
|||
|
|||
groups: |
|||
servers: |
|||
- name: localhost |
|||
address: 127.0.0.1 |
|||
checks: |
|||
- type: ping |
|||
- name: localhost |
|||
address: 127.0.0.1 |
|||
checks: |
|||
- type: ping |
@ -0,0 +1,12 @@ |
|||
interval: 30s |
|||
api_key: bogus |
|||
|
|||
hosts: |
|||
- name: localhost |
|||
address: 127.0.0.1 |
|||
checks: |
|||
- type: ping |
|||
- name: localhost |
|||
address: 127.0.0.1 |
|||
checks: |
|||
- type: ping |
@ -0,0 +1,8 @@ |
|||
interval: 30s |
|||
api_key_file: testdata/bogus.key |
|||
|
|||
hosts: |
|||
- name: localhost |
|||
address: 127.0.0.1 |
|||
checks: |
|||
- type: ping |
@ -0,0 +1,7 @@ |
|||
interval: 30s |
|||
|
|||
hosts: |
|||
- name: localhost |
|||
address: 127.0.0.1 |
|||
checks: |
|||
- type: ping |
@ -0,0 +1,2 @@ |
|||
interval: 30s |
|||
api_key: bogus |
@ -1,3 +0,0 @@ |
|||
one "" three |
|||
"" five "" |
|||
seven |
@ -1,6 +0,0 @@ |
|||
"one |
|||
two" 'three |
|||
four' |
|||
|
|||
`five |
|||
six` |
@ -1,3 +0,0 @@ |
|||
"one" 'two' `three` |
|||
|
|||
`four` 'five' "six" |
@ -1,6 +0,0 @@ |
|||
one two three |
|||
four five |
|||
|
|||
|
|||
six |
|||
|
@ -1 +0,0 @@ |
|||
one two three four five six |
@ -1,157 +0,0 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"bufio" |
|||
"fmt" |
|||
"io" |
|||
"os" |
|||
"strings" |
|||
"unicode" |
|||
) |
|||
|
|||
const ( |
|||
TK_NONE = iota |
|||
TK_VAL |
|||
TK_QUOTE |
|||
TK_COMMENT |
|||
) |
|||
|
|||
type Tokenizer struct { |
|||
curline int |
|||
repline int |
|||
file *os.File |
|||
reader *bufio.Reader |
|||
text string |
|||
err error |
|||
} |
|||
|
|||
func NewTokenizer(fn string) (*Tokenizer, error) { |
|||
var err error |
|||
tk := &Tokenizer{curline: 1} |
|||
tk.file, err = os.Open(fn) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
tk.reader = bufio.NewReader(tk.file) |
|||
return tk, nil |
|||
} |
|||
|
|||
func (t *Tokenizer) Close() error { |
|||
return t.file.Close() |
|||
} |
|||
|
|||
func (t *Tokenizer) Scan() bool { |
|||
t.repline = t.curline |
|||
state := TK_NONE |
|||
t.text = "" |
|||
|
|||
var b strings.Builder |
|||
var quo rune |
|||
for { |
|||
var r rune |
|||
r, _, t.err = t.reader.ReadRune() |
|||
if t.err != nil { |
|||
break |
|||
} |
|||
if r == unicode.ReplacementChar { |
|||
t.err = fmt.Errorf("invalid utf-8 encoding on line %s", t.repline) |
|||
break |
|||
} |
|||
|
|||
switch state { |
|||
case TK_NONE: |
|||
// When between values, increment both the reported line
|
|||
// and the current line, since there's not yet anything
|
|||
// to report
|
|||
if r == '\n' { |
|||
t.repline++ |
|||
t.curline++ |
|||
} |
|||
|
|||
// If we're between values and we encounter a space
|
|||
// or a control character, ignore it
|
|||
if unicode.IsSpace(r) || unicode.IsControl(r) { |
|||
continue |
|||
} |
|||
|
|||
// If we're between values and we encounter a #, it's
|
|||
// the beginning of a comment
|
|||
if r == '#' { |
|||
state = TK_COMMENT |
|||
continue |
|||
} |
|||
|
|||
// If we're between values and we get a quote character
|
|||
// treat it as the beginning of a string literal
|
|||
if r == '"' || r == '\'' || r == '`' { |
|||
state = TK_QUOTE |
|||
quo = r |
|||
continue |
|||
} |
|||
|
|||
b.WriteRune(r) |
|||
state = TK_VAL |
|||
|
|||
case TK_VAL: |
|||
// In values, only increment the current line, so
|
|||
// if an error is reported, it reports the line
|
|||
// the value starts on
|
|||
if r == '\n' { |
|||
t.curline++ |
|||
} |
|||
|
|||
// If we're in a normal value and we encounter a space
|
|||
// or a control character, end value
|
|||
if unicode.IsSpace(r) || unicode.IsControl(r) { |
|||
goto end |
|||
} |
|||
b.WriteRune(r) |
|||
|
|||
case TK_QUOTE: |
|||
// In quotes, only increment the current line, so
|
|||
// if an error is reported, it reports the line
|
|||
// the quoted value starts on
|
|||
if r == '\n' { |
|||
t.curline++ |
|||
} |
|||
|
|||
// End this quote if it's another quote of the same rune
|
|||
if r == quo { |
|||
goto end |
|||
} |
|||
b.WriteRune(r) |
|||
|
|||
case TK_COMMENT: |
|||
// Comments are ignored, until a new line is encounter
|
|||
// at which point, increment the current and reported line
|
|||
if r == '\n' { |
|||
t.curline++ |
|||
t.repline++ |
|||
state = TK_NONE |
|||
} |
|||
continue |
|||
} |
|||
} |
|||
|
|||
end: |
|||
if t.err == nil || t.err == io.EOF { |
|||
t.text = b.String() |
|||
} |
|||
return t.err == nil |
|||
} |
|||
|
|||
func (t *Tokenizer) Text() string { |
|||
return t.text |
|||
} |
|||
|
|||
func (t *Tokenizer) Line() int { |
|||
return t.repline |
|||
} |
|||
|
|||
func (t *Tokenizer) Err() error { |
|||
if t.err == io.EOF { |
|||
return nil |
|||
} |
|||
return t.err |
|||
} |
@ -1,83 +0,0 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"testing" |
|||
) |
|||
|
|||
func TestSimpleSpaces(t *testing.T) { |
|||
runTest(t, "simple-spaces", |
|||
`[["one","two","three","four","five","six"]]`, |
|||
) |
|||
} |
|||
|
|||
func TestSimpleMultiline(t *testing.T) { |
|||
runTest(t, "simple-multiline", |
|||
`[["one","two","three"],["four","five"],[],[],["six"]]`, |
|||
) |
|||
} |
|||
|
|||
func TestQuotes(t *testing.T) { |
|||
runTest(t, "quotes", |
|||
`[["one","two","three"],[],["four","five","six"]]`, |
|||
) |
|||
} |
|||
|
|||
func TestQuotesMultiline(t *testing.T) { |
|||
runTest(t, "quotes-multiline", |
|||
`[["one\ntwo"],["three\nfour"],[],[],["five\n six"]]`, |
|||
) |
|||
} |
|||
|
|||
func TestQuotesEmpty(t *testing.T) { |
|||
runTest(t, "quotes-empty", |
|||
`[["one","","three"],["","five",""],["seven"]]`, |
|||
) |
|||
} |
|||
|
|||
func TestComments(t *testing.T) { |
|||
runTest(t, "comments", |
|||
`[[],["one"],[],["two"],[],["three"]]`, |
|||
) |
|||
} |
|||
|
|||
func TestCommentsInline(t *testing.T) { |
|||
runTest(t, "comments-inline", |
|||
`[["one"],["two#three"],[],["four"]]`, |
|||
) |
|||
} |
|||
|
|||
func runTest(t *testing.T, bn string, exp string) { |
|||
t.Logf("Running testdata/%s.tok.. ", bn) |
|||
tok, err := NewTokenizer("testdata/" + bn + ".tok") |
|||
if err != nil { |
|||
t.Fatalf("%s", err.Error()) |
|||
} |
|||
defer tok.Close() |
|||
|
|||
tokens := [][]string{} |
|||
for tok.Scan() { |
|||
ln := tok.Line() |
|||
tl := len(tokens) |
|||
if tl < ln { |
|||
for i := tl; i < ln; i++ { |
|||
tokens = append(tokens, []string{}) |
|||
} |
|||
} |
|||
tokens[ln-1] = append(tokens[ln-1], tok.Text()) |
|||
} |
|||
if tok.Err() != nil { |
|||
t.Fatalf("%s", tok.Err()) |
|||
} |
|||
|
|||
out, err := json.Marshal(tokens) |
|||
if err != nil { |
|||
t.Fatalf("%s", err) |
|||
} |
|||
|
|||
if exp != string(out) { |
|||
t.Logf("Expected: %s", exp) |
|||
t.Logf("Got: %s", out) |
|||
t.FailNow() |
|||
} |
|||
} |
@ -0,0 +1,63 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"fmt" |
|||
"os" |
|||
) |
|||
|
|||
var ERR_DUP_API error = fmt.Errorf( |
|||
"api key and api key file cannot both be defined", |
|||
) |
|||
var ERR_NO_API error = fmt.Errorf( |
|||
"no api key or api key file defined", |
|||
) |
|||
var ERR_NO_GROUPHOST error = fmt.Errorf( |
|||
"no groups or hosts configured", |
|||
) |
|||
var ERR_MISSING_KEYFILE error = fmt.Errorf( |
|||
"api key file not found", |
|||
) |
|||
|
|||
func (cfg *Config) Validate() error { |
|||
if cfg.APIKey != "" && cfg.APIKeyFile != "" { |
|||
return ERR_DUP_API |
|||
} |
|||
|
|||
if cfg.APIKey == "" && cfg.APIKeyFile == "" { |
|||
return ERR_NO_API |
|||
} |
|||
|
|||
if cfg.APIKeyFile != "" { |
|||
if _, err := os.Stat(cfg.APIKeyFile); err != nil { |
|||
return ERR_MISSING_KEYFILE |
|||
} |
|||
} |
|||
|
|||
if len(cfg.Groups) == 0 && len(cfg.Hosts) == 0 { |
|||
return ERR_NO_GROUPHOST |
|||
} |
|||
|
|||
set := make(map[string]bool) |
|||
for _, host := range cfg.Hosts { |
|||
name := host.NameOrAddress() |
|||
if _, exists := set[name]; exists { |
|||
return fmt.Errorf("duplicate host \"%s\"", name) |
|||
} |
|||
set[name] = true |
|||
} |
|||
|
|||
for group, hosts := range cfg.Groups { |
|||
set = make(map[string]bool) |
|||
for _, host := range hosts { |
|||
name := host.NameOrAddress() |
|||
if _, exists := set[name]; exists { |
|||
return fmt.Errorf( |
|||
"duplicate host \"%s\" in group \"%s\"", group, name, |
|||
) |
|||
} |
|||
set[name] = true |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue