From 4a8184652762efd294d5112ae4215a8adfe66d19 Mon Sep 17 00:00:00 2001 From: SerWax <69587784+SerWax@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:02:53 +0200 Subject: [PATCH 1/7] Add Zabbix API source Nodes can be sourced from Zabbix API. In Zabbix, add or modify template with the Macros that you then specify in Oxidized config. Example: source: default: zabbix zabbix: url: "https://zabbix.yourdomain.com/api_jsonrpc.php" token: "yourzabbixapitoken" template: "Cisco IOS by SNMP" map: username: "{$OXIDIZED_USERNAME}" password: "{$OXIDIZED_PASSWORD}" model: "{$OXIDIZED_MODEL}" vars_map: group: "{$OXIDIZED_GROUP}" # enable: "{$OXIDIZED_ENABLE}" --- lib/oxidized/source/zabbix.rb | 140 ++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 lib/oxidized/source/zabbix.rb diff --git a/lib/oxidized/source/zabbix.rb b/lib/oxidized/source/zabbix.rb new file mode 100644 index 000000000..02c49766b --- /dev/null +++ b/lib/oxidized/source/zabbix.rb @@ -0,0 +1,140 @@ +# ~/.config/oxidized/source/zabbix.rb +require 'net/http' +require 'json' +require 'uri' +require 'openssl' + +module Oxidized + module Source + class Zabbix < Source + class NoConfig < OxidizedError; end + + def initialize + super + @cfg = Oxidized.config.source.zabbix + unless @cfg.url && @cfg.token && @cfg.template + raise NoConfig, 'Please set source.zabbix.url, token and template' + end + + # Which macros you care about + @needed = { + @cfg.map.username.to_s => :username, + @cfg.map.password.to_s => :password, + @cfg.map.model.to_s => :model, + @cfg.vars_map.group.to_s => :group, + @cfg.vars_map.enable.to_s => :enable + } + end + + def load(_ = nil) + token = @cfg.token + + ### Step 1: Get the base template ID for your named template + tpl = rpc('template.get', + { filter: { host: [@cfg.template] }, + output: ['templateid'] }, + token).first or return [] + tpl_id = tpl['templateid'] + + ### Step 2: Fetch all hosts linked to that template + hosts = rpc('host.get', + { templateids: [tpl_id], + output: ['hostid','host'], + filter: { status: '0' }, + selectParentTemplates: ['templateid'] }, + token) || [] + return [] if hosts.empty? + + # Pre‐fetch interfaces + hostids = hosts.map { |h| h['hostid'] } + ifaces = rpc('hostinterface.get', + { hostids: hostids, + output: ['hostid','useip','ip','dns'] }, + token) || [] + + nodes = [] + + hosts.each do |h| + hid = h['hostid'] + + # 2a) Host’s direct templates + direct_tpls = (h['parentTemplates'] || []).map { |pt| pt['templateid'] } + + # 2b) For each direct template, fetch its parents + all_tpls = direct_tpls.dup + direct_tpls.each do |tid| + parents = rpc('template.get', + { templateids: [tid], + output: ['templateid'], + selectParentTemplates: ['templateid'] }, + token).first['parentTemplates'] rescue [] + parents.map { |pt| pt['templateid'] }.each do |pid| + all_tpls << pid unless all_tpls.include?(pid) + end + end + + # Build the full ID list: host + direct tpl + their parents + id_list = [hid] | all_tpls + + ### Step 3: Pull **all** macros for those IDs + macros = rpc('usermacro.get', + { output: 'extend', + hostids: id_list, + secrets: false }, + token) || [] + + # Map macros into a hash + macro_map = {} + macros.each do |m| + next unless @needed[m['macro'].to_s] + key = @needed[m['macro'].to_s] + macro_map[key] = node_var_interpolate(m['value'].to_s) + end + + # Build the node + node = { + name: h['host'], + ip: pick_ip(ifaces.select { |i| i['hostid'] == hid }) + }.merge(macro_map) + + # Only set enable if non‐empty + if node[:enable] && !node[:enable].strip.empty? + node[:vars] = { enable: node.delete(:enable) } + end + + node[:model] = node[:model].to_s if node[:model] + nodes << node + end + + nodes + end + + private + + # pick the first non-empty IP + def pick_ip(if_list) + ips = if_list.map { |i| i['ip'].to_s }.reject(&:empty?) + ips.first || '' + end + + # JSON-RPC helper, always returns an Array + def rpc(method, params, token) + uri = URI.parse(@cfg.url) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = (uri.scheme == 'https') + body = { jsonrpc: '2.0', method: method, params: params, id: 1 }.to_json + headers = { + 'Content-Type' => 'application/json-rpc', + 'Authorization' => "Bearer #{token}" + } + resp = http.post(uri.request_uri, body, headers) + parsed = JSON.parse(resp.body) + return [] if parsed['error'] + parsed['result'] || [] + rescue StandardError => e + Oxidized.logger.error "Zabbix RPC(#{method}): #{e.class} #{e.message}" + [] + end + end + end +end From 121e875c2c59b194a4cc5b82731b91ee3fd0f6ef Mon Sep 17 00:00:00 2001 From: SerWax <69587784+SerWax@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:04:12 +0200 Subject: [PATCH 2/7] Update CHANGELOG.md for Zabbix API source --- CHANGELOG.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1147701eb..e64b8dfe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). -## [Unreleased] +## [Unreleased 1] ### Added - +Nodes can be sourced from Zabbix API. In Zabbix, add or modify template with the Macros that you then specify in Oxidized config. Example: +source: + default: zabbix + zabbix: + url: "https://zabbix.yourdomain.com/api_jsonrpc.php" + token: "yourzabbixapitoken" + template: "Cisco IOS by SNMP" + map: + username: "{$OXIDIZED_USERNAME}" + password: "{$OXIDIZED_PASSWORD}" + model: "{$OXIDIZED_MODEL}" + vars_map: + group: "{$OXIDIZED_GROUP}" +# enable: "{$OXIDIZED_ENABLE}" ### Changed ### Fixed From 23ddc1e6e5c4ec858b460f72b9e44f12ebb88f40 Mon Sep 17 00:00:00 2001 From: SerWax <69587784+SerWax@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:06:08 +0200 Subject: [PATCH 3/7] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e64b8dfe1..a904e4289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ source: model: "{$OXIDIZED_MODEL}" vars_map: group: "{$OXIDIZED_GROUP}" -# enable: "{$OXIDIZED_ENABLE}" + enable: "{$OXIDIZED_ENABLE}" ### Changed ### Fixed From ec10dd2f6ff299b9c37fb2bb66c520eb0e2eda77 Mon Sep 17 00:00:00 2001 From: SerWax <69587784+SerWax@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:07:10 +0200 Subject: [PATCH 4/7] Add filtering by tags to zabbix source Add filtering by tags to zabbix source --- lib/oxidized/source/zabbix.rb | 77 ++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/lib/oxidized/source/zabbix.rb b/lib/oxidized/source/zabbix.rb index 02c49766b..f2fbb4f32 100644 --- a/lib/oxidized/source/zabbix.rb +++ b/lib/oxidized/source/zabbix.rb @@ -1,4 +1,10 @@ # ~/.config/oxidized/source/zabbix.rb +# +# ▸ Hosts can be selected by the template named in source.zabbix.template +# or by Zabbix tags configured in source.zabbix.tags +# ▸ Template macros fetched via usermacro.get hostids:[TEMPLATE_ID] quirk +# ▸ Host-level overrides win + require 'net/http' require 'json' require 'uri' @@ -12,11 +18,10 @@ class NoConfig < OxidizedError; end def initialize super @cfg = Oxidized.config.source.zabbix - unless @cfg.url && @cfg.token && @cfg.template - raise NoConfig, 'Please set source.zabbix.url, token and template' + unless @cfg.url && @cfg.token && (@cfg.template || @cfg.tags) + raise NoConfig, 'Please set source.zabbix.url, token and template or tags' end - # Which macros you care about @needed = { @cfg.map.username.to_s => :username, @cfg.map.password.to_s => :password, @@ -28,28 +33,16 @@ def initialize def load(_ = nil) token = @cfg.token + params = host_get_params(token) + return [] unless params - ### Step 1: Get the base template ID for your named template - tpl = rpc('template.get', - { filter: { host: [@cfg.template] }, - output: ['templateid'] }, - token).first or return [] - tpl_id = tpl['templateid'] - - ### Step 2: Fetch all hosts linked to that template - hosts = rpc('host.get', - { templateids: [tpl_id], - output: ['hostid','host'], - filter: { status: '0' }, - selectParentTemplates: ['templateid'] }, - token) || [] + hosts = rpc('host.get', params, token) || [] return [] if hosts.empty? - # Pre‐fetch interfaces hostids = hosts.map { |h| h['hostid'] } ifaces = rpc('hostinterface.get', { hostids: hostids, - output: ['hostid','useip','ip','dns'] }, + output: ['hostid', 'useip', 'ip', 'dns'] }, token) || [] nodes = [] @@ -57,10 +50,8 @@ def load(_ = nil) hosts.each do |h| hid = h['hostid'] - # 2a) Host’s direct templates direct_tpls = (h['parentTemplates'] || []).map { |pt| pt['templateid'] } - # 2b) For each direct template, fetch its parents all_tpls = direct_tpls.dup direct_tpls.each do |tid| parents = rpc('template.get', @@ -73,31 +64,27 @@ def load(_ = nil) end end - # Build the full ID list: host + direct tpl + their parents id_list = [hid] | all_tpls - ### Step 3: Pull **all** macros for those IDs macros = rpc('usermacro.get', { output: 'extend', hostids: id_list, secrets: false }, token) || [] - # Map macros into a hash macro_map = {} macros.each do |m| next unless @needed[m['macro'].to_s] + key = @needed[m['macro'].to_s] macro_map[key] = node_var_interpolate(m['value'].to_s) end - # Build the node node = { name: h['host'], ip: pick_ip(ifaces.select { |i| i['hostid'] == hid }) }.merge(macro_map) - # Only set enable if non‐empty if node[:enable] && !node[:enable].strip.empty? node[:vars] = { enable: node.delete(:enable) } end @@ -111,13 +98,46 @@ def load(_ = nil) private - # pick the first non-empty IP + def host_get_params(token) + params = { + output: ['hostid', 'host'], + filter: { status: '0' }, + selectParentTemplates: ['templateid'] + } + + if @cfg.template + tpl = rpc('template.get', + { filter: { host: [@cfg.template] }, + output: ['templateid'] }, + token).first + return nil unless tpl + + params[:templateids] = [tpl['templateid']] + end + + params[:tags] = tag_filters if @cfg.tags + params + end + + def tag_filters + array_config(@cfg.tags).map { |tag| hash_config(tag) } + end + + def array_config(value) + value.is_a?(Array) ? value : [value] + end + + def hash_config(config) + config.each_with_object({}) do |(key, value), hash| + hash[key.to_s] = value + end + end + def pick_ip(if_list) ips = if_list.map { |i| i['ip'].to_s }.reject(&:empty?) ips.first || '' end - # JSON-RPC helper, always returns an Array def rpc(method, params, token) uri = URI.parse(@cfg.url) http = Net::HTTP.new(uri.host, uri.port) @@ -130,6 +150,7 @@ def rpc(method, params, token) resp = http.post(uri.request_uri, body, headers) parsed = JSON.parse(resp.body) return [] if parsed['error'] + parsed['result'] || [] rescue StandardError => e Oxidized.logger.error "Zabbix RPC(#{method}): #{e.class} #{e.message}" From ecf82be40fc41e663a49999eba64342cdf40d521 Mon Sep 17 00:00:00 2001 From: SerWax <69587784+SerWax@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:14:44 +0200 Subject: [PATCH 5/7] re-add inline comments --- lib/oxidized/source/zabbix.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/oxidized/source/zabbix.rb b/lib/oxidized/source/zabbix.rb index f2fbb4f32..f23e60b60 100644 --- a/lib/oxidized/source/zabbix.rb +++ b/lib/oxidized/source/zabbix.rb @@ -1,3 +1,6 @@ +Keep those comments. Here is the same minimal tag-filter version with your inline comments restored: + +```ruby # ~/.config/oxidized/source/zabbix.rb # # ▸ Hosts can be selected by the template named in source.zabbix.template @@ -22,6 +25,7 @@ def initialize raise NoConfig, 'Please set source.zabbix.url, token and template or tags' end + # Which macros you care about @needed = { @cfg.map.username.to_s => :username, @cfg.map.password.to_s => :password, @@ -39,6 +43,7 @@ def load(_ = nil) hosts = rpc('host.get', params, token) || [] return [] if hosts.empty? + # Pre‐fetch interfaces hostids = hosts.map { |h| h['hostid'] } ifaces = rpc('hostinterface.get', { hostids: hostids, @@ -50,8 +55,10 @@ def load(_ = nil) hosts.each do |h| hid = h['hostid'] + # 2a) Host’s direct templates direct_tpls = (h['parentTemplates'] || []).map { |pt| pt['templateid'] } + # 2b) For each direct template, fetch its parents all_tpls = direct_tpls.dup direct_tpls.each do |tid| parents = rpc('template.get', @@ -64,14 +71,17 @@ def load(_ = nil) end end + # Build the full ID list: host + direct tpl + their parents id_list = [hid] | all_tpls + # Pull all macros for those IDs macros = rpc('usermacro.get', { output: 'extend', hostids: id_list, secrets: false }, token) || [] + # Map macros into a hash macro_map = {} macros.each do |m| next unless @needed[m['macro'].to_s] @@ -80,11 +90,13 @@ def load(_ = nil) macro_map[key] = node_var_interpolate(m['value'].to_s) end + # Build the node node = { name: h['host'], ip: pick_ip(ifaces.select { |i| i['hostid'] == hid }) }.merge(macro_map) + # Only set enable if non‐empty if node[:enable] && !node[:enable].strip.empty? node[:vars] = { enable: node.delete(:enable) } end @@ -133,11 +145,13 @@ def hash_config(config) end end + # pick the first non-empty IP def pick_ip(if_list) ips = if_list.map { |i| i['ip'].to_s }.reject(&:empty?) ips.first || '' end + # JSON-RPC helper, always returns an Array def rpc(method, params, token) uri = URI.parse(@cfg.url) http = Net::HTTP.new(uri.host, uri.port) @@ -159,3 +173,4 @@ def rpc(method, params, token) end end end +``` From 8ec2c19ad4c1920e3468009d3314ac4c96381630 Mon Sep 17 00:00:00 2001 From: SerWax <69587784+SerWax@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:18:41 +0200 Subject: [PATCH 6/7] Update Zabbix source with tags --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a904e4289..0aa2a586d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). -## [Unreleased 1] +## [Unreleased] ### Added +- Add Zabbix source. Nodes can be sourced from Zabbix API. In Zabbix, add or modify template with the Macros that you then specify in Oxidized config. Example: source: default: zabbix @@ -20,6 +21,9 @@ source: vars_map: group: "{$OXIDIZED_GROUP}" enable: "{$OXIDIZED_ENABLE}" + + - Update Zabbix source to implement filtering by tags are requested by other users. + - ### Changed ### Fixed From 66f1ee6d443824fd6c8c5b885a197e37f55705bb Mon Sep 17 00:00:00 2001 From: SerWax <69587784+SerWax@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:19:22 +0200 Subject: [PATCH 7/7] Update Zabbix source with tags --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aa2a586d..8beae66fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,8 @@ source: group: "{$OXIDIZED_GROUP}" enable: "{$OXIDIZED_ENABLE}" - - Update Zabbix source to implement filtering by tags are requested by other users. - - +- Update Zabbix source to implement filtering by tags are requested by other users. + ### Changed ### Fixed