diff --git a/lib/puppet/provider/grafana.rb b/lib/puppet/provider/grafana.rb new file mode 100644 index 000000000..b28a84913 --- /dev/null +++ b/lib/puppet/provider/grafana.rb @@ -0,0 +1,86 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +require 'cgi' +require 'json' +require 'net/http' + +class Puppet::Provider::Grafana < Puppet::Provider + # Helper methods + def grafana_host + unless @grafana_host + @grafana_host = URI.parse(resource[:grafana_url]).host + end + @grafana_host + end + + def grafana_port + unless @grafana_port + @grafana_port = URI.parse(resource[:grafana_url]).port + end + @grafana_port + end + + def grafana_scheme + unless @grafana_scheme + @grafana_scheme = URI.parse(resource[:grafana_url]).scheme + end + @grafana_scheme + end + + # Return a Net::HTTP::Response object + def send_request(operation="GET", path="", data=nil, search_path={}) + request = nil + encoded_search = "" + + if URI.respond_to?(:encode_www_form) + encoded_search = URI.encode_www_form(search_path) + else + # Ideally we would have use URI.encode_www_form but it isn't + # available with Ruby 1.8.x that ships with CentOS 6.5. + encoded_search = search_path.to_a.map do |x| + x.map{|y| CGI.escape(y.to_s)}.join('=') + end + encoded_search = encoded_search.join('&') + end + uri = URI.parse("%s://%s:%d%s?%s" % [ + self.grafana_scheme, self.grafana_host, self.grafana_port, + path, encoded_search]) + + case operation.upcase + when 'POST' + request = Net::HTTP::Post.new(uri.request_uri) + request.body = data.to_json() + when 'PUT' + request = Net::HTTP::Put.new(uri.request_uri) + request.body = data.to_json() + when 'GET' + request = Net::HTTP::Get.new(uri.request_uri) + when 'DELETE' + request = Net::HTTP::Delete.new(uri.request_uri) + else + raise Puppet::Error, "Unsupported HTTP operation '%s'" % operation + end + + request.content_type = 'application/json' + if resource[:grafana_user] and resource[:grafana_user] + request.basic_auth resource[:grafana_user], resource[:grafana_password] + end + + return Net::HTTP.start(self.grafana_host, self.grafana_port) do |http| + http.request(request) + end + end +end + diff --git a/lib/puppet/provider/grafana_datasource/grafana.rb b/lib/puppet/provider/grafana_datasource/grafana.rb new file mode 100644 index 000000000..b37f548bb --- /dev/null +++ b/lib/puppet/provider/grafana_datasource/grafana.rb @@ -0,0 +1,184 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +require 'json' + +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'grafana')) + +Puppet::Type.type(:grafana_datasource).provide(:grafana, :parent => Puppet::Provider::Grafana) do + desc "Support for Grafana datasources" + + defaultfor :kernel => 'Linux' + + def datasources + response = self.send_request('GET', '/api/datasources') + if response.code != '200' + fail("Fail to retrieve datasources (HTTP response: %s/%s)" % + [response.code, response.body]) + end + + begin + datasources = JSON.parse(response.body) + rescue JSON::ParserError + fail("Fail to parse response: %s" % response.body) + end + + datasources.collect do |datasource| + { + :id => datasource["id"], + :name => datasource["name"], + :url => datasource["url"], + :type => datasource["type"], + :user => datasource["user"], + :password => datasource["password"], + :database => datasource["database"], + :access_mode => datasource["access"], + :is_default => datasource["isDefault"] ? :true : :false, + :json_data => datasource["jsonData"] + } + end + end + + def datasource + unless @datasource + @datasource = self.datasources.find { |x| x[:name] == resource[:name] } + end + @datasource + end + + def datasource=(value) + @datasource = value + end + + def type + self.datasource[:type] + end + + def type=(value) + resource[:type] = value + self.save_datasource() + end + + def url + self.datasource[:url] + end + + def url=(value) + resource[:url] = value + self.save_datasource() + end + + def access_mode + self.datasource[:access_mode] + end + + def access_mode=(value) + self.resource[:access_mode] = value + self.save_datasource() + end + + def database + self.datasource[:database] + end + + def database=(value) + resource[:database] = value + self.save_datasource() + end + + def user + self.datasource[:user] + end + + def user=(value) + resource[:user] = value + self.save_datasource() + end + + def password + self.datasource[:password] + end + + def password=(value) + resource[:password] = value + self.save_datasource() + end + + def is_default + self.datasource[:is_default] + end + + def is_default=(value) + resource[:is_default] = value + self.save_datasource() + end + + def json_data + self.datasource[:json_data] + end + + def json_data=(value) + resource[:json_data] = value + self.save_datasource() + end + + def save_datasource + data = { + :name => resource[:name], + :type => resource[:type], + :url => resource[:url], + :access => resource[:access_mode], + :database => resource[:database], + :user => resource[:user], + :password => resource[:password], + :isDefault => (resource[:is_default] == :true), + :jsonData => resource[:json_data], + } + + if self.datasource.nil? + response = self.send_request('POST', '/api/datasources', data) + else + data[:id] = self.datasource[:id] + response = self.send_request('PUT', '/api/datasources/%s' % self.datasource[:id], data) + end + + if response.code != '200' + fail("Failed to create save '%s' (HTTP response: %s/'%s')" % + [resource[:name], response.code, response.body]) + end + self.datasource = nil + end + + def delete_datasource + response = self.send_request('DELETE', '/api/datasources/%s' % self.datasource[:id]) + + if response.code != '200' + fail("Failed to delete datasource '%s' (HTTP response: %s/'%s')" % + [resource[:name], response.code, response.body]) + end + self.datasource = nil + end + + def create + self.save_datasource() + end + + def destroy + self.delete_datasource() + end + + def exists? + self.datasource + end +end diff --git a/lib/puppet/type/grafana_datasource.rb b/lib/puppet/type/grafana_datasource.rb new file mode 100644 index 000000000..c16ba85b4 --- /dev/null +++ b/lib/puppet/type/grafana_datasource.rb @@ -0,0 +1,91 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +Puppet::Type.newtype(:grafana_datasource) do + @doc = "Manage datasources in Grafana" + + ensurable + + newparam(:name, :namevar => true) do + desc "The name of the datasource." + end + + newparam(:grafana_url) do + desc "The URL of the Grafana server" + defaultto "" + + validate do |value| + unless value =~ /^https?:\/\// + raise ArgumentError , "'%s' is not a valid URL" % value + end + end + end + + newparam(:grafana_user) do + desc "The username for the Grafana server" + end + + newparam(:grafana_password) do + desc "The password for the Grafana server" + end + + newproperty(:url) do + desc "The URL of the datasource" + + validate do |value| + unless value =~ /^https?:\/\// + raise ArgumentError , "'%s' is not a valid URL" % value + end + end + end + + newproperty(:type) do + desc "The datasource type" + newvalues(:influxdb, :elasticsearch, :graphite, :kairosdb, :opentsdb, :prometheus) + end + + newproperty(:user) do + desc "The username for the datasource (optional)" + end + + newproperty(:password) do + desc "The password for the datasource (optional)" + end + + newproperty(:database) do + desc "The name of the database (optional)" + end + + newproperty(:access_mode) do + desc "Whether the datasource is accessed directly or not by the clients" + newvalues(:direct, :proxy) + defaultto :direct + end + + newproperty(:is_default) do + desc "Whether the datasource is the default one" + newvalues(:true, :false) + defaultto :false + end + + newproperty(:json_data) do + desc "Additional JSON data to configure the datasource (optional)" + + validate do |value| + unless value.nil? or value.is_a?(Hash) then + raise ArgumentError , "json_data should be a Hash!" + end + end + end +end diff --git a/spec/unit/puppet/type/grafana_datasource_type_spec.rb b/spec/unit/puppet/type/grafana_datasource_type_spec.rb new file mode 100644 index 000000000..b7cd78020 --- /dev/null +++ b/spec/unit/puppet/type/grafana_datasource_type_spec.rb @@ -0,0 +1,38 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +require 'spec_helper' + +describe Puppet::Type.type(:grafana_datasource) do + context "when setting parameters" do + + it "should fail if grafana_url isn't HTTP-based" do + expect { + described_class.new :name => "foo", :grafana_url => "example.com", :content => "{}", :ensure => :present + }.to raise_error(Puppet::Error, /not a valid URL/) + end + + it "should fail if json_data isn't valid" do + expect { + described_class.new :name => "foo", :grafana_url => "http://example.com", :json_data => "invalid", :ensure => :present + }.to raise_error(Puppet::Error, /json_data should be a Hash/) + end + + it "should accept valid parameters" do + resource = described_class.new :name => "foo", :grafana_url => "http://example.com", :url => 'http://influx.example.com' + expect(resource[:name]).to eq('foo') + expect(resource[:grafana_url]).to eq('http://example.com') + expect(resource[:url]).to eq('http://influx.example.com') + end + end +end