pal_tool 0.2.1
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/.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
|