diff --git a/.kitchen.yml b/.kitchen.yml index 25d1b6d..6cb2471 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -19,3 +19,12 @@ suites: run_list: - "recipe[zabbix::default]" attributes: +- name: screen_resource + run_list: + - recipe[apt] + - recipe[zabbix_test::screen_resource] + attributes: + excludes: + - ubuntu-10.04 + - centos-5.9 + - centos-6.4 diff --git a/Berksfile b/Berksfile index 44bc4fc..6014543 100644 --- a/Berksfile +++ b/Berksfile @@ -1,3 +1,8 @@ source 'https://api.berkshelf.com' metadata + +cookbook 'apt' +cookbook 'mysql', '~> 5.5.4' + +cookbook 'zabbix_test', :path => './test/kitchen/cookbooks/zabbix_test' diff --git a/providers/screen.rb b/providers/screen.rb new file mode 100644 index 0000000..267b79a --- /dev/null +++ b/providers/screen.rb @@ -0,0 +1,208 @@ +action :create_or_update do + Chef::Zabbix.with_connection(new_resource.server_connection) do |connection| + get_screen_request = { + :method => 'screen.get', + :params => { + :filter => { + :name => new_resource.name + } + } + } + screens = connection.query(get_screen_request) + + if screens.size == 0 + Chef::Log.info "Screen does not exists. Proceeding to create this screen on the Zabbix server: '#{new_resource.name}'" + run_action :create + new_resource.updated_by_last_action(true) + else + Chef::Log.info "Going to update this screen (if needed): '#{new_resource.name}'" + run_action :update + end + end +end + +action :create do + Chef::Zabbix.with_connection(new_resource.server_connection) do |connection| + + looked_up_screen_items = add_resource_ids_for_screen_items(new_resource.screen_items, connection) + + request = { + :method => 'screen.create', + :params => { + :name => new_resource.name, + :hsize => new_resource.hsize, + :vsize => new_resource.vsize, + :screenitems => looked_up_screen_items + } + } + Chef::Log.info "Creating new screen: '#{new_resource.name}'" + connection.query(request) + end + new_resource.updated_by_last_action(true) +end + +action :update do + Chef::Zabbix.with_connection(new_resource.server_connection) do |connection| + + get_screen_request = { + :method => 'screen.get', + :params => { + :filter => { + :name => new_resource.name + }, + :output => :extend, + :selectScreenItems => :extend + } + } + screen = connection.query(get_screen_request) + if screen.nil? || screen.empty? + Chef::Application.fatal! "Could not find screen for update: '#{new_resource.name}'" + else + screen = screen.first + end + + looked_up_screen_items = add_resource_ids_for_screen_items(new_resource.screen_items, connection) + + need_to_update = false + + %w(name vsize hsize).each do |attr_name| + need_to_update = true if screen[attr_name] != new_resource.send(attr_name).to_s + end + + # We sort both of the arrays to make sure we can compare their elements + # one by one + screen['screenitems'].sort_by! { |hsh| hsh['resourceid'] } + looked_up_screen_items.sort_by! { |hsh| hsh[:resourceid] } + + looked_up_screen_items.each_with_index do |screen_item, index| + screen_item.each do |key, value| + need_to_update = true if screen['screenitems'][index].nil? || value.to_s != screen['screenitems'][index][key.to_s] + end + end + + if need_to_update + screen_update_request = { + :method => 'screen.update', + :params => { + :screenid => screen['screenid'], + :name => new_resource.name, + :hsize => new_resource.hsize, + :vsize => new_resource.vsize, + :screenitems => looked_up_screen_items + } + } + Chef::Log.info "Updating screen '#{new_resource.name}'" + connection.query(screen_update_request) + new_resource.updated_by_last_action(true) + else + Chef::Log.info "The attributes of screen '#{new_resource.name}' are already up-to-date, doing nothing" + end + + end +end + +action :delete do + Chef::Zabbix.with_connection(new_resource.server_connection) do |connection| + + get_screen_request = { + :method => 'screen.get', + :params => { + :filter => { + :name => new_resource.name + }, + :output => :shorten + } + } + screen = connection.query(get_screen_request) + if screen.nil? || screen.empty? + Chef::Application.fatal! "Could not find screen for deletion: '#{new_resource.name}'" + else + screen = screen.first + end + + screen_delete_request = { + :method => 'screen.delete', + :params => [ + screen['screenid'] + ] + } + Chef::Log.info "Deleting screen '#{new_resource.name}'" + result = connection.query(screen_delete_request) + Application.fatal! "Error deleting screen '#{new_resource.name}', see Chef errors" if result.nil? || result.empty? || result['screenids'].nil? || result['screenids'].empty? || !result['screenids'].include?(screen['screenid']) + new_resource.updated_by_last_action(true) + end +end + +def load_current_resource + run_context.include_recipe 'zabbix::_providers_common' + require 'zabbixapi' +end + +def add_resource_ids_for_screen_items(screen_items, connection) + returned_screen_items = [] + screen_items.each do |current_screen_item| + if current_screen_item[:resourceid].is_a? Fixnum + # We make it to a String for possible later comparison/sorting, because the API + # can only return String values anyway + current_screen_item[:resourceid] = current_screen_item[:resourceid].to_s + returned_screen_items.push current_screen_item + else + case current_screen_item[:resourcetype] + when 0 + Chef::Application.fatal! "When 'resourcetype' is 0 and 'resourceid' is a Hash, then you must set 'name' in that Hash" if current_screen_item[:resourceid][:name].nil? + Chef::Log.debug "Checking graph id for: host: '#{current_screen_item[:resourceid][:host]}', name: '#{current_screen_item[:resourceid][:name]}'" + current_screen_item[:resourceid] = get_graph_id_from_graph_name(current_screen_item[:resourceid][:host], current_screen_item[:resourceid][:name], connection) + when 1, 3 + Chef::Application.fatal! "When 'resourcetype' is 1 or 3 and 'resourceid' is a Hash, then you must set 'key_' in that Hash" if current_screen_item[:resourceid][:key_].nil? + Chef::Log.debug "Checking item id for: host: '#{current_screen_item[:resourceid][:host]}', key: '#{current_screen_item[:resourceid][:key_]}'" + current_screen_item[:resourceid] = get_item_id_from_item_key(current_screen_item[:resourceid][:host], current_screen_item[:resourceid][:key_], connection) + else + Chef::Application.fatal! "You either have to supply a correct Numeric 'resourceid' paramater or if you use a lookup Hash instead, then the 'resourcetype' must be 0, 1 or 3!" + end + returned_screen_items.push current_screen_item + end + end + returned_screen_items +end + +def get_graph_id_from_graph_name(host_name, name, connection) + return_id = nil + get_graph_id_request = { + :method => 'host.get', + :params => { + :selectGraphs => ['graphid', 'name'], + :output => :shorten, + :filter => { + :host => host_name + } + } + } + graph_id_result = connection.query(get_graph_id_request) + Chef::Application.fatal! "Could not find graphs at all for host: '#{host_name}'" if graph_id_result.nil? || graph_id_result.empty? + graph_id_result.first['graphs'].each do |graph| + return_id = graph['graphid'] if graph['name'] == name + end + Chef::Application.fatal! "Could not find graph id for host: '#{host_name}', name: '#{name}'" if return_id.nil? + return_id +end + +def get_item_id_from_item_key(host_name, key_, connection) + return_id = nil + get_item_id_request = { + :method => 'host.get', + :params => { + :selectItems => ['itemid', 'key_'], + :output => :shorten, + :filter => { + :host => host_name + } + } + } + item_id_result = connection.query(get_item_id_request) + Chef::Application.fatal! "Could not find items at all for host: '#{host_name}'" if item_id_result.nil? || item_id_result.empty? + item_id_result.first['items'].each do |item| + return_id = item['itemid'] if item['key_'] == key_ + end + Chef::Application.fatal! "Could not find item id for host: '#{host_name}', key_: '#{key_}'" if return_id.nil? + return_id +end diff --git a/resources/screen.rb b/resources/screen.rb new file mode 100644 index 0000000..168849c --- /dev/null +++ b/resources/screen.rb @@ -0,0 +1,62 @@ +actions :create_or_update, :create, :update, :delete +default_action :create_or_update + +attribute :name, :kind_of => String, :required => true + +attribute :hsize, :kind_of => Integer, :default => 1 +attribute :vsize, :kind_of => Integer, :default => 1 + +# This accepts an Array of Zabbix ScreenItem objects supplied as Ruby +# Hashes for the attributes of attached ScreenItem objects +# +# The `resourceid` is a special attribute, which can either be: +# - Fixnum: in that case that will be used in the Zabbix API as-is +# - Hash: containing the `:host` symbol as a key to indicate which +# host machine's resources will we use, and also containing either of these: +# - name: only valid when the `resourcetype` is 0, it will be used to get +# the graph of the given host as a normal graph resource +# - key_: only valid when the `resourcetype` is 1, it will identify the item +# of the given host to use as a simple graph resource +# Please note: only `resourcetype` 0, 1 and 3 lookups are supported currently +# +# Example: +# [{ +# resourcetype: 1, +# colspan: 1, +# rowspan: 1, +# elements: 25, +# height: 200, +# resourceid: {host: 'Host Name', key_: 'system.cpu.load[percpu,avg1]'}, +# width: 320, +# x: 1, +# y: 1 +# }, +# { +# resourcetype: 0, +# colspan: 1, +# rowspan: 1, +# elements: 25, +# height: 200, +# resourceid: {host: 'Host Name', name: 'CPU load'}, +# width: 320, +# x: 1, +# y: 1 +# }, +# { +# resourcetype: 0 +# colspan: 1, +# rowspan: 1, +# elements: 25, +# height: 200, +# resourceid: 581, +# width: 320, +# x: 1, +# y: 1 +# }] +attribute :screen_items, :kind_of => Array, :default => [] + +# This is a Ruby Hash object with 3 required parameters used for connecting to +# the Zabbix server's API. An example could be this: +# { url: "https://zabbix.server.address.com/api_jsonrpc.php", user: 'Admin', +# password: 'zabbix' } +attribute :server_connection, :kind_of => Hash, :default => {} diff --git a/test/kitchen/Kitchenfile b/test/kitchen/Kitchenfile index 9fd07f4..49a10c1 100644 --- a/test/kitchen/Kitchenfile +++ b/test/kitchen/Kitchenfile @@ -1,6 +1,7 @@ cookbook "zabbix" do configuration "default" configuration "server_source" + configuration "screen_resource" exclude :platform => 'centos' exclude :platform => 'debian' exclude :platform => 'redhat' diff --git a/test/kitchen/cookbooks/zabbix_test/metadata.rb b/test/kitchen/cookbooks/zabbix_test/metadata.rb index df72ca4..a5cfd3b 100644 --- a/test/kitchen/cookbooks/zabbix_test/metadata.rb +++ b/test/kitchen/cookbooks/zabbix_test/metadata.rb @@ -1,5 +1,8 @@ +name 'zabbix_test' maintainer 'Efactures' maintainer_email 'nacer.laradji@gmail.com' license 'Apache 2.0' description 'This cookbook is used with test-kitchen to test the parent, zabbix cookbook.' version '1.0.0' + +depends 'zabbix' diff --git a/test/kitchen/cookbooks/zabbix_test/recipes/screen_resource.rb b/test/kitchen/cookbooks/zabbix_test/recipes/screen_resource.rb new file mode 100644 index 0000000..88f5d27 --- /dev/null +++ b/test/kitchen/cookbooks/zabbix_test/recipes/screen_resource.rb @@ -0,0 +1,76 @@ +# Author:: Pal David Gergely () +# Cookbook Name:: zabbix-test +# Recipe:: screen_resource +# +# Copyright 2015, Pal David Gergely +# +# Apache 2.0 + +# First a working Zabbix server is needed to have an API to test the resource +# against it, so intalling it with a database +node.normal['zabbix']['server']['install'] = true +node.normal['zabbix']['server']['version'] = '2.2.8' +node.normal['zabbix']['database']['dbpassword'] = 'foobar' +include_recipe 'mysql::server' +# Needed for the mysql gem install +package 'libmysqlclient-dev' +include_recipe 'zabbix::database' +include_recipe 'zabbix::server' +include_recipe 'zabbix::web' + +# Now trigger an immediate restart on Apache to get the Zabbix API working +# before using it, TODO: not very nice, but needed +service 'apache2' do + action :reload +end + +# Also add the VM itself to Zabbix hosts, since monitored items are needed for +# the screen items to be able to test them +zabbix_host 'localhost' do + parameters( + groupNames: ['Linux servers'], + templates: ['Template OS Linux'], + interfaces: [Chef::Zabbix::API::HostInterface.new(type: Chef::Zabbix::API::HostInterfaceType.new(1), main: 1, useip: 1, ip: '127.0.0.1', port: 10_050)] + ) + server_connection( + url: 'http://127.0.0.1/api_jsonrpc.php', + user: 'Admin', + password: 'zabbix' + ) +end + +# Then create a simple test screen to validate that the screen resource is +# working +zabbix_screen 'test-screen' do + hsize 2 + vsize 2 + screen_items [ + { + resourcetype: 1, + colspan: 1, + rowspan: 1, + elements: 25, + height: 200, + resourceid: { host: 'localhost', key_: 'system.cpu.load[percpu,avg1]' }, + width: 320, + x: 0, + y: 0 + }, + { + resourcetype: 0, + colspan: 0, + rowspan: 0, + elements: 25, + height: 200, + resourceid: { host: 'localhost', name: 'CPU load' }, + width: 320, + x: 1, + y: 1 + } + ] + server_connection( + url: 'http://127.0.0.1/api_jsonrpc.php', + user: 'Admin', + password: 'zabbix' + ) +end