├── .gitignore ├── LICENSE ├── README.md ├── api_version.go ├── api_version_test.go ├── client.go ├── cmd ├── example │ └── main.go └── zbx │ ├── README.md │ ├── client.go │ ├── field_types.go │ ├── host.go │ ├── hostgroup.go │ ├── item.go │ ├── main.go │ ├── maintenance.go │ ├── trigger.go │ └── tui.go ├── doc.go ├── go.mod ├── go.sum ├── internal ├── errlog │ └── errlog.go ├── outlog │ ├── outlog.go │ └── outlog_test.go ├── rpc │ ├── client.go │ ├── host.go │ ├── hostgroup.go │ ├── item.go │ ├── maintenance.go │ └── trigger.go └── slicex │ ├── slicex.go │ └── slicex_test.go └── tests └── setup_zabbix_postgresql_lxd.md /.gitignore: -------------------------------------------------------------------------------- 1 | zbx.linux_amd64.tar.gz 2 | cmd/zbx/zbx 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Hiroaki Nakamura 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-zabbix 2 | ========= 3 | 4 | A minimal Zabbix API client for Go. 5 | See https://www.zabbix.com/documentation/6.0/en/manual/api for the Zabbix API. 6 | 7 | This library is in very early stage of development. The API is not frozen yet. 8 | 9 | ## Install 10 | 11 | You can download a static-linked executable for Linux from 12 | https://github.com/hnakamur/go-zabbix/releases. 13 | 14 | Or you can install from the source with the following command: 15 | 16 | ``` 17 | go install -trimpath -tags netgo github.com/hnakamur/go-zabbix/cmd/zbx@latest 18 | ``` 19 | 20 | ## License 21 | MIT 22 | -------------------------------------------------------------------------------- /api_version.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type APIVersion struct { 12 | Major int 13 | Minor int 14 | Patch int 15 | PreRelType PreRelType 16 | PreRelVer int 17 | } 18 | 19 | var ErrInvalidZabbixVer = errors.New("invalid Zabbix version") 20 | 21 | var versionRegex = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:(alpha|beta|rc)(\d+))?$`) 22 | 23 | func MustParseAPIVersion(ver string) APIVersion { 24 | v, err := ParseAPIVersion(ver) 25 | if err != nil { 26 | panic(err) 27 | } 28 | return v 29 | } 30 | 31 | func ParseAPIVersion(ver string) (APIVersion, error) { 32 | var v APIVersion 33 | m := versionRegex.FindStringSubmatch(ver) 34 | if len(m) != 6 { 35 | return v, ErrInvalidZabbixVer 36 | } 37 | 38 | major, err := strconv.Atoi(m[1]) 39 | if err != nil { 40 | return v, ErrInvalidZabbixVer 41 | } 42 | v.Major = major 43 | 44 | minor, err := strconv.Atoi(m[2]) 45 | if err != nil { 46 | return v, ErrInvalidZabbixVer 47 | } 48 | v.Minor = minor 49 | 50 | patch, err := strconv.Atoi(m[3]) 51 | if err != nil { 52 | return v, ErrInvalidZabbixVer 53 | } 54 | v.Patch = patch 55 | 56 | if m[4] != "" { 57 | switch m[4] { 58 | case "alpha": 59 | v.PreRelType = Alpha 60 | case "beta": 61 | v.PreRelType = Beta 62 | case "rc": 63 | v.PreRelType = RC 64 | } 65 | 66 | preReleaseVer, err := strconv.Atoi(m[5]) 67 | if err != nil { 68 | return v, ErrInvalidZabbixVer 69 | } 70 | v.PreRelVer = preReleaseVer 71 | } 72 | return v, nil 73 | } 74 | 75 | func (v APIVersion) String() string { 76 | var b strings.Builder 77 | fmt.Fprintf(&b, "%d.%d.%d", v.Major, v.Minor, v.Patch) 78 | if v.PreRelType != Release { 79 | fmt.Fprintf(&b, "%s%d", v.PreRelType, v.PreRelVer) 80 | } 81 | return b.String() 82 | } 83 | 84 | func (v APIVersion) Compare(w APIVersion) int { 85 | if v.Major < w.Major { 86 | return -1 87 | } 88 | if v.Major > w.Major { 89 | return 1 90 | } 91 | 92 | if v.Minor < w.Minor { 93 | return -1 94 | } 95 | if v.Minor > w.Minor { 96 | return 1 97 | } 98 | 99 | if v.Patch < w.Patch { 100 | return -1 101 | } 102 | if v.Patch > w.Patch { 103 | return 1 104 | } 105 | 106 | if v.PreRelType < w.PreRelType { 107 | return -1 108 | } 109 | if v.PreRelType > w.PreRelType { 110 | return 1 111 | } 112 | 113 | if v.PreRelVer < w.PreRelVer { 114 | return -1 115 | } 116 | if v.PreRelVer > w.PreRelVer { 117 | return 1 118 | } 119 | 120 | return 0 121 | } 122 | 123 | type PreRelType int 124 | 125 | const ( 126 | Alpha PreRelType = iota - 3 127 | Beta 128 | RC // Release Candidate 129 | Release 130 | ) 131 | 132 | func (t PreRelType) String() string { 133 | switch t { 134 | case Alpha: 135 | return "alpha" 136 | case Beta: 137 | return "beta" 138 | case RC: 139 | return "rc" 140 | default: 141 | return "" 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /api_version_test.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import "testing" 4 | 5 | func TestParseAPIVersion(t *testing.T) { 6 | testCases := []string{ 7 | "7.0.0alpha2", 8 | "6.4.4rc1", 9 | "6.4.3", 10 | "6.4.0beta6", 11 | } 12 | for _, c := range testCases { 13 | ver, err := ParseAPIVersion(c) 14 | if err != nil { 15 | t.Errorf("parse failed: input=%s, err=%v", c, err) 16 | } 17 | if got, want := ver.String(), c; got != want { 18 | t.Errorf("formatted version mismatch with input, got=%s, want=%s", got, want) 19 | } 20 | } 21 | } 22 | 23 | func TestAPIVersionCompare(t *testing.T) { 24 | testCases := []struct { 25 | v string 26 | w string 27 | want int 28 | }{ 29 | {v: "7.0.0", w: "7.0.0alpha2", want: 1}, 30 | {v: "7.0.0alpha1", w: "7.0.0alpha2", want: -1}, 31 | {v: "7.0.0rc1", w: "7.0.0alpha2", want: 1}, 32 | {v: "7.0.0rc1", w: "7.0.0beta2", want: 1}, 33 | {v: "7.0.0rc1", w: "7.0.0rc1", want: 0}, 34 | {v: "7.0.0", w: "7.0.0", want: 0}, 35 | {v: "7.0.0rc1", w: "6.0.0rc1", want: 1}, 36 | {v: "6.4.0rc1", w: "6.0.0rc1", want: 1}, 37 | {v: "6.2.7", w: "6.0.13", want: 1}, 38 | {v: "6.2.5", w: "6.2.6rc1", want: -1}, 39 | } 40 | for _, c := range testCases { 41 | v, err := ParseAPIVersion(c.v) 42 | if err != nil { 43 | t.Errorf("parse failed: input=%s, err=%v", c.v, err) 44 | } 45 | w, err := ParseAPIVersion(c.w) 46 | if err != nil { 47 | t.Errorf("parse failed: input=%s, err=%v", c.w, err) 48 | } 49 | if got, want := v.Compare(w), c.want; got != want { 50 | t.Errorf("compare result mismatch, v=%s, want=%s, got=%d, want=%d", c.v, c.w, got, want) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net/http" 12 | "net/url" 13 | "sync" 14 | "sync/atomic" 15 | ) 16 | 17 | const contentType = "application/json-rpc" 18 | const jsonrpcVersion = "2.0" 19 | const jsonrpcFilename = "api_jsonrpc.php" 20 | const loginMethod = "user.login" 21 | 22 | // Client represents a client for Zabbix JSON-RPC API. 23 | type Client struct { 24 | httpClient *http.Client 25 | apiURL string 26 | host string 27 | debug bool 28 | 29 | requestID atomic.Uint64 30 | auth string 31 | 32 | apiVerOnce sync.Once 33 | apiVer APIVersion 34 | } 35 | 36 | type ClientOpt func(c *Client) 37 | 38 | func WithHost(host string) ClientOpt { 39 | return func(c *Client) { 40 | c.host = host 41 | } 42 | } 43 | 44 | func WithHTTPClient(httpClient *http.Client) ClientOpt { 45 | return func(c *Client) { 46 | c.httpClient = httpClient 47 | } 48 | } 49 | 50 | func WithDebug(debug bool) ClientOpt { 51 | return func(c *Client) { 52 | c.debug = debug 53 | } 54 | } 55 | 56 | func WithAPIToken(token string) ClientOpt { 57 | return func(c *Client) { 58 | c.auth = token 59 | } 60 | } 61 | 62 | // NewClient creates a client for Zabbix JSON-RPC API. 63 | // zabbixURL is something like http://example.com/zabbix/, and not like 64 | // http://example.com/zabbix/index.php. 65 | func NewClient(zabbixURL string, opts ...ClientOpt) (*Client, error) { 66 | c := &Client{} 67 | for _, opt := range opts { 68 | opt(c) 69 | } 70 | 71 | u, err := url.Parse(zabbixURL) 72 | if err != nil { 73 | return nil, err 74 | } 75 | c.apiURL = u.JoinPath(jsonrpcFilename).String() 76 | if c.httpClient == nil { 77 | c.httpClient = http.DefaultClient 78 | } 79 | return c, nil 80 | } 81 | 82 | type loginParams struct { 83 | Username string `json:"username"` 84 | Password string `json:"password"` 85 | } 86 | 87 | type oldLoginParams struct { 88 | User string `json:"user"` 89 | Password string `json:"password"` 90 | } 91 | 92 | // Login sends a "user.login" request to the server. 93 | // If the login is successful, the session ID will be returned from the server. 94 | // It is kept in the Client and it will be set to requests created with Call 95 | // method called after this call of Login method. 96 | func (c *Client) Login(ctx context.Context, username, password string) error { 97 | apiVer, err := c.APIVersion(ctx) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | var params any 103 | if apiVer.Compare(APIVersion{Major: 6, Minor: 4, Patch: 0, 104 | PreRelType: Beta, PreRelVer: 5}) >= 0 { 105 | // https://support.zabbix.com/browse/ZBXNEXT-8085 106 | params = &loginParams{Username: username, Password: password} 107 | } else { 108 | params = &oldLoginParams{User: username, Password: password} 109 | } 110 | 111 | var auth string 112 | if err := c.Call(ctx, loginMethod, params, &auth); err != nil { 113 | return err 114 | } 115 | if auth == "" { 116 | return errors.New("user.login API should have return a valid (non-empty) auth") 117 | } 118 | 119 | c.auth = auth 120 | return nil 121 | } 122 | 123 | // Call sends a JSON-RPC request to the server. 124 | // The caller of this method must pass a pointer to the appropriate type of result. 125 | // The appropriate type is different for method and params. 126 | func (c *Client) Call(ctx context.Context, method string, params, result any) error { 127 | type responseCommon struct { 128 | Jsonrpc string `json:"jsonrpc"` 129 | Error *APIError `json:"error"` 130 | ID uint64 `json:"id"` 131 | } 132 | 133 | var res struct { 134 | responseCommon 135 | Result any `json:"result"` 136 | } 137 | res.Result = result 138 | req, err := c.internalCall(ctx, method, params, &res) 139 | if c.debug { 140 | c.debugRPCCall(req, err) 141 | } 142 | if err != nil { 143 | return &CallError{ 144 | ID: req.ID, 145 | Method: req.Method, 146 | Params: req.Params, 147 | Err: err, 148 | } 149 | } 150 | if res.Error != nil { 151 | return &CallError{ 152 | ID: req.ID, 153 | Method: req.Method, 154 | Params: req.Params, 155 | Err: res.Error, 156 | } 157 | } 158 | if res.ID != req.ID { 159 | return &CallError{ 160 | ID: req.ID, 161 | Method: req.Method, 162 | Params: req.Params, 163 | Err: fmt.Errorf("response ID (%d) does not match resquest ID (%d)", 164 | res.ID, req.ID), 165 | } 166 | } 167 | return nil 168 | } 169 | 170 | const hiddenSecretForLog = "(secret)" 171 | 172 | func (c *Client) debugRPCCall(req *rpcRequest, err error) { 173 | req2 := *req 174 | if req2.Method == loginMethod { 175 | if p, ok := req2.Params.(*loginParams); ok { 176 | req2.Params = &loginParams{ 177 | Username: p.Username, 178 | Password: hiddenSecretForLog, 179 | } 180 | } else if p, ok := req2.Params.(*oldLoginParams); ok { 181 | req2.Params = &oldLoginParams{ 182 | User: p.User, 183 | Password: hiddenSecretForLog, 184 | } 185 | } 186 | } 187 | if req2.Auth != nil && req2.Auth != "" { 188 | req2.Auth = hiddenSecretForLog 189 | } 190 | req2Bytes, err2 := json.Marshal(req2) 191 | if err2 != nil { 192 | panic(err2) 193 | } 194 | log.Printf("DEBUG request=%s, response=%s, status=%d, err=%v", 195 | string(req2Bytes), string(req.respBodyBytes), req.statusCode, err) 196 | } 197 | 198 | // CallError is the error type returned by Client.Call method. 199 | // The concret type of the Err field is APIError or other error. 200 | type CallError struct { 201 | ID uint64 `json:"id"` 202 | Method string `json:"method"` 203 | Params any `json:"params"` 204 | Err error `json:"error"` 205 | } 206 | 207 | var _ error = (*CallError)(nil) 208 | 209 | func (e *CallError) Error() string { 210 | data, err := json.Marshal(e) 211 | if err != nil { 212 | panic(err) 213 | } 214 | return string(data) 215 | } 216 | 217 | func (e *CallError) Unwrap() error { 218 | return e.Err 219 | } 220 | 221 | type ErrorCode int 222 | 223 | const ( 224 | // ErrorCodeNone represents the error which is not an error object from 225 | // Zabbix JSON-RPC API but some other error occurred in the client side. 226 | ErrorCodeNone ErrorCode = 0 227 | 228 | ErrorCodeParse ErrorCode = -32700 229 | ErrorCodeInvalidRequest ErrorCode = -32600 230 | ErrorCodeMethodNotFound ErrorCode = -32601 231 | ErrorCodeInvalidParams ErrorCode = -32602 232 | ErrorCodeInternal ErrorCode = -32603 233 | ErrorCodeApplication ErrorCode = -32500 234 | ErrorCodeSystem ErrorCode = -32400 235 | ErrorCodeTransport ErrorCode = -32300 236 | ) 237 | 238 | // GetErrorCode returns the Code field if err or a unwrapped error is APIError 239 | // or ErrorCodeNone otherwise. 240 | func GetErrorCode(err error) ErrorCode { 241 | var apiErr *APIError 242 | if errors.As(err, &apiErr) { 243 | return apiErr.Code 244 | } 245 | return ErrorCodeNone 246 | } 247 | 248 | // APIError is an error object in responses from Zabbix JSON-RPC API. 249 | // https://www.zabbix.com/documentation/current/en/manual/api#error-handling 250 | type APIError struct { 251 | Code ErrorCode `json:"code"` 252 | Message string `json:"message"` 253 | Data string `json:"data"` 254 | } 255 | 256 | var _ error = (*APIError)(nil) 257 | 258 | func (e *APIError) Error() string { 259 | data, err := json.Marshal(e) 260 | if err != nil { 261 | panic(err) 262 | } 263 | return string(data) 264 | } 265 | 266 | // APIVersion returns APIVersion. 267 | // For the first call of this method, a request is sent to the server and 268 | // the result will be cached. 269 | // For subsequent call of this method, it returns the cached value. 270 | func (c *Client) APIVersion(ctx context.Context) (APIVersion, error) { 271 | var err error 272 | c.apiVerOnce.Do(func() { 273 | c.apiVer, err = c.getAPIVersion(ctx) 274 | }) 275 | if err != nil { 276 | return c.apiVer, err 277 | } 278 | return c.apiVer, nil 279 | } 280 | 281 | func (c *Client) getAPIVersion(ctx context.Context) (APIVersion, error) { 282 | var v APIVersion 283 | var ver string 284 | if err := c.Call(ctx, "apiinfo.version", []string{}, &ver); err != nil { 285 | return v, err 286 | } 287 | v, err := ParseAPIVersion(ver) 288 | if err != nil { 289 | return v, err 290 | } 291 | return v, nil 292 | } 293 | 294 | func (c *Client) internalCall(ctx context.Context, method string, params, result any) (req *rpcRequest, err error) { 295 | req = c.newRPCRequest(method, params) 296 | httpReq, err := c.newHTTPRequestWithContext(ctx, req) 297 | if err != nil { 298 | return req, err 299 | } 300 | httpRes, err := c.httpClient.Do(httpReq) 301 | if err != nil { 302 | return req, err 303 | } 304 | defer httpRes.Body.Close() 305 | 306 | bodyBytes, err := io.ReadAll(httpRes.Body) 307 | if err != nil { 308 | return req, err 309 | } 310 | req.statusCode = httpRes.StatusCode 311 | req.respBodyBytes = bodyBytes 312 | if err := json.Unmarshal(bodyBytes, result); err != nil { 313 | return req, err 314 | } 315 | return req, nil 316 | } 317 | 318 | type rpcRequest struct { 319 | Jsonrpc string `json:"jsonrpc"` 320 | Method string `json:"method"` 321 | Params any `json:"params"` 322 | ID uint64 `json:"id"` 323 | Auth any `json:"auth,omitempty"` 324 | 325 | statusCode int `json:"-"` 326 | respBodyBytes []byte `json:"-"` 327 | } 328 | 329 | func (c *Client) newRPCRequest(method string, params any) *rpcRequest { 330 | reqID := c.requestID.Add(1) 331 | 332 | r := &rpcRequest{ 333 | Jsonrpc: jsonrpcVersion, 334 | Method: method, 335 | Params: params, 336 | ID: reqID, 337 | } 338 | if c.auth != "" && method != loginMethod { 339 | r.Auth = c.auth 340 | } 341 | return r 342 | } 343 | 344 | func (c *Client) newHTTPRequestWithContext(ctx context.Context, r *rpcRequest) (*http.Request, error) { 345 | b, err := json.Marshal(r) 346 | if err != nil { 347 | return nil, err 348 | } 349 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURL, bytes.NewReader(b)) 350 | if err != nil { 351 | return nil, err 352 | } 353 | if c.host != "" { 354 | req.Host = c.host 355 | } 356 | req.Header.Set("Content-Type", contentType) 357 | return req, nil 358 | } 359 | -------------------------------------------------------------------------------- /cmd/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "os" 8 | "strconv" 9 | 10 | "github.com/hnakamur/go-zabbix" 11 | ) 12 | 13 | type myClient struct { 14 | *zabbix.Client 15 | } 16 | 17 | func (c *myClient) getHostCount(ctx context.Context) (int64, error) { 18 | params := struct { 19 | CountOutput bool `json:"countOutput"` 20 | }{ 21 | CountOutput: true, 22 | } 23 | var countStr string 24 | if err := c.Client.Call(ctx, "host.get", params, &countStr); err != nil { 25 | return 0, err 26 | } 27 | count, err := strconv.ParseInt(countStr, 10, 64) 28 | if err != nil { 29 | return 0, err 30 | } 31 | return count, nil 32 | } 33 | 34 | func main() { 35 | zabbixURL := flag.String("zabbix-url", "", "Zabbix URL (ex. http://example.com/zabbix)") 36 | username := flag.String("username", "", "user name") 37 | host := flag.String("host", "", "host header for selecting virtualhost") 38 | flag.Parse() 39 | 40 | password := os.Getenv("PASSWORD") 41 | if password == "" { 42 | log.Fatal("password must be set with \"PASSWORD\" environment variable") 43 | } 44 | 45 | var opts []zabbix.ClientOpt 46 | if *host != "" { 47 | opts = append(opts, zabbix.WithHost(*host)) 48 | } 49 | c, err := zabbix.NewClient(*zabbixURL, opts...) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | client := myClient{Client: c} 54 | 55 | ctx := context.Background() 56 | if err := client.Login(ctx, *username, password); err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | count, err := client.getHostCount(ctx) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | log.Printf("host count=%d", count) 65 | } 66 | -------------------------------------------------------------------------------- /cmd/zbx/README.md: -------------------------------------------------------------------------------- 1 | # zbx 2 | 3 | A command line tool to create, update, or delete a Zabbix maintenance. 4 | 5 | ## Limitations 6 | 7 | - Tested with Zabbix server version 6.0.16. 8 | - Maintenance problem tags are not supported. 9 | - Only supported `timeperiod_type` is "One time only". 10 | - Only one `timeperiod` is supported (multiple `timeperiod`s are not supported). 11 | 12 | See the following pages for Maintenance object properties and example. 13 | - https://www.zabbix.com/documentation/6.0/en/manual/api/reference/maintenance/object 14 | - https://www.zabbix.com/documentation/6.0/en/manual/api/reference/maintenance/get 15 | 16 | ## How to install 17 | 18 | ### Install binary 19 | 20 | An executable file for Linux amd64 can be downloaded from [releases](https://github.com/hnakamur/go-zabbix/releases). 21 | 22 | ### Install from source 23 | 24 | ``` 25 | go install -trimpath -tags netgo github.com/hnakamur/go-zabbix/cmd/zbx@latest 26 | ``` 27 | 28 | ### Usage 29 | 30 | 1. Set necessary environment variables 31 | ``` 32 | export ZBX_URL='http://zabbix.example.jp/zabbix' 33 | export ZBX_API_TOKEN='api_token_generated_by_zabbix' 34 | ``` 35 | 1. Learn a little about how to use 36 | ``` 37 | zbx help 38 | ``` 39 | -------------------------------------------------------------------------------- /cmd/zbx/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/hnakamur/go-zabbix/internal/rpc" 4 | 5 | type myClient struct { 6 | inner *rpc.Client 7 | } 8 | -------------------------------------------------------------------------------- /cmd/zbx/field_types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | type Timestamp time.Time 9 | 10 | func ParseTimestamp(epochSeconds string) (Timestamp, error) { 11 | ts, err := strconv.ParseInt(epochSeconds, 10, 64) 12 | if err != nil { 13 | return Timestamp{}, err 14 | } 15 | return Timestamp(time.Unix(ts, 0)), nil 16 | } 17 | 18 | func (t Timestamp) String() string { 19 | tt := time.Time(t) 20 | return strconv.FormatInt(tt.Unix(), 10) 21 | } 22 | 23 | type Seconds time.Duration 24 | 25 | func ParseSeconds(seconds string) (Seconds, error) { 26 | s, err := strconv.ParseInt(seconds, 10, 64) 27 | if err != nil { 28 | return 0, err 29 | } 30 | return Seconds(time.Duration(s) * time.Second), nil 31 | } 32 | 33 | func (s Seconds) String() string { 34 | seconds := int64(time.Duration(s) / time.Second) 35 | return strconv.FormatInt(seconds, 10) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/zbx/host.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/hnakamur/go-zabbix/internal/rpc" 8 | "github.com/hnakamur/go-zabbix/internal/slicex" 9 | "golang.org/x/exp/slices" 10 | ) 11 | 12 | type Host struct { 13 | HostID string 14 | Name string 15 | MaintenanceFrom time.Time 16 | MaintenanceStatus MaintenanceStatus 17 | MaintenanceType MaintenanceType 18 | MaintenanceID string 19 | } 20 | 21 | type MaintenanceStatus string 22 | 23 | const ( 24 | MaintenanceStatusNoMaintenance MaintenanceStatus = "0" 25 | MaintenanceStatusInEffect MaintenanceStatus = "1" 26 | ) 27 | 28 | func fromRPCHost(h rpc.Host) (Host, error) { 29 | maintenanceFrom, err := ParseTimestamp(h.MaintenanceFrom) 30 | if err != nil { 31 | return Host{}, err 32 | } 33 | 34 | return Host{ 35 | HostID: h.HostID, 36 | Name: h.Name, 37 | MaintenanceFrom: time.Time(maintenanceFrom), 38 | MaintenanceStatus: MaintenanceStatus(h.MaintenanceStatus), 39 | MaintenanceType: MaintenanceType(h.MaintenanceType), 40 | MaintenanceID: h.MaintenanceID, 41 | }, nil 42 | } 43 | 44 | func toRPCHost(h Host) (rpc.Host, error) { 45 | return rpc.Host{ 46 | HostID: h.HostID, 47 | Name: h.Name, 48 | // Keep empty values for readonly properties 49 | }, nil 50 | } 51 | 52 | func (c *myClient) GetHostsByNamesFullMatch(ctx context.Context, 53 | names []string) ([]Host, error) { 54 | rh, err := c.inner.GetHostsByNamesFullMatch(ctx, names) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return slicex.FailableMap(rh, fromRPCHost) 59 | } 60 | 61 | func (c *myClient) GetHostsByGroupIDs(ctx context.Context, 62 | groupIDs []string) ([]Host, error) { 63 | rh, err := c.inner.GetHostsByGroupIDs(ctx, groupIDs) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return slicex.FailableMap(rh, fromRPCHost) 68 | } 69 | 70 | func (c *myClient) GetHostsByHostIDs(ctx context.Context, 71 | hostIDs []string) ([]Host, error) { 72 | rh, err := c.inner.GetHostsByHostIDs(ctx, hostIDs) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return slicex.FailableMap(rh, fromRPCHost) 77 | } 78 | 79 | func sortHosts(hosts []Host) { 80 | slices.SortFunc(hosts, func(h1, h2 Host) bool { 81 | return h1.Name < h2.Name 82 | }) 83 | } 84 | 85 | func concatHostsDeDup(hosts ...[]Host) []Host { 86 | if hosts == nil { 87 | return nil 88 | } 89 | names := make(map[string]struct{}) 90 | var result []Host 91 | for _, hh := range hosts { 92 | for _, h := range hh { 93 | if _, ok := names[h.Name]; !ok { 94 | result = append(result, h) 95 | names[h.Name] = struct{}{} 96 | } 97 | } 98 | } 99 | return result 100 | } 101 | 102 | type Hosts []Host 103 | 104 | func (hh Hosts) allMaintenanceStatusExpected(expected MaintenanceStatus) bool { 105 | for _, h := range hh { 106 | if h.MaintenanceStatus != expected { 107 | return false 108 | } 109 | } 110 | return true 111 | } 112 | -------------------------------------------------------------------------------- /cmd/zbx/hostgroup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hnakamur/go-zabbix/internal/rpc" 7 | ) 8 | 9 | type HostGroup = rpc.HostGroup 10 | 11 | func (c *myClient) GetHostGroupsByNamesFullMatch(ctx context.Context, 12 | names []string) ([]HostGroup, error) { 13 | return c.inner.GetHostGroupsByNamesFullMatch(ctx, names) 14 | } 15 | 16 | func (c *myClient) GetNestedHostGroupsByAncestorNames(ctx context.Context, 17 | names []string) ([]HostGroup, error) { 18 | return c.inner.GetNestedHostGroupsByAncestorNames(ctx, names) 19 | } 20 | -------------------------------------------------------------------------------- /cmd/zbx/item.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/hnakamur/go-zabbix/internal/rpc" 4 | 5 | type Item = rpc.Item 6 | -------------------------------------------------------------------------------- /cmd/zbx/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/url" 9 | "os" 10 | "runtime/debug" 11 | "strings" 12 | "time" 13 | 14 | "golang.org/x/exp/slices" 15 | 16 | "github.com/hnakamur/go-zabbix" 17 | "github.com/hnakamur/go-zabbix/internal/errlog" 18 | "github.com/hnakamur/go-zabbix/internal/outlog" 19 | "github.com/hnakamur/go-zabbix/internal/rpc" 20 | "github.com/hnakamur/go-zabbix/internal/slicex" 21 | "github.com/urfave/cli/v2" 22 | ) 23 | 24 | const timeFormatRFC3339Minute = "2006-01-02T15:04" 25 | 26 | func main() { 27 | if err := run(os.Args); err != nil { 28 | errlog.Printf("ERROR %s", err) 29 | os.Exit(1) 30 | } 31 | } 32 | 33 | func run(args []string) error { 34 | app := &cli.App{ 35 | Name: "zbx", 36 | Usage: "command line tool for Zabbix", 37 | Version: Version(), 38 | Flags: []cli.Flag{ 39 | &cli.StringFlag{ 40 | Name: "url", 41 | Aliases: []string{"l"}, 42 | Usage: "Zabbix URL (ex. http://example.com/zabbix)", 43 | Required: true, 44 | EnvVars: []string{"ZBX_URL"}, 45 | }, 46 | &cli.StringFlag{ 47 | Name: "virtual-host", 48 | Usage: "virtual host on Zabbix server", 49 | EnvVars: []string{"ZBX_VIRTUAL_HOST"}, 50 | }, 51 | &cli.StringFlag{ 52 | Name: "username", 53 | Aliases: []string{"u"}, 54 | Usage: "login username", 55 | EnvVars: []string{"ZBX_USERNAME"}, 56 | }, 57 | &cli.StringFlag{ 58 | Name: "password", 59 | Aliases: []string{"p"}, 60 | Usage: "login password (shows prompt if both of this and token are empty)", 61 | EnvVars: []string{"ZBX_PASSWORD"}, 62 | }, 63 | &cli.StringFlag{ 64 | Name: "token", 65 | Usage: "Zabbix API token", 66 | EnvVars: []string{"ZBX_API_TOKEN"}, 67 | }, 68 | &cli.BoolFlag{ 69 | Name: "debug", 70 | Usage: "print JSON-RPC requests and responses", 71 | }, 72 | &cli.BoolFlag{ 73 | Name: "dry-run", 74 | Usage: "skip calling APIs to update, delete, or modify", 75 | }, 76 | &cli.GenericFlag{ 77 | Name: "log-flags", 78 | Value: &logFlagsValue{flags: log.LstdFlags}, 79 | Usage: "flags for logger (no prefix it set to empty)", 80 | EnvVars: []string{"ZBX_LOG_FLAGS"}, 81 | }, 82 | }, 83 | Commands: []*cli.Command{ 84 | { 85 | Name: "mainte", 86 | Usage: "create, update, or delete maintenance", 87 | Subcommands: []*cli.Command{ 88 | { 89 | Name: "create", 90 | Usage: "create a maintenance", 91 | Flags: []cli.Flag{ 92 | &cli.StringFlag{ 93 | Name: "name", 94 | Aliases: []string{"n"}, 95 | Required: true, 96 | Usage: "name of maintenance to create", 97 | }, 98 | &cli.StringFlag{ 99 | Name: "desc", 100 | Aliases: []string{"d"}, 101 | Usage: "description of maintenance", 102 | }, 103 | &cli.BoolFlag{ 104 | Name: "include-nested", 105 | Usage: "include hosts in nested host group whose names start with name + \"/\" of groups specified by -group flag", 106 | }, 107 | &cli.StringSliceFlag{ 108 | Name: "group", 109 | Aliases: []string{"g"}, 110 | Usage: "host group names", 111 | }, 112 | &cli.StringSliceFlag{ 113 | Name: "host", 114 | Aliases: []string{"H"}, 115 | Usage: "host names", 116 | }, 117 | &cli.TimestampFlag{ 118 | Name: "active-since", 119 | Layout: timeFormatRFC3339Minute, 120 | Timezone: time.Local, 121 | Usage: `active start time of maintenance (default: same as "--start-date")`, 122 | }, 123 | &cli.TimestampFlag{ 124 | Name: "active-till", 125 | Layout: timeFormatRFC3339Minute, 126 | Timezone: time.Local, 127 | Usage: `active end time of maintenance (default: "--start-date" + "--period")`, 128 | }, 129 | &cli.TimestampFlag{ 130 | Name: "start-date", 131 | Layout: timeFormatRFC3339Minute, 132 | Timezone: time.Local, 133 | Usage: "start time of maintenance (default: now)", 134 | }, 135 | &cli.DurationFlag{ 136 | Name: "period", 137 | Aliases: []string{"p"}, 138 | Required: true, 139 | Usage: "duration of maintenance", 140 | }, 141 | &cli.BoolFlag{ 142 | Name: "wait", 143 | Aliases: []string{"w"}, 144 | Usage: "wait for all hosts to in maintenance effect status", 145 | }, 146 | &cli.DurationFlag{ 147 | Name: "interval", 148 | Value: 30 * time.Second, 149 | Usage: "polling interval", 150 | }, 151 | }, 152 | Action: createMaintenanceAction, 153 | }, 154 | { 155 | Name: "get", 156 | Usage: "get maintenances", 157 | Action: getMaintenancesAction, 158 | }, 159 | { 160 | Name: "update", 161 | Usage: "update a maintenance", 162 | Flags: []cli.Flag{ 163 | &cli.StringFlag{ 164 | Name: "id", 165 | Aliases: []string{"i"}, 166 | Usage: `target maintenance ID (if empty, "--name" is used)`, 167 | }, 168 | &cli.StringFlag{ 169 | Name: "name", 170 | Aliases: []string{"n"}, 171 | Usage: `target maintenance name (used only if "--id" is not set)`, 172 | }, 173 | &cli.StringFlag{ 174 | Name: "new-name", 175 | Usage: `rename maintenance to this name`, 176 | }, 177 | &cli.StringFlag{ 178 | Name: "desc", 179 | Aliases: []string{"d"}, 180 | Usage: "description of maintenance", 181 | }, 182 | &cli.BoolFlag{ 183 | Name: "include-nested", 184 | Usage: "include hosts in nested host group whose names start with name + \"/\" of groups specified by -group flag", 185 | }, 186 | &cli.StringSliceFlag{ 187 | Name: "group", 188 | Aliases: []string{"g"}, 189 | Usage: "host group names (or set empty string just once to clear hostgroups)", 190 | }, 191 | &cli.StringSliceFlag{ 192 | Name: "host", 193 | Aliases: []string{"H"}, 194 | Usage: "host names (or set empty string just once to clear hosts)", 195 | }, 196 | &cli.TimestampFlag{ 197 | Name: "active-since", 198 | Layout: "2006-01-02T15:04", 199 | Timezone: time.Local, 200 | Usage: "active start time of maintenance", 201 | }, 202 | &cli.TimestampFlag{ 203 | Name: "active-till", 204 | Layout: "2006-01-02T15:04", 205 | Timezone: time.Local, 206 | Usage: "active end time of maintenance", 207 | }, 208 | &cli.TimestampFlag{ 209 | Name: "start-date", 210 | Layout: "2006-01-02T15:04", 211 | Timezone: time.Local, 212 | Usage: "start time of maintenance", 213 | }, 214 | &cli.DurationFlag{ 215 | Name: "period", 216 | Aliases: []string{"p"}, 217 | Usage: "duration of maintenance", 218 | }, 219 | &cli.BoolFlag{ 220 | Name: "wait", 221 | Aliases: []string{"w"}, 222 | Usage: "wait for all hosts to in maintenance effect status", 223 | }, 224 | &cli.DurationFlag{ 225 | Name: "interval", 226 | Value: 30 * time.Second, 227 | Usage: "polling interval", 228 | }, 229 | }, 230 | Action: updateMaintenanceAction, 231 | }, 232 | { 233 | Name: "delete", 234 | Usage: "delete maintenance(s)", 235 | Flags: []cli.Flag{ 236 | &cli.StringSliceFlag{ 237 | Name: "id", 238 | Aliases: []string{"i"}, 239 | Usage: `target maintenance(s) ID (can be mixed with "--name"(s))`, 240 | }, 241 | &cli.StringSliceFlag{ 242 | Name: "name", 243 | Aliases: []string{"n"}, 244 | Usage: `target maintenance(s) name (can be mixed with "--id"(s))`, 245 | }, 246 | }, 247 | Action: deleteMaintenanceAction, 248 | }, 249 | { 250 | Name: "status", 251 | Usage: "show host maintenance statuses", 252 | Flags: []cli.Flag{ 253 | &cli.StringFlag{ 254 | Name: "id", 255 | Aliases: []string{"i"}, 256 | Usage: `target maintenance ID (if empty, "--name" is used)`, 257 | }, 258 | &cli.StringFlag{ 259 | Name: "name", 260 | Aliases: []string{"n"}, 261 | Usage: `target maintenance name (used only if "--id" is not set)`, 262 | }, 263 | &cli.BoolFlag{ 264 | Name: "wait", 265 | Aliases: []string{"w"}, 266 | Usage: "wait for all hosts to in maintenance effect status", 267 | }, 268 | &cli.DurationFlag{ 269 | Name: "interval", 270 | Value: 30 * time.Second, 271 | Usage: "polling interval", 272 | }, 273 | }, 274 | Action: showStatusAction, 275 | }, 276 | }, 277 | }, 278 | { 279 | Name: "trigger", 280 | Usage: "disable, enable, or list griggers", 281 | Subcommands: []*cli.Command{ 282 | { 283 | Name: "disable", 284 | Usage: "disable triggers", 285 | Flags: []cli.Flag{ 286 | &cli.StringSliceFlag{ 287 | Name: "group", 288 | Aliases: []string{"g"}, 289 | Usage: "host group names", 290 | }, 291 | &cli.StringSliceFlag{ 292 | Name: "host", 293 | Aliases: []string{"H"}, 294 | Usage: "host names", 295 | }, 296 | &cli.StringSliceFlag{ 297 | Name: "id", 298 | Aliases: []string{"I"}, 299 | Usage: "trigger IDs", 300 | }, 301 | &cli.StringSliceFlag{ 302 | Name: "description", 303 | Aliases: []string{"D"}, 304 | Usage: "trigger descriptions (names)", 305 | }, 306 | }, 307 | Action: disableTriggersAction, 308 | }, 309 | { 310 | Name: "enable", 311 | Usage: "enable triggers", 312 | Flags: []cli.Flag{ 313 | &cli.StringSliceFlag{ 314 | Name: "group", 315 | Aliases: []string{"g"}, 316 | Usage: "host group names", 317 | }, 318 | &cli.StringSliceFlag{ 319 | Name: "host", 320 | Aliases: []string{"H"}, 321 | Usage: "host names", 322 | }, 323 | &cli.StringSliceFlag{ 324 | Name: "id", 325 | Aliases: []string{"I"}, 326 | Usage: "trigger IDs", 327 | }, 328 | &cli.StringSliceFlag{ 329 | Name: "description", 330 | Aliases: []string{"D"}, 331 | Usage: "trigger descriptions (names)", 332 | }, 333 | }, 334 | Action: enableTriggersAction, 335 | }, 336 | { 337 | Name: "get", 338 | Usage: "get triggers", 339 | Flags: []cli.Flag{ 340 | &cli.StringSliceFlag{ 341 | Name: "group", 342 | Aliases: []string{"g"}, 343 | Usage: "host group names", 344 | }, 345 | &cli.StringSliceFlag{ 346 | Name: "host", 347 | Aliases: []string{"H"}, 348 | Usage: "host names", 349 | }, 350 | &cli.StringSliceFlag{ 351 | Name: "id", 352 | Aliases: []string{"I"}, 353 | Usage: "trigger IDs", 354 | }, 355 | &cli.StringSliceFlag{ 356 | Name: "description", 357 | Aliases: []string{"D"}, 358 | Usage: "trigger descriptions (names)", 359 | }, 360 | }, 361 | Action: getTriggersAction, 362 | }, 363 | }, 364 | }, 365 | }, 366 | Before: func(cCtx *cli.Context) error { 367 | logFlags := cCtx.Generic("log-flags").(*logFlagsValue).flags 368 | outlog.SetFlags(logFlags) 369 | outlog.SetOutput(cCtx.App.Writer) 370 | errlog.SetFlags(logFlags) 371 | errlog.SetOutput(cCtx.App.ErrWriter) 372 | return nil 373 | }, 374 | } 375 | 376 | return app.Run(args) 377 | } 378 | 379 | type logFlagsValue struct { 380 | flags int 381 | } 382 | 383 | func (v *logFlagsValue) Set(value string) error { 384 | flags, err := outlog.ParseLogFlags(value) 385 | if err != nil { 386 | return err 387 | } 388 | v.flags = flags 389 | return nil 390 | } 391 | 392 | func (v *logFlagsValue) String() string { 393 | return outlog.LogFlags(v.flags).String() 394 | } 395 | 396 | func createMaintenanceAction(cCtx *cli.Context) error { 397 | hostNames := cCtx.StringSlice("host") 398 | groupNames := cCtx.StringSlice("group") 399 | if len(hostNames) == 0 && len(groupNames) == 0 { 400 | return errors.New(`at least "--host" or "--hostgroup" must be set`) 401 | } 402 | 403 | client, err := newClient(cCtx) 404 | if err != nil { 405 | return err 406 | } 407 | 408 | hostsJustID := []Host{} 409 | if len(hostNames) > 0 { 410 | hosts, err := client.GetHostsByNamesFullMatch(cCtx.Context, hostNames) 411 | if err != nil { 412 | return err 413 | } 414 | hostsJustID = slicex.Map(hosts, func(h Host) Host { 415 | return Host{HostID: h.HostID} 416 | }) 417 | } 418 | 419 | groupsJustID := []HostGroup{} 420 | if len(groupNames) > 0 { 421 | var groups []HostGroup 422 | if cCtx.Bool("include-nested") { 423 | groups, err = client.GetNestedHostGroupsByAncestorNames(cCtx.Context, groupNames) 424 | if err != nil { 425 | return err 426 | } 427 | if cCtx.Bool("debug") { 428 | groupNames := slicex.Map(groups, func(g HostGroup) string { 429 | return g.Name 430 | }) 431 | log.Printf("DEBUG expaneded groups=%s", groupNames) 432 | } 433 | } else { 434 | groups, err = client.GetHostGroupsByNamesFullMatch(cCtx.Context, groupNames) 435 | if err != nil { 436 | return err 437 | } 438 | } 439 | groupsJustID = slicex.Map(groups, func(g HostGroup) HostGroup { 440 | return HostGroup{GroupID: g.GroupID} 441 | }) 442 | } 443 | 444 | period := cCtx.Duration("period") 445 | startDate := cCtx.Timestamp("start-date") 446 | if startDate == nil { 447 | now := time.Now().Truncate(time.Minute) 448 | startDate = &now 449 | } 450 | 451 | activeSince := cCtx.Timestamp("active-since") 452 | if activeSince == nil { 453 | activeSince = startDate 454 | } 455 | 456 | activeTill := cCtx.Timestamp("active-till") 457 | if activeTill == nil { 458 | endDate := startDate.Add(period) 459 | activeTill = &endDate 460 | } 461 | 462 | maintenance := &Maintenance{ 463 | Name: cCtx.String("name"), 464 | ActiveSince: *activeSince, 465 | ActiveTill: *activeTill, 466 | Description: cCtx.String("desc"), 467 | MaintenaceType: MaintenanceTypeWithData, 468 | TagsEvalType: TagsEvalTypeAndOr, 469 | Groups: groupsJustID, 470 | Hosts: hostsJustID, 471 | TimePeriods: []TimePeriod{ 472 | { 473 | Period: period, 474 | TimeperiodType: TimeperiodTypeOnetimeOnly, 475 | StartDate: *startDate, 476 | }, 477 | }, 478 | } 479 | 480 | if cCtx.Bool("dry-run") { 481 | outlog.Printf("INFO skip creating maintenance due to dry run, name: %s", cCtx.String("name")) 482 | return nil 483 | } 484 | if err := client.CreateMaintenance(cCtx.Context, maintenance); err != nil { 485 | return err 486 | } 487 | 488 | u, err := maintenanceURL(cCtx, maintenance.MaintenaceID) 489 | if err != nil { 490 | return err 491 | } 492 | outlog.Printf("INFO created maintenance, url: %s", u.String()) 493 | 494 | if cCtx.Bool("wait") { 495 | if err := waitForMaintenanceInEffect(cCtx, client, maintenance.MaintenaceID); err != nil { 496 | return err 497 | } 498 | } 499 | 500 | return nil 501 | } 502 | 503 | func updateMaintenanceAction(cCtx *cli.Context) error { 504 | client, err := newClient(cCtx) 505 | if err != nil { 506 | return err 507 | } 508 | maintenance, err := getTargetMaintenance(cCtx, client) 509 | if err != nil { 510 | return err 511 | } 512 | if len(maintenance.TimePeriods) != 1 { 513 | return fmt.Errorf("unsupported TimePeriod count: got=%d, want=1", len(maintenance.TimePeriods)) 514 | } 515 | 516 | if hostNames := cCtx.StringSlice("host"); len(hostNames) > 0 { 517 | if len(hostNames) == 1 && hostNames[0] == "" { 518 | maintenance.Hosts = []Host{} 519 | } else { 520 | hosts, err := client.GetHostsByNamesFullMatch(cCtx.Context, hostNames) 521 | if err != nil { 522 | return err 523 | } 524 | maintenance.Hosts = slicex.Map(hosts, func(h Host) Host { 525 | return Host{HostID: h.HostID} 526 | }) 527 | } 528 | } else { 529 | maintenance.Hosts = slicex.Map(maintenance.Hosts, func(h Host) Host { 530 | return Host{HostID: h.HostID} 531 | }) 532 | } 533 | 534 | if groupNames := cCtx.StringSlice("group"); len(groupNames) > 0 { 535 | if len(groupNames) == 1 && groupNames[0] == "" { 536 | maintenance.Groups = []HostGroup{} 537 | } else { 538 | var groups []HostGroup 539 | if cCtx.Bool("include-nested") { 540 | groups, err = client.GetNestedHostGroupsByAncestorNames(cCtx.Context, groupNames) 541 | if err != nil { 542 | return err 543 | } 544 | if cCtx.Bool("debug") { 545 | groupNames := slicex.Map(groups, func(g HostGroup) string { 546 | return g.Name 547 | }) 548 | log.Printf("DEBUG expaneded groups=%s", groupNames) 549 | } 550 | } else { 551 | groups, err = client.GetHostGroupsByNamesFullMatch(cCtx.Context, groupNames) 552 | if err != nil { 553 | return err 554 | } 555 | } 556 | maintenance.Groups = slicex.Map(groups, func(g HostGroup) HostGroup { 557 | return HostGroup{GroupID: g.GroupID} 558 | }) 559 | } 560 | } else { 561 | maintenance.Groups = slicex.Map(maintenance.Groups, func(g HostGroup) HostGroup { 562 | return HostGroup{GroupID: g.GroupID} 563 | }) 564 | } 565 | 566 | if s := cCtx.String("new-name"); s != "" { 567 | maintenance.Name = s 568 | } 569 | if s := cCtx.String("desc"); s != "" { 570 | maintenance.Description = s 571 | } 572 | if t := cCtx.Timestamp("active-since"); t != nil { 573 | maintenance.ActiveSince = *t 574 | } 575 | if t := cCtx.Timestamp("active-till"); t != nil { 576 | maintenance.ActiveTill = *t 577 | } 578 | if t := cCtx.Timestamp("start-date"); t != nil { 579 | maintenance.TimePeriods[0].StartDate = *t 580 | } 581 | if t := cCtx.Timestamp("start-date"); t != nil { 582 | maintenance.TimePeriods[0].StartDate = *t 583 | } 584 | if d := cCtx.Duration("period"); d != 0 { 585 | maintenance.TimePeriods[0].Period = d 586 | } 587 | 588 | if cCtx.Bool("dry-run") { 589 | outlog.Printf("INFO skip updating maintenance due to dry run, name: %s, id: %s", maintenance.Name, maintenance.MaintenaceID) 590 | return nil 591 | } 592 | if err := client.UpdateMaintenance(cCtx.Context, maintenance); err != nil { 593 | return err 594 | } 595 | 596 | u, err := maintenanceURL(cCtx, maintenance.MaintenaceID) 597 | if err != nil { 598 | return err 599 | } 600 | outlog.Printf("INFO updated maintenance, url: %s", u.String()) 601 | 602 | if cCtx.Bool("wait") { 603 | if err := waitForMaintenanceInEffect(cCtx, client, maintenance.MaintenaceID); err != nil { 604 | return err 605 | } 606 | } 607 | 608 | return nil 609 | } 610 | 611 | func getMaintenancesAction(cCtx *cli.Context) error { 612 | client, err := newClient(cCtx) 613 | if err != nil { 614 | return err 615 | } 616 | 617 | maintenances, err := client.GetMaintenances(cCtx.Context) 618 | if err != nil { 619 | return err 620 | } 621 | slices.SortFunc(maintenances, func(a, b Maintenance) bool { 622 | return a.MaintenaceID < b.MaintenaceID 623 | }) 624 | 625 | outlog.Printf("INFO maintenance count: %d", len(maintenances)) 626 | for i, m := range maintenances { 627 | dm := toDisplayMaintenance(m) 628 | resultBytes, err := json.Marshal(dm) 629 | if err != nil { 630 | return err 631 | } 632 | outlog.Printf("INFO maintenance i=%d, %s", i, string(resultBytes)) 633 | } 634 | return nil 635 | } 636 | 637 | func deleteMaintenanceAction(cCtx *cli.Context) error { 638 | ids := cCtx.StringSlice("id") 639 | names := cCtx.StringSlice("name") 640 | if len(ids) == 0 && len(names) == 0 { 641 | return errors.New(`at least one "--name" or "--id" must be set`) 642 | } 643 | if slicex.ContainsDup(ids) { 644 | return errors.New(`duplicated IDs are set with "--id"`) 645 | } 646 | if slicex.ContainsDup(names) { 647 | return errors.New(`duplicated names are set with "--name"`) 648 | } 649 | 650 | client, err := newClient(cCtx) 651 | if err != nil { 652 | return err 653 | } 654 | 655 | var idsByIDs, idsByNames []string 656 | if len(ids) > 0 { 657 | idsByIDs, err = client.GetMaintenanceIDsByIDs(cCtx.Context, ids) 658 | if err != nil { 659 | return err 660 | } 661 | } 662 | if len(names) > 0 { 663 | idsByNames, err = client.GetMaintenanceIDsByNamesFullMatch(cCtx.Context, names) 664 | if err != nil { 665 | return err 666 | } 667 | } 668 | targetIDs := slicex.ConcatDeDup(idsByIDs, idsByNames) 669 | 670 | if cCtx.Bool("dry-run") { 671 | var b strings.Builder 672 | if len(ids) > 0 { 673 | fmt.Fprintf(&b, "ids: %s", strings.Join(ids, ", ")) 674 | } 675 | if len(names) > 0 { 676 | if b.Len() > 0 { 677 | b.WriteString(", ") 678 | } 679 | fmt.Fprintf(&b, "names: %s", strings.Join(names, ", ")) 680 | } 681 | outlog.Printf("INFO skip deleting maintenance due to dry run, %s", b.String()) 682 | return nil 683 | } 684 | deletedIDs, err := client.DeleteMaintenancesByIDs(cCtx.Context, targetIDs) 685 | if err != nil { 686 | return err 687 | } 688 | outlog.Printf("INFO targetIDs=%v, deletedIDs=%v", targetIDs, deletedIDs) 689 | return nil 690 | } 691 | 692 | func showStatusAction(cCtx *cli.Context) error { 693 | client, err := newClient(cCtx) 694 | if err != nil { 695 | return err 696 | } 697 | maintenance, err := getTargetMaintenance(cCtx, client) 698 | if err != nil { 699 | return err 700 | } 701 | 702 | hosts, err := getHostsInMaintenance(cCtx, client, maintenance) 703 | if err != nil { 704 | return err 705 | } 706 | 707 | if mainteBytes, err := json.Marshal(toDisplayMaintenance(*maintenance)); err != nil { 708 | return err 709 | } else { 710 | outlog.Printf("INFO maintenance=%s", string(mainteBytes)) 711 | } 712 | 713 | if err := logHosts(hosts); err != nil { 714 | return err 715 | } 716 | 717 | if cCtx.Bool("wait") { 718 | if err := waitForMaintenanceInEffect(cCtx, client, maintenance.MaintenaceID); err != nil { 719 | return err 720 | } 721 | } 722 | return nil 723 | } 724 | 725 | func waitForMaintenanceInEffect(cCtx *cli.Context, client *myClient, maintenanceID string) error { 726 | interval := cCtx.Duration("interval") 727 | var timer *time.Timer 728 | for { 729 | maintenance, err := client.GetMaintenanceByID(cCtx.Context, maintenanceID) 730 | if err != nil { 731 | return err 732 | } 733 | 734 | hosts, err := getHostsInMaintenance(cCtx, client, maintenance) 735 | if err != nil { 736 | return err 737 | } 738 | 739 | if Hosts(hosts).allMaintenanceStatusExpected(MaintenanceStatusInEffect) { 740 | outlog.Printf("INFO all hosts in specified maintenance become in effect status") 741 | if err := logHosts(hosts); err != nil { 742 | return err 743 | } 744 | return nil 745 | } 746 | 747 | if timer == nil { 748 | timer = time.NewTimer(interval) 749 | defer timer.Stop() 750 | } else { 751 | timer.Reset(interval) 752 | } 753 | outlog.Print("waiting for maintenance statuses change in all hosts...") 754 | select { 755 | case <-cCtx.Context.Done(): 756 | return nil 757 | case <-timer.C: 758 | } 759 | } 760 | } 761 | 762 | func getHostsInMaintenance(cCtx *cli.Context, client *myClient, maintenance *Maintenance) ([]Host, error) { 763 | var hosts []Host 764 | if len(maintenance.Groups) == 0 { 765 | hosts = concatHostsDeDup(maintenance.Hosts) 766 | } else { 767 | groupIDs := slicex.Map(maintenance.Groups, func(g HostGroup) string { 768 | return g.GroupID 769 | }) 770 | hostsInGroups, err := client.GetHostsByGroupIDs(cCtx.Context, groupIDs) 771 | if err != nil { 772 | return nil, err 773 | } 774 | hosts = concatHostsDeDup(maintenance.Hosts, hostsInGroups) 775 | } 776 | sortHosts(hosts) 777 | return hosts, nil 778 | } 779 | 780 | func logHosts(hosts []Host) error { 781 | hostsBytes, err := json.Marshal(slicex.Map(hosts, toDisplayHost)) 782 | if err != nil { 783 | return err 784 | } 785 | outlog.Printf("INFO hosts=%s", string(hostsBytes)) 786 | return nil 787 | } 788 | 789 | func newClient(cCtx *cli.Context) (*myClient, error) { 790 | zabbixURL := cCtx.String("url") 791 | hostHeader := cCtx.String("virtual-host") 792 | 793 | var opts []zabbix.ClientOpt 794 | if hostHeader != "" { 795 | opts = append(opts, zabbix.WithHost(hostHeader)) 796 | } 797 | 798 | token := cCtx.String("token") 799 | if token != "" { 800 | opts = append(opts, zabbix.WithAPIToken(token)) 801 | } 802 | opts = append(opts, zabbix.WithDebug(cCtx.Bool("debug"))) 803 | 804 | c, err := zabbix.NewClient(zabbixURL, opts...) 805 | if err != nil { 806 | return nil, err 807 | } 808 | 809 | client := &myClient{inner: &rpc.Client{Client: c}} 810 | if token == "" { 811 | if err := login(cCtx, client); err != nil { 812 | return nil, err 813 | } 814 | } 815 | 816 | return client, nil 817 | } 818 | 819 | func getTargetMaintenance(cCtx *cli.Context, client *myClient) (*Maintenance, error) { 820 | id := cCtx.String("id") 821 | name := cCtx.String("name") 822 | if (id == "" && name == "") || (id != "" && name != "") { 823 | return nil, errors.New(`just one of "--name" or "--id" must be set`) 824 | } 825 | 826 | var maintenance *Maintenance 827 | var err error 828 | if id != "" { 829 | maintenance, err = client.GetMaintenanceByID(cCtx.Context, id) 830 | } else if name != "" { 831 | maintenance, err = client.GetMaintenanceByNameFullMatch(cCtx.Context, name) 832 | } 833 | if err != nil { 834 | return nil, err 835 | } 836 | return maintenance, nil 837 | } 838 | 839 | func disableTriggersAction(cCtx *cli.Context) error { 840 | triggerIDs := cCtx.StringSlice("id") 841 | hostNames := cCtx.StringSlice("host") 842 | groupNames := cCtx.StringSlice("group") 843 | if len(triggerIDs) == 0 && len(hostNames) == 0 && len(groupNames) == 0 { 844 | return errors.New(`at least one of "--id", "--host", or "--hostgroup" must be set`) 845 | } 846 | 847 | descriptions := cCtx.StringSlice("description") 848 | 849 | client, err := newClient(cCtx) 850 | if err != nil { 851 | return err 852 | } 853 | 854 | triggerIDs, err = client.GetTriggerIDs(cCtx.Context, triggerIDs, hostNames, groupNames, descriptions) 855 | if err != nil { 856 | return err 857 | } 858 | 859 | if len(triggerIDs) == 0 { 860 | return errors.New("no trigger matched") 861 | } 862 | 863 | ids, err := client.SetTriggersStatus(cCtx.Context, triggerIDs, rpc.TriggerStatusDisabled) 864 | // Print updated trigger IDs before returning the error 865 | // since some triggers may be updated. 866 | enc := json.NewEncoder(os.Stdout) 867 | if err := enc.Encode(ids); err != nil { 868 | return err 869 | } 870 | if err != nil { 871 | return err 872 | } 873 | 874 | return nil 875 | } 876 | 877 | func enableTriggersAction(cCtx *cli.Context) error { 878 | triggerIDs := cCtx.StringSlice("id") 879 | hostNames := cCtx.StringSlice("host") 880 | groupNames := cCtx.StringSlice("group") 881 | if len(triggerIDs) == 0 && len(hostNames) == 0 && len(groupNames) == 0 { 882 | return errors.New(`at least one of "--id", "--host", or "--hostgroup" must be set`) 883 | } 884 | 885 | descriptions := cCtx.StringSlice("description") 886 | 887 | client, err := newClient(cCtx) 888 | if err != nil { 889 | return err 890 | } 891 | 892 | triggerIDs, err = client.GetTriggerIDs(cCtx.Context, triggerIDs, hostNames, groupNames, descriptions) 893 | if err != nil { 894 | return err 895 | } 896 | 897 | if len(triggerIDs) == 0 { 898 | return errors.New("no trigger matched") 899 | } 900 | 901 | ids, err := client.SetTriggersStatus(cCtx.Context, triggerIDs, rpc.TriggerStatusEnabled) 902 | // Print updated trigger IDs before returning the error 903 | // since some triggers may be updated. 904 | enc := json.NewEncoder(os.Stdout) 905 | if err := enc.Encode(ids); err != nil { 906 | return err 907 | } 908 | if err != nil { 909 | return err 910 | } 911 | 912 | return nil 913 | } 914 | 915 | func getTriggersAction(cCtx *cli.Context) error { 916 | triggerIDs := cCtx.StringSlice("id") 917 | hostNames := cCtx.StringSlice("host") 918 | groupNames := cCtx.StringSlice("group") 919 | if len(triggerIDs) == 0 && len(hostNames) == 0 && len(groupNames) == 0 { 920 | return errors.New(`at least one of "--id", "--host", or "--hostgroup" must be set`) 921 | } 922 | 923 | descriptions := cCtx.StringSlice("description") 924 | 925 | client, err := newClient(cCtx) 926 | if err != nil { 927 | return err 928 | } 929 | 930 | triggers, err := client.GetTriggers(cCtx.Context, triggerIDs, hostNames, groupNames, descriptions) 931 | if err != nil { 932 | return err 933 | } 934 | slices.SortFunc(triggers, func(a, b Trigger) bool { 935 | return a.TriggerID < b.TriggerID 936 | }) 937 | 938 | displayTriggers := make([]displayTrigger, len(triggers)) 939 | for i, t := range triggers { 940 | displayTriggers[i] = toDisplayTrigger(t) 941 | } 942 | enc := json.NewEncoder(os.Stdout) 943 | if err := enc.Encode(displayTriggers); err != nil { 944 | return err 945 | } 946 | return nil 947 | } 948 | 949 | func login(cCtx *cli.Context, c *myClient) error { 950 | username := cCtx.String("username") 951 | password := cCtx.String("password") 952 | 953 | if username == "" { 954 | return errors.New(`"--token" or "--username" must be set`) 955 | } 956 | 957 | if password == "" { 958 | p, err := readSecret("Enter password for Zabbix:") 959 | if err != nil { 960 | return err 961 | } 962 | password = string(p) 963 | } 964 | 965 | if err := c.inner.Login(cCtx.Context, username, password); err != nil { 966 | return err 967 | } 968 | return nil 969 | } 970 | 971 | func maintenanceURL(cCtx *cli.Context, maintenanceID string) (*url.URL, error) { 972 | zabbixURL, err := url.Parse(cCtx.String("url")) 973 | if err != nil { 974 | return nil, err 975 | } 976 | 977 | u := zabbixURL.JoinPath("maintenance.php") 978 | v := url.Values{} 979 | v.Add("form", "update") 980 | v.Add("maintenanceid", maintenanceID) 981 | u.RawQuery = v.Encode() 982 | return u, nil 983 | } 984 | 985 | func Version() string { 986 | // This code is copied from 987 | // https://blog.lufia.org/entry/2020/12/18/002238 988 | 989 | info, ok := debug.ReadBuildInfo() 990 | if !ok { 991 | return "(devel)" 992 | } 993 | return info.Main.Version 994 | } 995 | -------------------------------------------------------------------------------- /cmd/zbx/maintenance.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/hnakamur/go-zabbix/internal/rpc" 9 | "github.com/hnakamur/go-zabbix/internal/slicex" 10 | ) 11 | 12 | // https://www.zabbix.com/documentation/6.0/en/manual/api/reference/maintenance/object 13 | type Maintenance struct { 14 | MaintenaceID string 15 | Name string 16 | ActiveSince time.Time 17 | ActiveTill time.Time 18 | Description string 19 | MaintenaceType MaintenanceType 20 | TagsEvalType TagsEvalType 21 | Groups []HostGroup 22 | Hosts []Host 23 | TimePeriods []TimePeriod 24 | } 25 | 26 | type MaintenanceType string 27 | 28 | const ( 29 | MaintenanceTypeWithData MaintenanceType = "0" 30 | MaintenanceTypeNoData MaintenanceType = "1" 31 | ) 32 | 33 | type TagsEvalType string 34 | 35 | const ( 36 | TagsEvalTypeAndOr TagsEvalType = "0" 37 | TagsEvalTypeOr TagsEvalType = "1" 38 | ) 39 | 40 | func fromPRCMaintenance(m rpc.Maintenance) (Maintenance, error) { 41 | activeSince, err := ParseTimestamp(m.ActiveSince) 42 | if err != nil { 43 | return Maintenance{}, err 44 | } 45 | activeTill, err := ParseTimestamp(m.ActiveTill) 46 | if err != nil { 47 | return Maintenance{}, err 48 | } 49 | hosts, err := slicex.FailableMap(m.Hosts, fromRPCHost) 50 | if err != nil { 51 | return Maintenance{}, err 52 | } 53 | timePeriods, err := slicex.FailableMap(m.TimePeriods, fromRPCTimePeriod) 54 | if err != nil { 55 | return Maintenance{}, err 56 | } 57 | 58 | return Maintenance{ 59 | MaintenaceID: m.MaintenaceID, 60 | Name: m.Name, 61 | ActiveSince: time.Time(activeSince), 62 | ActiveTill: time.Time(activeTill), 63 | Description: m.Description, 64 | MaintenaceType: MaintenanceType(m.MaintenaceType), 65 | TagsEvalType: TagsEvalType(m.TagsEvalType), 66 | Groups: m.Groups, 67 | Hosts: hosts, 68 | TimePeriods: timePeriods, 69 | }, nil 70 | } 71 | 72 | func toRPCMaintenance(m Maintenance) (rpc.Maintenance, error) { 73 | rpcHosts, err := slicex.FailableMap(m.Hosts, toRPCHost) 74 | if err != nil { 75 | return rpc.Maintenance{}, err 76 | } 77 | rpcTimePeriods, err := slicex.FailableMap(m.TimePeriods, toRPCTimePeriod) 78 | if err != nil { 79 | return rpc.Maintenance{}, err 80 | } 81 | 82 | return rpc.Maintenance{ 83 | MaintenaceID: m.MaintenaceID, 84 | Name: m.Name, 85 | ActiveSince: Timestamp(m.ActiveSince).String(), 86 | ActiveTill: Timestamp(m.ActiveTill).String(), 87 | Description: m.Description, 88 | MaintenaceType: string(m.MaintenaceType), 89 | TagsEvalType: string(m.TagsEvalType), 90 | Groups: m.Groups, 91 | Hosts: rpcHosts, 92 | TimePeriods: rpcTimePeriods, 93 | }, nil 94 | } 95 | 96 | type TimeperiodType string 97 | 98 | const ( 99 | TimeperiodTypeOnetimeOnly TimeperiodType = "0" 100 | TimeperiodTypeDaily TimeperiodType = "2" 101 | TimeperiodTypeWeekly TimeperiodType = "3" 102 | TimeperiodTypeMonthly TimeperiodType = "4" 103 | ) 104 | 105 | type TimePeriod struct { 106 | TimeperiodID string 107 | Period time.Duration 108 | TimeperiodType TimeperiodType 109 | StartDate time.Time 110 | } 111 | 112 | func fromRPCTimePeriod(p rpc.TimePeriod) (TimePeriod, error) { 113 | period, err := ParseSeconds(p.Period) 114 | if err != nil { 115 | return TimePeriod{}, err 116 | } 117 | startDate, err := ParseTimestamp(p.StartDate) 118 | if err != nil { 119 | return TimePeriod{}, err 120 | } 121 | 122 | return TimePeriod{ 123 | TimeperiodID: p.TimeperiodID, 124 | Period: time.Duration(period), 125 | TimeperiodType: TimeperiodType(p.TimeperiodType), 126 | StartDate: time.Time(startDate), 127 | }, nil 128 | } 129 | 130 | func toRPCTimePeriod(p TimePeriod) (rpc.TimePeriod, error) { 131 | return rpc.TimePeriod{ 132 | TimeperiodID: p.TimeperiodID, 133 | Period: Seconds(p.Period).String(), 134 | TimeperiodType: string(p.TimeperiodType), 135 | StartDate: Timestamp(p.StartDate).String(), 136 | }, nil 137 | } 138 | 139 | func (c *myClient) GetMaintenances(ctx context.Context) ([]Maintenance, error) { 140 | rm, err := c.inner.GetMaintenances(ctx) 141 | if err != nil { 142 | return nil, err 143 | } 144 | return slicex.FailableMap(rm, fromPRCMaintenance) 145 | } 146 | 147 | func (c *myClient) GetMaintenanceByID(ctx context.Context, maintenanceID string) (*Maintenance, error) { 148 | rm, err := c.inner.GetMaintenanceByID(ctx, maintenanceID) 149 | if err != nil { 150 | return nil, err 151 | } 152 | m, err := fromPRCMaintenance(*rm) 153 | if err != nil { 154 | return nil, err 155 | } 156 | return &m, nil 157 | } 158 | 159 | func (c *myClient) GetMaintenanceByNameFullMatch(ctx context.Context, name string) (*Maintenance, error) { 160 | rm, err := c.inner.GetMaintenanceByNameFullMatch(ctx, name) 161 | if err != nil { 162 | return nil, err 163 | } 164 | m, err := fromPRCMaintenance(*rm) 165 | if err != nil { 166 | return nil, err 167 | } 168 | return &m, nil 169 | } 170 | 171 | func (c *myClient) CreateMaintenance(ctx context.Context, m *Maintenance) error { 172 | rm, err := toRPCMaintenance(*m) 173 | if err != nil { 174 | return err 175 | } 176 | if err := c.inner.CreateMaintenance(ctx, &rm); err != nil { 177 | return err 178 | } 179 | m.MaintenaceID = rm.MaintenaceID 180 | return nil 181 | } 182 | 183 | func (c *myClient) UpdateMaintenance(ctx context.Context, m *Maintenance) error { 184 | rm, err := toRPCMaintenance(*m) 185 | if err != nil { 186 | return err 187 | } 188 | return c.inner.UpdateMaintenance(ctx, &rm) 189 | } 190 | 191 | func (c *myClient) GetMaintenanceIDsByIDs(ctx context.Context, maintenanceIDs []string) ([]string, error) { 192 | return c.inner.GetMaintenanceIDsByIDs(ctx, maintenanceIDs) 193 | } 194 | 195 | func (c *myClient) GetMaintenanceIDsByNamesFullMatch(ctx context.Context, names []string) ([]string, error) { 196 | return c.inner.GetMaintenanceIDsByNamesFullMatch(ctx, names) 197 | } 198 | 199 | func (c *myClient) DeleteMaintenancesByIDs(ctx context.Context, ids []string) (deletedIDs []string, err error) { 200 | return c.inner.DeleteMaintenancesByIDs(ctx, ids) 201 | } 202 | 203 | type displayMaintenance struct { 204 | MaintenaceID string `json:"maintenanceid"` 205 | Name string `json:"name"` 206 | ActiveSince displayTimestamp `json:"active_since"` 207 | ActiveTill displayTimestamp `json:"active_till"` 208 | Description string `json:"description"` 209 | Groups []HostGroup `json:"groups"` 210 | Hosts []displayHost `json:"hosts"` 211 | TimePeriods []displayTimePeriod `json:"timeperiods"` 212 | } 213 | 214 | type displayHost struct { 215 | HostID string `json:"hostid"` 216 | Name string `json:"name"` 217 | MaintenanceFrom displayTimestamp `json:"maintenance_from"` 218 | MaintenanceStatus string `json:"maintenance_status"` 219 | MaintenanceType MaintenanceType `json:"maintenance_type"` 220 | MaintenanceID string `json:"maintenanceid"` 221 | } 222 | 223 | type displayTimePeriod struct { 224 | Period displayDuration `json:"period"` 225 | StartDate displayTimestamp `json:"start_date"` 226 | } 227 | 228 | func toDisplayMaintenance(m Maintenance) displayMaintenance { 229 | return displayMaintenance{ 230 | MaintenaceID: m.MaintenaceID, 231 | Name: m.Name, 232 | ActiveSince: displayTimestamp(m.ActiveSince), 233 | ActiveTill: displayTimestamp(m.ActiveTill), 234 | Description: m.Description, 235 | Groups: m.Groups, 236 | Hosts: slicex.Map(m.Hosts, toDisplayHost), 237 | TimePeriods: slicex.Map(m.TimePeriods, toDisplayTimePeriod), 238 | } 239 | } 240 | 241 | func toDisplayHost(h Host) displayHost { 242 | return displayHost{ 243 | HostID: h.HostID, 244 | Name: h.Name, 245 | MaintenanceFrom: displayTimestamp(time.Time(h.MaintenanceFrom)), 246 | MaintenanceStatus: string(h.MaintenanceStatus), 247 | MaintenanceType: MaintenanceType(h.MaintenanceType), 248 | MaintenanceID: h.MaintenanceID, 249 | } 250 | } 251 | 252 | func toDisplayTimePeriod(tp TimePeriod) displayTimePeriod { 253 | return displayTimePeriod{ 254 | Period: displayDuration(tp.Period), 255 | StartDate: displayTimestamp(tp.StartDate), 256 | } 257 | } 258 | 259 | // Timestamp is an alias to time.Time. Timestamp is encoded a string whose value 260 | // is seconds from the Unix epoch time. 261 | type displayTimestamp time.Time 262 | 263 | // MarshalJSON returns a string of seconds from the Unix Epoch time. 264 | func (t displayTimestamp) MarshalJSON() ([]byte, error) { 265 | return []byte(`"` + t.String() + `"`), nil 266 | } 267 | 268 | // UnmarshalJSON reads a string of seconds from the Unix Epoch time. 269 | func (t *displayTimestamp) UnmarshalJSON(data []byte) error { 270 | s := strings.Trim(string(data), `"`) 271 | parsed, err := time.Parse(s, timeFormatRFC3339Minute) 272 | if err != nil { 273 | return err 274 | } 275 | *t = displayTimestamp(parsed) 276 | return nil 277 | } 278 | 279 | func (t displayTimestamp) String() string { 280 | return time.Time(t).Format(timeFormatRFC3339Minute) 281 | } 282 | 283 | type displayDuration time.Duration 284 | 285 | func (d displayDuration) MarshalJSON() ([]byte, error) { 286 | return []byte(`"` + d.String() + `"`), nil 287 | } 288 | 289 | func (d *displayDuration) UnmarshalJSON(data []byte) error { 290 | s := strings.Trim(string(data), `"`) 291 | parsed, err := time.ParseDuration(s) 292 | if err != nil { 293 | return err 294 | } 295 | *d = displayDuration(parsed) 296 | return nil 297 | } 298 | 299 | func (d displayDuration) String() string { 300 | return time.Duration(d).String() 301 | } 302 | -------------------------------------------------------------------------------- /cmd/zbx/trigger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/hnakamur/go-zabbix/internal/rpc" 8 | "github.com/hnakamur/go-zabbix/internal/slicex" 9 | ) 10 | 11 | // https://www.zabbix.com/documentation/6.0/en/manual/api/reference/trigger/object 12 | type Trigger struct { 13 | TriggerID string 14 | Description string 15 | Expression string 16 | EventName string 17 | Comments string 18 | Error string 19 | LastChange time.Time 20 | State string 21 | Status string 22 | URL string 23 | Value string 24 | Groups []HostGroup 25 | Hosts []Host 26 | Items []Item 27 | } 28 | 29 | func (c *myClient) GetTriggers(ctx context.Context, triggerIDs, hostNames, groupNames, descriptions []string) ([]Trigger, error) { 30 | var hostIDs, groupIDs, itemIDs []string 31 | if len(hostNames) > 0 { 32 | hosts, err := c.inner.GetHostsByNamesFullMatch(ctx, hostNames) 33 | if err != nil { 34 | return nil, err 35 | } 36 | hostIDs = slicex.Map(hosts, func(h rpc.Host) string { 37 | return h.HostID 38 | }) 39 | } 40 | if len(groupNames) > 0 { 41 | groups, err := c.inner.GetNestedHostGroupsByAncestorNames(ctx, groupNames) 42 | if err != nil { 43 | return nil, err 44 | } 45 | groupIDs = slicex.Map(groups, func(h rpc.HostGroup) string { 46 | return h.GroupID 47 | }) 48 | } 49 | 50 | triggers, err := c.inner.GetTriggers(ctx, triggerIDs, hostIDs, groupIDs, itemIDs, descriptions) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return slicex.FailableMap(triggers, fromPRCTrigger) 55 | } 56 | 57 | func (c *myClient) SetTriggersStatus(ctx context.Context, triggerIDs []string, status rpc.TriggerStatus) ([]string, error) { 58 | var updatedIDs []string 59 | 60 | for _, triggerID := range triggerIDs { 61 | ids, err := c.inner.SetTriggersStatus(ctx, triggerID, status) 62 | updatedIDs = append(updatedIDs, ids...) 63 | if err != nil { 64 | return updatedIDs, err 65 | } 66 | } 67 | return updatedIDs, nil 68 | } 69 | 70 | func (c *myClient) GetTriggerIDs(ctx context.Context, triggerIDs, hostNames, groupNames, descriptions []string) ([]string, error) { 71 | var hostIDs, groupIDs, itemIDs []string 72 | if len(hostNames) > 0 { 73 | hosts, err := c.inner.GetHostsByNamesFullMatch(ctx, hostNames) 74 | if err != nil { 75 | return nil, err 76 | } 77 | hostIDs = slicex.Map(hosts, func(h rpc.Host) string { 78 | return h.HostID 79 | }) 80 | } 81 | if len(groupNames) > 0 { 82 | groups, err := c.inner.GetNestedHostGroupsByAncestorNames(ctx, groupNames) 83 | if err != nil { 84 | return nil, err 85 | } 86 | groupIDs = slicex.Map(groups, func(h rpc.HostGroup) string { 87 | return h.GroupID 88 | }) 89 | } 90 | 91 | triggerIDs, err := c.inner.GetTriggerIDs(ctx, triggerIDs, hostIDs, groupIDs, itemIDs, descriptions) 92 | if err != nil { 93 | return nil, err 94 | } 95 | return triggerIDs, nil 96 | } 97 | 98 | func fromPRCTrigger(t rpc.Trigger) (Trigger, error) { 99 | lastChange, err := ParseTimestamp(t.LastChange) 100 | if err != nil { 101 | return Trigger{}, err 102 | } 103 | 104 | hosts, err := slicex.FailableMap(t.Hosts, fromRPCHost) 105 | if err != nil { 106 | return Trigger{}, err 107 | } 108 | 109 | return Trigger{ 110 | TriggerID: t.TriggerID, 111 | Description: t.Description, 112 | Expression: t.Expression, 113 | EventName: t.EventName, 114 | Comments: t.Comments, 115 | Error: t.Error, 116 | LastChange: time.Time(lastChange), 117 | State: t.State, 118 | Status: t.Status, 119 | URL: t.URL, 120 | Value: t.Value, 121 | Groups: t.Groups, 122 | Hosts: hosts, 123 | Items: t.Items, 124 | }, nil 125 | } 126 | 127 | func toRPCTrigger(t Trigger) (rpc.Trigger, error) { 128 | return rpc.Trigger{ 129 | TriggerID: t.TriggerID, 130 | Description: t.Description, 131 | Expression: t.Expression, 132 | EventName: t.EventName, 133 | Comments: t.Comments, 134 | Error: t.Error, 135 | Status: t.Status, 136 | URL: t.URL, 137 | // Keep empty values for readonly properties 138 | }, nil 139 | } 140 | 141 | type displayTrigger struct { 142 | TriggerID string `json:"triggerid,omitempty"` 143 | Description string `json:"description,omitempty"` 144 | Expression string `json:"expression,omitempty"` 145 | EventName string `json:"event_name,omitempty"` 146 | Comments string `json:"comments,omitempty"` 147 | Error string `json:"error,omitempty"` 148 | LastChange displayTimestamp `json:"lastchange,omitempty"` 149 | State string `json:"state,omitempty"` 150 | Status string `json:"status,omitempty"` 151 | URL string `json:"url,omitempty"` 152 | Value string `json:"value,omitempty"` 153 | Groups []HostGroup `json:"groups"` 154 | Hosts []displayHost `json:"hosts"` 155 | Items []Item `json:"items"` 156 | } 157 | 158 | func toDisplayTrigger(t Trigger) displayTrigger { 159 | return displayTrigger{ 160 | TriggerID: t.TriggerID, 161 | Description: t.Description, 162 | Expression: t.Expression, 163 | EventName: t.EventName, 164 | Comments: t.Comments, 165 | Error: t.Error, 166 | LastChange: displayTimestamp(time.Time(t.LastChange)), 167 | State: t.State, 168 | Status: t.Status, 169 | URL: t.URL, 170 | Value: t.Value, 171 | Groups: t.Groups, 172 | Hosts: slicex.Map(t.Hosts, toDisplayHost), 173 | Items: t.Items, 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /cmd/zbx/tui.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The age Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // Copied from https://github.com/FiloSottile/age/blob/v1.1.1/cmd/age/tui.go 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "os" 13 | "runtime" 14 | 15 | "golang.org/x/term" 16 | ) 17 | 18 | // readSecret reads a value from the terminal with no echo. The prompt is ephemeral. 19 | func readSecret(prompt string) (s []byte, err error) { 20 | err = withTerminal(func(in, out *os.File) error { 21 | fmt.Fprintf(out, "%s ", prompt) 22 | defer clearLine(out) 23 | s, err = term.ReadPassword(int(in.Fd())) 24 | return err 25 | }) 26 | return 27 | } 28 | 29 | // clearLine clears the current line on the terminal, or opens a new line if 30 | // terminal escape codes don't work. 31 | func clearLine(out io.Writer) { 32 | const ( 33 | CUI = "\033[" // Control Sequence Introducer 34 | CPL = CUI + "F" // Cursor Previous Line 35 | EL = CUI + "K" // Erase in Line 36 | ) 37 | 38 | // First, open a new line, which is guaranteed to work everywhere. Then, try 39 | // to erase the line above with escape codes. 40 | // 41 | // (We use CRLF instead of LF to work around an apparent bug in WSL2's 42 | // handling of CONOUT$. Only when running a Windows binary from WSL2, the 43 | // cursor would not go back to the start of the line with a simple LF. 44 | // Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.) 45 | fmt.Fprintf(out, "\r\n"+CPL+EL) 46 | } 47 | 48 | // withTerminal runs f with the terminal input and output files, if available. 49 | // withTerminal does not open a non-terminal stdin, so the caller does not need 50 | // to check stdinInUse. 51 | func withTerminal(f func(in, out *os.File) error) error { 52 | if runtime.GOOS == "windows" { 53 | in, err := os.OpenFile("CONIN$", os.O_RDWR, 0) 54 | if err != nil { 55 | return err 56 | } 57 | defer in.Close() 58 | out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0) 59 | if err != nil { 60 | return err 61 | } 62 | defer out.Close() 63 | return f(in, out) 64 | } else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil { 65 | defer tty.Close() 66 | return f(tty, tty) 67 | } else if term.IsTerminal(int(os.Stdin.Fd())) { 68 | return f(os.Stdin, os.Stdin) 69 | } else { 70 | return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package zabbix is a minimal Zabbix API client library. 2 | // 3 | // This library is provides the only minimal lower level APIs. 4 | // 5 | // Users can refer to https://www.zabbix.com/documentation/6.0/en/manual/api 6 | // and create functions for their use cases. 7 | package zabbix 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hnakamur/go-zabbix 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/urfave/cli/v2 v2.25.7 7 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 8 | golang.org/x/term v0.9.0 9 | ) 10 | 11 | require ( 12 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 13 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 14 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 15 | golang.org/x/sys v0.9.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= 6 | github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 7 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 8 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 9 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= 10 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 11 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= 12 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 13 | golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= 14 | golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= 15 | -------------------------------------------------------------------------------- /internal/errlog/errlog.go: -------------------------------------------------------------------------------- 1 | package errlog 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | ) 9 | 10 | var logger = log.New(os.Stderr, "", log.LstdFlags) 11 | 12 | func Print(v ...any) { 13 | logger.Output(2, fmt.Sprint(v...)) 14 | } 15 | 16 | func Printf(format string, v ...any) { 17 | logger.Output(2, fmt.Sprintf(format, v...)) 18 | } 19 | 20 | func SetFlags(flag int) { 21 | logger.SetFlags(flag) 22 | } 23 | 24 | func SetOutput(w io.Writer) { 25 | logger.SetOutput(w) 26 | } 27 | -------------------------------------------------------------------------------- /internal/outlog/outlog.go: -------------------------------------------------------------------------------- 1 | package outlog 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | var logger = log.New(os.Stdout, "", log.LstdFlags) 13 | 14 | func Print(v ...any) { 15 | logger.Output(2, fmt.Sprint(v...)) 16 | } 17 | 18 | func Printf(format string, v ...any) { 19 | logger.Output(2, fmt.Sprintf(format, v...)) 20 | } 21 | 22 | func SetFlags(flag int) { 23 | logger.SetFlags(flag) 24 | } 25 | 26 | func SetOutput(w io.Writer) { 27 | logger.SetOutput(w) 28 | } 29 | 30 | func ParseLogFlags(s string) (int, error) { 31 | if s == "" { 32 | return 0, nil 33 | } 34 | var flags int 35 | words := strings.Split(s, "|") 36 | for _, word := range words { 37 | word = strings.TrimSpace(word) 38 | switch word { 39 | case "date": 40 | flags |= log.Ldate 41 | case "time": 42 | flags |= log.Ltime 43 | case "microseconds": 44 | flags |= log.Lmicroseconds 45 | case "longfile": 46 | flags |= log.Llongfile 47 | case "shortfile": 48 | flags |= log.Lshortfile 49 | case "UTC": 50 | flags |= log.LUTC 51 | case "msgprefix": 52 | flags |= log.Lmsgprefix 53 | case "stdFlags": 54 | flags |= log.LstdFlags 55 | default: 56 | return 0, errors.New(`must be "stdFlags", "date", "time", "microseconds", "longfile", "shortfile", "UTC", "msgprefix", or combination of them with "|"`) 57 | } 58 | } 59 | return flags, nil 60 | } 61 | 62 | type LogFlags int 63 | 64 | func (f LogFlags) String() string { 65 | var b strings.Builder 66 | 67 | flagsTable := []struct { 68 | flag int 69 | name string 70 | }{ 71 | // LstdFlags must be the first element since LstdFlags = Ldate | Ltime 72 | {flag: log.LstdFlags, name: "stdFlags"}, 73 | {flag: log.Ldate, name: "date"}, 74 | {flag: log.Ltime, name: "time"}, 75 | {flag: log.Lmicroseconds, name: "microseconds"}, 76 | {flag: log.Llongfile, name: "longfile"}, 77 | {flag: log.Lshortfile, name: "shortfile"}, 78 | {flag: log.LUTC, name: "UTC"}, 79 | {flag: log.Lmsgprefix, name: "msgprefix"}, 80 | } 81 | rest := int(f) 82 | for _, t := range flagsTable { 83 | if rest&t.flag == t.flag { 84 | if b.Len() > 0 { 85 | b.WriteByte('|') 86 | } 87 | b.WriteString(t.name) 88 | rest ^= t.flag 89 | } 90 | } 91 | return b.String() 92 | } 93 | -------------------------------------------------------------------------------- /internal/outlog/outlog_test.go: -------------------------------------------------------------------------------- 1 | package outlog 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | ) 7 | 8 | func TestParseLogFlags(t *testing.T) { 9 | t.Run("success", func(t *testing.T) { 10 | testCases := []struct { 11 | input string 12 | want int 13 | }{ 14 | {input: "", want: 0}, 15 | {input: "stdFlags", want: log.LstdFlags}, 16 | {input: "date | time", want: log.LstdFlags}, 17 | {input: "stdFlags | microseconds", want: log.LstdFlags | log.Lmicroseconds}, 18 | } 19 | for _, c := range testCases { 20 | got, err := ParseLogFlags(c.input) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | if got != c.want { 25 | log.Printf("result mismatch, input=%s, got=0x%x, want=0x%x", 26 | c.input, got, c.want) 27 | } 28 | } 29 | }) 30 | t.Run("error", func(t *testing.T) { 31 | testCases := []string{ 32 | "stsFlags2", 33 | "date | | time", 34 | } 35 | for _, c := range testCases { 36 | if _, err := ParseLogFlags(c); err == nil { 37 | t.Errorf("want error but got no error, input=%s", c) 38 | } 39 | } 40 | }) 41 | } 42 | 43 | func TestLogFlagsString(t *testing.T) { 44 | testCases := []struct { 45 | input int 46 | want string 47 | }{ 48 | {input: 0, want: ""}, 49 | {input: log.LstdFlags, want: "stdFlags"}, 50 | {input: log.Ldate | log.Ltime, want: "stdFlags"}, 51 | {input: log.Ldate | log.Llongfile, want: "date|longfile"}, 52 | } 53 | for _, c := range testCases { 54 | got := LogFlags(c.input).String() 55 | if got != c.want { 56 | t.Errorf("result mismatch, input=0x%x, got=%s, want=%s", c.input, got, c.want) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/rpc/client.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import "github.com/hnakamur/go-zabbix" 4 | 5 | type Client struct { 6 | *zabbix.Client 7 | } 8 | -------------------------------------------------------------------------------- /internal/rpc/host.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "slices" 7 | "strings" 8 | ) 9 | 10 | type Host struct { 11 | HostID string `json:"hostid"` 12 | Name string `json:"name,omitempty"` 13 | MaintenanceFrom string `json:"maintenance_from,omitempty"` 14 | MaintenanceStatus string `json:"maintenance_status,omitempty"` 15 | MaintenanceType string `json:"maintenance_type,omitempty"` 16 | MaintenanceID string `json:"maintenanceid,omitempty"` 17 | } 18 | 19 | var selectHosts = []string{"hostid", "name", "maintenance_from", 20 | "maintenance_status", "maintenance_type", "maintenanceid"} 21 | 22 | func (c *Client) GetHostsByNamesFullMatch(ctx context.Context, 23 | names []string) ([]Host, error) { 24 | type Names struct { 25 | Name []string `json:"name"` 26 | } 27 | 28 | params := struct { 29 | Output any `json:"output"` 30 | Filter any `json:"filter"` 31 | }{ 32 | Output: selectHosts, 33 | Filter: Names{ 34 | Name: names, 35 | }, 36 | } 37 | var hosts []Host 38 | if err := c.Client.Call(ctx, "host.get", params, &hosts); err != nil { 39 | return nil, err 40 | } 41 | 42 | var notFoundNames []string 43 | for _, name := range names { 44 | if !slices.ContainsFunc(hosts, func(host Host) bool { 45 | return host.Name == name 46 | }) { 47 | notFoundNames = append(notFoundNames, name) 48 | } 49 | } 50 | if len(notFoundNames) > 0 { 51 | return nil, fmt.Errorf("hosts not found: %s", strings.Join(notFoundNames, ", ")) 52 | } 53 | return hosts, nil 54 | } 55 | 56 | func (c *Client) GetHostsByHostIDs(ctx context.Context, 57 | hostIDs []string) ([]Host, error) { 58 | params := struct { 59 | Output any `json:"output"` 60 | HostIDs any `json:"hostids"` 61 | }{ 62 | Output: selectHosts, 63 | HostIDs: hostIDs, 64 | } 65 | var hosts []Host 66 | if err := c.Client.Call(ctx, "host.get", params, &hosts); err != nil { 67 | return nil, err 68 | } 69 | 70 | var notFoundHostIDs []string 71 | for _, hostID := range hostIDs { 72 | if !slices.ContainsFunc(hosts, func(host Host) bool { 73 | return host.HostID == hostID 74 | }) { 75 | notFoundHostIDs = append(notFoundHostIDs, hostID) 76 | } 77 | } 78 | if len(notFoundHostIDs) > 0 { 79 | return nil, fmt.Errorf("host IDs not found: %s", strings.Join(notFoundHostIDs, ", ")) 80 | } 81 | return hosts, nil 82 | } 83 | 84 | func (c *Client) GetHostsByGroupIDs(ctx context.Context, 85 | groupIDs []string) ([]Host, error) { 86 | params := struct { 87 | Output any `json:"output"` 88 | GroupIDs any `json:"groupids"` 89 | }{ 90 | Output: selectHosts, 91 | GroupIDs: groupIDs, 92 | } 93 | var hosts []Host 94 | if err := c.Client.Call(ctx, "host.get", params, &hosts); err != nil { 95 | return nil, err 96 | } 97 | return hosts, nil 98 | } 99 | -------------------------------------------------------------------------------- /internal/rpc/hostgroup.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "slices" 7 | "strings" 8 | ) 9 | 10 | type HostGroup struct { 11 | GroupID string `json:"groupid"` 12 | Name string `json:"name,omitempty"` 13 | } 14 | 15 | var selectGroups = []string{"groupid", "name"} 16 | 17 | func (c *Client) GetHostGroupsByNamesFullMatch(ctx context.Context, 18 | names []string) ([]HostGroup, error) { 19 | type Names struct { 20 | Name []string `json:"name"` 21 | } 22 | 23 | params := struct { 24 | Output any `json:"output"` 25 | Filter any `json:"filter"` 26 | }{ 27 | Output: []string{"groupid", "name"}, 28 | Filter: Names{ 29 | Name: names, 30 | }, 31 | } 32 | var groups []HostGroup 33 | if err := c.Client.Call(ctx, "hostgroup.get", params, &groups); err != nil { 34 | return nil, err 35 | } 36 | 37 | var notFoundNames []string 38 | for _, name := range names { 39 | if !slices.ContainsFunc(groups, func(grp HostGroup) bool { 40 | return grp.Name == name 41 | }) { 42 | notFoundNames = append(notFoundNames, name) 43 | } 44 | } 45 | if len(notFoundNames) > 0 { 46 | return nil, fmt.Errorf("host groups not found: %s", strings.Join(notFoundNames, ", ")) 47 | } 48 | return groups, nil 49 | } 50 | 51 | func (c *Client) GetNestedHostGroupsByAncestorNames(ctx context.Context, 52 | names []string) ([]HostGroup, error) { 53 | 54 | params := struct { 55 | Output any `json:"output"` 56 | }{ 57 | Output: []string{"groupid", "name"}, 58 | } 59 | var groups []HostGroup 60 | if err := c.Client.Call(ctx, "hostgroup.get", params, &groups); err != nil { 61 | return nil, err 62 | } 63 | 64 | var notFoundNames []string 65 | for _, name := range names { 66 | if !slices.ContainsFunc(groups, func(grp HostGroup) bool { 67 | return grp.Name == name 68 | }) { 69 | notFoundNames = append(notFoundNames, name) 70 | } 71 | } 72 | if len(notFoundNames) > 0 { 73 | return nil, fmt.Errorf("host groups not found: %s", strings.Join(notFoundNames, ", ")) 74 | } 75 | return filterHostGroupsByAncestorNames(groups, names), nil 76 | } 77 | 78 | func filterHostGroupsByAncestorNames(groups []HostGroup, names []string) []HostGroup { 79 | var filteredGroups []HostGroup 80 | for _, group := range groups { 81 | for _, name := range names { 82 | if group.Name == name || strings.HasPrefix(group.Name, name+"/") { 83 | filteredGroups = append(filteredGroups, group) 84 | } 85 | } 86 | } 87 | return filteredGroups 88 | } 89 | -------------------------------------------------------------------------------- /internal/rpc/item.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | // https://www.zabbix.com/documentation/6.0/en/manual/api/reference/item/object 4 | 5 | type Item struct { 6 | ItemID string `json:"itemid"` 7 | HostID string `json:"hostid"` 8 | Key string `json:"key_"` 9 | Name string `json:"name"` 10 | Type string `json:"type"` 11 | } 12 | 13 | var selectItems = []string{"itemid", "hostid", "key_", "name", "type"} 14 | -------------------------------------------------------------------------------- /internal/rpc/maintenance.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hnakamur/go-zabbix/internal/slicex" 8 | ) 9 | 10 | // https://www.zabbix.com/documentation/6.0/en/manual/api/reference/maintenance/object 11 | 12 | type Maintenance struct { 13 | MaintenaceID string `json:"maintenanceid,omitempty"` 14 | Name string `json:"name,omitempty"` 15 | ActiveSince string `json:"active_since,omitempty"` 16 | ActiveTill string `json:"active_till,omitempty"` 17 | Description string `json:"description,omitempty"` 18 | MaintenaceType string `json:"maintenance_type,omitempty"` 19 | TagsEvalType string `json:"tags_evaltype,omitempty"` 20 | Groups []HostGroup `json:"groups"` 21 | Hosts []Host `json:"hosts"` 22 | TimePeriods []TimePeriod `json:"timeperiods,omitempty"` 23 | } 24 | 25 | type TimePeriod struct { 26 | TimeperiodID string `json:"timeperiodid,omitempty"` 27 | Period string `json:"period"` 28 | TimeperiodType string `json:"timeperiod_type"` 29 | StartDate string `json:"start_date"` 30 | } 31 | 32 | var selectTimeperiods = []string{"timeperiodid", "period", "timeperiod_type", 33 | "start_date"} 34 | 35 | func (c *Client) GetMaintenances(ctx context.Context) ([]Maintenance, error) { 36 | params := struct { 37 | Output any `json:"output"` 38 | SelectGroups any `json:"selectGroups"` 39 | SelectHosts any `json:"selectHosts"` 40 | SelectTimeperiods any `json:"selectTimeperiods"` 41 | }{ 42 | Output: "extend", 43 | SelectGroups: selectGroups, 44 | SelectHosts: selectHosts, 45 | SelectTimeperiods: selectTimeperiods, 46 | } 47 | var rm []Maintenance 48 | if err := c.Client.Call(ctx, "maintenance.get", params, &rm); err != nil { 49 | return nil, err 50 | } 51 | return rm, nil 52 | } 53 | 54 | func (c *Client) GetMaintenanceByID(ctx context.Context, maintenanceID string) (*Maintenance, error) { 55 | type Filter struct { 56 | MaintenanceID []string `json:"maintenanceid"` 57 | } 58 | 59 | params := struct { 60 | Output any `json:"output"` 61 | SelectGroups any `json:"selectGroups"` 62 | SelectHosts any `json:"selectHosts"` 63 | SelectTimeperiods any `json:"selectTimeperiods"` 64 | Filter any `json:"filter"` 65 | }{ 66 | Output: "extend", 67 | SelectGroups: selectGroups, 68 | SelectHosts: selectHosts, 69 | SelectTimeperiods: selectTimeperiods, 70 | Filter: Filter{MaintenanceID: []string{maintenanceID}}, 71 | } 72 | var rm []Maintenance 73 | if err := c.Client.Call(ctx, "maintenance.get", params, &rm); err != nil { 74 | return nil, err 75 | } 76 | if len(rm) != 1 { 77 | return nil, fmt.Errorf("unexpected maintenance count, got=%d, want=1", len(rm)) 78 | } 79 | return &rm[0], nil 80 | } 81 | 82 | func (c *Client) GetMaintenanceByNameFullMatch(ctx context.Context, name string) (*Maintenance, error) { 83 | type Names struct { 84 | Name []string `json:"name"` 85 | } 86 | 87 | params := struct { 88 | Output any `json:"output"` 89 | SelectGroups any `json:"selectGroups"` 90 | SelectHosts any `json:"selectHosts"` 91 | SelectTimeperiods any `json:"selectTimeperiods"` 92 | Filter any `json:"filter"` 93 | }{ 94 | Output: "extend", 95 | SelectGroups: selectGroups, 96 | SelectHosts: selectHosts, 97 | SelectTimeperiods: selectTimeperiods, 98 | Filter: Names{Name: []string{name}}, 99 | } 100 | var rm []Maintenance 101 | if err := c.Client.Call(ctx, "maintenance.get", params, &rm); err != nil { 102 | return nil, err 103 | } 104 | if len(rm) != 1 { 105 | return nil, fmt.Errorf("unexpected maintenance count, got=%d, want=1", len(rm)) 106 | } 107 | return &rm[0], nil 108 | } 109 | 110 | func (c *Client) CreateMaintenance(ctx context.Context, m *Maintenance) error { 111 | type MaintenanceIDs struct { 112 | MaintenanceIDs []string `json:"maintenanceids"` 113 | } 114 | 115 | var ids MaintenanceIDs 116 | if err := c.Client.Call(ctx, "maintenance.create", m, &ids); err != nil { 117 | return err 118 | } 119 | if len(ids.MaintenanceIDs) != 1 { 120 | return fmt.Errorf("unexpected ids length: %d", len(ids.MaintenanceIDs)) 121 | } 122 | m.MaintenaceID = ids.MaintenanceIDs[0] 123 | return nil 124 | } 125 | 126 | func (c *Client) UpdateMaintenance(ctx context.Context, m *Maintenance) error { 127 | type MaintenanceIDs struct { 128 | MaintenanceIDs []string `json:"maintenanceids"` 129 | } 130 | 131 | var ids MaintenanceIDs 132 | if err := c.Client.Call(ctx, "maintenance.update", m, &ids); err != nil { 133 | return err 134 | } 135 | if len(ids.MaintenanceIDs) != 1 { 136 | return fmt.Errorf("unexpected ids length: %d", len(ids.MaintenanceIDs)) 137 | } 138 | m.MaintenaceID = ids.MaintenanceIDs[0] 139 | return nil 140 | } 141 | 142 | func (c *Client) GetMaintenanceIDsByIDs(ctx context.Context, maintenanceIDs []string) ([]string, error) { 143 | type rpcFilter struct { 144 | MaintenanceID []string `json:"maintenanceid"` 145 | } 146 | 147 | params := struct { 148 | Output any `json:"output"` 149 | Filter any `json:"filter"` 150 | }{ 151 | Output: "maintenanceid", 152 | Filter: rpcFilter{MaintenanceID: maintenanceIDs}, 153 | } 154 | var rm []Maintenance 155 | if err := c.Client.Call(ctx, "maintenance.get", params, &rm); err != nil { 156 | return nil, err 157 | } 158 | if len(rm) != len(maintenanceIDs) { 159 | return nil, fmt.Errorf("unexpected maintenance count returned by GetMaintenanceIDsByID: got=%d, want=%d", len(rm), len(maintenanceIDs)) 160 | } 161 | ids := slicex.Map(rm, func(m Maintenance) string { 162 | return m.MaintenaceID 163 | }) 164 | return ids, nil 165 | } 166 | 167 | func (c *Client) GetMaintenanceIDsByNamesFullMatch(ctx context.Context, names []string) ([]string, error) { 168 | type rpcFilter struct { 169 | Name []string `json:"name"` 170 | } 171 | 172 | params := struct { 173 | Output any `json:"output"` 174 | Filter any `json:"filter"` 175 | }{ 176 | Output: "maintenanceid", 177 | Filter: rpcFilter{Name: names}, 178 | } 179 | var result []Maintenance 180 | if err := c.Client.Call(ctx, "maintenance.get", params, &result); err != nil { 181 | return nil, err 182 | } 183 | if len(result) != len(names) { 184 | return nil, fmt.Errorf("unexpected maintenance count returned by GetMaintenanceIDsByNamesFullMatch: got=%d, want=%d", len(result), len(names)) 185 | } 186 | maintenanceIDs := slicex.Map(result, func(m Maintenance) string { 187 | return m.MaintenaceID 188 | }) 189 | return maintenanceIDs, nil 190 | } 191 | 192 | func (c *Client) DeleteMaintenancesByIDs(ctx context.Context, ids []string) (deletedIDs []string, err error) { 193 | type MaintenanceIDs struct { 194 | MaintenanceIDs []string `json:"maintenanceids"` 195 | } 196 | 197 | var maintenances MaintenanceIDs 198 | if err := c.Client.Call(ctx, "maintenance.delete", ids, &maintenances); err != nil { 199 | return nil, err 200 | } 201 | return maintenances.MaintenanceIDs, nil 202 | } 203 | -------------------------------------------------------------------------------- /internal/rpc/trigger.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hnakamur/go-zabbix/internal/slicex" 7 | ) 8 | 9 | // https://www.zabbix.com/documentation/6.0/en/manual/api/reference/trigger/object 10 | 11 | type Trigger struct { 12 | TriggerID string `json:"triggerid,omitempty"` 13 | Description string `json:"description,omitempty"` 14 | Expression string `json:"expression,omitempty"` 15 | EventName string `json:"event_name,omitempty"` 16 | Comments string `json:"comments,omitempty"` 17 | Error string `json:"error,omitempty"` 18 | LastChange string `json:"lastchange,omitempty"` 19 | State string `json:"state,omitempty"` 20 | Status string `json:"status,omitempty"` 21 | URL string `json:"url,omitempty"` 22 | Value string `json:"value,omitempty"` 23 | Groups []HostGroup `json:"groups"` 24 | Hosts []Host `json:"hosts"` 25 | Items []Item `json:"items"` 26 | } 27 | 28 | func (c *Client) GetTriggers(ctx context.Context, triggerIDs, hostIDs, groupIDs, itemIDs, descriptions []string) ([]Trigger, error) { 29 | type descriptionsFilter struct { 30 | Descriptions []string `json:"description"` 31 | } 32 | 33 | var filter *descriptionsFilter 34 | if len(descriptions) > 0 { 35 | filter = &descriptionsFilter{ 36 | Descriptions: descriptions, 37 | } 38 | } 39 | params := struct { 40 | TriggerIDs []string `json:"triggerids,omitempty"` 41 | Output string `json:"output"` 42 | Filter *descriptionsFilter `json:"filter,omitempty"` 43 | HostIDs []string `json:"hostids,omitempty"` 44 | GroupIDs []string `json:"groupids,omitempty"` 45 | ItemIDs []string `json:"itemids,omitempty"` 46 | SelectGroups []string `json:"selectGroups"` 47 | SelectHosts []string `json:"selectHosts"` 48 | SelectItems []string `json:"selectItems"` 49 | }{ 50 | TriggerIDs: triggerIDs, 51 | Output: "extend", 52 | Filter: filter, 53 | HostIDs: hostIDs, 54 | GroupIDs: groupIDs, 55 | ItemIDs: itemIDs, 56 | SelectGroups: selectGroups, 57 | SelectHosts: selectHosts, 58 | SelectItems: selectItems, 59 | } 60 | var triggers []Trigger 61 | if err := c.Client.Call(ctx, "trigger.get", params, &triggers); err != nil { 62 | return nil, err 63 | } 64 | return triggers, nil 65 | } 66 | 67 | func (c *Client) GetTriggerIDs(ctx context.Context, triggerIDs, hostIDs, groupIDs, itemIDs, descriptions []string) ([]string, error) { 68 | triggers, err := c.GetTriggers(ctx, triggerIDs, hostIDs, groupIDs, itemIDs, descriptions) 69 | if err != nil { 70 | return nil, err 71 | } 72 | return slicex.Map(triggers, func(t Trigger) string { 73 | return t.TriggerID 74 | }), nil 75 | } 76 | 77 | type TriggerStatus string 78 | 79 | const ( 80 | TriggerStatusEnabled TriggerStatus = "0" 81 | TriggerStatusDisabled TriggerStatus = "1" 82 | ) 83 | 84 | func (c *Client) SetTriggersStatus(ctx context.Context, triggerID string, status TriggerStatus) ([]string, error) { 85 | type TriggerIDs struct { 86 | TriggerIDs []string `json:"triggerids"` 87 | } 88 | var ids TriggerIDs 89 | 90 | params := struct { 91 | TriggerID string `json:"triggerid"` 92 | Status string `json:"status"` 93 | }{ 94 | TriggerID: triggerID, 95 | Status: string(status), 96 | } 97 | if err := c.Client.Call(ctx, "trigger.update", params, &ids); err != nil { 98 | return nil, err 99 | } 100 | return ids.TriggerIDs, nil 101 | } 102 | -------------------------------------------------------------------------------- /internal/slicex/slicex.go: -------------------------------------------------------------------------------- 1 | package slicex 2 | 3 | import ( 4 | "golang.org/x/exp/constraints" 5 | "golang.org/x/exp/maps" 6 | "golang.org/x/exp/slices" 7 | ) 8 | 9 | func Map[E1, E2 any](x []E1, conv func(e E1) E2) []E2 { 10 | if x == nil { 11 | return nil 12 | } 13 | if len(x) == 0 { 14 | return []E2{} 15 | } 16 | res := make([]E2, len(x)) 17 | for i, e := range x { 18 | res[i] = conv(e) 19 | } 20 | return res 21 | } 22 | 23 | func FailableMap[E1, E2 any](x []E1, conv func(e E1) (E2, error)) ([]E2, error) { 24 | if x == nil { 25 | return nil, nil 26 | } 27 | if len(x) == 0 { 28 | return []E2{}, nil 29 | } 30 | res := make([]E2, len(x)) 31 | for i, e := range x { 32 | e2, err := conv(e) 33 | if err != nil { 34 | return nil, err 35 | } 36 | res[i] = e2 37 | } 38 | return res, nil 39 | } 40 | 41 | func ContainsDup[T comparable](x []T) bool { 42 | if len(x) == 0 { 43 | return false 44 | } 45 | 46 | s := make(map[T]struct{}) 47 | for _, e := range x { 48 | if _, ok := s[e]; ok { 49 | return true 50 | } 51 | s[e] = struct{}{} 52 | } 53 | return false 54 | } 55 | 56 | func ConcatDeDup[T constraints.Ordered](x ...[]T) []T { 57 | if x == nil { 58 | return nil 59 | } 60 | s := make(map[T]struct{}) 61 | for _, xx := range x { 62 | for _, e := range xx { 63 | if _, ok := s[e]; !ok { 64 | s[e] = struct{}{} 65 | } 66 | } 67 | } 68 | ret := maps.Keys(s) 69 | slices.Sort(ret) 70 | return ret 71 | } 72 | -------------------------------------------------------------------------------- /internal/slicex/slicex_test.go: -------------------------------------------------------------------------------- 1 | package slicex 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | ) 7 | 8 | func TestMap(t *testing.T) { 9 | testCases := []struct { 10 | input []int32 11 | want []int64 12 | }{ 13 | {input: nil, want: nil}, 14 | {input: []int32{}, want: []int64{}}, 15 | {input: []int32{1}, want: []int64{2}}, 16 | {input: []int32{1, 2}, want: []int64{2, 4}}, 17 | } 18 | for _, c := range testCases { 19 | got := Map(c.input, func(i int32) int64 { 20 | return 2 * int64(i) 21 | }) 22 | if want := c.want; !slices.Equal(got, want) { 23 | t.Errorf("result mismatch, input=%v, got=%v, want=%v", c.input, got, want) 24 | } 25 | } 26 | } 27 | 28 | func TestConcatDeDup(t *testing.T) { 29 | testCases := []struct { 30 | input [][]int 31 | want []int 32 | }{ 33 | {input: nil, want: nil}, 34 | {input: [][]int{}, want: []int{}}, 35 | {input: [][]int{{}, nil}, want: []int{}}, 36 | {input: [][]int{{1}, {1, 2}, {2, 3}}, want: []int{1, 2, 3}}, 37 | } 38 | for _, c := range testCases { 39 | if got, want := ConcatDeDup(c.input...), c.want; !slices.Equal(got, want) { 40 | t.Errorf("result mismatch, input=%v, got=%v, want=%v", c.input, got, want) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/setup_zabbix_postgresql_lxd.md: -------------------------------------------------------------------------------- 1 | # Set up Zabbix with PostgreSQL in a LXD Ubuntu 22.04 LTS container 2 | 3 | * Install steps references 4 | - LXD: [Getting started - LXD documentation](https://linuxcontainers.org/lxd/docs/latest/getting_started/) 5 | (Japanese: [LXD を使い始めるには - LXD ドキュメント](https://lxd-ja.readthedocs.io/ja/latest/getting_started/)) 6 | - PostgreSQL: https://wiki.postgresql.org/wiki/Apt 7 | - Zabbix: https://www.zabbix.com/download?zabbix=6.0&os_distribution=ubuntu&os_version=22.04&components=server_frontend_agent&db=pgsql&ws=nginx 8 | 9 | ```bash 10 | lxc launch ubuntu:22.04 zabbix 11 | ``` 12 | 13 | ```bash 14 | cat <<'EOF' > setup_zabbix.sh 15 | #!/bin/bash 16 | 17 | DB_PASSWORD=zbxpass 18 | 19 | apt-get -y install curl ca-certificates 20 | curl -sSL -o /etc/apt/keyrings/pgdg.asc 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x7FCC7D46ACCC4CF8' 21 | echo "deb [signed-by=/etc/apt/keyrings/pgdg.asc] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list 22 | 23 | apt-get update 24 | apt-get -y install postgresql-15 25 | 26 | curl -sSLO https://repo.zabbix.com/zabbix/6.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_6.0-4+ubuntu22.04_all.deb 27 | dpkg -i zabbix-release_6.0-4+ubuntu22.04_all.deb 28 | apt-get update 29 | 30 | apt-get -y install zabbix-server-pgsql zabbix-frontend-php php8.1-pgsql zabbix-nginx-conf zabbix-sql-scripts zabbix-agent 31 | 32 | sudo -iu postgres psql -c "CREATE USER zabbix PASSWORD '$DB_PASSWORD';" 33 | sudo -iu postgres createdb -O zabbix zabbix 34 | 35 | echo "*:*:*:zabbix:$DB_PASSWORD" > /var/lib/postgresql/.pgpass 36 | chown postgres: /var/lib/postgresql/.pgpass 37 | chmod 600 /var/lib/postgresql/.pgpass 38 | 39 | zcat /usr/share/zabbix-sql-scripts/postgresql/server.sql.gz | sudo -iu postgres psql -h localhost -U zabbix 40 | 41 | cp -p /etc/zabbix/zabbix_server.conf /etc/zabbix/zabbix_server.conf.orig 42 | sed '/^# DBPassword=/a\ 43 | \ 44 | DBPassword='"$DB_PASSWORD" /etc/zabbix/zabbix_server.conf.orig > /etc/zabbix/zabbix_server.conf 45 | 46 | rm /etc/nginx/sites-enabled/default 47 | 48 | cp -p /etc/zabbix/nginx.conf /etc/zabbix/nginx.conf.orig 49 | 50 | sed 's/^# *listen *8080;/ listen 80;/;s/^# *server_name *example.com;/ server_name example.com;/' /etc/zabbix/nginx.conf.orig > /etc/zabbix/nginx.conf 51 | 52 | systemctl stop apache2 53 | systemctl mask apache2 54 | 55 | systemctl restart zabbix-server zabbix-agent nginx php8.1-fpm 56 | systemctl enable zabbix-server zabbix-agent nginx php8.1-fpm 57 | EOF 58 | ``` 59 | 60 | ```bash 61 | chmod +x setup_zabbix.sh 62 | lxc file push setup_zabbix.sh zabbix/usr/local/sbin/ 63 | lxc exec zabbix /usr/local/sbin/setup_zabbix.sh 64 | ``` 65 | 66 | ```bash 67 | ipv4addr=$(lxc exec zabbix -- ip -4 -br addr show dev eth0 | awk '{sub(/\/.*/, "", $3); print $3}') 68 | echo open http://$ipv4addr and complete web interface installation 69 | ``` 70 | 71 | https://www.zabbix.com/documentation/current/en/manual/installation/frontend 72 | 73 | * Configure DB connection 74 | - Password にsetup_zabbix.sh内のDB_PASSWORDと同じ値を入力 75 | - Database TLS encryptionのチェックを外す 76 | 77 | * Settings 78 | - Zabbix server name にsetup_zabbix.sh内で/etc/zabbix/nginx.conf内の 79 | server_nameに指定したホスト名(上記の例ではexample.com)を入力 80 | - Default time zone を (UTC+09:00) Asia/Tokyo に設定 81 | 82 | https://www.zabbix.com/documentation/current/en/manual/quickstart/login#:~:text=This%20is%20the%20Zabbix%20welcome,in%20as%20a%20Zabbix%20superuser. 83 | 84 | * Administrator username: Admin 85 | * Initial password: zabbix 86 | 87 | ## Add group hosts and hosts 88 | 89 | ```bash 90 | lxc project create zabbix --config features.images=false --config features.profiles=false 91 | lxc stop zabbix 92 | lxc move zabbix zabbix --project default --target-project zabbix 93 | 94 | lxc project switch zabbix 95 | ``` 96 | 97 | ```bash 98 | CLIENTS=$(echo sv0{1,2}-grp{1,2} | tr ' ' '\n') 99 | ``` 100 | 101 | ```bash 102 | echo "$CLIENTS" | xargs -I % -P 0 lxc launch ubuntu:22.04 % 103 | ``` 104 | 105 | ```bash 106 | ZBX_SERVER_IP=$(lxc info zabbix | sed -n '/^ *eth0:$/,/^ *inet:/{/^ *inet:/{s/^ *inet: *\([^/]*\).*/\1/;p}}') 107 | cat < zabbix_server.conf 108 | Server=$ZBX_SERVER_IP 109 | ServerActive=$ZBX_SERVER_IP 110 | EOF 111 | ``` 112 | 113 | ```bash 114 | cat < setup_zabbix_client.sh 115 | #!/bin/bash 116 | curl -sSLO https://repo.zabbix.com/zabbix/6.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_6.0-4+ubuntu22.04_all.deb 117 | dpkg -i zabbix-release_6.0-4+ubuntu22.04_all.deb 118 | apt-get update 119 | rm zabbix-release_6.0-4+ubuntu22.04_all.deb 120 | apt-get -y install zabbix-agent 121 | EOF 122 | 123 | chmod +x setup_zabbix_client.sh 124 | ``` 125 | 126 | ```bash 127 | lxc file push setup_zabbix_client.sh sv01-grp1/usr/local/sbin/ 128 | lxc exec sv01-grp1 -- /usr/local/sbin/setup_zabbix_client.sh 129 | 130 | lxc file push zabbix_server.conf sv01-grp1//etc/zabbix/zabbix_agentd.d/ 131 | 132 | echo "$CLIENTS" | xargs -I % -P 0 lxc file push setup_zabbix_client.sh %/usr/local/sbin/ 133 | echo "$CLIENTS" | xargs -I % -P 0 lxc exec % -- /usr/local/sbin/setup_zabbix_client.sh 134 | 135 | echo "$CLIENTS" | xargs -I % -P 0 lxc file push zabbix_server.conf %/etc/zabbix/zabbix_agentd.d/ 136 | echo "$CLIENTS" | xargs -I % -P 0 lxc exec % -- systemctl restart zabbix-agent 137 | ``` 138 | --------------------------------------------------------------------------------