pal_tool 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +132 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Dockerfile +10 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +72 -0
- data/LICENSE.txt +21 -0
- data/README.md +124 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/pal +47 -0
- data/lib/pal/common/local_file_utils.rb +37 -0
- data/lib/pal/common/object_helpers.rb +27 -0
- data/lib/pal/common/safe_hash_parse.rb +87 -0
- data/lib/pal/configuration.rb +77 -0
- data/lib/pal/handler/base.rb +138 -0
- data/lib/pal/handler/definitions/aws_cur.json +8 -0
- data/lib/pal/handler/manager.rb +30 -0
- data/lib/pal/handler/processor.rb +84 -0
- data/lib/pal/log.rb +29 -0
- data/lib/pal/main.rb +63 -0
- data/lib/pal/operation/actions.rb +106 -0
- data/lib/pal/operation/exporter.rb +183 -0
- data/lib/pal/operation/filter_evaluator.rb +249 -0
- data/lib/pal/operation/processor_context.rb +50 -0
- data/lib/pal/operation/projection.rb +302 -0
- data/lib/pal/plugin.rb +61 -0
- data/lib/pal/request/metadata.rb +19 -0
- data/lib/pal/request/runbook.rb +54 -0
- data/lib/pal/version.rb +5 -0
- data/lib/pal.rb +43 -0
- data/plugins/PLUGINS.md +1 -0
- data/plugins/operation/terminal_exporter_impl.rb +14 -0
- data/templates/DOCUMENTATION.md +46 -0
- data/templates/aws/data_transfer/data_transfer_breakdown.json +93 -0
- data/templates/aws/ec2/ec2_compute_hourly_breakdown.json +63 -0
- data/templates/aws/ec2/ec2_operation_breakdown.json +64 -0
- data/templates/aws/ec2/ec2_spend_breakdown.json +63 -0
- data/templates/aws/global_resource_and_usage_type_costs.json +41 -0
- data/templates/aws/kms/kms_usage_counts.json +52 -0
- data/templates/aws/kms/kms_usage_list.json +80 -0
- data/templates/aws/kms/list_of_kms_keys.json +57 -0
- data/templates/aws/reserved_instances/all_reserved_instance_expiries.json +41 -0
- data/templates/aws/reserved_instances/reserved_instance_opportunities.json +60 -0
- data/templates/aws/summary_cost_between_date_range.json +43 -0
- data/templates/aws/summary_daily_breakdown_costs.json +39 -0
- data/templates/azure/global_resource_type_summary.json +47 -0
- metadata +136 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pal"
|
4
|
+
require "pal/configuration"
|
5
|
+
require "pal/common/local_file_utils"
|
6
|
+
require "pal/common/safe_hash_parse"
|
7
|
+
require "pal/common/object_helpers"
|
8
|
+
|
9
|
+
module Pal
|
10
|
+
module Operation
|
11
|
+
# Probably a bad name, its really a processor that calls an export impl - w
|
12
|
+
class Exporter
|
13
|
+
include Log
|
14
|
+
include ObjectHelpers
|
15
|
+
|
16
|
+
# @return [Array<Pal::Operation::BaseExportHandler>]
|
17
|
+
attr_reader :export_types
|
18
|
+
|
19
|
+
# @return [Array<String>]
|
20
|
+
attr_accessor :properties
|
21
|
+
|
22
|
+
# @return [Actions]
|
23
|
+
attr_reader :actions
|
24
|
+
|
25
|
+
# @param [Pal::Operation::ProcessorContext] ctx
|
26
|
+
# @return [Array, Hash]
|
27
|
+
def perform_export(ctx)
|
28
|
+
log_info("About to extract required data defined in #{ctx.candidates.size} rows")
|
29
|
+
extracted_rows, extracted_columns = extract(ctx, @properties)
|
30
|
+
|
31
|
+
if @actions&.processable?
|
32
|
+
log_info("Actions have been defined, going off to extract.")
|
33
|
+
extracted_rows, extracted_columns = @actions.process(extracted_rows, extracted_columns)
|
34
|
+
end
|
35
|
+
|
36
|
+
@export_types.each do |t|
|
37
|
+
log_info("Exporting for [#{t.class}] triggered ...")
|
38
|
+
t.run_export(extracted_rows, extracted_columns)
|
39
|
+
log_info("... export for [#{t.class}] completed")
|
40
|
+
end
|
41
|
+
|
42
|
+
[extracted_rows, extracted_columns]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# @param [Array<String>] properties
|
48
|
+
# @return [Array]
|
49
|
+
# rubocop:disable Metrics/AbcSize
|
50
|
+
# rubocop:disable Metrics/MethodLength
|
51
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
52
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
53
|
+
def extract(ctx, properties)
|
54
|
+
all_columns = ctx.column_headers.keys
|
55
|
+
|
56
|
+
properties = all_columns if properties.nil? || properties.empty?
|
57
|
+
|
58
|
+
extractable_properties = {}
|
59
|
+
properties.each do |property|
|
60
|
+
unless all_columns.include? property
|
61
|
+
log_warn("[#{property}] not found in column headers.")
|
62
|
+
next
|
63
|
+
end
|
64
|
+
|
65
|
+
extractable_properties[property] = ctx.column_headers[property]
|
66
|
+
end
|
67
|
+
|
68
|
+
extracted_rows = ctx.candidates.map do |row|
|
69
|
+
extractable_properties.map do |key, value_idx|
|
70
|
+
value = row[value_idx]
|
71
|
+
value ? ctx.cast(key, value) : "<Missing>"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
new_extractable_properties = {}
|
76
|
+
extractable_properties.keys.each_with_index do |key, idx|
|
77
|
+
new_extractable_properties[key] = idx
|
78
|
+
end
|
79
|
+
|
80
|
+
[extracted_rows, new_extractable_properties]
|
81
|
+
end
|
82
|
+
# rubocop:enable Metrics/AbcSize
|
83
|
+
# rubocop:enable Metrics/MethodLength
|
84
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
85
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
86
|
+
|
87
|
+
def actions=(opts)
|
88
|
+
@actions = Pal::Operation::Actions.new.from_hash(opts)
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Array<Pal::Operation::BaseExportHandler>]
|
92
|
+
def types=(types_conf)
|
93
|
+
@export_types = types_conf.map do |type_conf|
|
94
|
+
name = type_conf["name"]
|
95
|
+
settings = type_conf["settings"]
|
96
|
+
|
97
|
+
clazz_name = "Pal::Operation::#{name.to_s.capitalize}ExporterImpl"
|
98
|
+
Kernel.const_get(clazz_name).new(settings)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
module FileExportable
|
104
|
+
# @param [String] file_path
|
105
|
+
# @param [String] contents
|
106
|
+
# @param [String] file_extension
|
107
|
+
def write_to_file(file_path, file_name, file_extension, contents)
|
108
|
+
file_location = "#{file_path}/#{file_name || Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S%-z")}"
|
109
|
+
Pal::Common::LocalFileUtils.with_file(file_location, file_extension) do |file|
|
110
|
+
file.write(contents)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class BaseExportHandler
|
116
|
+
include Pal::Configuration
|
117
|
+
include Pal::Log
|
118
|
+
|
119
|
+
# @return [Hash] settings
|
120
|
+
attr_accessor :settings
|
121
|
+
|
122
|
+
# @param [Hash] settings
|
123
|
+
def initialize(settings)
|
124
|
+
@settings = settings
|
125
|
+
end
|
126
|
+
|
127
|
+
# @param [Array] rows
|
128
|
+
# @param [Hash] columns
|
129
|
+
# Extract values, call export.
|
130
|
+
def run_export(rows, columns)
|
131
|
+
if rows.empty?
|
132
|
+
Pal.logger.warn("No results were found, will not export.")
|
133
|
+
return
|
134
|
+
end
|
135
|
+
|
136
|
+
_export(rows, columns)
|
137
|
+
end
|
138
|
+
|
139
|
+
protected
|
140
|
+
|
141
|
+
# @abstract
|
142
|
+
# @param [Array] _rows
|
143
|
+
# @param [Hash] _columns
|
144
|
+
def _export(_rows, _columns)
|
145
|
+
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class CsvExporterImpl < BaseExportHandler
|
150
|
+
include FileExportable
|
151
|
+
|
152
|
+
# @param [Array] rows
|
153
|
+
# @param [Hash] column_headers
|
154
|
+
def _export(rows, column_headers)
|
155
|
+
file_contents = []
|
156
|
+
file_contents << column_headers.keys.join(",")
|
157
|
+
|
158
|
+
rows.each do |row|
|
159
|
+
file_contents << row.join(",")
|
160
|
+
end
|
161
|
+
|
162
|
+
write_to_file(
|
163
|
+
@settings["output_dir"], @settings["file_name"] || "pal", "csv", file_contents.join("\n")
|
164
|
+
)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
require "terminal-table"
|
169
|
+
|
170
|
+
class TableExporterImpl < BaseExportHandler
|
171
|
+
# @param [Array] rows
|
172
|
+
# @param [Hash] column_headers
|
173
|
+
def _export(rows, column_headers)
|
174
|
+
title = @settings["title"] || "<No Title Set>"
|
175
|
+
style = @settings["style"] || {}
|
176
|
+
|
177
|
+
table = Terminal::Table.new(title: title, headings: column_headers.keys, rows: rows, style: style)
|
178
|
+
puts table
|
179
|
+
end
|
180
|
+
end
|
181
|
+
# do json exporter
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "pal/common/object_helpers"
|
5
|
+
require "pal/common/safe_hash_parse"
|
6
|
+
|
7
|
+
module Pal
|
8
|
+
module Operation
|
9
|
+
# Filter evaluator runs the filter processes to identify candidates
|
10
|
+
class FilterEvaluator
|
11
|
+
# @return [Rule]
|
12
|
+
attr_reader :rule
|
13
|
+
|
14
|
+
def initialize(filters)
|
15
|
+
@rule = RuleFactory.from_hash(filters)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [Array] row
|
19
|
+
# @param [Hash] column_headers
|
20
|
+
# @return [Boolean]
|
21
|
+
def test_property(row, column_headers)
|
22
|
+
return true if @rule.nil?
|
23
|
+
|
24
|
+
eval_ctx = EvaluationContext.new(row, column_headers)
|
25
|
+
@rule.evaluate(eval_ctx)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Class to manage the rules provided
|
30
|
+
class RuleFactory
|
31
|
+
# @param [Hash] rule_hash
|
32
|
+
# @return [Rule]
|
33
|
+
# rubocop:disable Metrics/AbcSize
|
34
|
+
def self.from_hash(rule_hash)
|
35
|
+
return nil if rule_hash.nil? || rule_hash.keys.empty?
|
36
|
+
|
37
|
+
if rule_hash.key?("condition")
|
38
|
+
condition = rule_hash.fetch("condition")
|
39
|
+
rules = RuleFactory.from_group_rules(rule_hash)
|
40
|
+
|
41
|
+
return AndGroupRule.new(rules) if condition.casecmp("and").zero?
|
42
|
+
return OrGroupRule.new(rules) if condition.casecmp("or").zero?
|
43
|
+
|
44
|
+
raise "Invalid condition [#{condition}] passed."
|
45
|
+
end
|
46
|
+
|
47
|
+
return OperatorRule.new(rule_hash) if rule_hash.key?("operator")
|
48
|
+
|
49
|
+
raise "Hash is malformed."
|
50
|
+
end
|
51
|
+
# rubocop:enable Metrics/AbcSize
|
52
|
+
|
53
|
+
# @return [Array<Rule>]
|
54
|
+
def self.from_group_rules(group_rule)
|
55
|
+
rules = group_rule.fetch("rules", [])
|
56
|
+
|
57
|
+
rules.map do |rule_hash|
|
58
|
+
from_hash(rule_hash)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Rule
|
64
|
+
def evaluate(eval_ctx)
|
65
|
+
_evaluate(eval_ctx)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# @abstract
|
71
|
+
def _evaluate(_eval_ctx)
|
72
|
+
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class GroupRule < Rule
|
77
|
+
# @return [Array<Rule>]
|
78
|
+
attr_reader :rule_group
|
79
|
+
|
80
|
+
# @param [Array<Rule>] rule_group
|
81
|
+
def initialize(rule_group)
|
82
|
+
@rule_group = rule_group
|
83
|
+
super()
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class AndGroupRule < GroupRule
|
88
|
+
private
|
89
|
+
|
90
|
+
def _evaluate(eval_ctx)
|
91
|
+
@rule_group.all? do |rule|
|
92
|
+
rule.evaluate(eval_ctx)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class OrGroupRule < GroupRule
|
98
|
+
private
|
99
|
+
|
100
|
+
def _evaluate(eval_ctx)
|
101
|
+
@rule_group.any? do |rule|
|
102
|
+
rule.evaluate(eval_ctx)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class OperatorRule < Rule
|
108
|
+
# @return [String]
|
109
|
+
attr_reader :operator, :field
|
110
|
+
|
111
|
+
# @return [Object]
|
112
|
+
attr_reader :comparison_value
|
113
|
+
|
114
|
+
# @return [Symbol]
|
115
|
+
attr_reader :type
|
116
|
+
|
117
|
+
def initialize(rule_hash)
|
118
|
+
super()
|
119
|
+
@field = rule_hash.fetch("field")
|
120
|
+
@type = rule_hash.fetch("type").to_sym
|
121
|
+
@operator = rule_hash.fetch("operator").to_sym
|
122
|
+
@comparison_value = rule_hash.fetch("value")
|
123
|
+
end
|
124
|
+
|
125
|
+
# @param [EvaluationContext] eval_ctx
|
126
|
+
# @return [Boolean]
|
127
|
+
def _evaluate(eval_ctx)
|
128
|
+
operator_candidates = get_operators_for_type(@type)
|
129
|
+
|
130
|
+
property = eval_ctx.get_value(@field)
|
131
|
+
return false unless property
|
132
|
+
|
133
|
+
proc = operator_candidates.fetch(@operator, proc do |_x, _y|
|
134
|
+
raise "Invalid operator given - [#{@operator}]. Valid candidates are [#{operator_candidates.keys.join(", ")}]"
|
135
|
+
end)
|
136
|
+
|
137
|
+
converted_comparison = convert_property(@type, @comparison_value)
|
138
|
+
converted_property = convert_property(@type, property)
|
139
|
+
|
140
|
+
proc.call(converted_property, converted_comparison)
|
141
|
+
end
|
142
|
+
|
143
|
+
# @param [Symbol] type
|
144
|
+
# @return [Hash{Symbol->Proc}]
|
145
|
+
def get_operators_for_type(type)
|
146
|
+
case type
|
147
|
+
when :string
|
148
|
+
string_operators
|
149
|
+
when :number
|
150
|
+
number_operators
|
151
|
+
when :date
|
152
|
+
date_operators
|
153
|
+
when :json
|
154
|
+
json_operators
|
155
|
+
else
|
156
|
+
raise "Missing filter operator for [#{type}], valid candidates: [string, number, tag, date]"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# @param [Symbol] type
|
161
|
+
# @return [Object]
|
162
|
+
def convert_property(type, prop)
|
163
|
+
case type
|
164
|
+
when :string
|
165
|
+
prop.to_s
|
166
|
+
when :number
|
167
|
+
prop.is_a?(Array) ? prop : prop.to_f
|
168
|
+
when :json
|
169
|
+
prop
|
170
|
+
when :date
|
171
|
+
Date.parse(prop)
|
172
|
+
else
|
173
|
+
raise "Missing property operator for [#{type}], valid candidates: [string, number, json, date]"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
# rubocop:disable Metrics/AbcSize
|
180
|
+
# @return [Hash{Symbol->Proc}]
|
181
|
+
def string_operators
|
182
|
+
{
|
183
|
+
equal: proc { |x, y| x == y },
|
184
|
+
not_equal: proc { |x, y| x != y },
|
185
|
+
begins_with: proc { |x, y| x.start_with?(y) },
|
186
|
+
not_begins_with: proc { |x, y| !x.start_with?(y) },
|
187
|
+
ends_with: proc { |x, y| x.end_with?(y) },
|
188
|
+
not_ends_with: proc { |x, y| !x.end_with?(y) },
|
189
|
+
contains: proc { |x, y| x.include?(y) },
|
190
|
+
not_contains: proc { |x, y| !x.include?(y) },
|
191
|
+
is_empty: proc { |x, _y| x.empty? },
|
192
|
+
is_not_empty: proc { |x, _y| !x.empty? }
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
# @return [Hash{Symbol->Proc}]
|
197
|
+
def number_operators
|
198
|
+
{
|
199
|
+
equal: proc { |x, y| x == y },
|
200
|
+
not_equal: proc { |x, y| x != y },
|
201
|
+
less: proc { |x, y| x < y },
|
202
|
+
less_or_equal: proc { |x, y| x <= y },
|
203
|
+
greater: proc { |x, y| x > y },
|
204
|
+
greater_or_equal: proc { |x, y| x >= y },
|
205
|
+
between: proc { |x, y| y.is_a?(Array) ? x.between?(y.first, y.last) : false },
|
206
|
+
not_between: proc { |x, y| y.is_a?(Array) ? !x.between?(y.first, y.last) : false }
|
207
|
+
}
|
208
|
+
end
|
209
|
+
alias date_operators number_operators
|
210
|
+
|
211
|
+
# @return [Hash{Symbol->Proc}]
|
212
|
+
def json_operators
|
213
|
+
{
|
214
|
+
key_equal: proc do |x, y|
|
215
|
+
!SafeHashParse.extract_from_json(x, y).empty?
|
216
|
+
end,
|
217
|
+
key_not_equal: proc do |x, y|
|
218
|
+
SafeHashParse.extract_from_json(x, y).empty?
|
219
|
+
end,
|
220
|
+
value_equal: proc do |x, y|
|
221
|
+
SafeHashParse.all_values_from_json(x).include?(y)
|
222
|
+
end,
|
223
|
+
value_not_equal: proc do |x, y|
|
224
|
+
!SafeHashParse.all_values_from_json(x).include?(y)
|
225
|
+
end,
|
226
|
+
jpath: proc do |x, y|
|
227
|
+
path, value = y.split("=")
|
228
|
+
SafeHashParse.extract_from_json(x, path).include?(value)
|
229
|
+
end
|
230
|
+
}
|
231
|
+
end
|
232
|
+
# rubocop:enable Metrics/AbcSize
|
233
|
+
end
|
234
|
+
|
235
|
+
class EvaluationContext
|
236
|
+
attr_accessor :row, :column_headers
|
237
|
+
|
238
|
+
def initialize(row, column_headers)
|
239
|
+
@row = row
|
240
|
+
@column_headers = column_headers
|
241
|
+
end
|
242
|
+
|
243
|
+
def get_value(key)
|
244
|
+
idx = @column_headers.fetch(key, -1)
|
245
|
+
idx.zero? || idx.positive? ? @row[idx] : nil
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module Pal
|
6
|
+
module Operation
|
7
|
+
class ProcessorContext
|
8
|
+
attr_accessor :total_row_count, :current_file_row_count, :candidates, :column_headers, :column_type_definitions
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@total_row_count = 0
|
12
|
+
@current_file_row_count = 0
|
13
|
+
@candidates = []
|
14
|
+
@column_headers = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Array<String>] row
|
18
|
+
def extract_column_headers(row)
|
19
|
+
row.each_with_index { |column, idx| @column_headers[column] = idx }
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_candidate(row)
|
23
|
+
@candidates << row
|
24
|
+
end
|
25
|
+
|
26
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
27
|
+
def cast(column_header, value)
|
28
|
+
return value unless @column_type_definitions&.key?(column_header)
|
29
|
+
|
30
|
+
case @column_type_definitions[column_header]["data_type"]
|
31
|
+
when "string"
|
32
|
+
value.to_s
|
33
|
+
when "decimal"
|
34
|
+
value.to_f
|
35
|
+
when "integer"
|
36
|
+
value.to_i
|
37
|
+
when "date_time"
|
38
|
+
DateTime.parse(value)
|
39
|
+
when "date"
|
40
|
+
Date.parse(value)
|
41
|
+
when nil
|
42
|
+
value
|
43
|
+
else
|
44
|
+
value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|