puppetdb_query 0.0.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 483f7ff2212fc5b4b01df7c5e743848648e7796e
4
+ data.tar.gz: 86cecf8ac71a8ee6281f351c76a4ac10460def0e
5
+ SHA512:
6
+ metadata.gz: 830b95936dd945d8540ee67a643d31faca45f4454b37eb2f2983809bed15d8e7594182fe29b84eee1e33977e30e97d45799dd0403752a146b32e61ce0c5321e7
7
+ data.tar.gz: 03b48691e946b2ecf0fc129e7a78a57c16f75c0d7fb267a843e8dc9b502d64cc80a5785a17089bae320c2b1d717171aa26058331c521c680a927b1c0d68b5eca
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.idea/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /*.gem
6
+ /tester.rb
7
+ /test/
8
+ /_yardoc/
9
+ /coverage/
10
+ /doc/
11
+ /pkg/
12
+ /spec/reports/
13
+ /tmp/
data/.rubocop.yml ADDED
@@ -0,0 +1,102 @@
1
+ # This yaml file describes which files are excluded in rubocop run.
2
+ # It can be in the project home directory or in the $HOME folder.
3
+
4
+ # Common configuration.
5
+ AllCops:
6
+ Include:
7
+ - 'lib/**/'
8
+ - 'exe/*'
9
+
10
+ Exclude:
11
+ - 'scripts/**/*'
12
+ - 'vendor/**/*'
13
+ - 'bin/**/*'
14
+ - 'bundle/**/*'
15
+ - 'local-gems/**/*'
16
+ - '**/*.sh'
17
+ - '**/Gemfile'
18
+ - 'tester.rb'
19
+ - 'test/**/*'
20
+
21
+ # --- XXXLength-Section --------------------------------------------------------------------
22
+ # too long lines, methods and classes are annoying,
23
+ LineLength:
24
+ Enabled: true
25
+ Max: 100
26
+
27
+ MethodLength:
28
+ Enabled: true
29
+ Max: 35
30
+
31
+ Metrics/AbcSize:
32
+ Max: 40
33
+
34
+ ClassLength:
35
+ Enabled: true
36
+ Max: 140
37
+
38
+ # --- Style Cops - Section -----------------------------------------------------------------
39
+ # Don't be so dogmatic about Hash-Style! Both are fine for us
40
+ HashSyntax:
41
+ Enabled: true
42
+
43
+ # From Ruby 2.x on there is no need for this anymore, so why bothering now?
44
+ Encoding:
45
+ Enabled: false
46
+
47
+ # Ensable following message: Documentation: Missing top-level class documentation comment.
48
+ Documentation:
49
+ Enabled: true
50
+
51
+ # check filename conventions
52
+ FileName:
53
+ Enabled: true
54
+
55
+ # this 3-digit thing for portnumbers? oh, come on!
56
+ NumericLiterals:
57
+ Enabled: false
58
+
59
+ # ok, one should avoid global vars, but from time to time we need them
60
+ Style/GlobalVars:
61
+ Enabled: true
62
+
63
+ Style/RegexpLiteral:
64
+ Enabled: true
65
+
66
+ Style/AlignParameters:
67
+ EnforcedStyle: "with_fixed_indentation"
68
+
69
+ Style/BracesAroundHashParameters:
70
+ EnforcedStyle: "context_dependent"
71
+
72
+ Style/EachWithObject:
73
+ Enabled: false
74
+
75
+ # we now the special global variables by heart
76
+ Style/SpecialGlobalVars:
77
+ Enabled: false
78
+
79
+ # we don't care about quoting style
80
+ Style/StringLiterals:
81
+ Enabled: false
82
+
83
+ # for easier line moving
84
+ Style/TrailingCommaInLiteral:
85
+ Enabled: false
86
+
87
+
88
+ # --- Complexity - Section -----------------------------------------------------------------
89
+ # as old McCabe says:
90
+ #
91
+ # Cyclomatic Complexity Risk Evaluation...
92
+ # 1-10 A simple module without much risk
93
+ # 11-20 A more complex module with moderate risk
94
+ # 21-50 A complex module of high risk
95
+ # 51 and greater An untestable program of very high risk
96
+ CyclomaticComplexity:
97
+ Max: 10
98
+
99
+ # Lint-Section -----------------------------------------------------------------------------
100
+ # what is soooo bad about blablubb.match /..../ compared to blablubb.match(/..../)?
101
+ Lint/AmbiguousRegexpLiteral:
102
+ Enabled: true
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ sudo: false
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - 2.1.9
8
+ - 2.2.5
9
+ - 2.3.1
10
+ - jruby-19mode
11
+ - jruby-head
12
+ - rbx
13
+ - rbx-19mode
14
+ matrix:
15
+ fast_finish: true
16
+ allow_failures:
17
+ - rvm: jruby-head
18
+ - rvm: rbx
19
+ - rvm: rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ # only for local testing but not needed for spec tests
6
+ group :test do
7
+ gem 'ruby-puppetdb', '=1.5.3' if RUBY_VERSION !~ /^1\./
8
+ gem 'puppet', '=3.8.7' if RUBY_VERSION !~ /^1\./
9
+ gem "rubocop", '=0.39.0' if RUBY_VERSION =~ /^1\./
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Michael Meyling
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # puppetdb_query - query puppetdb data from other sources
2
+
3
+ Just store and update your puppet facts also in another database and query nodes or facts from that other database.
4
+ This can speed up your queries enormously and reduce the load on your puppet database.
5
+
6
+ ## General
7
+
8
+ The puppet database schema is not designed for complicated queries on numerous nodes. Here we provide
9
+ an implementation for storing and querying node facts in a mongodb.
10
+
11
+ You must simply establish a sync job to read the data from your puppetdb and write it to a mongodb.
12
+
13
+ Currently the implementation supports only puppetdb api V 4.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'puppetdb_query'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install puppetdb_query
30
+
31
+ ## Usage within program
32
+
33
+ ```ruby
34
+ require "mongo"
35
+ require "puppetdb_query"
36
+ require "pp"
37
+
38
+ MONGO_HOSTS = ['mongo.myhost.org:27017']
39
+ MONGO_OPTIONS = { database: 'puppetdb', user: 'ops', password: 'very secret' }
40
+
41
+ pm = PuppetDBQuery::MongoQuery.new(MONGO_HOSTS, MONGO_OPTIONS)
42
+ pm.nodes({processorcount: '4', lvm_support: true})
43
+ pm.facts({processorcount: '4', lvm_support: true}, ["macaddress", "operatingsystem"])
44
+
45
+ mongo = PuppetDBQuery::ToMongo.new
46
+ query = mongo.query("processorcount='4' and lvm_support=true")
47
+ pp query
48
+ pm.nodes(query)
49
+ pm.facts(query, ["macaddress", "operatingsystem"])
50
+ ```
51
+
52
+ ## Development
53
+
54
+ After checking out the repo, run `bundle install` to install dependencies.
55
+
56
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
57
+
58
+ ## Contributing
59
+
60
+ 1. Fork it ( https://github.com/m-31/puppetdb_query/fork )
61
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
62
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
63
+ 4. Push to the branch (`git push origin my-new-feature`)
64
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'rake'
2
+
3
+ require "rubocop/rake_task"
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc "Run RuboCop on the lib directory"
7
+ RuboCop::RakeTask.new(:rubocop) do |task|
8
+ task.formatters = ["fuubar"]
9
+ task.options = ["-D"]
10
+ task.options = task.options + ["--fail-level", "E"] if RUBY_VERSION =~ /^1\./
11
+ task.fail_on_error = true
12
+ end
13
+
14
+ RSpec::Core::RakeTask.new(:spec) do |t|
15
+ t.pattern = 'spec/**/*_spec.rb'
16
+ end
17
+
18
+ task :default => ["spec", "rubocop"]
19
+
@@ -0,0 +1,45 @@
1
+ require 'logger'
2
+
3
+ module PuppetDBQuery
4
+ # for logger access just include this module
5
+ module Logging
6
+ class << self
7
+ attr_writer :logger
8
+
9
+ def logger
10
+ unless @logger
11
+ @logger = Logger.new($stdout)
12
+ @logger.level = (ENV['LOG_LEVEL'] || Logger::DEBUG).to_i
13
+ end
14
+ @logger
15
+ end
16
+ end
17
+
18
+ # addition
19
+ def self.included(base)
20
+ # rubocop:disable Lint/NestedMethodDefinition
21
+ class << base
22
+ def logger
23
+ # :nocov:
24
+ Logging.logger
25
+ # :nocov:
26
+ end
27
+
28
+ def logger=(logger)
29
+ # :nocov:
30
+ Logging.logger = logger
31
+ # :nocov:
32
+ end
33
+ end
34
+ # rubocop:enable Lint/NestedMethodDefinition
35
+ end
36
+
37
+ def logger
38
+ Logging.logger
39
+ end
40
+
41
+ def logger=(logger)
42
+ Logging.logger = logger
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,134 @@
1
+ require 'time'
2
+
3
+ require_relative "logging"
4
+
5
+ module PuppetDBQuery
6
+ # access nodes and their facts from mongo database
7
+ class MongoDB
8
+ include Logging
9
+ attr_reader :connection
10
+ attr_reader :nodes_collection
11
+ attr_reader :nodes_properties_collection
12
+ attr_reader :meta_collection
13
+
14
+ # initialize access to mongodb
15
+ #
16
+ # You might want to adjust the logging level, for example:
17
+ # ::Mongo::Logger.logger.level = logger.level
18
+ #
19
+ # @param connection mongodb connection, should already be switched to correct database
20
+ # @param nodes symbol for collection that contains nodes with their facts
21
+ # @param nodes_properties symbol for collection for nodes with their update timestamps
22
+ # @param meta symbol for collection with update metadata
23
+ def initialize(connection, nodes = :nodes, nodes_properties = :nodes_properties, meta = :meta)
24
+ @connection = connection
25
+ @nodes_collection = nodes
26
+ @nodes_propeties_collection = nodes_properties
27
+ @meta_collection = meta
28
+ end
29
+
30
+ # get node names that fulfill given mongodb query
31
+ def query_nodes(query)
32
+ collection = connection[nodes_collection]
33
+ collection.find(query).batch_size(999).projection(_id: 1).map { |k| k[:_id] }
34
+ end
35
+
36
+ # get nodes and their facts that fulfill given mongodb query
37
+ def query_facts(query, facts)
38
+ fields = Hash[facts.collect { |fact| [fact.to_sym, 1] }]
39
+ collection = connection[nodes_collection]
40
+ result = {}
41
+ collection.find(query).batch_size(999).projection(fields).each do |values|
42
+ id = values.delete('_id')
43
+ result[id] = values
44
+ end
45
+ result
46
+ end
47
+
48
+ # get all node names
49
+ def nodes
50
+ collection = connection[nodes_collection]
51
+ collection.find.batch_size(999).projection(_id: 1).map { |k| k[:_id] }
52
+ end
53
+
54
+ # get facts for given node name
55
+ def node_facts(node)
56
+ collection = connection[nodes_collection]
57
+ result = collection.find(_id: node).limit(999).batch_size(999).to_a.first
58
+ result.delete("_id") if result
59
+ result
60
+ end
61
+
62
+ # get all nodes and their facts
63
+ def facts
64
+ collection = connection[nodes_collection]
65
+ result = {}
66
+ collection.find.batch_size(999).each do |values|
67
+ id = values.delete('_id')
68
+ result[id] = values
69
+ end
70
+ result
71
+ end
72
+
73
+ # update or insert facts for given node name
74
+ def node_update(node, facts)
75
+ connection[nodes_collection].find(_id: node).replace_one(facts, upsert: true)
76
+ rescue ::Mongo::Error::OperationFailure => e
77
+ # mongodb doesn't support keys with a dot
78
+ # see https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names
79
+ # as a dirty workaround we delete the document and insert it ;-)
80
+ # The dotted field .. in .. is not valid for storage. (57)
81
+ raise e unless e.message =~ /The dotted field /
82
+ connection[nodes_collection].find(_id: node).delete_one
83
+ connection[nodes_collection].insert_one(facts.merge(_id: node))
84
+ end
85
+
86
+ # delete node data for given node name
87
+ def node_delete(node)
88
+ connection[nodes_collection].find(_id: node).delete_one
89
+ end
90
+
91
+ # update node properties
92
+ def node_properties_update(new_node_properties, ts_begin)
93
+ collection = connection[nodes_properties_collection]
94
+ old_names = collection.find.batch_size(999).projection(_id: 1).map { |k| k[:_id] }
95
+ delete = old_names - new_node_properties.keys
96
+ collection.insert_many(nodes_properties.map { |k, v| v.dup.tap { v[:_id] = k } })
97
+ collection.delete_many(_id: { '$in' => delete })
98
+ ts_end = Time.iso8601(Time.now)
99
+ connection[meta_collection].find_one_and_update(
100
+ {},
101
+ {
102
+ '$set' => {
103
+ last_node_properties_update: {
104
+ ts_begin: ts_begin,
105
+ ts_end: ts_end
106
+ }
107
+ }
108
+ },
109
+ { upsert: true }
110
+ )
111
+ end
112
+
113
+ # update or insert timestamps for given fact update method
114
+ def meta_fact_update(method, ts_begin, ts_end)
115
+ connection[meta_collection].find_one_and_update(
116
+ {},
117
+ {
118
+ '$set' => {
119
+ last_fact_update: {
120
+ ts_begin: ts_begin,
121
+ ts_end: ts_end,
122
+ method: method
123
+ },
124
+ method => {
125
+ ts_begin: ts_begin,
126
+ ts_end: ts_end
127
+ }
128
+ }
129
+ },
130
+ { upsert: true }
131
+ )
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,38 @@
1
+ require_relative "tokenizer"
2
+
3
+ module PuppetDBQuery
4
+ # operator with priority and representation information
5
+ class Operator
6
+ attr_reader :symbol
7
+ attr_reader :infix
8
+ attr_reader :priority
9
+ attr_reader :minimum
10
+ attr_reader :maximum
11
+ attr_reader :string
12
+
13
+ def initialize(symbol, infix, priority, minimum, maximum = nil)
14
+ @symbol = symbol
15
+ @infix = infix
16
+ @priority = priority
17
+ @minimum = minimum
18
+ @maximum = maximum
19
+ @string = Tokenizer.symbol_to_string(symbol)
20
+ end
21
+
22
+ def infix?
23
+ infix
24
+ end
25
+
26
+ def prefix?
27
+ !infix
28
+ end
29
+
30
+ def ==(other)
31
+ other.class == self.class && other.symbol == symbol
32
+ end
33
+
34
+ def to_s
35
+ @string
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,186 @@
1
+ require_relative "operator"
2
+ require_relative "term"
3
+ require_relative "tokenizer"
4
+ require_relative "logging"
5
+
6
+ module PuppetDBQuery
7
+ # parse a puppetdb query string into #PuppetDBQuery::Term s
8
+ class Parser
9
+ include Logging
10
+
11
+ def self.parse(puppetdb_query)
12
+ Parser.new.parse(puppetdb_query)
13
+ end
14
+
15
+ # these are the operators we understand
16
+ # rubocop:disable Style/ExtraSpacing
17
+ AND = Operator.new(:and, true, 100, 2)
18
+ OR = Operator.new(:or, true, 90, 2)
19
+ NOT = Operator.new(:not, false, 1, 1, 1)
20
+ EQUAL = Operator.new(:equal, true, 200, 2, 2)
21
+ NOT_EQUAL = Operator.new(:not_equal, true, 200, 2, 2)
22
+ MATCH = Operator.new(:match, true, 200, 2, 2)
23
+ # rubocop:enable Style/ExtraSpacing
24
+
25
+ # map certain symbols (we get them from a tokenizer) to our operators
26
+ OPERATORS = {
27
+ AND.symbol => AND,
28
+ OR.symbol => OR,
29
+ NOT.symbol => NOT,
30
+ EQUAL.symbol => EQUAL,
31
+ NOT_EQUAL.symbol => NOT_EQUAL,
32
+ MATCH.symbol => MATCH,
33
+ }.freeze
34
+
35
+ attr_reader :symbols # array of symbols
36
+ attr_reader :position # current parsing position in array of symbols
37
+
38
+ # parse query and get resulting array of PuppetDBQuery::Term s
39
+ def parse(query)
40
+ @symbols = Tokenizer.symbols(query)
41
+ @position = 0
42
+ r = []
43
+ r << read_maximal_term(0) until empty?
44
+ r
45
+ end
46
+
47
+ private
48
+
49
+ # Reads next maximal term. The following input doesn't make the term ore complete.
50
+ # Respects the priority of operators by comparing it to the given value.
51
+ def read_maximal_term(priority)
52
+ return nil if empty?
53
+ first = read_minimal_term
54
+ term = add_next_infix_terms(priority, first)
55
+ logger.debug "read maximal term: #{term}"
56
+ term
57
+ end
58
+
59
+ # Read next following term. This is a complete term but some infix operator
60
+ # or some terms for an infix operator might follow.
61
+ # rubocop:disable Metrics/PerceivedComplexity
62
+ def read_minimal_term
63
+ term = nil
64
+ operator = get_operator
65
+ if operator
66
+ error("'#{operator}' is no prefix operator") unless operator.prefix?
67
+ read_token
68
+ term = Term.new(operator)
69
+ arg = read_maximal_term(operator.priority)
70
+ term.add(arg)
71
+ logger.debug "read_minimal_term: #{term}"
72
+ return term
73
+ end
74
+ # no prefix operator found
75
+ token = get_token
76
+ if token == :begin
77
+ read_token
78
+ term = read_maximal_term(0)
79
+ error "'#{Tokenizer.symbol_to_string(:end)}' expected " unless read_token == :end
80
+ elsif token == :list_begin
81
+ read_token
82
+ term = read_maximal_term(0)
83
+ error "'#{Tokenizer.symbol_to_string(:list_end)}' expected " unless read_token == :list_end
84
+ else
85
+ error("no operator #{get_operator} expected here") if get_operator
86
+ token = read_token
87
+ logger.debug "atom found: #{token}"
88
+ term = token
89
+ end
90
+ logger.debug "read minimal term: #{term}"
91
+ term
92
+ end
93
+ # rubocop:enable Metrics/PerceivedComplexity
94
+
95
+ # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
96
+ # rubocop:disable Metrics/MethodLength,Metrics/PerceivedComplexity
97
+ def add_next_infix_terms(priority, first)
98
+ old_operator = nil
99
+ term = first
100
+ loop do
101
+ # we expect an infix operator
102
+ operator = get_operator
103
+ logger.debug "we found operator '#{operator}'" if operator
104
+ if operator.nil? || operator.prefix? || operator.priority <= priority
105
+ logger.debug "'#{operator}' is prefex '#{operator && operator.prefix?}' or has less" \
106
+ " priority #{operator && operator.priority} than #{priority}"
107
+ logger.debug "get_next_infix_terms: #{term}"
108
+ return term
109
+ end
110
+ if old_operator.nil? || old_operator.priority >= operator.priority
111
+ # old operator has not less priority
112
+ read_token
113
+ new_term = read_maximal_term(operator.priority)
114
+ error("to few arguments for operator '#{operator}'") if new_term.nil?
115
+ logger.debug "is '#{old_operator}' == '#{operator}' : #{old_operator == operator}"
116
+ if old_operator == operator
117
+ if operator.maximum && term.args.size + 1 >= operator.maximum
118
+ raise "to much arguments for operator '#{operator}'"
119
+ end
120
+ term.add(new_term)
121
+ else
122
+ also_new_term = Term.new(operator)
123
+ also_new_term.add(term)
124
+ also_new_term.add(new_term)
125
+ term = also_new_term
126
+ end
127
+ else
128
+ # old operator has less priority
129
+ new_term = read_maximal_term(operator.priority)
130
+ error("to few arguments for operator '#{operator}'") if new_term.nil?
131
+ also_new_term = Term.new(operator)
132
+ also_new_term.add(term)
133
+ also_new_term.add(new_term)
134
+ term = also_new_term
135
+ end
136
+ old_operator = operator
137
+ end
138
+ end
139
+ # rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity
140
+ # rubocop:enable Metrics/MethodLength,Metrics/PerceivedComplexity
141
+
142
+ # rubocop:disable Style/AccessorMethodName
143
+ def get_operator
144
+ OPERATORS[get_token]
145
+ end
146
+ # rubocop:enable Style/AccessorMethodName
147
+
148
+ def read_token
149
+ return nil if empty?
150
+ token = symbols[position]
151
+ @position += 1
152
+ token
153
+ end
154
+
155
+ def empty?
156
+ position >= symbols.size
157
+ end
158
+
159
+ # rubocop:disable Style/AccessorMethodName
160
+ def get_token
161
+ return nil if empty?
162
+ symbols[position]
163
+ end
164
+ # rubocop:enable Style/AccessorMethodName
165
+
166
+ def error(message)
167
+ length = Tokenizer.query(symbols[0..position]).size
168
+ raise "parsing query failed\n#{message}\n\n#{Tokenizer.query(symbols)}\n#{' ' * length}^"
169
+ end
170
+ end
171
+ end
172
+
173
+ if $0 == __FILE__
174
+ require "pp"
175
+ query = "facts=-7.4E1 and fucts=8 and fits=true or application_vertical='ops' and" \
176
+ " (application_group=\"live\"or application_group='prelive-production')"
177
+ puts query
178
+ query = PuppetDBQuery::Tokenizer.idem(query)
179
+ puts query
180
+ query = PuppetDBQuery::Tokenizer.idem(query)
181
+ puts query
182
+
183
+ parser = PuppetDBQuery::Parser.new
184
+ tree = parser.parse(query)
185
+ pp tree
186
+ end