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.
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,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pal"
4
+
5
+ module Pal
6
+ module Operation
7
+ class Projection
8
+ include Log
9
+
10
+ # @return [String]
11
+ attr_reader :type
12
+
13
+ # @return [String]
14
+ attr_reader :property
15
+
16
+ # @param [String] type
17
+ # @param [String] property
18
+ def initialize(type, property)
19
+ @type = type
20
+ @property = property
21
+ end
22
+
23
+ # @param [Array<String>] group_by_rules # export columns
24
+ # @param [Hash] groups
25
+ # @param [Hash] column_headers
26
+ # @return [Array] rows, column_headers
27
+ def process(group_by_rules, groups, column_headers)
28
+ log_info("Calling down to projection impl [#{type}]")
29
+ _process_impl(group_by_rules, groups, column_headers)
30
+ end
31
+
32
+ # @return [Boolean]
33
+ def processable?
34
+ !(@type.nil? || @property.nil?)
35
+ end
36
+
37
+ private
38
+
39
+ # @abstract
40
+ # @param [Array<String>] _group_by_rules
41
+ # @param [Hash] _groups
42
+ # @param [Hash] _column_headers
43
+ # @return [Array] rows, column_headers
44
+ def _process_impl(_group_by_rules, _groups, _column_headers)
45
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
46
+ end
47
+ end
48
+
49
+ class SumProjectionImpl < Projection
50
+ # @param [String] property
51
+ def initialize(property)
52
+ super("sum", property)
53
+ end
54
+
55
+ private
56
+
57
+ # @param [Array<String>] group_by_rules
58
+ # @param [Hash] groups
59
+ # @param [Hash] column_headers
60
+ # @return [Array] rows, column_headers
61
+ # rubocop:disable Metrics/AbcSize
62
+ # rubocop:disable Metrics/MethodLength
63
+ def _process_impl(group_by_rules, groups, column_headers)
64
+ rows = []
65
+ sum_column_idx = column_headers[@property]
66
+
67
+ raise "Missing column. Please include [#{@property}] in columns #{column_headers.keys}." unless sum_column_idx
68
+
69
+ groups.each_key do |key|
70
+ sum = 0.0
71
+
72
+ row = groups[key]
73
+
74
+ row.each do |entry|
75
+ sum += entry[sum_column_idx].to_f
76
+ end
77
+
78
+ arr = []
79
+ group_by_rules.each do |gb|
80
+ idx = column_headers[gb]
81
+ arr << row[0][idx]
82
+ end
83
+
84
+ arr << sum.round(8)
85
+ rows << arr
86
+ end
87
+
88
+ column_headers = {}
89
+ group_by_rules.each_with_index { |gb, idx| column_headers[gb] = idx }
90
+ column_headers["sum_#{@property}"] = group_by_rules.size
91
+
92
+ [rows, column_headers]
93
+ end
94
+ # rubocop:enable Metrics/MethodLength
95
+ # rubocop:enable Metrics/AbcSize
96
+ end
97
+
98
+ class DistinctProjectionImpl < Projection
99
+ # @param [String] property
100
+ def initialize(property)
101
+ super("distinct", property)
102
+ end
103
+
104
+ private
105
+
106
+ # @param [Array<String>] _group_by_rules
107
+ # @param [Hash] groups
108
+ # @param [Hash] column_headers
109
+ # @return [Array] rows, column_headers
110
+ def _process_impl(_group_by_rules, groups, column_headers)
111
+ rows = []
112
+ distinct_column_idx = column_headers[@property]
113
+
114
+ raise "Missing column index. Please include [#{@property}] in column extraction." unless distinct_column_idx
115
+
116
+ groups.each_key do |key|
117
+ row = groups[key]
118
+
119
+ row.each do |entry|
120
+ prop = entry[distinct_column_idx]
121
+ rows << prop unless rows.include?(prop)
122
+ end
123
+ end
124
+
125
+ column_headers = {}
126
+ column_headers["distinct_#{@property}"] = 0
127
+
128
+ [rows.map { |x| [x] }, column_headers]
129
+ end
130
+ end
131
+
132
+ class MaxMinProjectionImpl < Projection
133
+ private
134
+
135
+ # @param [Array<String>] _group_by_rules
136
+ # @param [Hash] groups
137
+ # @param [Hash] column_headers
138
+ # @return [Array] rows, column_headers
139
+ # rubocop:disable Metrics/AbcSize
140
+ def _process_impl(_group_by_rules, groups, column_headers)
141
+ max_vals = {}
142
+ max_column_idx = column_headers[@property]
143
+
144
+ groups.each_key do |key|
145
+ row = groups[key]
146
+
147
+ row.each do |entry|
148
+ prop_val = entry[max_column_idx].to_f
149
+
150
+ max_vals[key] = entry unless max_vals.key?(key)
151
+ max_vals[key] = entry if _comparator_proc.call(max_vals[key][0][max_column_idx].to_f, prop_val)
152
+ end
153
+ end
154
+
155
+ rows = max_vals.values
156
+
157
+ new_column_headers = {}
158
+ column_headers.keys.each_with_index { |ch, idx| new_column_headers[ch] = idx }
159
+
160
+ [rows, new_column_headers]
161
+ end
162
+ # rubocop:enable Metrics/AbcSize
163
+ #
164
+
165
+ def _comparator_proc
166
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
167
+ end
168
+ end
169
+
170
+ class MaxProjectionImpl < MaxMinProjectionImpl
171
+ # @param [String] property
172
+ def initialize(property)
173
+ super("max", property)
174
+ end
175
+
176
+ private
177
+
178
+ def _comparator_proc
179
+ proc { |x, y| x < y }
180
+ end
181
+ end
182
+
183
+ class MinProjectionImpl < MaxMinProjectionImpl
184
+ # @param [String] property
185
+ def initialize(property)
186
+ super("min", property)
187
+ end
188
+
189
+ private
190
+
191
+ def _comparator_proc
192
+ proc { |x, y| x > y }
193
+ end
194
+ end
195
+
196
+ class DefaultProjectionImpl < Projection
197
+ # @param [String] property
198
+ def initialize(property)
199
+ super("default", property)
200
+ end
201
+
202
+ # @param [Array<String>] _group_by_rules
203
+ # @param [Hash] groups
204
+ # @param [Hash] column_headers
205
+ # @return [Array] rows, column_headers
206
+ def _process_impl(_group_by_rules, groups, column_headers)
207
+ [groups.values, column_headers]
208
+ end
209
+ end
210
+
211
+ class AverageProjectionImpl < Projection
212
+ # @param [String] property
213
+ def initialize(property)
214
+ super("average", property)
215
+ end
216
+
217
+ private
218
+
219
+ # @param [Array<String>] group_by_rules
220
+ # @param [Hash] groups
221
+ # @param [Hash] column_headers
222
+ # @return [Array] rows, column_headers
223
+ # rubocop:disable Metrics/AbcSize
224
+ # rubocop:disable Metrics/MethodLength
225
+ def _process_impl(group_by_rules, groups, column_headers)
226
+ rows = []
227
+ sum_column_idx = column_headers[@property]
228
+
229
+ groups.each_key do |key|
230
+ sum = 0.0
231
+
232
+ records = groups[key]
233
+ records.each do |entry|
234
+ sum += entry[sum_column_idx].to_f
235
+ end
236
+
237
+ arr = []
238
+ group_by_rules.each do |gb|
239
+ next if gb.eql? @property
240
+
241
+ idx = column_headers[gb]
242
+ arr << records[0][idx]
243
+ end
244
+
245
+ arr << sum.round(8) / records.size
246
+ rows << arr
247
+ end
248
+
249
+ column_headers = {}
250
+ group_by_rules.each_with_index do |gb, idx|
251
+ next if gb.eql? @property
252
+
253
+ column_headers[gb] = idx
254
+ end
255
+
256
+ column_headers["average_#{@property}"] = group_by_rules.size - 1 # remove default
257
+
258
+ [rows, column_headers]
259
+ end
260
+ # rubocop:enable Metrics/AbcSize
261
+ # rubocop:enable Metrics/MethodLength
262
+ end
263
+
264
+ class CountProjectionImpl < Projection
265
+ # @param [String] property
266
+ def initialize(property)
267
+ super("count", property)
268
+ end
269
+
270
+ private
271
+
272
+ # @param [Array<String>] _group_by_rules
273
+ # @param [Hash] groups
274
+ # @param [Hash] column_headers
275
+ # @return [Array] rows, column_headers
276
+ # rubocop:disable Metrics/AbcSize
277
+ def _process_impl(_group_by_rules, groups, column_headers)
278
+ distinct_column_idx = column_headers[@property]
279
+ raise "Missing column index. Please include [#{@property}] in column extraction." unless distinct_column_idx
280
+
281
+ count_map = {}
282
+
283
+ groups.each_key do |key|
284
+ groups[key].each do |entry|
285
+ prop = entry[distinct_column_idx]
286
+
287
+ count_map[prop] = 0 unless count_map[prop]
288
+ count_map[prop] += 1
289
+ end
290
+ end
291
+
292
+ column_headers = {}
293
+ column_headers[@property] = 0
294
+ column_headers["count_#{@property}"] = 0
295
+
296
+ [count_map.map { |k, v| [k, v] }, column_headers]
297
+ end
298
+ # rubocop:enable Metrics/AbcSize
299
+ end
300
+ end
301
+ end
302
+
data/lib/pal/plugin.rb ADDED
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pal"
4
+ require "pal/log"
5
+
6
+ module Pal
7
+ module Plugin
8
+ include Log
9
+
10
+ def register_plugins(plugin_dir="plugins")
11
+ log_info "Registering any plugins for directory [#{plugin_dir}]"
12
+ PluginManager.instance.register(plugin_dir)
13
+ end
14
+
15
+ class PluginManager
16
+ include Singleton
17
+ include Log
18
+
19
+ def register(plugin_dir)
20
+ candidates = Dir.glob("#{plugin_dir}/**/*.rb").select { |e| File.file? e }
21
+
22
+ log_info "Found a total of [#{candidates.size}] candidates"
23
+ candidates.each do |file_path|
24
+ full_clazz_name = get_clazz_name(file_path)
25
+
26
+ next unless defined?(full_clazz_name)
27
+ next unless full_clazz_name.start_with?("Pal::")
28
+
29
+ log_info "[#{full_clazz_name}] has passed validation and will be loaded"
30
+
31
+ load file_path
32
+
33
+ validate_plugin(full_clazz_name)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def get_clazz_name(file_path)
40
+ mod, clazz = file_path.split("/")[-2..]
41
+ full_clazz = clazz.split("_").map(&:capitalize).join("").gsub(".rb", "")
42
+ "Pal::#{mod.capitalize}::#{full_clazz}"
43
+ end
44
+
45
+ # @param [String] full_clazz_name
46
+ # @raise [RuntimeError]
47
+ def validate_plugin(full_clazz_name)
48
+ clazz_ins = Kernel.const_get(full_clazz_name)
49
+ ancestors = clazz_ins.ancestors
50
+
51
+ valid_candidates = [Pal::Operation::BaseExportHandler]
52
+ unless valid_candidates.find { |a| ancestors.include?(a) }
53
+ log_error("Invalid plugin has been given. Valid plugin candidates are: #{valid_candidates.inspect}")
54
+ raise "Invalid plugin given!"
55
+ end
56
+
57
+ true
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pal
4
+ module Request
5
+ class Metadata
6
+ include Pal::ObjectHelpers
7
+
8
+ # @return [String]
9
+ attr_accessor :version, :name, :description, :handler
10
+
11
+ def initialize(opts={})
12
+ @description = opts["description"]
13
+ @version = opts["version"]
14
+ @handler = opts["handler"]
15
+ @name = opts["name"]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pal/operation/filter_evaluator"
4
+ require "pal/operation/exporter"
5
+ require "pal/operation/actions"
6
+ require "pal/request/metadata"
7
+ require "pal/common/object_helpers"
8
+ require "json"
9
+
10
+ module Pal
11
+ module Request
12
+ class Runbook
13
+ include ObjectHelpers
14
+
15
+ # @return [Pal::Request::Metadata]
16
+ attr_reader :metadata
17
+
18
+ # @return [Pal::Operation::FilterEvaluator]
19
+ attr_reader :filters
20
+
21
+ # @return [Pal::Operation::Exporter]
22
+ attr_reader :exporter
23
+
24
+ # @return [Pal::Operation::Actions]
25
+ attr_reader :actions
26
+
27
+ # @return [Pal::Operation::Transforms]
28
+ attr_reader :transforms
29
+
30
+ # @return [Hash]
31
+ attr_accessor :column_overrides
32
+
33
+ # @param [Array<Hash>] opts
34
+ def filters=(opts)
35
+ @filters = Pal::Operation::FilterEvaluator.new(opts)
36
+ end
37
+
38
+ # @param [Hash] opts
39
+ def metadata=(opts)
40
+ @metadata = Pal::Request::Metadata.new(opts)
41
+ end
42
+
43
+ # @param [Hash] opts
44
+ def exporter=(opts)
45
+ @exporter = Pal::Operation::Exporter.new.from_hash(opts)
46
+ end
47
+
48
+ # # @param [Hash] opts
49
+ # def transforms=(opts)
50
+ # @transforms = Pal::Operation::Transforms.new(opts)
51
+ # end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pal
4
+ VERSION = "0.2.1"
5
+ end
data/lib/pal.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pal/version"
4
+ require "pal/main"
5
+ require "pal/configuration"
6
+ require "pal/log"
7
+
8
+ require "pal/operation/filter_evaluator"
9
+ require "pal/handler/processor"
10
+ require "pal/operation/exporter"
11
+ require "pal/operation/actions"
12
+
13
+ require "pal/request/runbook"
14
+ require "pal/handler/manager"
15
+
16
+ require "logger"
17
+
18
+ # Entry point for Pal services
19
+ module Pal
20
+ class << self
21
+ attr_writer :logger
22
+
23
+ def logger
24
+ @logger ||= Logger.new($stdout).tap do |log|
25
+ log.progname = name
26
+ end
27
+ end
28
+ end
29
+
30
+ # Exception classes
31
+ class ValidationError < StandardError
32
+ attr_reader :errors
33
+
34
+ def initialize(errors, msg="Invalid Request")
35
+ super(msg)
36
+ @errors = errors
37
+ end
38
+
39
+ def message
40
+ "Validation error: [#{@errors.join(", ")}]"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1 @@
1
+ ## Plugins Documentation
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pal"
4
+ require "pal/operation/exporter"
5
+
6
+ module Pal
7
+ module Operation
8
+ class TerminalExporterImpl < BaseExportHandler
9
+ def _export(rows, _column_headers)
10
+ puts "Inside plugin! You passed me [#{rows.size}] rows"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,46 @@
1
+ Documentation for Templates
2
+ ===========================
3
+
4
+ Templates are made up of three parts - metadata, filters and exporter. At is most simplest, only the metadata field needs to be provided.
5
+
6
+ ##Metadata
7
+ ```json
8
+ "metadata" : {
9
+ "version" : "2022-04-02",
10
+ "name" : "<name>",
11
+ "description" : "<description>"
12
+ "handler" : "<handler>"
13
+ },...
14
+ ```
15
+
16
+ ###Keys
17
+ - **version**: API version, default is [2022-04-02]
18
+ - **name**: Name of the template.
19
+ - **description**: Description of the template.
20
+ - **handler**: Specifies the type of spreadsheet to process. Default is [AwsCur]
21
+
22
+ -------------------------------------------
23
+
24
+ ##Filters
25
+ ```json
26
+ ...
27
+ "filters": {
28
+ "condition": "<and/or>",
29
+ "rules": [{
30
+ "field": "<column_header>",
31
+ "type": "<number|string>",
32
+ "operator": "<operator>",
33
+ "value": "<value>"
34
+ }
35
+ ]
36
+ },...
37
+ ```
38
+ ###Keys
39
+ - **condition**: Must be either and/or. Can be nested.
40
+ - **rules**: List of rules
41
+ - **field**: Field found in spreadsheet.
42
+ - **type**: Data type, either string or number.
43
+ - **operator**: Predicate to validate against - <equal, not_equal,..>.
44
+ - **value**: Value to validate against.
45
+
46
+
@@ -0,0 +1,93 @@
1
+ {
2
+ "metadata" : {
3
+ "version" : "2022-04-02",
4
+ "name" : "Data Transfer Breakdown",
5
+ "handler" : "AwsCur",
6
+ "description" : "Spend breakdown for data transfer (From/To)"
7
+ },
8
+ "filters": {
9
+ "condition": "AND",
10
+ "rules": [
11
+ {
12
+ "field": "product/productFamily",
13
+ "type": "string",
14
+ "operator": "equal",
15
+ "value": "Data Transfer"
16
+ },
17
+ {
18
+ "condition": "OR",
19
+ "rules": [
20
+ {
21
+ "field": "lineItem/LineItemType",
22
+ "type": "string",
23
+ "operator": "equal",
24
+ "value": "Usage"
25
+ },
26
+ {
27
+ "field": "lineItem/LineItemType",
28
+ "type": "string",
29
+ "operator": "equal",
30
+ "value": "DiscountedUsage"
31
+ },
32
+ {
33
+ "field": "lineItem/LineItemType",
34
+ "type": "string",
35
+ "operator": "equal",
36
+ "value": "SavingsPlanCoveredUsage"
37
+ }
38
+ ]
39
+ }
40
+ ]
41
+ },
42
+ "exporter" : {
43
+ "types" : [{
44
+ "name" : "table",
45
+ "settings" : {
46
+ "title" : "Data Transfer by Product and Type"
47
+ }
48
+ }],
49
+ "properties" : [
50
+ "lineItem/UsageStartDate",
51
+ "lineItem/UsageAccountId",
52
+ "lineItem/ProductCode",
53
+ "lineItem/ResourceId",
54
+ "lineItem/UsageType",
55
+ "product/fromLocation",
56
+ "product/toLocation",
57
+ "product/productFamily",
58
+ "lineItem/UnblendedCost"
59
+ ],
60
+ "actions" : {
61
+ "group_by" : [
62
+ "lineItem/ProductCode",
63
+ "lineItem/UsageAccountId",
64
+ "lineItem/UsageType",
65
+ "product/fromLocation",
66
+ "product/toLocation",
67
+ "product/productFamily"
68
+ ],
69
+ "sort_by" : "lineItem/ProductCode",
70
+ "projection" : {
71
+ "type" : "sum",
72
+ "property" : "lineItem/UnblendedCost"
73
+ }
74
+ }
75
+ },
76
+ "column_overrides" : {
77
+ "lineItem/UsageStartDate" : {
78
+ "data_type": "date"
79
+ }
80
+ },
81
+ "__comments__" : {
82
+ "optional": {
83
+ "actions" : {
84
+ "group_by" : [
85
+ "lineItem/UsageStartDate",
86
+ "lineItem/ResourceId"
87
+ ]
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+