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 +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
|