puppetdb_query 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rubocop.yml +102 -0
- data/.travis.yml +19 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +64 -0
- data/Rakefile +19 -0
- data/lib/puppetdb_query/logging.rb +45 -0
- data/lib/puppetdb_query/mongodb.rb +134 -0
- data/lib/puppetdb_query/operator.rb +38 -0
- data/lib/puppetdb_query/parser.rb +186 -0
- data/lib/puppetdb_query/puppetdb.rb +90 -0
- data/lib/puppetdb_query/term.rb +31 -0
- data/lib/puppetdb_query/to_mongo.rb +52 -0
- data/lib/puppetdb_query/tokenizer.rb +185 -0
- data/lib/puppetdb_query/updater.rb +76 -0
- data/lib/puppetdb_query/version.rb +3 -0
- data/lib/puppetdb_query.rb +14 -0
- data/puppetdb_query.gemspec +28 -0
- data/spec/puppetdb_query/parser_spec.rb +40 -0
- data/spec/puppetdb_query/to_mongo_spec.rb +68 -0
- data/spec/puppetdb_query/tokenizer_spec.rb +44 -0
- data/spec/puppetdb_query_spec.rb +7 -0
- data/spec/spec_helper.rb +14 -0
- metadata +144 -0
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
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
|