pal_tool 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +132 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Dockerfile +10 -0
  6. data/Gemfile +14 -0
  7. data/Gemfile.lock +72 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +124 -0
  10. data/Rakefile +12 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/exe/pal +47 -0
  14. data/lib/pal/common/local_file_utils.rb +37 -0
  15. data/lib/pal/common/object_helpers.rb +27 -0
  16. data/lib/pal/common/safe_hash_parse.rb +87 -0
  17. data/lib/pal/configuration.rb +77 -0
  18. data/lib/pal/handler/base.rb +138 -0
  19. data/lib/pal/handler/definitions/aws_cur.json +8 -0
  20. data/lib/pal/handler/manager.rb +30 -0
  21. data/lib/pal/handler/processor.rb +84 -0
  22. data/lib/pal/log.rb +29 -0
  23. data/lib/pal/main.rb +63 -0
  24. data/lib/pal/operation/actions.rb +106 -0
  25. data/lib/pal/operation/exporter.rb +183 -0
  26. data/lib/pal/operation/filter_evaluator.rb +249 -0
  27. data/lib/pal/operation/processor_context.rb +50 -0
  28. data/lib/pal/operation/projection.rb +302 -0
  29. data/lib/pal/plugin.rb +61 -0
  30. data/lib/pal/request/metadata.rb +19 -0
  31. data/lib/pal/request/runbook.rb +54 -0
  32. data/lib/pal/version.rb +5 -0
  33. data/lib/pal.rb +43 -0
  34. data/plugins/PLUGINS.md +1 -0
  35. data/plugins/operation/terminal_exporter_impl.rb +14 -0
  36. data/templates/DOCUMENTATION.md +46 -0
  37. data/templates/aws/data_transfer/data_transfer_breakdown.json +93 -0
  38. data/templates/aws/ec2/ec2_compute_hourly_breakdown.json +63 -0
  39. data/templates/aws/ec2/ec2_operation_breakdown.json +64 -0
  40. data/templates/aws/ec2/ec2_spend_breakdown.json +63 -0
  41. data/templates/aws/global_resource_and_usage_type_costs.json +41 -0
  42. data/templates/aws/kms/kms_usage_counts.json +52 -0
  43. data/templates/aws/kms/kms_usage_list.json +80 -0
  44. data/templates/aws/kms/list_of_kms_keys.json +57 -0
  45. data/templates/aws/reserved_instances/all_reserved_instance_expiries.json +41 -0
  46. data/templates/aws/reserved_instances/reserved_instance_opportunities.json +60 -0
  47. data/templates/aws/summary_cost_between_date_range.json +43 -0
  48. data/templates/aws/summary_daily_breakdown_costs.json +39 -0
  49. data/templates/azure/global_resource_type_summary.json +47 -0
  50. 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