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