├── .github └── workflows │ └── go.yml ├── .gitignore ├── Makefile ├── config.go ├── docs.go ├── function.go ├── go.mod ├── go.sum ├── group.go ├── handle_hosts.go ├── handle_latest_data.go ├── handle_maintenances.go ├── handle_triggers.go ├── handle_users_groups.go ├── history.go ├── host.go ├── hosts_test.go ├── httptest.go ├── item.go ├── item_type.go ├── license.markdown ├── log.go ├── main.go ├── maintenance.go ├── maintenances_test.go ├── man.1 ├── man.markdown ├── parse_date_darwin.go ├── parse_date_linux.go ├── parse_date_windows.go ├── readme.markdown ├── responses.go ├── search.go ├── severity.go ├── spinner.go ├── timeperiod.go ├── trigger.go ├── users.go ├── zabbix.go └── zabbix_issue18_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go_version: ["1.11", "1.12", "1.13"] 10 | steps: 11 | - name: Set up Go ${{ matrix.go_version }} 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: ${{ matrix.go_version }} 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Get dependencies 20 | run: go get -v -t -d ./... 21 | 22 | - name: Lint 23 | uses: actions-contrib/golangci-lint@v0.1.0 24 | 25 | - name: Test 26 | run: go test -v ./... 27 | 28 | - name: Build 29 | run: make build 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /zabbixctl 2 | /zabbixctl.exe 3 | /zabbixctl.test 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_DATE=$(shell git log -1 --format="%cd" --date=short | sed s/-//g) 2 | BUILD_NUM=$(shell git rev-list --count HEAD) 3 | BUILD_HASH=$(shell git rev-parse --short HEAD) 4 | 5 | LDFLAGS="-X main.version=${BUILD_DATE}.${BUILD_NUM}_${BUILD_HASH}-1" 6 | GCFLAGS="-trimpath ${GOPATH}/src" 7 | 8 | build: 9 | go build -x -ldflags=${LDFLAGS} -gcflags ${GCFLAGS} . 10 | 11 | man: 12 | @ronn -r man.markdown 13 | 14 | clean: 15 | @git clean -ffdx 16 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/jinzhu/configor" 8 | ) 9 | 10 | type Config struct { 11 | Server struct { 12 | Address string `toml:"address" required:"true"` 13 | Username string `toml:"username" required:"true"` 14 | Password string `toml:"password" required:"true"` 15 | } `toml:"server"` 16 | Session struct { 17 | Path string `toml:"path"` 18 | } `toml:"session"` 19 | } 20 | 21 | func NewConfig(path string) (*Config, error) { 22 | config := &Config{} 23 | err := configor.Load(config, path) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | if strings.HasPrefix(config.Session.Path, "~/") { 29 | config.Session.Path = os.Getenv("HOME") + "/" + 30 | strings.TrimPrefix(config.Session.Path, "~/") 31 | } 32 | 33 | return config, nil 34 | } 35 | -------------------------------------------------------------------------------- /docs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | 5 | var ( 6 | version = "manual build" 7 | docs = `zabbixctl ` + version + os.ExpandEnv(` 8 | 9 | zabbixctl is tool for working with zabbix server api using command line 10 | interface, it provides effective way for operating on statuses of triggers, 11 | hosts latest data and groups of users. 12 | 13 | zabbixctl must be configurated before using, configuration file should be 14 | placed in ~/.config/zabbixctl.conf and must be written using following syntax: 15 | 16 | [server] 17 | address = "zabbix.local" 18 | username = "admin" 19 | password = "password" 20 | 21 | [session] 22 | path = "~/.cache/zabbixctl.session" 23 | 24 | zabbixctl will authorize in 'zabbix.local' server using given user 25 | credentials and save a zabbix session to a file ~/.cache/zabbixctl.session and 26 | at second run will use saved session instead of new authorization, by the way 27 | zabbix sessions have a ttl that by default equals to 15 minutes, so if saved 28 | zabbix session is outdated, zabbixctl will repeat authorization and rewrite the 29 | session file. 30 | 31 | Usage: 32 | zabbixctl [options] -T [/...] 33 | zabbixctl [options] -L ... [/...] 34 | zabbixctl [options] -G [/...] 35 | zabbixctl [options] -M [...] [/...] 36 | zabbixctl [options] -H [] 37 | zabbixctl -h | --help 38 | zabbixctl --version 39 | 40 | Workflow options: 41 | -T --triggers 42 | Search on zabbix triggers statuses. Triggers could be filtered using 43 | / argument, for example, search and acknowledge all triggers in a 44 | problem state and match the word 'cache': 45 | zabbixctl -Tp /cache 46 | 47 | -y --only-nack 48 | Show only not acknowledged triggers. 49 | 50 | -x --severity 51 | Specify minimum trigger severity. Once for information, twice for 52 | warning, three for disaster, four for high, five for disaster. 53 | 54 | -p --problem 55 | Show triggers that have a problem state. 56 | 57 | -r --recent 58 | Show triggers that have recently been in a problem state. 59 | 60 | -s --since 61 | Show triggers that have changed their state after the given time. 62 | [default: 7 days ago] 63 | 64 | -u --until 65 | Show triggers that have changed their state before the given time. 66 | 67 | -m --maintenance 68 | Show hosts in maintenance. 69 | 70 | -i --sort 71 | Show triggers sorted by specified fields. 72 | [default: lastchange,priority] 73 | 74 | -o --order 75 | Show triggers in specified order. 76 | [default: DESC] 77 | 78 | -n --limit 79 | Show specified amount of triggers. 80 | [default: 0] 81 | 82 | -k --acknowledge 83 | Acknowledge all retrieved triggers. 84 | 85 | -f --noconfirm 86 | Do not prompt acknowledge confirmation dialog. 87 | 88 | -d --extended 89 | Once for printing item's last value from the first component of the 90 | trigger expression. Twice for adding last value change date. Thrice for 91 | printing item description as well. 92 | 93 | -L --latest-data 94 | Search and show latest data for specified host(s). Hosts can be searched 95 | using wildcard character '*'. Latest data can be filtered using / 96 | argument, for example retrieve latest data for database nodes and search 97 | information about replication: 98 | zabbixctl -L dbnode-* /replication 99 | 100 | -g --graph 101 | Show links on graph pages. 102 | 103 | -w --stacked 104 | Output single link for the stacked graph of selected data. 105 | 106 | -b --normal 107 | Output single link for the normal (overlapping) graph of selected data. 108 | 109 | -G --groups 110 | Search and operate on configuration of users groups. 111 | 112 | -l --list 113 | Show list users in specified users group. 114 | 115 | -a --add 116 | Add specified to specified users group. 117 | 118 | -r --remove 119 | Remove specified from speicifed users group. 120 | 121 | -f --noconfirm 122 | Do not prompt confirmation dialog. 123 | 124 | -M --maintenances 125 | Search and operate on configuration of maintenance. 126 | Maintenance could be filtered using / argument, for example, 127 | search maintenance match the word 'update-kernel': 128 | zabbixctl -M dbnode-* /update-kernel 129 | 130 | -a --add 131 | Add new specified with timeperiod type once. 132 | 133 | --start 134 | Start date 'yyyy-mm-dd HH:MM'. Default now. 135 | 136 | --end 137 | Stop date 'yyyy-mm-dd HH:MM'. Default now + period. 138 | 139 | --period 140 | Period in m/h/d (minutes/hours/days). 141 | [default: 1d] 142 | 143 | -f --noconfirm 144 | Do not prompt confirmation dialog. 145 | 146 | -r --remove 147 | Remove specified . 148 | 149 | -z --read-stdin 150 | Read hosts from stdin. 151 | 152 | -H --hosts 153 | Search and operate with hosts. 154 | 155 | -r --remove 156 | Remove specified . 157 | 158 | 159 | Misc options: 160 | -c --config 161 | Use specified configuration file. 162 | [default: $HOME/.config/zabbixctl.conf] 163 | 164 | -v --verbosity 165 | Specify program output verbosity. 166 | Once for debug, twice for trace. 167 | 168 | -h --help 169 | Show this screen. 170 | 171 | --version 172 | Show version. 173 | `) 174 | usage = ` 175 | zabbixctl [options] -T [-v]... [-x]... [-d]... []... 176 | zabbixctl [options] -L [-v]... ... 177 | zabbixctl [options] -G [-v]... []... 178 | zabbixctl [options] -G [-v]... ... -a 179 | zabbixctl [options] -G [-v]... ... -r 180 | zabbixctl [options] -M [-v]... []... 181 | zabbixctl [options] -M [-v]... []... -a 182 | zabbixctl [options] -M [-v]... -r 183 | zabbixctl [options] -H [-v]... []... 184 | zabbixctl [options] -H [-v]... -r 185 | zabbixctl -h | --help 186 | zabbixctl --version 187 | ` 188 | options = ` 189 | Options: 190 | -T --triggers 191 | -y --only-nack 192 | -x --severity 193 | -p --problem 194 | -t --recent 195 | -s --since [default: 7 days ago] 196 | -u --until 197 | -m --maintenance 198 | -i --sort [default: lastchange,priority] 199 | -o --order [default: DESC] 200 | -n --limit [default: 0] 201 | -f --noconfirm 202 | -k --acknowledge 203 | -d --extended 204 | -L --latest-data 205 | -g --graph 206 | -w --stacked 207 | -b --normal 208 | -G --groups 209 | -a --add 210 | -r --remove 211 | -M --maintenances 212 | -z --read-stdin 213 | --period [default: 1d] 214 | --start 215 | --end 216 | -H --hosts 217 | -c --config [default: $HOME/.config/zabbixctl.conf] 218 | -v --verbosity 219 | -h --help 220 | --version 221 | ` 222 | ) 223 | -------------------------------------------------------------------------------- /function.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Function struct { 4 | ID string `json:"functionid"` 5 | ItemID string `json:"itemid"` 6 | TriggerID string `json:"triggerid"` 7 | Name string `json:"function"` 8 | Parameter string `json:"parameter"` 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kovetskiy/zabbixctl 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/jinzhu/configor v1.1.1 7 | github.com/kovetskiy/godocs v0.0.0-20160817104724-2d9428f80f34 8 | github.com/kovetskiy/lorg v0.0.0-20190701130800-9c6042b7edb0 9 | github.com/kovetskiy/spinner-go v0.0.0-20190814120732-cf21f43a9fe5 10 | github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317 // indirect 11 | github.com/olekukonko/tablewriter v0.0.3 12 | github.com/reconquest/karma-go v0.0.0-20190930125156-7b5c19ad6eab 13 | github.com/simplereach/timeutils v1.2.0 14 | github.com/stretchr/testify v1.4.0 15 | github.com/zazab/zhash v0.0.0-20170403032415-ad45b89afe7a // indirect 16 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/jinzhu/configor v1.1.1 h1:gntDP+ffGhs7aJ0u8JvjCDts2OsxsI7bnz3q+jC+hSY= 6 | github.com/jinzhu/configor v1.1.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= 7 | github.com/kovetskiy/godocs v0.0.0-20160817104724-2d9428f80f34 h1:bUWVdQQT5QLGFudBaGJE9v3Zf0EdDiDd5h8vAj9sMVg= 8 | github.com/kovetskiy/godocs v0.0.0-20160817104724-2d9428f80f34/go.mod h1:u+dvDLWaaxNkHMvhUK1IBXTNS3fhDvdy3BqQrZM8w/k= 9 | github.com/kovetskiy/lorg v0.0.0-20190701130800-9c6042b7edb0 h1:qyB36y4QwsBSYabKGABEYLYpK0k/VP/+SbeZQxwbF1c= 10 | github.com/kovetskiy/lorg v0.0.0-20190701130800-9c6042b7edb0/go.mod h1:B8HeKAukXULNzWWsW5k/SQyDkiQZPn7lTBJDB46MZ9I= 11 | github.com/kovetskiy/spinner-go v0.0.0-20190814120732-cf21f43a9fe5 h1:CtL8OxfKfJlN1aAEyqWxGdRew2xy5ARgzhiwHEEUiks= 12 | github.com/kovetskiy/spinner-go v0.0.0-20190814120732-cf21f43a9fe5/go.mod h1:/t7OToingbLN+h4HTdD8QtFN1k9/rJEfdkX46wc55R4= 13 | github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k= 14 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 15 | github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317 h1:hhGN4SFXgXo61Q4Sjj/X9sBjyeSa2kdpaOzCO+8EVQw= 16 | github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= 17 | github.com/olekukonko/tablewriter v0.0.3 h1:i0LBnzgiChAWHJYTQAZJDOgf8MNxAVYZJ2m63SIDimI= 18 | github.com/olekukonko/tablewriter v0.0.3/go.mod h1:YZeBtGzYYEsCHp2LST/u/0NDwGkRoBtmn1cIWCJiS6M= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/reconquest/karma-go v0.0.0-20190930125156-7b5c19ad6eab h1:NPOguOXVFBXRaBt4uPQLhvLkNwQgT0M+PZwxBGxgrrQ= 22 | github.com/reconquest/karma-go v0.0.0-20190930125156-7b5c19ad6eab/go.mod h1:oTXKs9J7KQ1gCpnvSwCbH9vlvELZFfUSbEbrr2ABeo0= 23 | github.com/simplereach/timeutils v1.2.0 h1:btgOAlu9RW6de2r2qQiONhjgxdAG7BL6je0G6J/yPnA= 24 | github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 27 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 28 | github.com/zazab/zhash v0.0.0-20170403032415-ad45b89afe7a h1:8gf6DUwu6F8Fh3rN8Ei9TM66KkWrNC04FP3HlcbxPuQ= 29 | github.com/zazab/zhash v0.0.0-20170403032415-ad45b89afe7a/go.mod h1:P+yVThXQrjx7yGmgsdI4WQ/XDDmcyBMZzK1b39TXteA= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= 32 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 33 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 34 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 35 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Group struct { 4 | ID string `json:"groupid"` 5 | Name string `json:"name"` 6 | } 7 | -------------------------------------------------------------------------------- /handle_hosts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/olekukonko/tablewriter" 8 | karma "github.com/reconquest/karma-go" 9 | ) 10 | 11 | func handleHosts( 12 | zabbix *Zabbix, 13 | config *Config, 14 | args map[string]interface{}, 15 | ) error { 16 | var ( 17 | hostnames, _ = parseSearchQuery(args[""].([]string)) 18 | removeHost, _ = args["--remove"].(string) 19 | 20 | err error 21 | hostsTable, hosts []Host 22 | ) 23 | 24 | destiny := karma.Describe("method", "handleHosts") 25 | 26 | switch { 27 | case removeHost != "": 28 | 29 | err = handleRemoveHosts(zabbix, config, args) 30 | if err != nil { 31 | return destiny.Describe( 32 | "error", err, 33 | ).Reason( 34 | "can't remove zabbix hosts", 35 | ) 36 | } 37 | 38 | default: 39 | 40 | for _, hostname := range hostnames { 41 | hosts, err = searchHosts(zabbix, hostname) 42 | if err != nil { 43 | return destiny.Describe( 44 | "error", err, 45 | ).Describe( 46 | "hostname", hostname, 47 | ).Reason( 48 | "can't search zabbix hosts", 49 | ) 50 | } 51 | hostsTable = append(hostsTable, hosts...) 52 | 53 | } 54 | if len(hostsTable) > 0 { 55 | err = printHostsTable(hostsTable) 56 | if err != nil { 57 | debugf("Error: %+v", err) 58 | } 59 | } 60 | 61 | } 62 | return nil 63 | } 64 | 65 | func handleRemoveHosts( 66 | zabbix *Zabbix, 67 | config *Config, 68 | args map[string]interface{}, 69 | ) error { 70 | var ( 71 | removeHost, _ = args["--remove"].(string) 72 | confirmation = !args["--noconfirm"].(bool) 73 | 74 | err error 75 | hosts []Host 76 | ) 77 | 78 | destiny := karma.Describe( 79 | "method", "removeHost", 80 | ).Describe( 81 | "hostname", removeHost, 82 | ) 83 | 84 | hosts, err = searchHosts(zabbix, removeHost) 85 | if err != nil { 86 | return destiny.Describe( 87 | "error", err, 88 | ).Reason( 89 | "can't obtain zabbix hosts", 90 | ) 91 | } 92 | 93 | if len(hosts) == 0 { 94 | return nil 95 | } 96 | if len(hosts) > 1 { 97 | return destiny.Reason( 98 | "found more then one uniq host", 99 | ) 100 | } 101 | 102 | err = printHostsTable(hosts) 103 | if err != nil { 104 | debugf("Error: %+v", err) 105 | } 106 | 107 | if confirmation { 108 | if !confirmHost("removing", removeHost) { 109 | return nil 110 | } 111 | } 112 | 113 | err = withSpinner( 114 | ":: Requesting for removing host", 115 | func() error { 116 | payload := []string{hosts[0].ID} 117 | _, err = zabbix.RemoveHosts(payload) 118 | return err 119 | }, 120 | ) 121 | return err 122 | } 123 | 124 | func searchHosts(zabbix *Zabbix, hostname string) ([]Host, error) { 125 | 126 | var ( 127 | hosts []Host 128 | err error 129 | ) 130 | 131 | if len(hostname) == 0 { 132 | return hosts, nil 133 | } 134 | 135 | params := Params{ 136 | "search": Params{ 137 | "name": hostname, 138 | }, 139 | "output": []string{ 140 | "host", 141 | }, 142 | "searchWildcardsEnabled": "1", 143 | } 144 | 145 | err = withSpinner( 146 | ":: Requesting information about hosts", 147 | func() error { 148 | hosts, err = zabbix.GetHosts(params) 149 | return err 150 | }, 151 | ) 152 | 153 | return hosts, err 154 | } 155 | 156 | func printHostsTable(hosts []Host) error { 157 | 158 | var lines = [][]string{} 159 | 160 | for _, host := range hosts { 161 | line := []string{ 162 | host.ID, 163 | host.Name, 164 | } 165 | lines = append(lines, line) 166 | } 167 | table := tablewriter.NewWriter(os.Stdout) 168 | table.SetHeader([]string{"ID", "Name"}) 169 | table.AppendBulk(lines) 170 | table.Render() 171 | 172 | return nil 173 | } 174 | 175 | func confirmHost(messages, host string) bool { 176 | 177 | var value string 178 | fmt.Fprintf( 179 | os.Stderr, 180 | "\n:: Proceed with %s host %s? [Y/n]:", 181 | messages, 182 | host, 183 | ) 184 | 185 | _, err := fmt.Scanln(&value) 186 | if err != nil { 187 | debugf("Error: %+v", err) 188 | } 189 | return value == "" || value == "Y" || value == "y" 190 | } 191 | -------------------------------------------------------------------------------- /handle_latest_data.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "text/tabwriter" 8 | 9 | "github.com/reconquest/karma-go" 10 | ) 11 | 12 | func handleLatestData( 13 | zabbix *Zabbix, 14 | config *Config, 15 | args map[string]interface{}, 16 | ) error { 17 | var ( 18 | hostnames, pattern = parseSearchQuery(args[""].([]string)) 19 | graphs = args["--graph"].(bool) 20 | stackedGraph = args["--stacked"].(bool) 21 | normalGraph = args["--normal"].(bool) 22 | table = tabwriter.NewWriter(os.Stdout, 1, 4, 2, ' ', 0) 23 | ) 24 | 25 | if len(hostnames) == 0 { 26 | return errors.New("no hostname specified") 27 | } 28 | 29 | var hosts []Host 30 | var err error 31 | 32 | err = withSpinner( 33 | ":: Requesting information about hosts", 34 | func() error { 35 | hosts, err = zabbix.GetHosts(Params{ 36 | "monitored_hosts": "1", 37 | "with_items": "1", 38 | "with_monitored_items": "1", 39 | "with_monitored_triggers": "1", 40 | "search": Params{ 41 | "name": hostnames, 42 | }, 43 | "searchWildcardsEnabled": "1", 44 | "output": []string{ 45 | "host", 46 | }, 47 | }) 48 | return err 49 | }, 50 | ) 51 | 52 | if err != nil { 53 | return karma.Format( 54 | err, 55 | "can't obtain zabbix hosts", 56 | ) 57 | } 58 | 59 | var ( 60 | identifiers = []string{} 61 | hash = map[string]Host{} 62 | ) 63 | 64 | for _, host := range hosts { 65 | identifiers = append(identifiers, host.ID) 66 | hash[host.ID] = host 67 | } 68 | 69 | debugf("* hosts identifiers: %s", identifiers) 70 | 71 | var ( 72 | items []Item 73 | webchecks []HTTPTest 74 | ) 75 | 76 | err = withSpinner( 77 | ":: Requesting information about hosts items & web scenarios", 78 | func() error { 79 | errs := make(chan error) 80 | 81 | go func() { 82 | var err error 83 | 84 | items, err = zabbix.GetItems(Params{ 85 | "hostids": identifiers, 86 | "webitems": "1", 87 | }) 88 | 89 | errs <- err 90 | }() 91 | 92 | go func() { 93 | var err error 94 | 95 | webchecks, err = zabbix.GetHTTPTests(Params{ 96 | "hostids": identifiers, 97 | "expandName": "1", 98 | "selectSteps": "extend", 99 | }) 100 | 101 | errs <- err 102 | }() 103 | 104 | for _, err := range []error{<-errs, <-errs} { 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | 110 | return nil 111 | }, 112 | ) 113 | 114 | if err != nil { 115 | return karma.Format( 116 | err, 117 | "can't obtain zabbix items", 118 | ) 119 | } 120 | 121 | var matchedItemIDs = []string{} 122 | 123 | for _, item := range items { 124 | line := fmt.Sprintf( 125 | "%s\t%s\t%s\t%s\t%-10s", 126 | hash[item.HostID].Name, item.Type.String(), item.Format(), 127 | item.DateTime(), item.LastValue, 128 | ) 129 | 130 | if pattern != "" && !matchPattern(pattern, line) { 131 | continue 132 | } 133 | 134 | fmt.Fprint(table, line) 135 | 136 | if graphs { 137 | fmt.Fprintf(table, "\t%s", zabbix.GetGraphURL(item.ID)) 138 | } 139 | 140 | fmt.Fprint(table, "\n") 141 | 142 | matchedItemIDs = append(matchedItemIDs, item.ID) 143 | } 144 | 145 | for _, check := range webchecks { 146 | line := fmt.Sprintf( 147 | "%s\t%s\t%s", 148 | hash[check.HostID].Name, `scenario`, check.Format(), 149 | ) 150 | 151 | if pattern != "" && !matchPattern(pattern, line) { 152 | continue 153 | } 154 | 155 | fmt.Fprintln(table, line) 156 | } 157 | 158 | switch { 159 | case stackedGraph: 160 | fmt.Println(zabbix.GetStackedGraphURL(matchedItemIDs)) 161 | 162 | case normalGraph: 163 | fmt.Println(zabbix.GetNormalGraphURL(matchedItemIDs)) 164 | 165 | default: 166 | err = table.Flush() 167 | if err != nil { 168 | debugf("Error: %+v", err) 169 | } 170 | } 171 | 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /handle_maintenances.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/olekukonko/tablewriter" 12 | karma "github.com/reconquest/karma-go" 13 | ) 14 | 15 | func handleMaintenances( 16 | zabbix *Zabbix, 17 | config *Config, 18 | args map[string]interface{}, 19 | ) error { 20 | 21 | var ( 22 | err error 23 | addMaintenance, _ = args["--add"].(string) 24 | removeMaintenance, _ = args["--remove"].(string) 25 | 26 | maintenances []Maintenance 27 | ) 28 | 29 | destiny := karma.Describe("method", "handleMaintenances") 30 | 31 | switch { 32 | 33 | case addMaintenance != "": 34 | 35 | maintenances, err = searchMaintenances( 36 | zabbix, 37 | Params{ 38 | "search": Params{ 39 | "name": addMaintenance, 40 | }, 41 | "selectGroups": "extend", 42 | "selectHosts": "extend", 43 | }) 44 | if err != nil { 45 | return destiny.Describe( 46 | "error", err, 47 | ).Reason( 48 | "can't obtain zabbix maintenances", 49 | ) 50 | } 51 | 52 | switch len(maintenances) { 53 | 54 | case 0: 55 | err = handleAddMaintenance(zabbix, config, args) 56 | case 1: 57 | err = handleUpdateMaintenance(zabbix, config, args, maintenances) 58 | } 59 | 60 | case removeMaintenance != "": 61 | 62 | maintenances, err = searchMaintenances( 63 | zabbix, 64 | Params{ 65 | "search": Params{ 66 | "name": removeMaintenance, 67 | }, 68 | "selectGroups": "extend", 69 | "selectHosts": "extend", 70 | }) 71 | if err != nil { 72 | return destiny.Describe( 73 | "error", err, 74 | ).Reason( 75 | "can't obtain zabbix maintenances", 76 | ) 77 | } 78 | err = handleRemoveMaintenance(zabbix, config, args, maintenances) 79 | 80 | default: 81 | err = handleListMaintenances(zabbix, config, args) 82 | } 83 | 84 | if err != nil { 85 | return destiny.Describe( 86 | "error", err, 87 | ).Reason( 88 | "can't operate with maintenance", 89 | ) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func handleAddMaintenance( 96 | zabbix *Zabbix, 97 | config *Config, 98 | args map[string]interface{}, 99 | ) error { 100 | 101 | var ( 102 | hostnames, _ = parseSearchQuery(args[""].([]string)) 103 | addMaintenance, _ = args["--add"].(string) 104 | confirmation = !args["--noconfirm"].(bool) 105 | fromStdin = args["--read-stdin"].(bool) 106 | 107 | hostids = []string{} 108 | uniqHosts = make(map[string]bool) 109 | err error 110 | hosts []Host 111 | params Params 112 | ) 113 | 114 | destiny := karma.Describe( 115 | "method", "AddMaintenance", 116 | ).Describe( 117 | "name", addMaintenance, 118 | ) 119 | 120 | if fromStdin { 121 | scanner := bufio.NewScanner(os.Stdin) 122 | for scanner.Scan() { 123 | hostnames = append(hostnames, scanner.Text()) 124 | } 125 | } 126 | 127 | for _, hostname := range hostnames { 128 | foundHosts, err := searchHosts(zabbix, hostname) 129 | if err != nil { 130 | return destiny.Describe( 131 | "error", err, 132 | ).Describe( 133 | "hostname", hostname, 134 | ).Reason( 135 | "can't obtain zabbix hosts", 136 | ) 137 | } 138 | 139 | for _, host := range foundHosts { 140 | if _, value := uniqHosts[host.ID]; !value { 141 | uniqHosts[host.ID] = true 142 | hostids = append(hostids, host.ID) 143 | hosts = append(hosts, host) 144 | } 145 | } 146 | } 147 | 148 | err = printHostsTable(hosts) 149 | if err != nil { 150 | debugf("Error: %+v", err) 151 | } 152 | 153 | if confirmation { 154 | if !confirmMaintenance("create", addMaintenance) { 155 | return nil 156 | } 157 | } 158 | 159 | timeperiod, activeTill, err := createTimeperiod(args) 160 | if err != nil { 161 | return destiny.Describe( 162 | "error", err, 163 | ).Reason( 164 | "can't create timeperiod for maintenance", 165 | ) 166 | } 167 | 168 | params = Params{ 169 | "name": addMaintenance, 170 | "active_since": timeperiod.StartDate, 171 | "active_till": activeTill, 172 | "hostids": hostids, 173 | "timeperiods": []Timeperiod{timeperiod}, 174 | } 175 | err = withSpinner( 176 | ":: Requesting for create to specified maintenance", 177 | func() error { 178 | _, err = zabbix.CreateMaintenance(params) 179 | return err 180 | }, 181 | ) 182 | if err != nil { 183 | return destiny.Describe( 184 | "error", err, 185 | ).Reason( 186 | "can't create zabbix maintenance", 187 | ) 188 | } 189 | if err != nil { 190 | return destiny.Describe( 191 | "error", err, 192 | ).Reason( 193 | "can't create zabbix maintenance", 194 | ) 195 | } 196 | return nil 197 | } 198 | 199 | func handleUpdateMaintenance( 200 | zabbix *Zabbix, 201 | config *Config, 202 | args map[string]interface{}, 203 | maintenances []Maintenance, 204 | ) error { 205 | 206 | var ( 207 | hostnames, _ = parseSearchQuery(args[""].([]string)) 208 | addMaintenance, _ = args["--add"].(string) 209 | confirmation = !args["--noconfirm"].(bool) 210 | fromStdin, _ = args["--read-stdin"].(bool) 211 | 212 | hostids = []string{} 213 | uniqHosts = make(map[string]bool) 214 | err error 215 | hosts []Host 216 | params Params 217 | ) 218 | 219 | destiny := karma.Describe( 220 | "method", "UpdateMaintenance", 221 | ).Describe( 222 | "name", addMaintenance, 223 | ) 224 | 225 | if fromStdin { 226 | scanner := bufio.NewScanner(os.Stdin) 227 | for scanner.Scan() { 228 | hostnames = append(hostnames, scanner.Text()) 229 | } 230 | } 231 | 232 | for _, hostname := range hostnames { 233 | foundHosts, err := searchHosts(zabbix, hostname) 234 | if err != nil { 235 | return destiny.Describe( 236 | "error", err, 237 | ).Describe( 238 | "hostname", hostname, 239 | ).Reason( 240 | "can't obtain zabbix hosts", 241 | ) 242 | } 243 | 244 | for _, host := range foundHosts { 245 | if _, value := uniqHosts[host.ID]; !value { 246 | uniqHosts[host.ID] = true 247 | hostids = append(hostids, host.ID) 248 | hosts = append(hosts, host) 249 | } 250 | } 251 | } 252 | 253 | maintenance := maintenances[0] 254 | for _, host := range maintenance.Hosts { 255 | if _, value := uniqHosts[host.ID]; !value { 256 | uniqHosts[host.ID] = true 257 | hostids = append(hostids, host.ID) 258 | hosts = append(hosts, host) 259 | } 260 | } 261 | 262 | err = printHostsTable(hosts) 263 | if err != nil { 264 | debugf("Error: %+v", err) 265 | } 266 | 267 | if confirmation { 268 | if fromStdin { 269 | fmt.Println("Use flag -z with -f only.") 270 | return nil 271 | } 272 | 273 | if !confirmMaintenance("updating", addMaintenance) { 274 | return nil 275 | } 276 | } 277 | 278 | params = Params{ 279 | "maintenanceid": maintenance.ID, 280 | "hostids": hostids, 281 | } 282 | err = withSpinner( 283 | ":: Requesting for updating hosts to specified maintenance", 284 | func() error { 285 | _, err = zabbix.UpdateMaintenance(params) 286 | return err 287 | }, 288 | ) 289 | if err != nil { 290 | return destiny.Describe( 291 | "error", err, 292 | ).Reason( 293 | "can't create zabbix maintenance", 294 | ) 295 | } 296 | return nil 297 | } 298 | 299 | func handleRemoveMaintenance( 300 | zabbix *Zabbix, 301 | config *Config, 302 | args map[string]interface{}, 303 | maintenances []Maintenance, 304 | ) error { 305 | 306 | var ( 307 | removeMaintenance, _ = args["--remove"].(string) 308 | confirmation = !args["--noconfirm"].(bool) 309 | 310 | pattern string 311 | err error 312 | extend = true 313 | ) 314 | 315 | destiny := karma.Describe( 316 | "method", "RemoveMaintenances", 317 | ).Describe( 318 | "name", removeMaintenance, 319 | ) 320 | 321 | if len(maintenances) != 1 { 322 | return destiny.Reason( 323 | "can't remove more then one maintenance", 324 | ) 325 | } 326 | 327 | err = printMaintenancesTable(maintenances, pattern, extend) 328 | if err != nil { 329 | debugf("Error: %+v", err) 330 | } 331 | 332 | if confirmation { 333 | if !confirmMaintenance("removing", removeMaintenance) { 334 | return nil 335 | } 336 | } 337 | 338 | err = withSpinner( 339 | ":: Requesting for removing hosts to specified maintenance", 340 | func() error { 341 | maintenance := maintenances[0].ID 342 | payload := []string{maintenance} 343 | _, err = zabbix.RemoveMaintenance(payload) 344 | return err 345 | }, 346 | ) 347 | if err != nil { 348 | return destiny.Describe( 349 | "error", err, 350 | ).Reason( 351 | "can't remove zabbix maintenances", 352 | ) 353 | } 354 | return nil 355 | } 356 | 357 | func handleListMaintenances( 358 | zabbix *Zabbix, 359 | config *Config, 360 | args map[string]interface{}, 361 | ) error { 362 | 363 | var ( 364 | hostnames, pattern = parseSearchQuery(args[""].([]string)) 365 | 366 | hostids = []string{} 367 | groupids = []string{} 368 | extend bool 369 | err error 370 | hosts []Host 371 | groups []Group 372 | maintenances []Maintenance 373 | ) 374 | 375 | destiny := karma.Describe("method", "ListMaintenances") 376 | 377 | params := Params{} 378 | 379 | for _, hostname := range hostnames { 380 | hosts, err = searchHosts(zabbix, hostname) 381 | if err != nil { 382 | return destiny.Describe( 383 | "error", err, 384 | ).Describe( 385 | "hostname", hostname, 386 | ).Reason( 387 | "can't obtain zabbix hosts", 388 | ) 389 | } 390 | 391 | for _, host := range hosts { 392 | hostids = append(hostids, host.ID) 393 | } 394 | } 395 | 396 | if len(hostids) > 0 { 397 | 398 | err = withSpinner( 399 | ":: Requesting information about groups", 400 | func() error { 401 | groups, err = zabbix.GetGroups(Params{ 402 | "output": "extend", 403 | "selectGroups": "extend", 404 | "hostids": hostids, 405 | }) 406 | return err 407 | }, 408 | ) 409 | if err != nil { 410 | return destiny.Describe( 411 | "error", err, 412 | ).Reason( 413 | "can't obtain zabbix groups", 414 | ) 415 | } 416 | 417 | for _, group := range groups { 418 | groupids = append(groupids, group.ID) 419 | } 420 | 421 | params["hostids"] = hostids 422 | params["groupids"] = groupids 423 | } 424 | 425 | if len(hostnames) > 0 || pattern != "" { 426 | extend = true 427 | params["selectGroups"] = "extend" 428 | params["selectHosts"] = "extend" 429 | } 430 | 431 | maintenances, err = searchMaintenances(zabbix, params) 432 | if err != nil { 433 | return destiny.Describe( 434 | "error", err, 435 | ).Reason( 436 | "can't obtain zabbix maintenances", 437 | ) 438 | } 439 | 440 | err = printMaintenancesTable(maintenances, pattern, extend) 441 | if err != nil { 442 | debugf("Error: %+v", err) 443 | } 444 | return nil 445 | } 446 | 447 | func printMaintenancesTable(maintenances []Maintenance, pattern string, 448 | extend bool) error { 449 | 450 | var lines = [][]string{} 451 | 452 | for _, maintenance := range maintenances { 453 | if pattern != "" && !matchPattern(pattern, maintenance.GetString()) { 454 | continue 455 | } 456 | 457 | // calculate max row number 458 | size := []int{len(maintenance.Timeperiods)} 459 | 460 | if extend { 461 | size = append(size, len(maintenance.Hosts)) 462 | size = append(size, len(maintenance.Groups)) 463 | 464 | sort.Slice( 465 | size, 466 | func(i, j int) bool { return size[i] > size[j] }, 467 | ) 468 | } 469 | 470 | for i := 0; i < size[0]; i++ { 471 | 472 | var ( 473 | timeperiodType, timeperiodStarDate, hostName, groupName string 474 | maintenanceID, maintenanceName, maintenanceTypeCollect string 475 | maintenanceSince, maintenanceTill, maintenanceStatus string 476 | timeperiodPeriod string 477 | ) 478 | 479 | if i == 0 { 480 | maintenanceID = maintenance.ID 481 | maintenanceName = maintenance.Name 482 | maintenanceSince = maintenance.GetDateTime(maintenance.Since) 483 | maintenanceTill = maintenance.GetDateTime(maintenance.Till) 484 | maintenanceStatus = maintenance.GetStatus() 485 | maintenanceTypeCollect = maintenance.GetTypeCollect() 486 | } 487 | 488 | if len(maintenance.Timeperiods) > i { 489 | timeperiodType = maintenance.Timeperiods[i].GetType() 490 | timeperiodStarDate = maintenance.Timeperiods[i].GetStartDate() 491 | timeperiodPeriod = strconv.FormatInt( 492 | int64(maintenance.Timeperiods[i].GetPeriodMinute()), 10, 493 | ) 494 | } 495 | if len(maintenance.Groups) > i { 496 | groupName = maintenance.Groups[i].Name 497 | } 498 | if len(maintenance.Hosts) > i { 499 | hostName = maintenance.Hosts[i].Name 500 | } 501 | 502 | line := []string{ 503 | maintenanceID, 504 | maintenanceName, 505 | maintenanceSince, 506 | maintenanceTill, 507 | maintenanceStatus, 508 | maintenanceTypeCollect, 509 | timeperiodType, 510 | timeperiodStarDate, 511 | timeperiodPeriod, 512 | } 513 | 514 | if extend { 515 | line = append(line, groupName, hostName) 516 | } 517 | lines = append(lines, line) 518 | } 519 | } 520 | 521 | if len(lines) == 0 { 522 | return nil 523 | } 524 | 525 | table := tablewriter.NewWriter(os.Stdout) 526 | header := []string{ 527 | "ID", 528 | "Name", 529 | "Since", 530 | "Till", 531 | "Status", 532 | "Data", 533 | "Type", 534 | "Start", 535 | "Minute", 536 | } 537 | if extend { 538 | header = append(header, "Group", "Host") 539 | } 540 | table.SetHeader(header) 541 | table.AppendBulk(lines) 542 | table.Render() 543 | 544 | return nil 545 | } 546 | 547 | func searchMaintenances(zabbix *Zabbix, extend Params) ([]Maintenance, error) { 548 | 549 | var ( 550 | maintenances []Maintenance 551 | err error 552 | ) 553 | 554 | params := Params{ 555 | "output": "extend", 556 | "selectTimeperiods": "extend", 557 | "searchWildcardsEnabled": "1", 558 | } 559 | 560 | for key, value := range extend { 561 | params[key] = value 562 | } 563 | 564 | err = withSpinner( 565 | ":: Requesting information about maintenances", 566 | func() error { 567 | maintenances, err = zabbix.GetMaintenances(params) 568 | return err 569 | }, 570 | ) 571 | return maintenances, err 572 | } 573 | 574 | func createTimeperiod( 575 | args map[string]interface{}, 576 | ) (Timeperiod, string, error) { 577 | 578 | var ( 579 | startDate, _ = args["--start"].(string) 580 | endDate, _ = args["--end"].(string) 581 | period, _ = args["--period"].(string) 582 | 583 | timeperiod Timeperiod 584 | activeTill int64 585 | ) 586 | 587 | periodSeconds, err := parsePeriod(period) 588 | if err != nil { 589 | return timeperiod, "", err 590 | } 591 | 592 | activeSince, err := parseDate(startDate) 593 | if err != nil { 594 | return timeperiod, "", err 595 | } 596 | 597 | switch { 598 | case endDate == "": 599 | activeTill = activeSince + periodSeconds 600 | 601 | case endDate != "": 602 | activeTill, err = parseDate(endDate) 603 | if err != nil { 604 | return timeperiod, "", err 605 | } 606 | 607 | if activeTill < activeSince+periodSeconds { 608 | activeTill = activeSince + periodSeconds 609 | } 610 | } 611 | 612 | timeperiod.TypeID = "0" 613 | timeperiod.Every = "1" 614 | timeperiod.Month = "0" 615 | timeperiod.DayOfWeek = "0" 616 | timeperiod.Day = "1" 617 | timeperiod.StartTime = "0" 618 | timeperiod.StartDate = strconv.FormatInt(int64(activeSince), 10) 619 | timeperiod.Period = strconv.FormatInt(int64(periodSeconds), 10) 620 | 621 | return timeperiod, strconv.FormatInt(int64(activeTill), 10), nil 622 | } 623 | 624 | func parsePeriod(targets string) (int64, error) { 625 | 626 | var ( 627 | err error 628 | days, hours, minutes int64 629 | ) 630 | 631 | destiny := karma.Describe( 632 | "method", "parsePeriod", 633 | ).Describe( 634 | "period", targets, 635 | ) 636 | 637 | switch { 638 | case strings.HasSuffix(targets, "d"): 639 | days, err = strconv.ParseInt( 640 | strings.TrimSuffix(targets, "d"), 10, 64, 641 | ) 642 | return days * 86400, nil 643 | case strings.HasSuffix(targets, "h"): 644 | hours, err = strconv.ParseInt( 645 | strings.TrimSuffix(targets, "h"), 10, 64, 646 | ) 647 | return hours * 3600, nil 648 | case strings.HasSuffix(targets, "m"): 649 | minutes, err = strconv.ParseInt( 650 | strings.TrimSuffix(targets, "m"), 10, 64, 651 | ) 652 | return minutes * 60, nil 653 | } 654 | return 0, destiny.Describe( 655 | "error", err, 656 | ).Describe( 657 | "period", targets, 658 | ).Reason("can't parse") 659 | } 660 | 661 | func confirmMaintenance(messages, maintenance string) bool { 662 | 663 | var value string 664 | fmt.Fprintf( 665 | os.Stderr, 666 | "\n:: Proceed with %s to maintenance %s? [Y/n]:", 667 | messages, 668 | maintenance, 669 | ) 670 | 671 | _, err := fmt.Scanln(&value) 672 | if err != nil { 673 | debugf("Error: %+v", err) 674 | } 675 | return value == "" || value == "Y" || value == "y" 676 | } 677 | -------------------------------------------------------------------------------- /handle_triggers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "text/tabwriter" 8 | 9 | "github.com/reconquest/karma-go" 10 | ) 11 | 12 | type ExtendedOutput int 13 | 14 | const ( 15 | ExtendedOutputNone ExtendedOutput = iota 16 | ExtendedOutputValue 17 | ExtendedOutputDate 18 | ExtendedOutputAll 19 | ) 20 | 21 | func handleTriggers( 22 | zabbix *Zabbix, 23 | config *Config, 24 | args map[string]interface{}, 25 | ) error { 26 | var ( 27 | acknowledge = args["--acknowledge"].(bool) 28 | words, pattern = parseSearchQuery(args[""].([]string)) 29 | confirmation = !args["--noconfirm"].(bool) 30 | extended = ExtendedOutput(args["--extended"].(int)) 31 | 32 | table = tabwriter.NewWriter(os.Stdout, 1, 4, 2, ' ', 0) 33 | ) 34 | 35 | if len(words) > 0 { 36 | return fmt.Errorf( 37 | "unexpected command line token '%s', "+ 38 | "use '/%s' for searching triggers", 39 | words[0], words[0], 40 | ) 41 | } 42 | 43 | params, err := parseParams(args) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | var triggers []Trigger 49 | 50 | err = withSpinner( 51 | ":: Requesting information about statuses of triggers", 52 | func() error { 53 | triggers, err = zabbix.GetTriggers(params) 54 | return err 55 | }, 56 | ) 57 | if err != nil { 58 | return karma.Format( 59 | err, 60 | "can't obtain zabbix triggers", 61 | ) 62 | } 63 | 64 | var history = map[string]ItemHistory{} 65 | 66 | if extended != ExtendedOutputNone { 67 | history, err = getTriggerItemsHistory(zabbix, triggers) 68 | if err != nil { 69 | return karma.Format( 70 | err, 71 | `can't obtain history for items of triggers`, 72 | ) 73 | } 74 | } 75 | 76 | debugln("* showing triggers table") 77 | if pattern != "" { 78 | debugf("** searching %s", pattern) 79 | } 80 | 81 | identifiers := []string{} 82 | for _, trigger := range triggers { 83 | if pattern != "" && !matchPattern(pattern, trigger.String()) { 84 | continue 85 | } 86 | 87 | fmt.Fprintf( 88 | table, 89 | "%s\t%s\t%s\t%s\t%s\t%s\t%s", 90 | trigger.LastEvent.ID, trigger.DateTime(), 91 | trigger.Severity(), 92 | trigger.StatusProblem(), 93 | trigger.StatusAcknowledge(), 94 | trigger.GetHostName(), 95 | trigger.Description, 96 | ) 97 | 98 | if len(trigger.Functions) > 0 { 99 | if last, ok := history[trigger.Functions[0].ItemID]; ok { 100 | if extended >= ExtendedOutputValue { 101 | fmt.Fprintf(table, "\t%s", last.History.String()) 102 | } 103 | 104 | if extended >= ExtendedOutputDate { 105 | fmt.Fprintf(table, "\t%s", last.History.DateTime()) 106 | } 107 | 108 | if extended >= ExtendedOutputAll { 109 | fmt.Fprintf(table, "\t%s", last.Item.Format()) 110 | } 111 | } 112 | } 113 | 114 | fmt.Fprint(table, "\n") 115 | 116 | identifiers = append(identifiers, trigger.LastEvent.ID) 117 | } 118 | 119 | err = table.Flush() 120 | if err != nil { 121 | return err 122 | } 123 | 124 | if !acknowledge || len(identifiers) == 0 { 125 | return nil 126 | } 127 | 128 | if confirmation { 129 | if !confirmAcknowledge() { 130 | return nil 131 | } 132 | } 133 | 134 | err = withSpinner( 135 | ":: Acknowledging specified triggers", 136 | func() error { 137 | return zabbix.Acknowledge(identifiers) 138 | }, 139 | ) 140 | 141 | if err != nil { 142 | return karma.Format( 143 | err, 144 | "can't acknowledge triggers %s", 145 | identifiers, 146 | ) 147 | } 148 | 149 | fmt.Fprintln(os.Stderr, ":: Acknowledged") 150 | 151 | return nil 152 | } 153 | 154 | func getTriggerItemsHistory( 155 | zabbix *Zabbix, 156 | triggers []Trigger, 157 | ) (map[string]ItemHistory, error) { 158 | history := map[string]ItemHistory{} 159 | 160 | itemIDs := []string{} 161 | for _, trigger := range triggers { 162 | if len(trigger.Functions) > 0 { 163 | itemIDs = append(itemIDs, trigger.Functions[0].ItemID) 164 | } 165 | } 166 | 167 | items, err := zabbix.GetItems(Params{ 168 | "itemids": itemIDs, 169 | }) 170 | if err != nil { 171 | return nil, karma.Format( 172 | err, 173 | `can't obtain items of triggers`, 174 | ) 175 | } 176 | 177 | err = withSpinner( 178 | ":: Requesting history for items of triggers", 179 | func() error { 180 | for _, item := range items { 181 | lastValues, err := zabbix.GetHistory(Params{ 182 | "history": item.ValueType, 183 | "itemids": item.ID, 184 | "limit": 1, 185 | }) 186 | if err != nil { 187 | return karma.Format( 188 | err, 189 | `can't obtain history (type '%s') for item '%s'`, 190 | item.ValueType, 191 | item.ID, 192 | ) 193 | } 194 | 195 | if len(lastValues) == 0 { 196 | continue 197 | } 198 | 199 | history[item.ID] = ItemHistory{ 200 | Item: item, 201 | History: lastValues[0], 202 | } 203 | } 204 | 205 | return nil 206 | }, 207 | ) 208 | 209 | return history, err 210 | } 211 | 212 | func parseParams(args map[string]interface{}) (Params, error) { 213 | var ( 214 | severity = args["--severity"].(int) 215 | onlyNotAck = args["--only-nack"].(bool) 216 | maintenance = args["--maintenance"].(bool) 217 | problem = args["--problem"].(bool) 218 | recent = args["--recent"].(bool) 219 | since, _ = args["--since"].(string) 220 | until, _ = args["--until"].(string) 221 | sort = strings.Split(args["--sort"].(string), ",") 222 | order = args["--order"].(string) 223 | limit = args["--limit"].(string) 224 | ) 225 | 226 | params := Params{ 227 | "sortfield": sort, 228 | "sortorder": order, 229 | "min_severity": severity, 230 | "limit": limit, 231 | } 232 | 233 | if onlyNotAck { 234 | params["withLastEventUnacknowledged"] = "1" 235 | } 236 | 237 | if maintenance { 238 | params["maintenance"] = "1" 239 | } 240 | 241 | if recent { 242 | params["only_true"] = "1" 243 | } 244 | 245 | if problem { 246 | params["filter"] = Params{"value": "1"} 247 | } 248 | 249 | var err error 250 | if until != "" { 251 | params["lastChangeTill"], err = parseDateTime(until) 252 | } else if since != "" { 253 | params["lastChangeSince"], err = parseDateTime(since) 254 | } 255 | 256 | return params, err 257 | } 258 | 259 | func confirmAcknowledge() bool { 260 | var value string 261 | fmt.Fprintf(os.Stderr, "\n:: Proceed with acknowledge? [Y/n]: ") 262 | _, err := fmt.Scanln(&value) 263 | if err != nil { 264 | debugf("Error: %+v", err) 265 | } 266 | return value == "" || value == "Y" || value == "y" 267 | } 268 | -------------------------------------------------------------------------------- /handle_users_groups.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "text/tabwriter" 9 | 10 | "github.com/reconquest/karma-go" 11 | ) 12 | 13 | func handleUsersGroups( 14 | zabbix *Zabbix, 15 | config *Config, 16 | args map[string]interface{}, 17 | ) error { 18 | var ( 19 | groups, pattern = parseSearchQuery(args[""].([]string)) 20 | 21 | addUser, _ = args["--add"].(string) 22 | removeUser, _ = args["--remove"].(string) 23 | confirmation = !args["--noconfirm"].(bool) 24 | 25 | table = tabwriter.NewWriter(os.Stdout, 1, 4, 2, ' ', 0) 26 | ) 27 | 28 | var usersgroups []UserGroup 29 | var err error 30 | 31 | err = withSpinner( 32 | ":: Requesting information about users groups", 33 | func() error { 34 | usersgroups, err = getUsersGroups(zabbix, groups) 35 | return err 36 | }, 37 | ) 38 | 39 | if err != nil { 40 | return karma.Format( 41 | err, 42 | "can't obtain users groups %s", groups, 43 | ) 44 | } 45 | 46 | found := false 47 | for _, group := range usersgroups { 48 | names := []string{} 49 | for _, user := range group.Users { 50 | names = append(names, user.Alias) 51 | } 52 | 53 | line := fmt.Sprintf( 54 | "%s\t%s\t%s", 55 | group.GetStatus(), group.Name, 56 | strings.Join(names, " "), 57 | ) 58 | 59 | if pattern != "" && !matchPattern(pattern, line) { 60 | continue 61 | } 62 | 63 | fmt.Fprintln(table, line) 64 | found = true 65 | } 66 | 67 | err = table.Flush() 68 | if err != nil { 69 | debugf("Error: %+v", err) 70 | } 71 | 72 | if !found || (addUser == "" && removeUser == "") { 73 | return nil 74 | } 75 | 76 | switch { 77 | case addUser != "": 78 | if confirmation { 79 | if !confirmAdding(addUser) { 80 | return nil 81 | } 82 | } 83 | 84 | user, err := getUser(zabbix, addUser) 85 | if err != nil { 86 | return karma.Format( 87 | err, 88 | "can't obtain user '%s'", addUser, 89 | ) 90 | } 91 | 92 | err = withSpinner( 93 | ":: Requesting for adding user to specified groups", 94 | func() error { 95 | return zabbix.AddUserToGroups(usersgroups, user) 96 | }, 97 | ) 98 | 99 | if err != nil { 100 | return karma.Format( 101 | err, 102 | "can't add user '%s' to specified users groups", 103 | user.Alias, 104 | ) 105 | } 106 | 107 | case removeUser != "": 108 | if confirmation { 109 | if !confirmRemoving(removeUser) { 110 | return nil 111 | } 112 | } 113 | 114 | user, err := getUser(zabbix, removeUser) 115 | if err != nil { 116 | return karma.Format( 117 | err, 118 | "can't obtain user '%s'", removeUser, 119 | ) 120 | } 121 | 122 | err = withSpinner( 123 | ":: Requesting for removing user from specified groups", 124 | func() error { 125 | return zabbix.RemoveUserFromGroups(usersgroups, user) 126 | }, 127 | ) 128 | 129 | if err != nil { 130 | return karma.Format( 131 | err, 132 | "can't remove user '%s' from specified users groups", 133 | removeUser, 134 | ) 135 | } 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func getUser(zabbix *Zabbix, username string) (User, error) { 142 | users, err := zabbix.GetUsers(Params{ 143 | "search": Params{ 144 | "alias": username, 145 | }, 146 | }) 147 | if err != nil { 148 | return User{}, karma.Format( 149 | err, "can't obtain user with specified name", 150 | ) 151 | } 152 | 153 | if len(users) == 0 { 154 | return User{}, errors.New("user with specified name not found") 155 | } 156 | 157 | return users[0], nil 158 | } 159 | 160 | func getUsersGroups(zabbix *Zabbix, groups []string) ([]UserGroup, error) { 161 | var params Params 162 | if len(groups) == 0 { 163 | params = Params{ 164 | "selectUsers": "1", 165 | } 166 | } else { 167 | params = Params{ 168 | "selectUsers": "1", 169 | "search": Params{ 170 | "name": groups, 171 | }, 172 | "searchWildcardsEnabled": "1", 173 | } 174 | } 175 | 176 | usersgroups, err := zabbix.GetUsersGroups(params) 177 | if err != nil { 178 | return nil, karma.Format( 179 | err, 180 | "can't obtain zabbix users groups", 181 | ) 182 | } 183 | 184 | var ( 185 | usersIdentifiers = []string{} 186 | usersIdentifiersHash = map[string]User{} 187 | ) 188 | 189 | for _, usersgroup := range usersgroups { 190 | for _, user := range usersgroup.Users { 191 | usersIdentifiers = append(usersIdentifiers, user.ID) 192 | } 193 | } 194 | 195 | users, err := zabbix.GetUsers( 196 | Params{ 197 | "userids": usersIdentifiers, 198 | }, 199 | ) 200 | if err != nil { 201 | return nil, karma.Format( 202 | err, 203 | "can't obtain users from specified groups %q", 204 | usersIdentifiers, 205 | ) 206 | } 207 | 208 | for _, user := range users { 209 | usersIdentifiersHash[user.ID] = user 210 | } 211 | 212 | for _, usersgroup := range usersgroups { 213 | for i, user := range usersgroup.Users { 214 | usersgroup.Users[i] = usersIdentifiersHash[user.ID] 215 | } 216 | } 217 | 218 | return usersgroups, nil 219 | } 220 | 221 | func confirmAdding(user string) bool { 222 | var value string 223 | fmt.Fprintf( 224 | os.Stderr, 225 | "\n:: Proceed with adding user %s to specified groups? [Y/n]: ", 226 | user, 227 | ) 228 | _, err := fmt.Scanln(&value) 229 | if err != nil { 230 | debugf("Error: %+v", err) 231 | } 232 | return value == "" || value == "Y" || value == "y" 233 | } 234 | 235 | func confirmRemoving(user string) bool { 236 | var value string 237 | fmt.Fprintf( 238 | os.Stderr, 239 | "\n:: Proceed with removing user %s from specified groups? [Y/n]: ", 240 | user, 241 | ) 242 | _, err := fmt.Scanln(&value) 243 | if err != nil { 244 | debugf("Error: %+v", err) 245 | } 246 | return value == "" || value == "Y" || value == "y" 247 | } 248 | -------------------------------------------------------------------------------- /history.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | type History struct { 10 | ItemID string `json:"itemid"` 11 | Value interface{} `json:"value"` 12 | Clock string `json:"clock"` 13 | } 14 | 15 | type ItemHistory struct { 16 | Item 17 | History 18 | } 19 | 20 | func (history *History) String() string { 21 | return fmt.Sprint(history.Value) 22 | } 23 | 24 | func (history *History) date() time.Time { 25 | date, err := strconv.ParseInt(history.Clock, 10, 64) 26 | if err != nil { 27 | debugf("Error: %+v", err) 28 | } 29 | return time.Unix(date, 0) 30 | } 31 | 32 | func (history *History) DateTime() string { 33 | if history.Clock == "0" { 34 | return "-" 35 | } 36 | 37 | return history.date().Format("2006-01-02 15:04:05") 38 | } 39 | -------------------------------------------------------------------------------- /host.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Host struct { 4 | ID string `json:"hostid"` 5 | Name string `json:"host"` 6 | } 7 | 8 | type Hosts struct { 9 | ID []string `json:"hostids"` 10 | } 11 | -------------------------------------------------------------------------------- /hosts_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const ( 13 | // https://www.zabbix.com/documentation/3.4/manual/api/reference/host/get 14 | hostsGet = ` 15 | { 16 | "jsonrpc": "2.0", 17 | "result": [ 18 | { 19 | "maintenances": [], 20 | "hostid": "10160", 21 | "proxy_hostid": "0", 22 | "host": "Zabbix server", 23 | "status": "0", 24 | "disable_until": "0", 25 | "error": "", 26 | "available": "0", 27 | "errors_from": "0", 28 | "lastaccess": "0", 29 | "ipmi_authtype": "-1", 30 | "ipmi_privilege": "2", 31 | "ipmi_username": "", 32 | "ipmi_password": "", 33 | "ipmi_disable_until": "0", 34 | "ipmi_available": "0", 35 | "snmp_disable_until": "0", 36 | "snmp_available": "0", 37 | "maintenanceid": "0", 38 | "maintenance_status": "0", 39 | "maintenance_type": "0", 40 | "maintenance_from": "0", 41 | "ipmi_errors_from": "0", 42 | "snmp_errors_from": "0", 43 | "ipmi_error": "", 44 | "snmp_error": "", 45 | "jmx_disable_until": "0", 46 | "jmx_available": "0", 47 | "jmx_errors_from": "0", 48 | "jmx_error": "", 49 | "name": "Zabbix server", 50 | "description": "The Zabbix monitoring server.", 51 | "tls_connect": "1", 52 | "tls_accept": "1", 53 | "tls_issuer": "", 54 | "tls_subject": "", 55 | "tls_psk_identity": "", 56 | "tls_psk": "" 57 | }, 58 | { 59 | "maintenances": [], 60 | "hostid": "10167", 61 | "proxy_hostid": "0", 62 | "host": "Linux server", 63 | "status": "0", 64 | "disable_until": "0", 65 | "error": "", 66 | "available": "0", 67 | "errors_from": "0", 68 | "lastaccess": "0", 69 | "ipmi_authtype": "-1", 70 | "ipmi_privilege": "2", 71 | "ipmi_username": "", 72 | "ipmi_password": "", 73 | "ipmi_disable_until": "0", 74 | "ipmi_available": "0", 75 | "snmp_disable_until": "0", 76 | "snmp_available": "0", 77 | "maintenanceid": "0", 78 | "maintenance_status": "0", 79 | "maintenance_type": "0", 80 | "maintenance_from": "0", 81 | "ipmi_errors_from": "0", 82 | "snmp_errors_from": "0", 83 | "ipmi_error": "", 84 | "snmp_error": "", 85 | "jmx_disable_until": "0", 86 | "jmx_available": "0", 87 | "jmx_errors_from": "0", 88 | "jmx_error": "", 89 | "name": "Linux server", 90 | "description": "", 91 | "tls_connect": "1", 92 | "tls_accept": "1", 93 | "tls_issuer": "", 94 | "tls_subject": "", 95 | "tls_psk_identity": "", 96 | "tls_psk": "" 97 | } 98 | ], 99 | "id": 1 100 | }` 101 | 102 | // https://www.zabbix.com/documentation/3.4/manual/api/reference/host/delete 103 | hostsRemove = ` 104 | { 105 | "jsonrpc": "2.0", 106 | "result": { 107 | "hostids": [ 108 | "13", 109 | "32" 110 | ] 111 | }, 112 | "id": 1 113 | }` 114 | ) 115 | 116 | func TestHostsGet(t *testing.T) { 117 | test := assert.New(t) 118 | 119 | testserver := httptest.NewServer(http.HandlerFunc( 120 | func(w http.ResponseWriter, r *http.Request) { 121 | fmt.Fprint(w, hostsGet) 122 | }, 123 | )) 124 | defer testserver.Close() 125 | 126 | zabbix := &Zabbix{} 127 | zabbix.client = testserver.Client() 128 | zabbix.apiURL = testserver.URL 129 | 130 | hosts, err := zabbix.GetHosts(Params{}) 131 | 132 | test.NoError(err) 133 | test.Len(hosts, 2) 134 | 135 | test.Equal("10160", hosts[0].ID) 136 | test.Equal("Zabbix server", hosts[0].Name) 137 | test.Equal("10167", hosts[1].ID) 138 | test.Equal("Linux server", hosts[1].Name) 139 | } 140 | 141 | func TestHostsRemove(t *testing.T) { 142 | test := assert.New(t) 143 | 144 | testserver := httptest.NewServer(http.HandlerFunc( 145 | func(w http.ResponseWriter, r *http.Request) { 146 | fmt.Fprint(w, hostsRemove) 147 | }, 148 | )) 149 | defer testserver.Close() 150 | 151 | zabbix := &Zabbix{} 152 | zabbix.client = testserver.Client() 153 | zabbix.apiURL = testserver.URL 154 | 155 | var hostids Hosts 156 | payload := []string{"13", "32"} 157 | hostids, err := zabbix.RemoveHosts(payload) 158 | 159 | test.NoError(err) 160 | test.Len(hostids.ID, 2) 161 | 162 | test.Equal("13", hostids.ID[0]) 163 | test.Equal("32", hostids.ID[1]) 164 | } 165 | -------------------------------------------------------------------------------- /httptest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // HTTPTestStep represents single step in the web scenario. 10 | type HTTPTestStep struct { 11 | ID string `json:"httpstepid"` 12 | TestID string `json:"httptestid"` 13 | URL string `json:"url"` 14 | } 15 | 16 | // HTTPTest represents web scenario, which often used for simple step-by-step 17 | // external monitoring of websites via HTTP. 18 | type HTTPTest struct { 19 | ID string `json:"httptestid"` 20 | HostID string `json:"hostid"` 21 | Name string `json:"name"` 22 | Delay string `json:"delay"` 23 | NextCheck string `json:"nextcheck"` 24 | TemplateID string `json:"templateid"` 25 | 26 | Steps []HTTPTestStep `json:"steps"` 27 | } 28 | 29 | func (check *HTTPTest) DateTime() string { 30 | if check.NextCheck == "0" { 31 | return "-" 32 | } 33 | 34 | return check.date().Format("2006-01-02 15:04:05") 35 | } 36 | 37 | func (check *HTTPTest) date() time.Time { 38 | date, err := strconv.ParseInt(check.NextCheck, 10, 64) 39 | if err != nil { 40 | debugf("Error: %+v", err) 41 | } 42 | return time.Unix(date, 0) 43 | } 44 | 45 | func (check *HTTPTest) Format() string { 46 | return fmt.Sprintf( 47 | "%s (%d steps every %s seconds)\t%s (next)\t", 48 | check.Name, 49 | len(check.Steps), 50 | check.Delay, 51 | check.DateTime(), 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /item.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const ( 12 | TimeFormat = "2006-01-02 15:04:05" 13 | ) 14 | 15 | var ( 16 | reItemKeyParams = regexp.MustCompile(`\[([^\]]+)\]`) 17 | ) 18 | 19 | type Item struct { 20 | ID string `json:"itemid"` 21 | HostID string `json:"hostid"` 22 | Name string `json:"name"` 23 | ValueType string `json:"value_type"` 24 | LastValue string `json:"lastvalue"` 25 | LastClock interface{} `json:"lastclock"` 26 | Key string `json:"key_"` 27 | Type ItemType `json:"type"` 28 | } 29 | 30 | func (item *Item) DateTime() string { 31 | if item.getLastClock() == "0" { 32 | return "-" 33 | } 34 | 35 | return item.date().Format(TimeFormat) 36 | } 37 | 38 | func (item *Item) getLastClock() string { 39 | switch typed := item.LastClock.(type) { 40 | case string: 41 | return typed 42 | case float64: 43 | return fmt.Sprint(int64(typed)) 44 | default: 45 | panic("asdasdasd") 46 | } 47 | } 48 | 49 | func (item *Item) date() time.Time { 50 | date, err := strconv.ParseInt(item.getLastClock(), 10, 64) 51 | if err != nil { 52 | debugf("Error: %+v", err) 53 | } 54 | return time.Unix(date, 0) 55 | } 56 | 57 | func (item *Item) Format() string { 58 | name := item.Name 59 | 60 | match := reItemKeyParams.FindStringSubmatch(item.Key) 61 | if len(match) == 0 { 62 | return name 63 | } 64 | 65 | args := strings.Split(match[1], ",") 66 | for index, arg := range args { 67 | name = strings.Replace(name, fmt.Sprintf(`$%d`, index+1), arg, -1) 68 | } 69 | 70 | return name 71 | } 72 | -------------------------------------------------------------------------------- /item_type.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | ) 7 | 8 | type ItemType int 9 | 10 | const ( 11 | ItemTypeAgent ItemType = iota 12 | ItemTypeSNMPv1 13 | ItemTypeTrapper 14 | ItemTypeSimpleCheck 15 | ItemTypeSNMPv2 16 | ItemTypeInternal 17 | ItemTypeSNMPv3 18 | ItemTypeAgentActive 19 | ItemTypeAggregate 20 | ItemTypeWeb 21 | ItemTypeExternalCheck 22 | ItemTypeDatabaseMonitor 23 | ItemTypeIPMI 24 | ItemTypeSSH 25 | ItemTypeTELNET 26 | ItemTypeCalculated 27 | ItemTypeJMX 28 | ItemTypeSNMPTrap 29 | ) 30 | 31 | func (type_ *ItemType) UnmarshalJSON(data []byte) error { 32 | var stringValue string 33 | 34 | err := json.Unmarshal(data, &stringValue) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | intValue, err := strconv.ParseInt(stringValue, 10, 64) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | *type_ = ItemType(intValue) 45 | 46 | return nil 47 | } 48 | 49 | func (type_ ItemType) String() string { 50 | switch type_ { 51 | case ItemTypeAgent: 52 | return "agent" 53 | case ItemTypeTrapper: 54 | return "trapper" 55 | case ItemTypeSimpleCheck: 56 | return "check" 57 | case ItemTypeSNMPv2: 58 | return "snmp2" 59 | case ItemTypeInternal: 60 | return "internal" 61 | case ItemTypeSNMPv3: 62 | return "snmp3" 63 | case ItemTypeAgentActive: 64 | return "active" 65 | case ItemTypeAggregate: 66 | return "aggregate" 67 | case ItemTypeWeb: 68 | return "web" 69 | case ItemTypeExternalCheck: 70 | return "external" 71 | case ItemTypeDatabaseMonitor: 72 | return "dbmon" 73 | case ItemTypeIPMI: 74 | return "ipmi" 75 | case ItemTypeSSH: 76 | return "ssh" 77 | case ItemTypeTELNET: 78 | return "telnet" 79 | case ItemTypeCalculated: 80 | return "calc" 81 | case ItemTypeJMX: 82 | return "jmx" 83 | case ItemTypeSNMPTrap: 84 | return "snmp" 85 | default: 86 | return "unknown" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /license.markdown: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Egor Kovetskiy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "github.com/kovetskiy/lorg" 5 | import "github.com/kovetskiy/spinner-go" 6 | import "os" 7 | 8 | func getLogger() *lorg.Log { 9 | logger := lorg.NewLog() 10 | logger.SetFormat(lorg.NewFormat("${level:[%s]:left:true} %s")) 11 | 12 | return logger 13 | } 14 | 15 | func fatalf(format string, values ...interface{}) { 16 | if spinner.IsActive() { 17 | spinner.Stop() 18 | } 19 | 20 | fmt.Fprintf(os.Stderr, format+"\n", values...) 21 | os.Exit(1) 22 | } 23 | 24 | func fatalln(value interface{}) { 25 | fatalf("%s", value) 26 | } 27 | 28 | func debugf(format string, values ...interface{}) { 29 | logger.Debugf(format, values...) 30 | } 31 | 32 | func tracef(format string, values ...interface{}) { 33 | logger.Tracef(format, values...) 34 | } 35 | 36 | func debugln(value interface{}) { 37 | debugf("%s", value) 38 | } 39 | 40 | func traceln(value interface{}) { 41 | tracef("%s", value) 42 | } 43 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kovetskiy/godocs" 7 | "github.com/kovetskiy/lorg" 8 | "github.com/kovetskiy/spinner-go" 9 | "github.com/reconquest/karma-go" 10 | ) 11 | 12 | var ( 13 | debugMode bool 14 | traceMode bool 15 | 16 | logger = getLogger() 17 | ) 18 | 19 | func init() { 20 | spinner.SetInterval(time.Millisecond * 100) 21 | } 22 | 23 | func main() { 24 | args, err := godocs.Parse( 25 | docs, version, 26 | godocs.UsePager, godocs.Usage(usage), godocs.Options(options), 27 | ) 28 | if err != nil { 29 | fatalln(err) 30 | } 31 | 32 | switch args["--verbosity"].(int) { 33 | case 1: 34 | debugMode = true 35 | logger.SetLevel(lorg.LevelDebug) 36 | case 2: 37 | debugMode = true 38 | traceMode = true 39 | logger.SetLevel(lorg.LevelTrace) 40 | } 41 | 42 | config, err := NewConfig(args["--config"].(string)) 43 | if err != nil { 44 | fatalln( 45 | karma.Format( 46 | err, 47 | "problem with configuration", 48 | ), 49 | ) 50 | } 51 | 52 | zabbix, err := NewZabbix( 53 | config.Server.Address, 54 | config.Server.Username, 55 | config.Server.Password, 56 | config.Session.Path, 57 | ) 58 | if err != nil { 59 | fatalln(err) 60 | } 61 | 62 | switch { 63 | case args["--triggers"].(bool): 64 | err = handleTriggers(zabbix, config, args) 65 | case args["--latest-data"].(bool): 66 | err = handleLatestData(zabbix, config, args) 67 | case args["--groups"].(bool): 68 | err = handleUsersGroups(zabbix, config, args) 69 | case args["--maintenances"].(bool): 70 | err = handleMaintenances(zabbix, config, args) 71 | case args["--hosts"].(bool): 72 | err = handleHosts(zabbix, config, args) 73 | 74 | } 75 | 76 | if err != nil { 77 | fatalln(err) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /maintenance.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // Maintenance struct 9 | type Maintenance struct { 10 | ID string `json:"maintenanceid"` 11 | Name string `json:"name"` 12 | Type string `json:"maintenance_type"` 13 | Description string `json:"description"` 14 | Since string `json:"active_since"` 15 | Till string `json:"active_till"` 16 | Hosts []Host `json:"hosts"` 17 | Timeperiods []Timeperiod `json:"timeperiods"` 18 | Groups []Group `json:"groups"` 19 | } 20 | 21 | // Maintenances struct 22 | type Maintenances struct { 23 | ID []string `json:"maintenanceids"` 24 | } 25 | 26 | func (maintenance *Maintenance) GetString() string { 27 | return maintenance.ID + " " + 28 | maintenance.Name + " " + maintenance.Description 29 | } 30 | 31 | // https://www.zabbix.com/documentation/3.4/manual/api/reference/maintenance/object 32 | func (maintenance *Maintenance) GetTypeCollect() string { 33 | if maintenance.Type == "0" { 34 | return "COLLECT" 35 | } 36 | return "NO COLLECT" 37 | } 38 | 39 | func (maintenance *Maintenance) GetStatus() string { 40 | now := time.Now().Format("2006-01-02 15:04:05") 41 | maintenanceSince := maintenance.GetDateTime(maintenance.Since) 42 | maintenanceTill := maintenance.GetDateTime(maintenance.Till) 43 | 44 | if now >= maintenanceSince && now < maintenanceTill { 45 | return "ACTIVE" 46 | } 47 | return "EXPIRED" 48 | } 49 | 50 | func (maintenance *Maintenance) GetDateTime(unixtime string) string { 51 | date, err := strconv.ParseInt(unixtime, 10, 64) 52 | if err != nil { 53 | debugf("Error: %+v", err) 54 | } 55 | return time.Unix(date, 0).Format("2006-01-02 15:04:05") 56 | } 57 | -------------------------------------------------------------------------------- /maintenances_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const ( 14 | // https://www.zabbix.com/documentation/3.4/manual/api/reference/maintenance/get 15 | maintenanceGet = ` 16 | { 17 | "jsonrpc": "2.0", 18 | "result": [ 19 | { 20 | "maintenanceid": "3", 21 | "name": "Sunday maintenance", 22 | "maintenance_type": "0", 23 | "description": "", 24 | "active_since": "1358844540", 25 | "active_till": "1390466940", 26 | "groups": [ 27 | { 28 | "groupid": "4", 29 | "name": "Zabbix servers", 30 | "internal": "0" 31 | } 32 | ], 33 | "timeperiods": [ 34 | { 35 | "timeperiodid": "4", 36 | "timeperiod_type": "3", 37 | "every": "1", 38 | "month": "0", 39 | "dayofweek": "1", 40 | "day": "0", 41 | "start_time": "64800", 42 | "period": "3600", 43 | "start_date": "2147483647" 44 | } 45 | ] 46 | } 47 | ], 48 | "id": 1 49 | }` 50 | 51 | // https://www.zabbix.com/documentation/3.4/manual/api/reference/maintenance/delete 52 | maintenancesRemove = ` 53 | { 54 | "jsonrpc": "2.0", 55 | "result": { 56 | "maintenanceids": [ 57 | "3", 58 | "1" 59 | ] 60 | }, 61 | "id": 1 62 | } 63 | ` 64 | 65 | // https://www.zabbix.com/documentation/3.4/manual/api/reference/maintenance/create 66 | maintenanceCreate = ` 67 | { 68 | "jsonrpc": "2.0", 69 | "result": { 70 | "maintenanceids": [ 71 | "3" 72 | ] 73 | }, 74 | "id": 1 75 | } 76 | ` 77 | ) 78 | 79 | func TestMaintenanceGet(t *testing.T) { 80 | test := assert.New(t) 81 | 82 | testserver := httptest.NewServer(http.HandlerFunc( 83 | func(w http.ResponseWriter, r *http.Request) { 84 | fmt.Fprint(w, maintenanceGet) 85 | }, 86 | )) 87 | defer testserver.Close() 88 | 89 | zabbix := &Zabbix{} 90 | zabbix.client = testserver.Client() 91 | zabbix.apiURL = testserver.URL 92 | 93 | maintenances, err := zabbix.GetMaintenances(Params{ 94 | "search": Params{ 95 | "name": "Sunday maintenance", 96 | }, 97 | }) 98 | 99 | test.NoError(err) 100 | test.Len(maintenances, 1) 101 | 102 | test.Equal("3", maintenances[0].ID) 103 | test.Equal("Sunday maintenance", maintenances[0].Name) 104 | test.Equal("Zabbix servers", maintenances[0].Groups[0].Name) 105 | } 106 | 107 | func TestMaintenanceRemove(t *testing.T) { 108 | test := assert.New(t) 109 | 110 | testserver := httptest.NewServer(http.HandlerFunc( 111 | func(w http.ResponseWriter, r *http.Request) { 112 | fmt.Fprint(w, maintenancesRemove) 113 | }, 114 | )) 115 | defer testserver.Close() 116 | 117 | zabbix := &Zabbix{} 118 | zabbix.client = testserver.Client() 119 | zabbix.apiURL = testserver.URL 120 | 121 | payload := []string{"3", "1"} 122 | 123 | var maintenances Maintenances 124 | maintenances, err := zabbix.RemoveMaintenance(payload) 125 | 126 | test.NoError(err) 127 | test.Len(maintenances.ID, 2) 128 | 129 | test.Equal("3", maintenances.ID[0]) 130 | test.Equal("1", maintenances.ID[1]) 131 | } 132 | 133 | func TestMaintenanceCreate(t *testing.T) { 134 | test := assert.New(t) 135 | 136 | testserver := httptest.NewServer(http.HandlerFunc( 137 | func(w http.ResponseWriter, r *http.Request) { 138 | fmt.Fprint(w, maintenanceCreate) 139 | }, 140 | )) 141 | defer testserver.Close() 142 | 143 | zabbix := &Zabbix{} 144 | zabbix.client = testserver.Client() 145 | zabbix.apiURL = testserver.URL 146 | 147 | var timeperiod Timeperiod 148 | 149 | timeperiod.TypeID = "0" 150 | timeperiod.Every = "1" 151 | timeperiod.Month = "0" 152 | timeperiod.DayOfWeek = "0" 153 | timeperiod.Day = "1" 154 | timeperiod.StartTime = "0" 155 | timeperiod.StartDate = strconv.FormatInt(int64(1551092132), 10) 156 | timeperiod.Period = strconv.FormatInt(int64(3600), 10) 157 | 158 | params := Params{ 159 | "name": "test maintenance", 160 | "active_since": "1551092132", 161 | "active_till": "1551178532", 162 | "hostids": []string{"2"}, 163 | "timeperiods": []Timeperiod{timeperiod}, 164 | } 165 | 166 | var maintenances Maintenances 167 | maintenances, err := zabbix.RemoveMaintenance(params) 168 | 169 | test.NoError(err) 170 | test.Len(maintenances.ID, 1) 171 | 172 | test.Equal("3", maintenances.ID[0]) 173 | } 174 | -------------------------------------------------------------------------------- /man.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "ZABBIXCTL" "1" "February 2019" "" "" 5 | . 6 | .SH "NAME" 7 | \fBzabbixctl\fR \- tool for working with zabbix using command line interface 8 | . 9 | .SH "DESCRIPTIOPN" 10 | \fIzabbixctl\fR is tool for working with zabbix server api using command line interface, it provides effective way for operating on statuses of triggers, hosts latest data and groups of users\. 11 | . 12 | .SH "CONFIGURATION" 13 | \fIzabbixctl\fR must be configurated before using, configuration file should be placed in \fB~/\.config/zabbixctl\.conf\fR and must be written using following syntax: 14 | . 15 | .IP "" 4 16 | . 17 | .nf 18 | 19 | [server] 20 | address = "zabbix\.local" 21 | username = "admin" 22 | password = "password" 23 | 24 | [session] 25 | path = "~/\.cache/zabbixctl\.session" 26 | . 27 | .fi 28 | . 29 | .IP "" 0 30 | . 31 | .P 32 | \fIzabbixctl\fR will authorize in \'zabbix\.local\' server using given user credentials and save a zabbix session to a file \fB~/\.cache/zabbixctl\.session\fR and at second run will use saved session instead of new authorization, by the way zabbix sessions have a ttl that by default equals to 15 minutes, so if saved zabbix session is outdated, \fIzabbixctl\fR will repeat authorization and rewrite the session file\. 33 | . 34 | .SH "SYNOPSIS" 35 | . 36 | .nf 37 | 38 | zabbixctl [options] \-T [/\.\.\.] 39 | zabbixctl [options] \-L \.\.\. [/\.\.\.] 40 | zabbixctl [options] \-G [/\.\.\.] 41 | zabbixctl \-h | \-\-help 42 | zabbixctl \-\-version 43 | . 44 | .fi 45 | . 46 | .SH "OPTIONS" 47 | \fB\-T \-\-triggers\fR 48 | . 49 | .P 50 | Search on zabbix triggers statuses\. Triggers could be filtered using /\fIpattern\fR argument, for example, search and acknowledge all triggers in a problem state and match the word \'cache\': 51 | . 52 | .P 53 | \fBzabbixctl \-Tp /cache\fR 54 | . 55 | .P 56 | \fB\-y \-\-only\-nack\fR 57 | . 58 | .P 59 | Show only not acknowledged triggers\. 60 | . 61 | .P 62 | \fB\-x \-\-severity\fR 63 | . 64 | .P 65 | Specify minimum trigger severity\. Once for information, twice for warning, three for disaster, four for high, five for disaster\. 66 | . 67 | .P 68 | \fB\-p \-\-problem\fR 69 | . 70 | .P 71 | Show triggers that have a problem state\. 72 | . 73 | .P 74 | \fB\-r \-\-recent\fR 75 | . 76 | .P 77 | Show triggers that have recently been in a problem state\. 78 | . 79 | .P 80 | \fB\-s \-\-since \fIdate\fR\fR 81 | . 82 | .P 83 | Show triggers that have changed their state after the given time, default: 7 days ago\. 84 | . 85 | .P 86 | \fB\-u \-\-until \fIdate\fR\fR 87 | . 88 | .P 89 | Show triggers that have changed their state before the given time\. 90 | . 91 | .P 92 | \fB\-m \-\-maintenance\fR 93 | . 94 | .P 95 | Show hosts in maintenance\. 96 | . 97 | .P 98 | \fB\-i \-\-sort \fIfields\fR\fR 99 | . 100 | .P 101 | Show triggers sorted by specified fields, default: lastchange,priority\. 102 | . 103 | .P 104 | \fB\-o \-\-order \fIorder\fR\fR 105 | . 106 | .P 107 | Show triggers in specified order, default: DESC\. 108 | . 109 | .P 110 | \fB\-n \-\-limit \fIamount\fR\fR 111 | . 112 | .P 113 | Show specified amount of triggers\. 114 | . 115 | .P 116 | \fB\-k \-\-acknowledge\fR 117 | . 118 | .P 119 | Acknowledge all retrieved triggers\. 120 | . 121 | .P 122 | \fB\-f \-\-noconfirm\fR 123 | . 124 | .P 125 | Do not prompt acknowledge confirmation dialog\. 126 | . 127 | .P 128 | \fB\-L \-\-latest\-data\fR 129 | . 130 | .P 131 | Search and show latest data for specified host(s)\. Hosts can be searched using wildcard character \'*\'\. Latest data can be filtered using /\fIpattern\fR argument, for example retrieve latest data for database nodes and search information about replication: 132 | . 133 | .P 134 | \fBzabbixctl \-L dbnode\-* /replication\fR 135 | . 136 | .P 137 | \fB\-g \-\-graph\fR 138 | . 139 | .P 140 | Show links on graph pages\. 141 | . 142 | .P 143 | \fB\-G \-\-groups\fR 144 | . 145 | .P 146 | Search and operate on configuration of users groups\. 147 | . 148 | .P 149 | \fB\-l \-\-list\fR 150 | . 151 | .P 152 | Show list users in specified users group\. 153 | . 154 | .P 155 | \fB\-a \-\-add\fR 156 | . 157 | .P 158 | Add specified \fIuser\fR to specified users group\. 159 | . 160 | .P 161 | \fB\-r \-\-remove\fR 162 | . 163 | .P 164 | Remove specified \fIuser\fR from speicifed users group\. 165 | . 166 | .P 167 | \fB\-f \-\-noconfirm\fR 168 | . 169 | .P 170 | Do not prompt confirmation dialog\. 171 | . 172 | .SH "COMMON OPTIONS" 173 | \fB\-c \-\-config \fIpath\fR\fR 174 | . 175 | .P 176 | Use specified configuration file, default: \fB$HOME/\.config/zabbixctl\.conf\fR 177 | . 178 | .P 179 | \fB\-v \-\-verbosity\fR 180 | . 181 | .P 182 | Specify program output verbosity\. Once for debug, twice for trace\. 183 | . 184 | .P 185 | \fB\-h \-\-help\fR 186 | . 187 | .P 188 | Show this screen\. 189 | . 190 | .P 191 | \fB\-\-version\fR 192 | . 193 | .P 194 | Show version\. 195 | . 196 | .SH "EXAMPLES" 197 | \fIListing triggers in a problem state\fR 198 | . 199 | .IP "" 4 200 | . 201 | .nf 202 | 203 | zabbixctl \-Tp 204 | . 205 | .fi 206 | . 207 | .IP "" 0 208 | . 209 | .P 210 | \fIListing triggers that have recenty been in a problem state\fR 211 | . 212 | .IP "" 4 213 | . 214 | .nf 215 | 216 | zabbixctl \-Tr 217 | . 218 | .fi 219 | . 220 | .IP "" 0 221 | . 222 | .P 223 | \fIListing and filtering triggers that contain a word mysql\fR 224 | . 225 | .IP "" 4 226 | . 227 | .nf 228 | 229 | zabbixctl \-T /mysql 230 | . 231 | .fi 232 | . 233 | .IP "" 0 234 | . 235 | .P 236 | \fIListing and acknowledging triggers that severity level is DISASTER\fR 237 | . 238 | .IP "" 4 239 | . 240 | .nf 241 | 242 | zabbixctl \-T \-xxxxx \-k 243 | . 244 | .fi 245 | . 246 | .IP "" 0 247 | . 248 | .P 249 | \fIListing latest data for db nodes and filtering for information about replication lag\fR 250 | . 251 | .IP "" 4 252 | . 253 | .nf 254 | 255 | zabbixctl \-L dbnode* /lag 256 | . 257 | .fi 258 | . 259 | .IP "" 0 260 | . 261 | .P 262 | \fIOpening stacked graph for CPU quote use of selected containers\fR 263 | . 264 | .IP "" 4 265 | . 266 | .nf 267 | 268 | zabbixctl \-L \'container\-*\' /cpu quota \-\-stacked 269 | . 270 | .fi 271 | . 272 | .IP "" 0 273 | . 274 | .P 275 | \fIListing users groups that starts with \'HTTP_\'\fR 276 | . 277 | .IP "" 4 278 | . 279 | .nf 280 | 281 | zabbixctl \-G HTTP_* 282 | . 283 | .fi 284 | . 285 | .IP "" 0 286 | . 287 | .P 288 | \fIListing users groups that contain user admin\fR 289 | . 290 | .IP "" 4 291 | . 292 | .nf 293 | 294 | zabbixctl \-G /admin 295 | . 296 | .fi 297 | . 298 | .IP "" 0 299 | . 300 | .P 301 | \fIAdding user admin to groups that contain user guest\fR 302 | . 303 | .IP "" 4 304 | . 305 | .nf 306 | 307 | zabbixctl \-G /guest \-a admin 308 | . 309 | .fi 310 | . 311 | .IP "" 0 312 | . 313 | .SH "AUTHOR" 314 | Egor Kovetskiy \fIe\.kovetskiy@gmail\.com\fR 315 | . 316 | .SH "CONTRIBUTORS" 317 | Stanislav Seletskiy \fIs\.seletskiy@gmail\.com\fR 318 | . 319 | .P 320 | Andrey Kitsul \fIa\.kitsul@zarplata\.ru\fR 321 | . 322 | .P 323 | GitHub \fIhttps://github\.com/kovetskiy/zabbixctl\fR 324 | -------------------------------------------------------------------------------- /man.markdown: -------------------------------------------------------------------------------- 1 | zabbixctl(1) -- tool for working with zabbix using command line interface 2 | =========== 3 | 4 | ## DESCRIPTIOPN 5 | 6 | *zabbixctl* is tool for working with zabbix server api using command line 7 | interface, it provides effective way for operating on statuses of triggers, 8 | hosts latest data and groups of users. 9 | 10 | ## CONFIGURATION 11 | 12 | *zabbixctl* must be configurated before using, configuration file should be 13 | placed in `~/.config/zabbixctl.conf` and must be written using following syntax: 14 | 15 | [server] 16 | address = "zabbix.local" 17 | username = "admin" 18 | password = "password" 19 | 20 | [session] 21 | path = "~/.cache/zabbixctl.session" 22 | 23 | *zabbixctl* will authorize in 'zabbix.local' server using given user 24 | credentials and save a zabbix session to a file `~/.cache/zabbixctl.session` and 25 | at second run will use saved session instead of new authorization, by the way 26 | zabbix sessions have a ttl that by default equals to 15 minutes, so if saved 27 | zabbix session is outdated, *zabbixctl* will repeat authorization and rewrite 28 | the session file. 29 | 30 | ## SYNOPSIS 31 | 32 | zabbixctl [options] -T [/...] 33 | zabbixctl [options] -L ... [/...] 34 | zabbixctl [options] -G [/...] 35 | zabbixctl -h | --help 36 | zabbixctl --version 37 | 38 | ## OPTIONS 39 | 40 | **-T --triggers** 41 | 42 | Search on zabbix triggers statuses. Triggers could be filtered using / 43 | argument, for example, search and acknowledge all triggers in a problem state 44 | and match the word 'cache': 45 | 46 | `zabbixctl -Tp /cache` 47 | 48 | **-y --only-nack** 49 | 50 | Show only not acknowledged triggers. 51 | 52 | **-x --severity** 53 | 54 | Specify minimum trigger severity. Once for information, twice for warning, 55 | three for disaster, four for high, five for disaster. 56 | 57 | **-p --problem** 58 | 59 | Show triggers that have a problem state. 60 | 61 | **-r --recent** 62 | 63 | Show triggers that have recently been in a problem state. 64 | 65 | **-s --since ** 66 | 67 | Show triggers that have changed their state after the given time, default: 7 68 | days ago. 69 | 70 | **-u --until ** 71 | 72 | Show triggers that have changed their state before the given time. 73 | 74 | **-m --maintenance** 75 | 76 | Show hosts in maintenance. 77 | 78 | **-i --sort ** 79 | 80 | Show triggers sorted by specified fields, default: lastchange,priority. 81 | 82 | **-o --order ** 83 | 84 | Show triggers in specified order, default: DESC. 85 | 86 | **-n --limit ** 87 | 88 | Show specified amount of triggers. 89 | 90 | **-k --acknowledge** 91 | 92 | Acknowledge all retrieved triggers. 93 | 94 | **-f --noconfirm** 95 | 96 | Do not prompt acknowledge confirmation dialog. 97 | 98 | **-L --latest-data** 99 | 100 | Search and show latest data for specified host(s). Hosts can be searched 101 | using wildcard character '*'. Latest data can be filtered using / 102 | argument, for example retrieve latest data for database nodes and search 103 | information about replication: 104 | 105 | `zabbixctl -L dbnode-* /replication` 106 | 107 | **-g --graph** 108 | 109 | Show links on graph pages. 110 | 111 | **-G --groups** 112 | 113 | Search and operate on configuration of users groups. 114 | 115 | **-l --list** 116 | 117 | Show list users in specified users group. 118 | 119 | **-a --add** 120 | 121 | Add specified to specified users group. 122 | 123 | **-r --remove** 124 | 125 | Remove specified from speicifed users group. 126 | 127 | **-f --noconfirm** 128 | 129 | Do not prompt confirmation dialog. 130 | 131 | ## COMMON OPTIONS 132 | 133 | **-c --config ** 134 | 135 | Use specified configuration file, default: `$HOME/.config/zabbixctl.conf` 136 | 137 | **-v --verbosity** 138 | 139 | Specify program output verbosity. Once for debug, twice for trace. 140 | 141 | **-h --help** 142 | 143 | Show this screen. 144 | 145 | **--version** 146 | 147 | Show version. 148 | 149 | ## EXAMPLES 150 | 151 | *Listing triggers in a problem state* 152 | 153 | ``` 154 | zabbixctl -Tp 155 | ``` 156 | 157 | *Listing triggers that have recenty been in a problem state* 158 | 159 | ``` 160 | zabbixctl -Tr 161 | ``` 162 | 163 | *Listing and filtering triggers that contain a word mysql* 164 | 165 | ``` 166 | zabbixctl -T /mysql 167 | ``` 168 | 169 | *Listing and acknowledging triggers that severity level is DISASTER* 170 | 171 | ``` 172 | zabbixctl -T -xxxxx -k 173 | ``` 174 | 175 | *Listing latest data for db nodes and filtering for information about replication lag* 176 | 177 | ``` 178 | zabbixctl -L dbnode* /lag 179 | ``` 180 | 181 | *Opening stacked graph for CPU quote use of selected containers* 182 | 183 | ``` 184 | zabbixctl -L 'container-*' /cpu quota --stacked 185 | ``` 186 | 187 | *Listing users groups that starts with 'HTTP_'* 188 | 189 | ``` 190 | zabbixctl -G HTTP_* 191 | ``` 192 | 193 | *Listing users groups that contain user admin* 194 | 195 | ``` 196 | zabbixctl -G /admin 197 | ``` 198 | 199 | *Adding user admin to groups that contain user guest* 200 | 201 | ``` 202 | zabbixctl -G /guest -a admin 203 | ``` 204 | 205 | ## AUTHOR 206 | 207 | Egor Kovetskiy 208 | 209 | ## CONTRIBUTORS 210 | 211 | Stanislav Seletskiy 212 | 213 | Andrey Kitsul 214 | 215 | [GitHub](https://github.com/kovetskiy/zabbixctl) 216 | -------------------------------------------------------------------------------- /parse_date_darwin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | karma "github.com/reconquest/karma-go" 7 | "github.com/simplereach/timeutils" 8 | ) 9 | 10 | func parseDate(date string) (int64, error) { 11 | var dateUnix int64 12 | 13 | destiny := karma.Describe("method", "parseDate") 14 | 15 | if date == "" { 16 | timeNow := time.Now() 17 | dateUnix = timeNow.Unix() 18 | } else { 19 | dateParse, err := timeutils.ParseDateString(date) 20 | if err != nil { 21 | return dateUnix, destiny.Describe( 22 | "error", err, 23 | ).Describe( 24 | "date", date, 25 | ).Reason( 26 | "can't convert date to unixtime", 27 | ) 28 | } 29 | dateUnix = dateParse.Unix() 30 | } 31 | return dateUnix, nil 32 | } 33 | 34 | func parseDateTime(value string) (int64, error) { 35 | date, err := timeutils.ParseDateString(value) 36 | if err != nil { 37 | return 0, karma.Format(err, "can't parse datetime '%s'", value) 38 | } 39 | 40 | return date.Unix(), nil 41 | } 42 | -------------------------------------------------------------------------------- /parse_date_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | karma "github.com/reconquest/karma-go" 7 | "github.com/simplereach/timeutils" 8 | ) 9 | 10 | func parseDate(date string) (int64, error) { 11 | var dateUnix int64 12 | 13 | destiny := karma.Describe("method", "parseDate") 14 | 15 | if date == "" { 16 | timeNow := time.Now() 17 | dateUnix = timeNow.Unix() 18 | } else { 19 | dateParse, err := timeutils.ParseDateString(date) 20 | if err != nil { 21 | return dateUnix, destiny.Describe( 22 | "error", err, 23 | ).Describe( 24 | "date", date, 25 | ).Reason( 26 | "can't convert date to unixtime", 27 | ) 28 | } 29 | dateUnix = dateParse.Unix() 30 | } 31 | return dateUnix, nil 32 | } 33 | 34 | func parseDateTime(value string) (int64, error) { 35 | date, err := timeutils.ParseDateString(value) 36 | if err != nil { 37 | return 0, karma.Format(err, "can't parse datetime '%s'", value) 38 | } 39 | 40 | return date.Unix(), nil 41 | } 42 | -------------------------------------------------------------------------------- /parse_date_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | karma "github.com/reconquest/karma-go" 7 | ) 8 | 9 | func parseDate(date string) (int64, error) { 10 | var dateUnix int64 11 | 12 | destiny := karma.Describe("method", "parseDate") 13 | 14 | if date == "" { 15 | timeNow := time.Now() 16 | dateUnix = timeNow.Unix() 17 | } else { 18 | const RFC3339 = "2006-01-02 15:04" 19 | dateParse, err := time.Parse(RFC3339, date) 20 | if err != nil { 21 | return dateUnix, destiny.Describe( 22 | "error", err, 23 | ).Describe( 24 | "date", date, 25 | ).Reason( 26 | "can't convert date to unixtime", 27 | ) 28 | } 29 | dateUnix = dateParse.Unix() 30 | } 31 | return dateUnix, nil 32 | } 33 | 34 | func parseDateTime(value string) (int64, error) { 35 | const RFC3339 = "2006-01-02 15:04" 36 | date, err := time.Parse(RFC3339, value) 37 | if err != nil { 38 | return 0, karma.Format(err, "can't parse datetime '%s'", value) 39 | } 40 | 41 | return date.Unix(), nil 42 | } 43 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # zabbixctl 2 | 3 | **zabbixctl** is tool for working with zabbix server api using command line 4 | interface, it provides effective way for operating on statuses of triggers, 5 | hosts latest data and groups of users. 6 | 7 | ![dashboard](http://i.imgur.com/0WZkMN0.gif) 8 | 9 | ## State of the project 10 | 11 | I don't use zabbix daily anymore. Therefore, I don't commit into the project. But I'm still here and I'm willing to accept your contributions. 12 | 13 | ## Installation 14 | 15 | ``` 16 | go get github.com/kovetskiy/zabbixctl 17 | ``` 18 | 19 | afterwards executable will be placed as `$GOPATH/bin/zabbixctl` 20 | 21 | ## Configuration 22 | 23 | **zabbixctl** must be configurated before using, configuration file should be 24 | placed in `~/.config/zabbixctl.conf` and must be written using following 25 | syntax: 26 | 27 | ```toml 28 | [server] 29 | address = "zabbix.local" 30 | username = "admin" 31 | password = "password" 32 | 33 | [session] 34 | path = "~/.cache/zabbixctl.session" 35 | ``` 36 | 37 | **zabbixctl** will authorize in 'zabbix.local' server using given user 38 | credentials and save a zabbix session to a file `~/.cache/zabbixctl.session` 39 | and at second run will use saved session instead of new authorization, by the 40 | way zabbix sessions have a ttl that by default equals to 15 minutes, so if 41 | saved zabbix session is outdated, **zabbixctl** will repeat authorization and 42 | rewrite the session file. 43 | 44 | ## Usage 45 | 46 | ##### -T --triggers 47 | Search on zabbix triggers statuses. Triggers could be filtered using 48 | / argument, for example, search and acknowledge all triggers in a 49 | problem state and match the word 'cache': 50 | ``` 51 | zabbixctl -Tp /cache 52 | ``` 53 | 54 | ##### -y --only-nack 55 | Show only not acknowledged triggers. 56 | 57 | ##### -x --severity 58 | Specify minimum trigger severity. Once for information, twice for 59 | warning, three for disaster, four for high, five for disaster. 60 | 61 | ##### -p --problem 62 | Show triggers that have a problem state. 63 | 64 | ##### -r --recent 65 | Show triggers that have recently been in a problem state. 66 | 67 | ##### -s --since 68 | Show triggers that have changed their state after the given time, default: 7 69 | days ago 70 | 71 | ##### -u --until 72 | Show triggers that have changed their state before the given time. 73 | 74 | ##### -m --maintenance 75 | Show hosts in maintenance. 76 | 77 | ##### -i --sort 78 | Show triggers sorted by specified fields, default: lastchange,priority. 79 | 80 | ##### -o --order 81 | Show triggers in specified order, default: DESC. 82 | 83 | ##### -n --limit 84 | Show specified amount of triggers. 85 | 86 | ##### -k --acknowledge 87 | Acknowledge all retrieved triggers. 88 | 89 | ##### -f --noconfirm 90 | Do not prompt acknowledge confirmation dialog. 91 | 92 | ##### -L --latest-data 93 | Search and show latest data for specified host(s). Hosts can be searched using 94 | wildcard character '*'. Latest data can be filtered using / argument, 95 | for example retrieve latest data for database nodes and search information 96 | about replication: 97 | 98 | ``` 99 | zabbixctl -L dbnode* /replication 100 | ``` 101 | 102 | ##### -g --graph 103 | Show links on graph pages. 104 | 105 | ##### -G --groups 106 | Search and operate on configuration of users groups. 107 | 108 | ##### -l --list 109 | Show list users in specified users group. 110 | 111 | ##### -a --add 112 | Add specified to specified users group. 113 | 114 | ##### -r --remove 115 | Remove specified from speicifed users group. 116 | 117 | ##### -f --noconfirm 118 | Do not prompt confirmation dialog. 119 | 120 | ##### -w --stacked | -b --normal 121 | Returns single link which points to the stacked or normal graph for matched 122 | items. 123 | 124 | ##### -M --maintenances 125 | Search and operate on configuration of maintenance. Maintenance could be 126 | filtered using / argument, for example, search maintenance match the 127 | word 'update-kernel': 128 | 129 | ``` 130 | zabbixctl -M dbnode-* /update-kernel 131 | ``` 132 | 133 | ##### -a --add 134 | Add new specified with timeperiod type once. 135 | 136 | ##### -r --remove 137 | Remove specified . 138 | 139 | ##### -H --hosts 140 | Search and operate with host. 141 | 142 | ``` 143 | zabbixctl -H dbnode-* 144 | ``` 145 | 146 | ##### -r --remove 147 | Remove specified . 148 | 149 | ## Examples 150 | 151 | ### Listing triggers in a problem state 152 | 153 | ``` 154 | zabbixctl -Tp 155 | ``` 156 | 157 | ### Listing triggers that have recenty been in a problem state 158 | 159 | ``` 160 | zabbixctl -Tr 161 | ``` 162 | 163 | ### Listing and filtering triggers that contain a word mysql 164 | 165 | ``` 166 | zabbixctl -T /mysql 167 | ``` 168 | 169 | ### Listing and acknowledging triggers that severity level is DISASTER 170 | 171 | ``` 172 | zabbixctl -T -xxxxx -k 173 | ``` 174 | 175 | ### Listing latest data for db nodes and filtering for information about replication lag 176 | 177 | ``` 178 | zabbixctl -L dbnode* /lag 179 | ``` 180 | 181 | ### Opening stacked graph for CPU quote use of selected containers 182 | 183 | ``` 184 | zabbixctl -L 'container-*' /cpu quota --stacked 185 | ``` 186 | 187 | ### Listing users groups that starts with 'HTTP_' 188 | 189 | ``` 190 | zabbixctl -G HTTP_* 191 | ``` 192 | 193 | ### Listing users groups that contain user admin 194 | 195 | ``` 196 | zabbixctl -G /admin 197 | ``` 198 | 199 | ### Adding user admin to groups that contain user guest 200 | 201 | ``` 202 | zabbixctl -G /guest -a admin 203 | ``` 204 | 205 | ### Listing maintenances period 206 | 207 | ``` 208 | zabbixctl -M 209 | ``` 210 | 211 | ### Listing maintenances period with hostname like 'dbnode*' 212 | 213 | ``` 214 | zabbixctl -M dbnode* 215 | ``` 216 | 217 | ### Listing maintenances period with hostname like 'dbnode*' with filter 218 | maintenance name update-kernel 219 | 220 | ``` 221 | zabbixctl -M dbnode* /update-kernel 222 | ``` 223 | 224 | ### Add maintenance period name update-kernel with hostname like 'dbnode*' 225 | 226 | ``` 227 | zabbixctl -M dbnode* -a update-kernel 228 | ``` 229 | 230 | ### Add maintenance period name update-kernel with host from stdin (must be flag -f) 231 | 232 | axfr is a tool of your choice for retrieving domain information from your infrastructure DNS. 233 | 234 | ``` 235 | axfr | grep phpnode | zabbixctl -M -z -a update-kernel -f 236 | ``` 237 | 238 | ### Add maintenance period name update-kernel with hostname like 'dbnode*' and read additional 239 | host from stdin (must be flag -f) 240 | 241 | axfr is a tool of your choice for retrieving domain information from your infrastructure DNS. 242 | 243 | ``` 244 | axfr | grep phpnode | zabbixctl -M -z dbnode* -a update-kernel -f 245 | ``` 246 | 247 | ### Remove maintenance period name update-kernel 248 | 249 | ``` 250 | zabbixctl -M -r update-kernel 251 | ``` 252 | 253 | ### Search host with hostname like 'dbnode*' 254 | 255 | ``` 256 | zabbixctl -H dbnode* 257 | ``` 258 | 259 | ### Remove host with hostname 'dbnode1' (full uniq name) 260 | 261 | ``` 262 | zabbixctl -H -r dbnode1 263 | ``` 264 | 265 | ## License 266 | 267 | MIT. 268 | -------------------------------------------------------------------------------- /responses.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/reconquest/karma-go" 4 | 5 | type Response interface { 6 | Error() error 7 | } 8 | 9 | type ResponseRaw struct { 10 | Err struct { 11 | Data string `json:"data"` 12 | Message string `json:"message"` 13 | } `json:"error"` 14 | 15 | Result interface{} `json:"result"` 16 | } 17 | 18 | func (response *ResponseRaw) Error() error { 19 | if response.Err.Data != "" && response.Err.Message != "" { 20 | return karma.Push( 21 | response.Err.Message, 22 | response.Err.Data, 23 | ) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | type ResponseLogin struct { 30 | ResponseRaw 31 | Token string `json:"result"` 32 | } 33 | 34 | type ResponseAPIVersion struct { 35 | ResponseRaw 36 | Version string `json:"result"` 37 | } 38 | 39 | type ResponseTriggers struct { 40 | ResponseRaw 41 | Data map[string]Trigger `json:"result"` 42 | } 43 | 44 | type ResponseMaintenances struct { 45 | ResponseRaw 46 | Data []Maintenance `json:"result"` 47 | } 48 | 49 | // Response Create/Delete maintenance 50 | type ResponseMaintenancesArray struct { 51 | ResponseRaw 52 | Data Maintenances `json:"result"` 53 | } 54 | 55 | type ResponseItems struct { 56 | ResponseRaw 57 | Data []Item `json:"result"` 58 | } 59 | 60 | type ResponseHTTPTests struct { 61 | ResponseRaw 62 | Data []HTTPTest `json:"result"` 63 | } 64 | 65 | type ResponseHosts struct { 66 | ResponseRaw 67 | Data []Host `json:"result"` 68 | } 69 | 70 | // Response Create/Delete host 71 | type ResponseHostsArray struct { 72 | ResponseRaw 73 | Data Hosts `json:"result"` 74 | } 75 | 76 | type ResponseGroups struct { 77 | ResponseRaw 78 | Data []Group `json:"result"` 79 | } 80 | 81 | type ResponseUserGroup struct { 82 | ResponseRaw 83 | Data []UserGroup `json:"result"` 84 | } 85 | 86 | type ResponseUsers struct { 87 | ResponseRaw 88 | Data []User `json:"result"` 89 | } 90 | 91 | type ResponseHistory struct { 92 | ResponseRaw 93 | Data []History `json:"result"` 94 | } 95 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | func parseSearchQuery(targets []string) (words []string, pattern string) { 9 | var ( 10 | search bool 11 | query []string 12 | ) 13 | 14 | for _, target := range targets { 15 | if strings.HasPrefix(target, "/") { 16 | query = append( 17 | query, 18 | strings.TrimPrefix(target, "/"), 19 | ) 20 | 21 | search = true 22 | 23 | continue 24 | } 25 | 26 | if search { 27 | query = append(query, target) 28 | } else { 29 | words = append(words, target) 30 | } 31 | } 32 | 33 | return words, getSearchPattern(query) 34 | } 35 | 36 | func getSearchPattern(query []string) string { 37 | letters := strings.Split( 38 | strings.Replace( 39 | strings.Join(query, ""), 40 | " ", "", -1, 41 | ), 42 | "", 43 | ) 44 | for i, letter := range letters { 45 | letters[i] = regexp.QuoteMeta(letter) 46 | } 47 | 48 | pattern := strings.Join(letters, ".*") 49 | 50 | return pattern 51 | } 52 | 53 | func matchPattern(pattern, target string) bool { 54 | match, err := regexp.MatchString(strings.ToLower(pattern), 55 | strings.ToLower(target)) 56 | if err != nil { 57 | debugf("Error: %+v", err) 58 | } 59 | return match 60 | } 61 | -------------------------------------------------------------------------------- /severity.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Severity int 4 | 5 | var ( 6 | SeverityInformation Severity = 1 7 | SeverityWarning Severity = 2 8 | SeverityAverage Severity = 3 9 | SeverityHigh Severity = 4 10 | SeverityDisaster Severity = 5 11 | ) 12 | 13 | func (priority Severity) String() string { 14 | switch priority { 15 | case SeverityInformation: 16 | return "INFO" 17 | case SeverityWarning: 18 | return "WARN" 19 | case SeverityAverage: 20 | return "AVG" 21 | case SeverityHigh: 22 | return "HIGH" 23 | case SeverityDisaster: 24 | return "DISASTER" 25 | default: 26 | return "UNKNOWN" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spinner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/kovetskiy/spinner-go" 8 | ) 9 | 10 | func withSpinner(status string, method func() error) error { 11 | if debugMode { 12 | fmt.Fprintln(os.Stderr, status) 13 | return method() 14 | } 15 | 16 | return spinner.SetStatus(status + " ").Call(method) 17 | } 18 | -------------------------------------------------------------------------------- /timeperiod.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // Timeperiod struct 9 | type Timeperiod struct { 10 | ID string `json:"timeperiodid"` 11 | TypeID string `json:"timeperiod_type"` 12 | Every string `json:"every"` 13 | Month string `json:"month"` 14 | DayOfWeek string `json:"dayofweek"` 15 | Day string `json:"day"` 16 | StartTime string `json:"start_time"` 17 | Period string `json:"period"` 18 | StartDate string `json:"start_date"` 19 | } 20 | 21 | // https://www.zabbix.com/documentation/3.4/manual/api/reference/maintenance/object#time_period 22 | func (timeperiod *Timeperiod) GetType() string { 23 | switch timeperiod.TypeID { 24 | case "2": 25 | return "DAILY" 26 | case "3": 27 | return "WEEKLY" 28 | case "4": 29 | return "MONTHLY" 30 | default: 31 | return "ONCE" 32 | 33 | } 34 | } 35 | 36 | func (timeperiod *Timeperiod) GetStartDate() string { 37 | date, err := strconv.ParseInt(timeperiod.StartDate, 10, 64) 38 | if err != nil { 39 | debugf("Error: %+v", err) 40 | } 41 | return time.Unix(date, 0).Format("2006-01-02 15:04:05") 42 | } 43 | 44 | func (timeperiod *Timeperiod) GetPeriodMinute() int64 { 45 | period, err := strconv.ParseInt(timeperiod.Period, 10, 64) 46 | if err != nil { 47 | debugf("Error: %+v", err) 48 | } 49 | return (period / 60) 50 | } 51 | -------------------------------------------------------------------------------- /trigger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type Trigger struct { 10 | ID string `json:"triggerid"` 11 | Description string `json:"description"` 12 | Hostname string `json:"host"` 13 | Value string `json:"value"` 14 | Comments string `json:"comments"` 15 | Functions []Function `json:"functions"` 16 | LastChange string `json:"lastchange"` 17 | LastEvent struct { 18 | ID string `json:"eventid"` 19 | Acknowledged string `json:"acknowledged"` 20 | } `json:"lastEvent"` 21 | Hosts []struct { 22 | Hostid string `json:"hostid"` 23 | Name string `json:"name"` 24 | } `json:"hosts"` 25 | Priority string `json:"priority"` 26 | } 27 | 28 | func (trigger *Trigger) String() string { 29 | return trigger.LastEvent.ID + " " + 30 | trigger.Hostname + " " + trigger.Description 31 | } 32 | 33 | func (trigger *Trigger) GetHostName() string { 34 | if len(trigger.Hosts) > 0 { 35 | return trigger.Hosts[0].Name 36 | } 37 | return "" 38 | } 39 | 40 | func (trigger *Trigger) StatusAcknowledge() string { 41 | if trigger.LastEvent.Acknowledged == "1" { 42 | return "ACK" 43 | } 44 | 45 | return "NACK" 46 | } 47 | 48 | func (trigger *Trigger) StatusProblem() string { 49 | if trigger.Value == "1" { 50 | return "PROBLEM" 51 | } 52 | 53 | return "OK" 54 | } 55 | 56 | func (trigger *Trigger) Severity() Severity { 57 | value, err := strconv.Atoi(trigger.Priority) 58 | if err != nil { 59 | debugf("Error: %+v", err) 60 | } 61 | return Severity(value) 62 | } 63 | 64 | func (trigger *Trigger) DateTime() string { 65 | return trigger.date().Format("2006-01-02 15:04:05") 66 | } 67 | 68 | func (trigger *Trigger) Age() string { 69 | date := time.Since(trigger.date()) 70 | 71 | var ( 72 | seconds = int(date.Seconds()) % 60 73 | minutes = int(date.Minutes()) % 60 74 | hours = int(date.Hours()) 75 | days = hours / 24 76 | months = days / 30. 77 | ) 78 | 79 | var units []string 80 | 81 | units = addUnit(units, months, "mon") 82 | units = addUnit(units, days%7, "d") 83 | units = addUnit(units, hours%24, "h") 84 | units = addUnit(units, minutes, "m") 85 | units = addUnit(units, seconds, "s") 86 | 87 | return strings.Join(units, " ") 88 | } 89 | 90 | func (trigger *Trigger) date() time.Time { 91 | date, err := strconv.ParseInt(trigger.LastChange, 10, 64) 92 | if err != nil { 93 | debugf("Error: %+v", err) 94 | } 95 | return time.Unix(date, 0) 96 | } 97 | 98 | func addUnit(units []string, value int, unit string) []string { 99 | if value > 1 { 100 | units = append(units, strconv.Itoa(value)+unit) 101 | } 102 | 103 | return units 104 | } 105 | -------------------------------------------------------------------------------- /users.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type User struct { 4 | ID string `json:"userid"` 5 | Name string `json:"name"` 6 | Alias string `json:"alias"` 7 | } 8 | 9 | type UserGroup struct { 10 | ID string `json:"usrgrpid"` 11 | Name string `json:"name"` 12 | Status string `json:"users_status"` 13 | Users []User `json:"users"` 14 | } 15 | 16 | func (group *UserGroup) GetStatus() string { 17 | if group.Status == "0" { 18 | return "enabled" 19 | } 20 | 21 | return "disabled" 22 | } 23 | -------------------------------------------------------------------------------- /zabbix.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "reflect" 11 | "strings" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/reconquest/karma-go" 16 | ) 17 | 18 | const ( 19 | // 900 is default zabbix session ttl, -60 for safety 20 | ZabbixSessionTTL = 900 - 60 21 | ) 22 | 23 | var ( 24 | withAuthFlag = true 25 | withoutAuthFlag = false 26 | ) 27 | 28 | type Params map[string]interface{} 29 | 30 | type Request struct { 31 | RPC string `json:"jsonrpc"` 32 | Method string `json:"method"` 33 | Params interface{} `json:"params"` 34 | Auth string `json:"auth,omitempty"` 35 | ID int64 `json:"id"` 36 | } 37 | 38 | type Zabbix struct { 39 | basicURL string 40 | apiURL string 41 | session string 42 | client *http.Client 43 | requestID int64 44 | apiVersion string 45 | } 46 | 47 | func NewZabbix( 48 | address, username, password, sessionFile string, 49 | ) (*Zabbix, error) { 50 | var err error 51 | 52 | zabbix := &Zabbix{ 53 | client: &http.Client{}, 54 | } 55 | 56 | if !strings.Contains(address, "://") { 57 | address = "http://" + address 58 | } 59 | 60 | zabbix.basicURL = strings.TrimSuffix(address, "/") 61 | zabbix.apiURL = zabbix.basicURL + "/api_jsonrpc.php" 62 | 63 | if sessionFile != "" { 64 | debugln("* reading session file") 65 | 66 | err = zabbix.restoreSession(sessionFile) 67 | if err != nil { 68 | return nil, karma.Format( 69 | err, 70 | "can't restore zabbix session using file '%s'", 71 | sessionFile, 72 | ) 73 | } 74 | } else { 75 | debugln("* session feature is not used") 76 | } 77 | 78 | if zabbix.session == "" { 79 | err = zabbix.Login(username, password) 80 | if err != nil { 81 | return nil, karma.Format( 82 | err, 83 | "can't authorize user '%s' in zabbix server", 84 | username, 85 | ) 86 | } 87 | } else { 88 | debugln("* using session instead of authorization") 89 | } 90 | 91 | if sessionFile != "" { 92 | debugln("* rewriting session file") 93 | 94 | // always rewrite session file, it will change modify date 95 | err = zabbix.saveSession(sessionFile) 96 | if err != nil { 97 | return nil, karma.Format( 98 | err, 99 | "can't save zabbix session to file '%s'", 100 | sessionFile, 101 | ) 102 | } 103 | } 104 | 105 | if len(zabbix.apiVersion) < 1 { 106 | err = zabbix.GetAPIVersion() 107 | if err != nil { 108 | return nil, karma.Format( 109 | err, 110 | "can't get zabbix api version", 111 | ) 112 | } 113 | } 114 | 115 | return zabbix, nil 116 | } 117 | 118 | func (zabbix *Zabbix) restoreSession(path string) error { 119 | file, err := os.OpenFile( 120 | path, os.O_CREATE|os.O_RDWR, 0600, 121 | ) 122 | if err != nil { 123 | return karma.Format( 124 | err, "can't open session file", 125 | ) 126 | } 127 | 128 | stat, err := file.Stat() 129 | if err != nil { 130 | return karma.Format( 131 | err, "can't stat session file", 132 | ) 133 | } 134 | 135 | if time.Since(stat.ModTime()).Seconds() < ZabbixSessionTTL { 136 | session, err := ioutil.ReadAll(file) 137 | if err != nil { 138 | return karma.Format( 139 | err, "can't read session file", 140 | ) 141 | } 142 | 143 | zabbix.session = string(session) 144 | } else { 145 | debugln("* session is outdated") 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func (zabbix *Zabbix) saveSession(path string) error { 152 | err := ioutil.WriteFile(path, []byte(zabbix.session), 0600) 153 | if err != nil { 154 | return karma.Format( 155 | err, 156 | "can't write session file", 157 | ) 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func (zabbix *Zabbix) GetAPIVersion() error { 164 | var response ResponseAPIVersion 165 | 166 | debugln("* apiinfo.version") 167 | 168 | err := zabbix.call( 169 | "apiinfo.version", 170 | Params{}, 171 | &response, 172 | withoutAuthFlag, 173 | ) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | zabbix.apiVersion = response.Version 179 | 180 | return nil 181 | } 182 | 183 | func (zabbix *Zabbix) Login(username, password string) error { 184 | var response ResponseLogin 185 | 186 | debugln("* authorizing") 187 | 188 | err := zabbix.call( 189 | "user.login", 190 | Params{"user": username, "password": password}, 191 | &response, 192 | withAuthFlag, 193 | ) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | zabbix.session = response.Token 199 | 200 | return nil 201 | } 202 | 203 | func (zabbix *Zabbix) Acknowledge(identifiers []string) error { 204 | var response ResponseRaw 205 | 206 | debugln("* acknowledging triggers") 207 | 208 | params := Params{ 209 | "eventids": identifiers, 210 | "message": "ack", 211 | } 212 | 213 | if len(strings.Split(zabbix.apiVersion, ".")) < 1 { 214 | return karma.Format("can't parse zabbix version %s", zabbix.apiVersion) 215 | } 216 | 217 | majorZabbixVersion := strings.Split(zabbix.apiVersion, ".")[0] 218 | 219 | switch majorZabbixVersion { 220 | 221 | case "4": 222 | //https://www.zabbix.com/documentation/4.0/manual/api/reference/event/acknowledge 223 | params["action"] = 6 224 | 225 | case "3": 226 | //https://www.zabbix.com/documentation/3.4/manual/api/reference/event/acknowledge 227 | params["action"] = 1 228 | 229 | //default: 230 | //https://www.zabbix.com/documentation/1.8/api/event/acknowledge 231 | //https://www.zabbix.com/documentation/2.0/manual/appendix/api/event/acknowledge 232 | 233 | } 234 | 235 | err := zabbix.call( 236 | "event.acknowledge", 237 | params, 238 | &response, 239 | withAuthFlag, 240 | ) 241 | if err != nil { 242 | return err 243 | } 244 | 245 | return nil 246 | } 247 | 248 | func (zabbix *Zabbix) GetTriggers(extend Params) ([]Trigger, error) { 249 | debugln("* retrieving triggers list") 250 | 251 | params := Params{ 252 | "monitored": true, 253 | "selectHosts": []string{"name"}, 254 | "selectGroups": []string{"groupid", "name"}, 255 | "selectLastEvent": "extend", 256 | "selectFunctions": "extend", 257 | "expandExpression": true, 258 | "expandData": true, 259 | "expandDescription": true, 260 | "skipDependent": true, 261 | "preservekeys": true, 262 | } 263 | 264 | for key, value := range extend { 265 | params[key] = value 266 | } 267 | 268 | var response ResponseTriggers 269 | err := zabbix.call("trigger.get", params, &response, withAuthFlag) 270 | if err != nil { 271 | return nil, err 272 | } 273 | 274 | var triggers []Trigger 275 | for _, trigger := range unshuffle(response.Data) { 276 | triggers = append(triggers, trigger.(Trigger)) 277 | } 278 | 279 | return triggers, nil 280 | } 281 | 282 | func (zabbix *Zabbix) GetMaintenances(params Params) ([]Maintenance, error) { 283 | debugln("* retrieving maintenances list") 284 | 285 | var response ResponseMaintenances 286 | err := zabbix.call("maintenance.get", params, &response, withAuthFlag) 287 | if err != nil { 288 | return nil, err 289 | } 290 | 291 | var maintenances []Maintenance 292 | for _, maintenance := range response.Data { 293 | maintenances = append(maintenances, maintenance) 294 | } 295 | 296 | return maintenances, nil 297 | } 298 | 299 | func (zabbix *Zabbix) CreateMaintenance(params Params) (Maintenances, error) { 300 | debugln("* create maintenances list") 301 | 302 | var response ResponseMaintenancesArray 303 | 304 | err := zabbix.call("maintenance.create", params, &response, withAuthFlag) 305 | 306 | return response.Data, err 307 | } 308 | 309 | func (zabbix *Zabbix) UpdateMaintenance(params Params) (Maintenances, error) { 310 | debugln("* update maintenances list") 311 | 312 | var response ResponseMaintenancesArray 313 | 314 | err := zabbix.call("maintenance.update", params, &response, withAuthFlag) 315 | 316 | return response.Data, err 317 | } 318 | 319 | func (zabbix *Zabbix) RemoveMaintenance(params interface{}) (Maintenances, error) { 320 | debugln("* remove maintenances") 321 | 322 | var response ResponseMaintenancesArray 323 | 324 | err := zabbix.call("maintenance.delete", params, &response, withAuthFlag) 325 | 326 | return response.Data, err 327 | } 328 | 329 | func (zabbix *Zabbix) GetItems(params Params) ([]Item, error) { 330 | debugln("* retrieving items list") 331 | 332 | var response ResponseItems 333 | err := zabbix.call("item.get", params, &response, withAuthFlag) 334 | if err != nil { 335 | return nil, err 336 | } 337 | 338 | return response.Data, nil 339 | } 340 | 341 | func (zabbix *Zabbix) GetHTTPTests(params Params) ([]HTTPTest, error) { 342 | debugln("* retrieving web scenarios list") 343 | 344 | var response ResponseHTTPTests 345 | err := zabbix.call("httptest.get", params, &response, withAuthFlag) 346 | if err != nil { 347 | return nil, err 348 | } 349 | 350 | return response.Data, nil 351 | } 352 | 353 | func (zabbix *Zabbix) GetUsersGroups(params Params) ([]UserGroup, error) { 354 | debugln("* retrieving users groups list") 355 | 356 | var response ResponseUserGroup 357 | err := zabbix.call("usergroup.get", params, &response, withAuthFlag) 358 | if err != nil { 359 | return nil, err 360 | } 361 | 362 | return response.Data, nil 363 | } 364 | 365 | func (zabbix *Zabbix) AddUserToGroups( 366 | groups []UserGroup, 367 | user User, 368 | ) error { 369 | for _, group := range groups { 370 | identifiers := []string{user.ID} 371 | 372 | for _, groupUser := range group.Users { 373 | identifiers = append(identifiers, groupUser.ID) 374 | } 375 | 376 | debugf("* adding user %s to group %s", user.Alias, group.Name) 377 | 378 | err := zabbix.call( 379 | "usergroup.update", 380 | Params{"usrgrpid": group.ID, "userids": identifiers}, 381 | &ResponseRaw{}, 382 | withAuthFlag, 383 | ) 384 | if err != nil { 385 | return karma.Format( 386 | err, 387 | "can't update usergroup %s", group.Name, 388 | ) 389 | } 390 | } 391 | 392 | return nil 393 | } 394 | 395 | func (zabbix *Zabbix) RemoveUserFromGroups( 396 | groups []UserGroup, 397 | user User, 398 | ) error { 399 | for _, group := range groups { 400 | identifiers := []string{} 401 | 402 | for _, groupUser := range group.Users { 403 | if groupUser.ID == user.ID { 404 | continue 405 | } 406 | 407 | identifiers = append(identifiers, groupUser.ID) 408 | } 409 | 410 | debugf("* removing user %s from group %s", user.Alias, group.Name) 411 | 412 | err := zabbix.call( 413 | "usergroup.update", 414 | Params{"usrgrpid": group.ID, "userids": identifiers}, 415 | &ResponseRaw{}, 416 | withAuthFlag, 417 | ) 418 | if err != nil { 419 | return karma.Format( 420 | err, 421 | "can't update usergroup %s", group.Name, 422 | ) 423 | } 424 | } 425 | 426 | return nil 427 | } 428 | 429 | func (zabbix *Zabbix) GetUsers(params Params) ([]User, error) { 430 | debugln("* retrieving users list") 431 | 432 | var response ResponseUsers 433 | err := zabbix.call("user.get", params, &response, withAuthFlag) 434 | if err != nil { 435 | return nil, err 436 | } 437 | 438 | return response.Data, nil 439 | } 440 | 441 | func (zabbix *Zabbix) GetHosts(params Params) ([]Host, error) { 442 | debugf("* retrieving hosts list") 443 | 444 | var response ResponseHosts 445 | err := zabbix.call("host.get", params, &response, withAuthFlag) 446 | if err != nil { 447 | return nil, err 448 | } 449 | 450 | return response.Data, nil 451 | } 452 | 453 | func (zabbix *Zabbix) RemoveHosts(params interface{}) (Hosts, error) { 454 | debugf("* remove hosts list") 455 | 456 | var response ResponseHostsArray 457 | err := zabbix.call("host.delete", params, &response, withAuthFlag) 458 | 459 | return response.Data, err 460 | } 461 | 462 | func (zabbix *Zabbix) GetGroups(params Params) ([]Group, error) { 463 | debugf("* retrieving groups list") 464 | 465 | var response ResponseGroups 466 | err := zabbix.call("hostgroup.get", params, &response, withAuthFlag) 467 | 468 | return response.Data, err 469 | } 470 | 471 | func (zabbix *Zabbix) GetGraphURL(identifier string) string { 472 | return zabbix.getGraphURL([]string{identifier}, "showgraph", "0") 473 | } 474 | 475 | func (zabbix *Zabbix) GetNormalGraphURL(identifiers []string) string { 476 | return zabbix.getGraphURL(identifiers, "batchgraph", "0") 477 | } 478 | 479 | func (zabbix *Zabbix) GetStackedGraphURL(identifiers []string) string { 480 | return zabbix.getGraphURL(identifiers, "batchgraph", "1") 481 | } 482 | 483 | func (zabbix *Zabbix) getGraphURL( 484 | identifiers []string, 485 | action string, 486 | graphType string, 487 | ) string { 488 | encodedIdentifiers := []string{} 489 | 490 | for _, identifier := range identifiers { 491 | encodedIdentifiers = append( 492 | encodedIdentifiers, 493 | "itemids%5B%5D="+identifier, 494 | ) 495 | } 496 | 497 | return zabbix.basicURL + fmt.Sprintf( 498 | "/history.php?action=%s&graphtype=%s&%s", 499 | action, 500 | graphType, 501 | strings.Join(encodedIdentifiers, "&"), 502 | ) 503 | } 504 | 505 | func (zabbix *Zabbix) GetHistory(extend Params) ([]History, error) { 506 | debugf("* retrieving items history") 507 | 508 | params := Params{ 509 | "output": "extend", 510 | "sortfield": "clock", 511 | "sortorder": "DESC", 512 | } 513 | 514 | for key, value := range extend { 515 | params[key] = value 516 | } 517 | 518 | var response ResponseHistory 519 | err := zabbix.call("history.get", params, &response, withAuthFlag) 520 | if err != nil { 521 | return nil, err 522 | } 523 | 524 | return response.Data, nil 525 | } 526 | 527 | func (zabbix *Zabbix) call( 528 | method string, params interface{}, response Response, authFlag bool, 529 | ) error { 530 | debugf("~> %s", method) 531 | debugParams(params) 532 | 533 | request := Request{ 534 | RPC: "2.0", 535 | Method: method, 536 | Params: params, 537 | ID: atomic.AddInt64(&zabbix.requestID, 1), 538 | } 539 | 540 | if authFlag { 541 | request.Auth = zabbix.session 542 | } 543 | 544 | buffer, err := json.Marshal(request) 545 | if err != nil { 546 | return karma.Format( 547 | err, 548 | "can't encode request to JSON", 549 | ) 550 | } 551 | 552 | payload, err := http.NewRequest( 553 | "POST", 554 | zabbix.apiURL, 555 | bytes.NewReader(buffer), 556 | ) 557 | if err != nil { 558 | return karma.Format( 559 | err, 560 | "can't create http request", 561 | ) 562 | } 563 | 564 | payload.ContentLength = int64(len(buffer)) 565 | payload.Header.Add("Content-Type", "application/json-rpc") 566 | payload.Header.Add("User-Agent", "zabbixctl") 567 | 568 | resource, err := zabbix.client.Do(payload) 569 | if err != nil { 570 | return karma.Format( 571 | err, 572 | "http request to zabbix api failed", 573 | ) 574 | } 575 | 576 | body, err := ioutil.ReadAll(resource.Body) 577 | if err != nil { 578 | return karma.Format( 579 | err, 580 | "can't read zabbix api response body", 581 | ) 582 | } 583 | 584 | debugf("<~ %s", resource.Status) 585 | 586 | if traceMode { 587 | var tracing bytes.Buffer 588 | err = json.Indent(&tracing, body, "", " ") 589 | if err != nil { 590 | return karma.Format(err, "can't indent api response body") 591 | } 592 | tracef("<~ %s", tracing.String()) 593 | } 594 | 595 | err = json.Unmarshal(body, response) 596 | if err != nil { 597 | // There is can be bullshit case when zabbix sends empty `result` 598 | // array and json.Unmarshal triggers the error with message about 599 | // failed type conversion to map[]. 600 | // 601 | // So, we must check that err is not this case. 602 | var raw ResponseRaw 603 | rawErr := json.Unmarshal(body, &raw) 604 | if rawErr != nil { 605 | // return original error 606 | return err 607 | } 608 | 609 | if result, ok := raw.Result.([]interface{}); ok && len(result) == 0 { 610 | return nil 611 | } 612 | 613 | return err 614 | } 615 | 616 | err = response.Error() 617 | if err != nil { 618 | return karma.Format( 619 | err, 620 | "zabbix returned error while working with api method %s", 621 | method, 622 | ) 623 | } 624 | 625 | return nil 626 | } 627 | 628 | func debugParams(params interface{}, prefix ...string) { 629 | 630 | switch params.(type) { 631 | case Params: 632 | p, _ := params.(Params) 633 | for key, value := range p { 634 | if valueParams, ok := value.(Params); ok { 635 | debugParams(valueParams, append(prefix, key)...) 636 | continue 637 | } 638 | 639 | if key == "password" { 640 | value = "**********" 641 | } 642 | 643 | debugf( 644 | "** %s%s: %v", 645 | strings.Join(append(prefix, ""), "."), 646 | key, value, 647 | ) 648 | } 649 | case interface{}: 650 | if p, ok := params.([]string); ok { 651 | for _, value := range p { 652 | debugf("** %v", value) 653 | } 654 | } 655 | } 656 | } 657 | 658 | func unshuffle(target interface{}) []interface{} { 659 | tears := reflect.ValueOf(target) 660 | 661 | var values []interface{} 662 | for _, key := range tears.MapKeys() { 663 | values = append( 664 | values, 665 | tears.MapIndex(key).Interface(), 666 | ) 667 | } 668 | 669 | return values 670 | } 671 | -------------------------------------------------------------------------------- /zabbix_issue18_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const ( 14 | issue18Data = ` 15 | { 16 | "jsonrpc": "2.0", 17 | "result": [ 18 | { 19 | "itemid": "28494", 20 | "type": "2", 21 | "snmp_community": "", 22 | "snmp_oid": "", 23 | "hostid": "10084", 24 | "name": "Number of csvs today", 25 | "key_": "csv.today.count", 26 | "delay": "0", 27 | "history": "90d", 28 | "trends": "365d", 29 | "status": "0", 30 | "value_type": "3", 31 | "trapper_hosts": "", 32 | "units": "", 33 | "snmpv3_securityname": "", 34 | "snmpv3_securitylevel": "0", 35 | "snmpv3_authpassphrase": "", 36 | "snmpv3_privpassphrase": "", 37 | "formula": "", 38 | "error": "", 39 | "lastlogsize": "0", 40 | "logtimefmt": "", 41 | "templateid": "28381", 42 | "valuemapid": "0", 43 | "params": "", 44 | "ipmi_sensor": "", 45 | "authtype": "0", 46 | "username": "", 47 | "password": "", 48 | "publickey": "", 49 | "privatekey": "", 50 | "mtime": "0", 51 | "flags": "0", 52 | "interfaceid": "0", 53 | "port": "", 54 | "description": "", 55 | "inventory_link": "0", 56 | "lifetime": "30d", 57 | "snmpv3_authprotocol": "0", 58 | "snmpv3_privprotocol": "0", 59 | "state": "0", 60 | "snmpv3_contextname": "", 61 | "evaltype": "0", 62 | "jmx_endpoint": "", 63 | "master_itemid": "0", 64 | "timeout": "3s", 65 | "url": "", 66 | "query_fields": [], 67 | "posts": "", 68 | "status_codes": "200", 69 | "follow_redirects": "1", 70 | "post_type": "0", 71 | "http_proxy": "", 72 | "headers": [], 73 | "retrieve_mode": "0", 74 | "request_method": "1", 75 | "output_format": "0", 76 | "ssl_cert_file": "", 77 | "ssl_key_file": "", 78 | "ssl_key_password": "", 79 | "verify_peer": "0", 80 | "verify_host": "0", 81 | "allow_traps": "0", 82 | "lastclock": 1548924066, 83 | "lastns": 388228365, 84 | "lastvalue": "6", 85 | "prevvalue": "6" 86 | }, 87 | { 88 | "itemid": "28461", 89 | "type": "0", 90 | "snmp_community": "", 91 | "snmp_oid": "", 92 | "hostid": "10084", 93 | "name": "Indices count", 94 | "key_": "elastizabbix[cluster,indices.count]", 95 | "delay": "60", 96 | "history": "7d", 97 | "trends": "365d", 98 | "status": "0", 99 | "value_type": "3", 100 | "trapper_hosts": "", 101 | "units": "", 102 | "snmpv3_securityname": "", 103 | "snmpv3_securitylevel": "0", 104 | "snmpv3_authpassphrase": "", 105 | "snmpv3_privpassphrase": "", 106 | "formula": "", 107 | "error": "Unsupported item key.", 108 | "lastlogsize": "0", 109 | "logtimefmt": "", 110 | "templateid": "28351", 111 | "valuemapid": "0", 112 | "params": "", 113 | "ipmi_sensor": "", 114 | "authtype": "0", 115 | "username": "", 116 | "password": "", 117 | "publickey": "", 118 | "privatekey": "", 119 | "mtime": "0", 120 | "flags": "0", 121 | "interfaceid": "1", 122 | "port": "", 123 | "description": "", 124 | "inventory_link": "0", 125 | "lifetime": "30d", 126 | "snmpv3_authprotocol": "0", 127 | "snmpv3_privprotocol": "0", 128 | "state": "1", 129 | "snmpv3_contextname": "", 130 | "evaltype": "0", 131 | "jmx_endpoint": "", 132 | "master_itemid": "0", 133 | "timeout": "3s", 134 | "url": "", 135 | "query_fields": [], 136 | "posts": "", 137 | "status_codes": "200", 138 | "follow_redirects": "1", 139 | "post_type": "0", 140 | "http_proxy": "", 141 | "headers": [], 142 | "retrieve_mode": "0", 143 | "request_method": "1", 144 | "output_format": "0", 145 | "ssl_cert_file": "", 146 | "ssl_key_file": "", 147 | "ssl_key_password": "", 148 | "verify_peer": "0", 149 | "verify_host": "0", 150 | "allow_traps": "0", 151 | "lastclock": "0", 152 | "lastns": "0", 153 | "lastvalue": "0", 154 | "prevvalue": "0" 155 | } 156 | ] 157 | } 158 | ` 159 | ) 160 | 161 | func TestIssue18(t *testing.T) { 162 | test := assert.New(t) 163 | 164 | testserver := httptest.NewServer(http.HandlerFunc( 165 | func(w http.ResponseWriter, r *http.Request) { 166 | fmt.Fprint(w, issue18Data) 167 | }, 168 | )) 169 | defer testserver.Close() 170 | 171 | zabbix := &Zabbix{} 172 | zabbix.client = testserver.Client() 173 | zabbix.apiURL = testserver.URL 174 | 175 | items, err := zabbix.GetItems(Params{"hostids": []string{"10084"}}) 176 | test.NoError(err) 177 | test.Len(items, 2) 178 | 179 | test.Equal("1548924066", items[0].getLastClock()) 180 | test.Equal("0", items[1].getLastClock()) 181 | 182 | test.Equal(time.Unix(1548924066, 0).Format(TimeFormat), items[0].DateTime()) 183 | test.Equal("-", items[1].DateTime()) 184 | } 185 | --------------------------------------------------------------------------------