Christopher Ramey
3 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 |
package config |
||||
|
|
||||
import ( |
import ( |
||||
"fmt" |
|
||||
"git.binarythought.com/cdramey/alrm/alarm" |
|
||||
|
yaml "gopkg.in/yaml.v2" |
||||
|
"io" |
||||
"os" |
"os" |
||||
"time" |
"time" |
||||
) |
) |
||||
|
|
||||
type Config struct { |
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 { |
if err != nil { |
||||
return nil, err |
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 { |
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 |
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 |
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 |
package config |
||||
|
|
||||
import ( |
|
||||
"git.binarythought.com/cdramey/alrm/check" |
|
||||
) |
|
||||
|
|
||||
type Host struct { |
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