ruby-puppetdb 1.0.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. data/.gitignore +3 -0
  2. data/COPYING +202 -0
  3. data/Modulefile +7 -0
  4. data/README.md +233 -0
  5. data/Rakefile +24 -0
  6. data/bin/find-nodes +56 -0
  7. data/examples/nova_functions.pp +16 -0
  8. data/examples/query_node_examples.pp +12 -0
  9. data/examples/query_resource_examples.pp +48 -0
  10. data/lib/puppet/application/query.rb +22 -0
  11. data/lib/puppet/face/query.rb +60 -0
  12. data/lib/puppet/parser/functions/pdbfactquery.rb +31 -0
  13. data/lib/puppet/parser/functions/pdbnodequery.rb +38 -0
  14. data/lib/puppet/parser/functions/pdbnodequery_all.rb +40 -0
  15. data/lib/puppet/parser/functions/pdbquery.rb +41 -0
  16. data/lib/puppet/parser/functions/pdbresourcequery.rb +39 -0
  17. data/lib/puppet/parser/functions/pdbresourcequery_all.rb +37 -0
  18. data/lib/puppet/parser/functions/pdbstatusquery.rb +31 -0
  19. data/lib/puppet/parser/functions/query_facts.rb +27 -0
  20. data/lib/puppet/parser/functions/query_nodes.rb +27 -0
  21. data/lib/puppetdb.rb +2 -0
  22. data/lib/puppetdb/astnode.rb +86 -0
  23. data/lib/puppetdb/connection.rb +62 -0
  24. data/lib/puppetdb/grammar.y +57 -0
  25. data/lib/puppetdb/lexer.l +28 -0
  26. data/lib/puppetdb/lexer.rb +135 -0
  27. data/lib/puppetdb/parser.rb +368 -0
  28. data/lib/puppetdb/util.rb +18 -0
  29. data/ruby-puppetdb.gemspec +16 -0
  30. data/spec/spec_helper.rb +4 -0
  31. data/spec/unit/puppet/parser/functions/pdbfactquery_spec.rb +19 -0
  32. data/spec/unit/puppet/parser/functions/pdbnodequery_all_spec.rb +19 -0
  33. data/spec/unit/puppet/parser/functions/pdbnodequery_spec.rb +19 -0
  34. data/spec/unit/puppet/parser/functions/pdbquery_spec.rb +19 -0
  35. data/spec/unit/puppet/parser/functions/pdbresourcequery_all_spec.rb +19 -0
  36. data/spec/unit/puppet/parser/functions/pdbresourcequery_spec.rb +19 -0
  37. data/spec/unit/puppet/parser/functions/pdbstatusquery_spec.rb +19 -0
  38. data/spec/unit/puppet/parser/functions/query_facts_spec.rb +11 -0
  39. data/spec/unit/puppet/parser/functions/query_nodes_spec.rb +11 -0
  40. metadata +118 -0
@@ -0,0 +1,37 @@
1
+ module Puppet::Parser::Functions
2
+ newfunction(:pdbresourcequery_all, :type => :rvalue, :doc => "\
3
+ Perform a PuppetDB resource query
4
+
5
+ The first argument is the resource query.
6
+ Second argument is optional but allows you to specify the item you want
7
+ from the returned hash.
8
+
9
+ Returns an array of hashes or array of strings if second argument is provided.
10
+
11
+ Examples:
12
+ # Return an array of hashes describing all files that are owned by root.
13
+ $ret = pdbresourcequery_all(
14
+ ['and',
15
+ ['=','type','File'],
16
+ ['=',['parameter','owner'],'root']])
17
+
18
+ # Return an array of host names having those resources
19
+ $ret = pdbresourcequery_all(
20
+ ['and',
21
+ ['=','type','File'],
22
+ ['=',['parameter','owner'],'root']], 'certname')") do |args|
23
+
24
+ raise(Puppet::ParseError, "pdbresourcequery_all(): Wrong number of arguments " +
25
+ "given (#{args.size} for 1 or 2)") if args.size < 1 or args.size > 2
26
+
27
+ Puppet::Parser::Functions.autoloader.load(:pdbquery) unless Puppet::Parser::Functions.autoloader.loaded?(:pdbquery)
28
+
29
+ resq, info = args
30
+ ret = function_pdbquery(['resources', resq])
31
+ if info then
32
+ ret.collect {|x| x[info]}
33
+ else
34
+ ret
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ module Puppet::Parser::Functions
2
+ newfunction(:pdbstatusquery, :type => :rvalue, :doc => "\
3
+ Perform a PuppetDB node status query
4
+
5
+ The first argument is the node to get the status for.
6
+ Second argument is optional, if specified only return that specific bit of
7
+ status, one of 'name', 'deactivated', 'catalog_timestamp' and 'facts_timestamp'.
8
+
9
+ Returns an array of hashes or a array of strings if second argument is supplied.
10
+
11
+ Examples:
12
+ # Get status for foo.example.com
13
+ pdbstatusquery('foo.example.com')
14
+ # Get catalog_timestamp for foo.example.com
15
+ pdbstatusquery('foo.example.com', 'catalog_timestamp')") do |args|
16
+
17
+ raise(Puppet::ParseError, "pdbquery(): Wrong number of arguments " +
18
+ "given (#{args.size} for 1 or 2)") if args.size < 1 or args.size > 2
19
+
20
+ Puppet::Parser::Functions.autoloader.load(:pdbquery) unless Puppet::Parser::Functions.autoloader.loaded?(:pdbquery)
21
+
22
+ node, status = args
23
+
24
+ ret = function_pdbquery(["status/nodes/#{node}"])
25
+ if status then
26
+ ret[status]
27
+ else
28
+ ret
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ Puppet::Parser::Functions.newfunction(:query_facts, :type => :rvalue, :arity => 2, :doc => <<-EOT
2
+
3
+ accepts two arguments, a query used to discover nodes, and a list of facts
4
+ that should be returned from those hosts.
5
+
6
+ The query specified should conform to the following format:
7
+ (Type[title] and fact_name<operator>fact_value) or ...
8
+ Package[mysql-server] and cluster_id=my_first_cluster
9
+
10
+ The facts list provided should be an array of fact names.
11
+
12
+ The result is a hash that maps the name of the nodes to a hash of facts that
13
+ contains the facts specified.
14
+
15
+ EOT
16
+ ) do |args|
17
+ query, facts = args
18
+
19
+ require 'puppet/util/puppetdb'
20
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'puppetdb/connection'))
21
+
22
+ puppetdb = PuppetDB::Connection.new(Puppet::Util::Puppetdb.server, Puppet::Util::Puppetdb.port)
23
+ if query.is_a? String then
24
+ query = puppetdb.parse_query query
25
+ end
26
+ puppetdb.facts(facts, query)
27
+ end
@@ -0,0 +1,27 @@
1
+ Puppet::Parser::Functions.newfunction(:query_nodes, :type => :rvalue, :arity => -2, :doc => <<-EOT
2
+
3
+ accepts two arguments, a query used to discover nodes, and a optional
4
+ fact that should be returned.
5
+
6
+ The query specified should conform to the following format:
7
+ (Type[title] and fact_name<operator>fact_value) or ...
8
+ Package["mysql-server"] and cluster_id=my_first_cluster
9
+
10
+ The second argument should be single fact (this argument is optional)
11
+
12
+ EOT
13
+ ) do |args|
14
+ query, fact = args
15
+
16
+ require 'puppet/util/puppetdb'
17
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'puppetdb/connection'))
18
+
19
+ puppetdb = PuppetDB::Connection.new(Puppet::Util::Puppetdb.server, Puppet::Util::Puppetdb.port)
20
+ if fact then
21
+ query = puppetdb.parse_query query, :facts if query.is_a? String
22
+ puppetdb.facts([fact], query).each_value.to_a
23
+ else
24
+ query = puppetdb.parse_query query, :nodes if query.is_a? String
25
+ puppetdb.query(:nodes, query).collect { |n| n['name'] }
26
+ end
27
+ end
data/lib/puppetdb.rb ADDED
@@ -0,0 +1,2 @@
1
+ module PuppetDB
2
+ end
@@ -0,0 +1,86 @@
1
+ class PuppetDB::ASTNode
2
+ attr_accessor :type, :value, :children
3
+
4
+ def initialize(type, value, children=[])
5
+ @type = type
6
+ @value = value
7
+ @children = children
8
+ end
9
+
10
+ # Generate the the query code for a subquery
11
+ #
12
+ # @param from_mode [Symbol] the mode you want to subquery from
13
+ # @param to_mode [Symbol] the mode you want to subquery to
14
+ # @param query the query inside the subquery
15
+ # @return [Array] the resulting subquery
16
+ def subquery(from_mode, to_mode, query)
17
+ return query if from_mode == to_mode
18
+ return ['in', (from_mode == :nodes) ? 'name' : 'certname',
19
+ ['extract', (to_mode == :nodes) ? 'name' : 'certname',
20
+ ["select-#{to_mode.to_s}", query]]]
21
+ end
22
+
23
+ # Go through the AST and optimize boolean expressions into triplets etc
24
+ # Changes the AST in place
25
+ #
26
+ # @return The optimized AST
27
+ def optimize
28
+ case @type
29
+ when :booleanop
30
+ @children.each do |c|
31
+ if c.type == :booleanop and c.value == @value
32
+ c.children.each { |cc| @children << cc }
33
+ @children.delete c
34
+ end
35
+ end
36
+ end
37
+ @children.each { |c| c.optimize }
38
+ return self
39
+ end
40
+
41
+ # Evalutate the node and all children
42
+ #
43
+ # @param mode [Symbol] The query mode we are evaluating for
44
+ # @return [Array] the resulting PuppetDB query
45
+ def evaluate(mode = :nodes)
46
+ case @type
47
+ when :booleanop
48
+ return [@value.to_s, *evaluate_children(mode)]
49
+ when :subquery
50
+ return subquery(mode, @value, *evaluate_children(@value))
51
+ when :exp
52
+ case @value
53
+ when :equals then op = '='
54
+ when :greaterthan then op = '>'
55
+ when :lessthan then op = '<'
56
+ when :match then op = '~'
57
+ end
58
+
59
+ case mode
60
+ when :nodes # we are trying to query for facts but are in node mode, do a subquery
61
+ return subquery(mode, :facts, ['and', ['=', 'name', @children[0].evaluate(mode)], [op, 'value', @children[1].evaluate(mode)]])
62
+ when :facts
63
+ return ['and', ['=', 'name', @children[0].evaluate(mode)], [op, 'value', @children[1].evaluate(mode)]]
64
+ when :resources
65
+ return [op, ['parameter', @children[0].evaluate(mode)], @children[1].evaluate(mode)]
66
+ end
67
+ when :string
68
+ return @value.to_s
69
+ when :number
70
+ return @value
71
+ when :boolean
72
+ return @value
73
+ when :resourcetitle
74
+ return ['=', 'title', @value]
75
+ when :resourcetype
76
+ return ['=', 'type', @value]
77
+ end
78
+ end
79
+
80
+ # Evaluate all children nodes
81
+ #
82
+ # @return [Array] The evaluate results of the children nodes
83
+ def evaluate_children(mode)
84
+ return children.collect { |c| c.evaluate mode }
85
+ end
86
+ end
@@ -0,0 +1,62 @@
1
+ require 'puppetdb'
2
+
3
+ class PuppetDB::Connection
4
+ require 'rubygems'
5
+ require 'puppet'
6
+ require 'puppetdb/parser'
7
+ require 'puppet/network/http_pool'
8
+ require 'uri'
9
+ require 'json'
10
+
11
+ def initialize(host='puppetdb', port=443, use_ssl=true)
12
+ @host = host
13
+ @port = port
14
+ @use_ssl = use_ssl
15
+ @parser = PuppetDB::Parser.new
16
+ end
17
+
18
+ # Parse a query string into a PuppetDB query
19
+ #
20
+ # @param query [String] the query string to parse
21
+ # @param endpoint [Symbol] the endpoint for which the query should be evaluated
22
+ # @return [Array] the PuppetDB query
23
+ def parse_query(query, endpoint=:nodes)
24
+ @parser.scan_str(query).optimize.evaluate endpoint
25
+ end
26
+
27
+ # Get the listed facts for all nodes matching query
28
+ # return it as a hash of hashes
29
+ #
30
+ # @param facts [Array] the list of facts to fetch
31
+ # @param nodequery [Array] the query to find the nodes to fetch facts for
32
+ # @return [Hash] a hash of hashes with facts for each node mathing query
33
+ def facts(facts, nodequery, http=nil)
34
+ q = ['and', ['in', 'certname', ['extract', 'certname', ['select-facts', nodequery]]], ['or', *facts.collect { |f| ['=', 'name', f]}]]
35
+ facts = {}
36
+ query(:facts, q, http).each do |fact|
37
+ if facts.include? fact['certname'] then
38
+ facts[fact['certname']][fact['name']] = fact['value']
39
+ else
40
+ facts[fact['certname']] = {fact['name'] => fact['value']}
41
+ end
42
+ end
43
+ facts
44
+ end
45
+
46
+ # Execute a PuppetDB query
47
+ #
48
+ # @param endpoint [Symbol] :resources, :facts or :nodes
49
+ # @param query [Array] query to execute
50
+ # @return [Array] the results of the query
51
+ def query(endpoint, query=nil, http=nil)
52
+ http ||= Puppet::Network::HttpPool.http_instance(@host, @port, @use_ssl)
53
+ headers = { "Accept" => "application/json" }
54
+
55
+ uri = "/v2/#{endpoint.to_s}"
56
+ uri += URI.escape "?query=#{query.to_json}" unless query.nil? or query.empty?
57
+
58
+ resp, data = http.get(uri, headers)
59
+ raise Puppet::Error, "PuppetDB query error: [#{resp.code}] #{resp.msg}, query: #{query.to_json}" unless resp.kind_of?(Net::HTTPSuccess)
60
+ return PSON.parse(data)
61
+ end
62
+ end
@@ -0,0 +1,57 @@
1
+ # vim: syntax=ruby
2
+
3
+
4
+ class PuppetDB::Parser
5
+
6
+ token LPAREN RPAREN LBRACK RBRACK LBRACE RBRACE
7
+ token EQUALS NOTEQUALS MATCH LESSTHAN GREATERTHAN
8
+ token NOT AND OR
9
+ token NUMBER STRING BOOLEAN
10
+
11
+ prechigh
12
+ right NOT
13
+ left EQUALS MATCH LESSTHAN GREATERTHAN
14
+ left AND
15
+ left OR
16
+ preclow
17
+
18
+ rule
19
+ query: exp
20
+
21
+ exp: LPAREN exp RPAREN { result = val[1] }
22
+ | NOT exp { result = ASTNode.new :booleanop, :not, [val[1]] }
23
+ | exp AND exp { result = ASTNode.new :booleanop, :and, [val[0], val[2]] }
24
+ | exp OR exp { result = ASTNode.new :booleanop, :or, [val[0], val[2]] }
25
+ | string EQUALS string { result = ASTNode.new :exp, :equals, [val[0], val[2]] }
26
+ | string EQUALS boolean { result = ASTNode.new :exp, :equals, [val[0], val[2]] }
27
+ | string EQUALS number { result = ASTNode.new :exp, :equals, [val[0], val[2]] }
28
+ | string GREATERTHAN number { result = ASTNode.new :exp, :greaterthan, [val[0], val[2]] }
29
+ | string LESSTHAN number { result = ASTNode.new :exp, :lessthan, [val[0], val[2]] }
30
+ | string MATCH string { result = ASTNode.new :exp, :match, [val[0], val[2]] }
31
+ | string NOTEQUALS number { result = ASTNode.new :booleanop, :not, [ASTNode.new(:exp, :equals, [val[0], val[2]])] }
32
+ | string NOTEQUALS boolean { result = ASTNode.new :booleanop, :not, [ASTNode.new(:exp, :equals, [val[0], val[2]])] }
33
+ | string NOTEQUALS string { result = ASTNode.new :booleanop, :not, [ASTNode.new(:exp, :equals, [val[0], val[2]])] }
34
+ | ressubquery
35
+
36
+ ressubquery: restype { result = ASTNode.new :subquery, :resources, [val[0]] }
37
+ | restitle { result = ASTNode.new :subquery, :resources, [val[0]] }
38
+ | resparams { result = ASTNode.new :subquery, :resources, [val[0]] }
39
+ | restype restitle { result = ASTNode.new :subquery, :resources, [ASTNode.new(:booleanop, :and, [val[0], val[1]])] }
40
+ | restitle resparams { result = ASTNode.new :subquery, :resources, [ASTNode.new(:booleanop, :and, [val[0], val[1]])] }
41
+ | restype resparams { result = ASTNode.new :subquery, :resources, [ASTNode.new(:booleanop, :and, [val[0], val[1]])] }
42
+ | restype restitle resparams { result = ASTNode.new :subquery, :resources, [ASTNode.new(:booleanop, :and, [val[0], val[1], val[2]])] }
43
+
44
+ restype: STRING { result = ASTNode.new :resourcetype, val[0] }
45
+ restitle: LBRACK STRING RBRACK { result = ASTNode.new :resourcetitle, val[1] }
46
+ resparams: LBRACE exp RBRACE { result = val[1] }
47
+
48
+ string: STRING { result = ASTNode.new :string, val[0] }
49
+ number: NUMBER { result = ASTNode.new :number, val[0] }
50
+ boolean: BOOLEAN { result = ASTNode.new :boolean, val[0] }
51
+
52
+ end
53
+ ---- header ----
54
+ require 'puppetdb'
55
+ require 'puppetdb/lexer'
56
+ require 'puppetdb/astnode'
57
+
@@ -0,0 +1,28 @@
1
+ # vim: syntax=ruby
2
+
3
+ require 'json'
4
+
5
+ class PuppetDB::Lexer
6
+ rule
7
+ \s # whitespace no action
8
+ \( { [:LPAREN, text] }
9
+ \) { [:RPAREN, text] }
10
+ \[ { [:LBRACK, text] }
11
+ \] { [:RBRACK, text] }
12
+ \{ { [:LBRACE, text] }
13
+ \} { [:RBRACE, text] }
14
+ = { [:EQUALS, text] }
15
+ \!= { [:NOTEQUALS, text] }
16
+ ~ { [:MATCH, text] }
17
+ < { [:LESSTHAN, text] }
18
+ > { [:GREATERTHAN, text] }
19
+ not { [:NOT, text] }
20
+ and { [:AND, text] }
21
+ or { [:OR, text] }
22
+ true { [:BOOLEAN, true]}
23
+ false { [:BOOLEAN, false]}
24
+ -?\d+ { [:NUMBER, text.to_i] }
25
+ -?\d+\.?(\d+)? { [:NUMBER, text.to_f] }
26
+ \"(\\.|[^\\"])*\" { [:STRING, JSON.load(text)] }
27
+ [\w_:]+ { [:STRING, text] }
28
+ end
@@ -0,0 +1,135 @@
1
+ #--
2
+ # DO NOT MODIFY!!!!
3
+ # This file is automatically generated by rex 1.0.2
4
+ # from lexical definition file "puppetdb/lexer.l".
5
+ #++
6
+
7
+ require 'racc/parser'
8
+ # vim: syntax=ruby
9
+
10
+ require 'json'
11
+
12
+ module PuppetDB
13
+ class Lexer < Racc::Parser
14
+ require 'strscan'
15
+
16
+ class ScanError < StandardError ; end
17
+
18
+ attr_reader :lineno
19
+ attr_reader :filename
20
+
21
+ def scan_setup ; end
22
+
23
+ def action &block
24
+ yield
25
+ end
26
+
27
+ def scan_str( str )
28
+ scan_evaluate str
29
+ do_parse
30
+ end
31
+
32
+ def load_file( filename )
33
+ @filename = filename
34
+ open(filename, "r") do |f|
35
+ scan_evaluate f.read
36
+ end
37
+ end
38
+
39
+ def scan_file( filename )
40
+ load_file filename
41
+ do_parse
42
+ end
43
+
44
+ def next_token
45
+ @rex_tokens.shift
46
+ end
47
+
48
+ def scan_evaluate( str )
49
+ scan_setup
50
+ @rex_tokens = []
51
+ @lineno = 1
52
+ ss = StringScanner.new(str)
53
+ state = nil
54
+ until ss.eos?
55
+ text = ss.peek(1)
56
+ @lineno += 1 if text == "\n"
57
+ case state
58
+ when nil
59
+ case
60
+ when (text = ss.scan(/\s/))
61
+ ;
62
+
63
+ when (text = ss.scan(/\(/))
64
+ @rex_tokens.push action { [:LPAREN, text] }
65
+
66
+ when (text = ss.scan(/\)/))
67
+ @rex_tokens.push action { [:RPAREN, text] }
68
+
69
+ when (text = ss.scan(/\[/))
70
+ @rex_tokens.push action { [:LBRACK, text] }
71
+
72
+ when (text = ss.scan(/\]/))
73
+ @rex_tokens.push action { [:RBRACK, text] }
74
+
75
+ when (text = ss.scan(/\{/))
76
+ @rex_tokens.push action { [:LBRACE, text] }
77
+
78
+ when (text = ss.scan(/\}/))
79
+ @rex_tokens.push action { [:RBRACE, text] }
80
+
81
+ when (text = ss.scan(/=/))
82
+ @rex_tokens.push action { [:EQUALS, text] }
83
+
84
+ when (text = ss.scan(/\!=/))
85
+ @rex_tokens.push action { [:NOTEQUALS, text] }
86
+
87
+ when (text = ss.scan(/~/))
88
+ @rex_tokens.push action { [:MATCH, text] }
89
+
90
+ when (text = ss.scan(/</))
91
+ @rex_tokens.push action { [:LESSTHAN, text] }
92
+
93
+ when (text = ss.scan(/>/))
94
+ @rex_tokens.push action { [:GREATERTHAN, text] }
95
+
96
+ when (text = ss.scan(/not/))
97
+ @rex_tokens.push action { [:NOT, text] }
98
+
99
+ when (text = ss.scan(/and/))
100
+ @rex_tokens.push action { [:AND, text] }
101
+
102
+ when (text = ss.scan(/or/))
103
+ @rex_tokens.push action { [:OR, text] }
104
+
105
+ when (text = ss.scan(/true/))
106
+ @rex_tokens.push action { [:BOOLEAN, true]}
107
+
108
+ when (text = ss.scan(/false/))
109
+ @rex_tokens.push action { [:BOOLEAN, false]}
110
+
111
+ when (text = ss.scan(/-?\d+/))
112
+ @rex_tokens.push action { [:NUMBER, text.to_i] }
113
+
114
+ when (text = ss.scan(/-?\d+\.?(\d+)?/))
115
+ @rex_tokens.push action { [:NUMBER, text.to_f] }
116
+
117
+ when (text = ss.scan(/\"(\\.|[^\\"])*\"/))
118
+ @rex_tokens.push action { [:STRING, JSON.load(text)] }
119
+
120
+ when (text = ss.scan(/[\w_:]+/))
121
+ @rex_tokens.push action { [:STRING, text] }
122
+
123
+ else
124
+ text = ss.string[ss.pos .. -1]
125
+ raise ScanError, "can not match: '" + text + "'"
126
+ end # if
127
+
128
+ else
129
+ raise ScanError, "undefined state: '" + state.to_s + "'"
130
+ end # case state
131
+ end # until ss
132
+ end # def scan_evaluate
133
+
134
+ end # class
135
+ end # module