jcf 0.0.10 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9477e6264cb699c3505cfe016acb802bff09e9c71fe98072ea9a1598a021119
4
- data.tar.gz: 05a70e89d7593faa6308a6d0b78d03a3990466dae549a8343625288bf104a4ea
3
+ metadata.gz: 33389a4011279ad183dc738318b71355a50601dde4f602d867cb060b305e7a49
4
+ data.tar.gz: 42f129b8aa7ab515bd51fac1f2c8f78ee6785f8750debb5778c88fa2d062dca1
5
5
  SHA512:
6
- metadata.gz: ec4fa652bb3341babc362145fe150e9418944e3b5c2d5aeb2b798c7d82af39849a3c467b1432941638a9c4b0650f468b56c537e927c4ab5b7793884faf31e491
7
- data.tar.gz: 25bad72e4df60a12ef1df353abb6472e9db88623f9a4c7ebcd1e4150222bb92c4a2a63684582c9b5758eec353bdcab263b59cfdd67e259aeb42be071f9691827
6
+ metadata.gz: 35548f0ce298af159e532eb6b0517594d8e990857d8c5f77aab5e0b770920758e2a11f74ca063c113342adc1a46df120ed2d4577684ae69b60db90578a4a5d8d
7
+ data.tar.gz: 1fd422e325af12931d444728c4aca883eceaceab60bea50bc33e143bc62e70d783b1ed4103a2c181302fc6d5e7b44eeff70aa4ff53278cb12710e7b6ec806ee6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.0.12] - 2023-10-19
2
+
3
+ - Refactored broker backend querying for metrics to use a plugin system that can be extended to support other brokers. Currently only the AWS and S3 broker is supported, but it should be easy to add others.
4
+ - Add a `jcf plugins` command to list available plugins (currently only ones in the `lib/jcf/plugins` directory)
5
+ - Refactored specs for output formatters
6
+
7
+ ## [0.0.11] - 2023-10-12
8
+
9
+ - Add a tree output for a new command `jcf services NAME` that will show all the offerings and instances for that broker. In a tree. Because trees are cool.
10
+
1
11
  ## [0.0.10] - 2023-10-03
2
12
 
3
13
  - Added the template parsing options to metrics command
data/README.md CHANGED
@@ -1,8 +1,25 @@
1
- ![tests](https://github.com/fearoffish/paas-org-metric-gathering-gem/actions/workflows/main.yml/badge.svg)
1
+ [![tests](https://github.com/fearoffish/paas-org-metric-gathering-gem/actions/workflows/main.yml/badge.svg)](https://github.com/fearoffish/paas-org-metric-gathering-gem/actions)
2
2
  [![Gem Version](https://badge.fury.io/rb/jcf.svg)](https://badge.fury.io/rb/jcf)
3
3
 
4
4
  # Jcf
5
5
 
6
+ ## What is it?
7
+
8
+ If you use CloudFoundry you'll know that some commands/queries require you to use `cf curl` and that can get a little tedious. This gem is a wrapper around `cf curl` to make it easier to query the CloudFoundry API, as well as being able to query the CloudWatch metrics for the AWS services that are used by the CF services.
9
+
10
+ Some handy features:
11
+
12
+ ```
13
+ jcf u jamie # find all users with a partial username of jamie
14
+ jcf o foo # find all orgs with a partial name of foo
15
+ jcf sb s3 # find all service brokers with a partial name of s3
16
+ jcf so bucket # find all service offerings with a partial name of bucket
17
+ jcf sp medium # find all service plans with a partial name of medium
18
+
19
+ # find all metrics for the rds-broker service, postgres offering, output it to a csv file
20
+ jcf m postgres -t 'rdsbroker-{guid}' -o admin -f csv --ouput postgres-metrics.csv
21
+ ```
22
+
6
23
  ## Installation
7
24
 
8
25
  ```sh
@@ -134,3 +151,8 @@ The gem is available as open source under the terms of the [MIT License](https:/
134
151
  ## Code of Conduct
135
152
 
136
153
  Everyone interacting in the Jcf project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/fearoffish/paas-org-metric-gathering-gem/blob/main/CODE_OF_CONDUCT.md).
154
+
155
+ ## Credits
156
+
157
+ - Hanami for the [CLI framework](https://hanamirb.org/guides/1.3/cli/overview/)
158
+ I borrowed some ideas from there. Thanks!
data/lib/jcf/cf/base.rb CHANGED
@@ -23,6 +23,14 @@ module JCF
23
23
  { name: name, guid: guid, relationships: relationships }
24
24
  end
25
25
 
26
+ def keys
27
+ attributes.keys
28
+ end
29
+
30
+ def values
31
+ attributes.values
32
+ end
33
+
26
34
  def initialize(name: nil, guid: nil, relationships: nil)
27
35
  @name = name
28
36
  @guid = guid
@@ -32,12 +40,14 @@ module JCF
32
40
  class << self
33
41
  attr_accessor :endpoint
34
42
 
43
+ def keys; new.keys; end
44
+
35
45
  def find_by(attrs)
36
46
  objects = all
37
47
  objects.find_all do |obj|
38
- # only find by attributes we have and our object has
39
48
  keys = obj.attributes.keys & attrs.keys
40
49
  keys.all? do |key|
50
+ return true if attrs[key].nil?
41
51
  obj.attributes[key].include? attrs[key]
42
52
  end
43
53
  end
@@ -51,6 +61,7 @@ module JCF
51
61
  new(guid: guid).populate!
52
62
  end
53
63
 
64
+ # TODO: make this less greedy
54
65
  def all(params = {})
55
66
  params.compact!
56
67
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class Services
8
+ def self.first(name:)
9
+ # Find the broker, then find all offerings for that broker, then all plans
10
+ # { broker: { offerings: { plans: [] } }
11
+ # e.g. { "p-mysql (guid)": { "10mb (guid)": [], "100mb (guid)": [] } }
12
+ broker = ServiceBroker.first(name: name)
13
+ offerings = ServiceOffering.all(service_broker_guids: broker.guid).map do |offering|
14
+ plans = ServicePlan.all(service_offering_guids: offering.guid)
15
+ { offering.to_s => plans }
16
+ end
17
+ { broker.to_s => offerings }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -14,38 +14,51 @@ module JCF
14
14
  module Commands
15
15
  module CF
16
16
  class Metrics < Command
17
- argument :offering, required: true, values: %w[postgres aws-s3-bucket],
18
- desc: "Choose a service instance offering to query"
17
+ argument :broker, required: true,
18
+ desc: "Choose a service instance offering to query. Get the broker name from the command: jcf sb"
19
+ argument :iaas_plugin, aliases: ["-m"], required: true, type: :string,
20
+ values: ::JCF::Plugins.plugins.keys.collect(&:to_s),
21
+ desc: "Select a IaaS plugin this broker is backed by"
19
22
 
20
23
  option :template, aliases: ["-t"], required: true, type: :string,
21
- desc: "Template for backend service instance names e.g. \"rdsbroker-{guid}-{name}\""
24
+ desc: "Template for backend service instance names e.g. \"rdsbroker-{guid}-{name}\""
22
25
  option :values, aliases: ["-v"], required: false, type: :string, default: "",
23
- desc: "Values for the template. 'guid' is the service instance guid e.g. \"name=test\""
26
+ desc: "Values for the template. 'guid' is the service instance guid e.g. \"name=test\""
24
27
  option :org, aliases: ["-o"], required: true, type: :string,
25
- desc: "Choose an organization (can be multiple comma-separated)"
28
+ desc: "Choose an organization (can be multiple comma-separated)"
26
29
  option :name, aliases: ["-n"], type: :string, desc: "Choose a service instance name"
27
30
 
28
31
  def call(*_args, **options)
29
32
  validate_options(options)
33
+ JCF.plugin options[:iaas_plugin].to_sym
34
+ plugin = JCF::Plugins.plugins[options[:iaas_plugin].to_sym]
35
+
30
36
  orgs = options[:org].include?(",") ? options[:org].split(",") : [options[:org]]
31
37
 
38
+ brokers = service_brokers.select { |b| b.name == options[:broker] }
39
+ # find the offerings for those brokers
40
+ offerings = service_offerings.select do |o|
41
+ brokers.find { |b| b.guid == o.relationships.service_broker.guid }
42
+ end
43
+ if offerings && offerings.empty?
44
+ err.puts "No offerings found for broker #{options[:broker]}"
45
+ exit(1)
46
+ end
47
+ err.puts "Found #{offerings.count} offerings"
48
+
32
49
  orgs.each do |org|
33
50
  org_guid = organizations.find { |o| o.name == org }.guid
34
51
  err.puts "Found org guid: #{org_guid}"
35
52
 
36
- offering_guid = service_offerings.find { |s| s.name == options[:offering] }&.guid
37
- err.puts "Found offering guid: #{offering_guid}"
38
-
39
- unless offering_guid
40
- err.puts "No offerings found for type #{options[:offering]}"
41
- exit(1)
42
- end
43
-
44
- # find the plans for those gatherings
53
+ # find the plans for those offerings
45
54
  plan_guids = service_plans.find_all do |plan|
46
- plan.relationships.service_offering.guid == offering_guid
55
+ offerings.collect(&:guid).include? plan.relationships.service_offering.guid
47
56
  end.collect(&:guid)
48
57
  err.puts "Found plan guids: #{plan_guids.count}"
58
+ if plan_guids.empty?
59
+ err.puts "No plans found for offerings"
60
+ exit(1)
61
+ end
49
62
 
50
63
  # look up the instances that match the plans and org
51
64
  # "/v3/service_instances?organization_guids=${org_guids}&service_plan_guids=${plan_guids}"
@@ -56,11 +69,9 @@ module JCF
56
69
  instances.select! { |i| i.name.include? options[:name] } if options[:name]
57
70
  err.puts "Found instances: #{instances.count}"
58
71
 
59
- cw = JCF::AWS::CloudWatch.new
60
- values = []
72
+ values = {}
61
73
 
62
74
  Thread.abort_on_exception = true
63
- # use a the number of processors as the number of threads
64
75
  instances.each_slice(Concurrent.processor_count) do |slice|
65
76
  slice.collect do |instance|
66
77
  service_plan = instance.relationships.service_plan.populate!
@@ -68,42 +79,42 @@ module JCF
68
79
  service_broker = service_offering.relationships.service_broker.populate!
69
80
 
70
81
  Thread.new do
71
- m = JCF::CF::Metric.new
72
- m.name = (instance.name || "")
73
- err.puts "Getting metrics for #{m.name}"
74
- m.region = ENV["AWS_REGION"]
75
- m.deploy_env = options[:env]
76
- m.organization = org
77
- m.organization_guid = org_guid
82
+ metrics = {}
83
+ metrics[:name] = (instance.name || "")
84
+ metrics[:instance_guid] = instance.guid
85
+ err.puts "Getting metrics for #{instance.name}"
86
+ metrics[:region] = ENV["AWS_REGION"]
87
+ metrics[:organization] = org
88
+ metrics[:organization_guid] = org_guid
78
89
  space = spaces.find { |s| s.guid == instance.relationships.space.guid }
79
- m.space = space.name
80
- m.space_guid = space.guid
81
- m.service_broker_name = service_broker.name
82
- m.service_broker_guid = service_broker.guid
83
- m.service_offering = service_offering.name
84
- m.service_plan = service_plan.name
90
+ metrics[:space] = space.name
91
+ metrics[:space_guid] = space.guid
92
+ metrics[:service_broker_name] = service_broker.name
93
+ metrics[:service_broker_guid] = service_broker.guid
94
+ metrics[:service_offering] = service_offering.name
95
+ metrics[:service_plan] = service_plan.name
85
96
 
86
97
  template = options[:template]
87
98
  t_values = parse_values(options[:values], instance.guid)
88
99
 
89
- if service_offering.name == "postgres"
90
- m.storage_used = to_gb(cw.rds_storage_used(name: JCF::CLI.template_parser(template, t_values)) || "")
91
- m.storage_allocated = to_gb(cw.storage_allocated(name: JCF::CLI.template_parser(template, t_values)) || "")
92
- m.storage_free = to_gb(cw.storage_free(name: JCF::CLI.template_parser(template, t_values)) || "")
93
- m.iops = (cw.iops(name: JCF::CLI.template_parser(template, t_values)).to_fs(:rounded, precision: 0) || "")
94
- m.cpu = (cw.cpu(name: JCF::CLI.template_parser(template, t_values)).to_fs(:rounded, precision: 0) || "")
95
- end
96
-
97
- if service_offering.name == "aws-s3-bucket"
98
- m.storage_used = to_gb(cw.s3_storage_used(name: JCF::CLI.template_parser(template, t_values)) || "")
100
+ t = JCF::CLI.template_parser(template, t_values)
101
+ plugin.new(name: t).metrics.each do |k, v|
102
+ metrics[k] = v
99
103
  end
100
- values << m
104
+ values[instance.guid] = metrics
101
105
  end
102
- end.map(&:value)
106
+ end.each(&:join)
103
107
  end
104
108
 
105
- values << JCF::CF::Metric.new if values.empty?
106
- out.puts formatter.format(values)
109
+ # values = { guid: { name: "name", space: "space" }, guid2: { name: "name2", space: "space2" } }
110
+ output = Hash.new { |hash, key| hash[key] = [] }
111
+ values.each do |guid, metrics|
112
+ metrics.each do |k, v|
113
+ output[k] << v
114
+ end
115
+ end
116
+
117
+ out.puts formatter.format(data: output)
107
118
  err.puts "Done."
108
119
  end
109
120
  end
@@ -141,13 +152,7 @@ module JCF
141
152
  end
142
153
 
143
154
  def service_brokers
144
- JCF::CF::ServiceBroker.all
145
- end
146
-
147
- def to_gb(bytes)
148
- Filesize.from("#{bytes} b").to("GB").to_fs(:rounded, precision: 2)
149
- rescue ArgumentError
150
- 0
155
+ @service_brokers ||= JCF::CF::ServiceBroker.all
151
156
  end
152
157
  end
153
158
  end
@@ -8,14 +8,17 @@ module JCF
8
8
  module Commands
9
9
  module CF
10
10
  class Organizations < Command
11
+ include JCF::CF
12
+
11
13
  argument :name, required: false, desc: "Organization name"
12
14
 
13
15
  def call(name: nil, **)
14
- if name
15
- out.puts formatter.format(JCF::CF::Organization.find_by(name: name))
16
+ values = if name
17
+ Organization.find_by(name: name).collect(&:values)
16
18
  else
17
- out.puts formatter.format(JCF::CF::Organization.all)
19
+ Organization.all.collect(&:values)
18
20
  end
21
+ out.puts formatter.format(headers: Organization.keys, values: values)
19
22
  end
20
23
  end
21
24
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require_relative "../../errors"
5
+ require "tty-tree"
6
+
7
+ module JCF
8
+ module CLI
9
+ module Commands
10
+ module CF
11
+ class Services < Command
12
+ argument :broker, required: false, desc: "Broker name"
13
+
14
+ def call(broker: nil, **)
15
+ # gather all service offerings and plans for a single broker
16
+ if broker
17
+ out.puts formatter.format(JCF::CF::Services.first(name: broker), tree: true)
18
+ else
19
+ # TODO: this is a stub, it should be a list of all brokers
20
+ # out.puts formatter.format(JCF::CF::Organization.all)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JCF
4
+ module CLI
5
+ module Commands
6
+ class Plugins < Command
7
+ desc "Show JCF plugins"
8
+
9
+ def call(*)
10
+ out.puts "JCF Plugins:"
11
+ plugin_dir = File.join(JCF.root, "jcf", "plugins", "*.rb")
12
+ pp "plugin dir: #{plugin_dir}" if ENV["DEBUG"]
13
+ Dir[plugin_dir].each do |plugin|
14
+ out.puts " file: #{File.basename(plugin, ".rb")}" if ENV["DEBUG"]
15
+ JCF::Plugins.load_plugin(File.basename(plugin, ".rb").to_sym)
16
+ end
17
+ JCF::Plugins.plugins.each do |plugin|
18
+ out.puts " #{plugin}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -7,11 +7,13 @@ module JCF
7
7
 
8
8
  def self.register_commands!
9
9
  register "version", Commands::Version, aliases: ["v", "-v", "--version"]
10
+ register "plugins", Commands::Plugins
10
11
  register "metrics", Commands::CF::Metrics, aliases: ["m"]
11
12
 
12
13
  register "organizations", Commands::CF::Organizations, aliases: %w[o orgs organization]
13
14
  register "spaces", Commands::CF::Spaces, aliases: %w[s space]
14
15
  register "users", Commands::CF::Users, aliases: %w[u user]
16
+ register "services", Commands::CF::Services
15
17
  register "service_brokers", Commands::CF::ServiceBrokers, aliases: %w[sb service_broker]
16
18
  register "service_offerings", Commands::CF::ServiceOfferings, aliases: %w[so service_offering]
17
19
  register "service_instances", Commands::CF::ServiceInstances, aliases: %w[si service_instance]
@@ -7,24 +7,12 @@ module JCF
7
7
  module OutputFormatters
8
8
  class CSV
9
9
  class << self
10
- def format(data)
11
- case data
12
- in Array
13
- array = data.collect(&:serializable_hash)
14
- generate_csv(headers: array.first&.keys, values: array)
15
- in Hash
16
- generate_csv(headers: data.keys.sort, values: [data.serializable_hash])
17
- else
18
- generate_csv(headers: [], values: [])
19
- end
20
- end
21
-
22
- private
10
+ def format(data:)
11
+ return "" if data.nil? || data.empty?
23
12
 
24
- def generate_csv(headers:, values:)
25
- ::CSV.generate(headers: headers, write_headers: true) do |csv|
26
- values.each do |hash|
27
- csv << hash.values
13
+ ::CSV.generate(headers: data.keys, write_headers: true) do |csv|
14
+ data.values.transpose.each do |value|
15
+ csv << value
28
16
  end
29
17
  end
30
18
  end
@@ -6,12 +6,10 @@ module JCF
6
6
  module CLI
7
7
  module OutputFormatters
8
8
  class JSON
9
- def self.format(data)
10
- if data.is_a?(Enumerable)
11
- ::JSON.generate data.collect(&:serializable_hash)
12
- else
13
- ::JSON.generate data.serializable_hash
14
- end
9
+ def self.format(data:)
10
+ return "{}" if data.nil? || data.empty?
11
+
12
+ data.to_json
15
13
  end
16
14
  end
17
15
  end
@@ -7,9 +7,19 @@ module JCF
7
7
  module OutputFormatters
8
8
  class Text
9
9
  class << self
10
- def format(data)
11
- return "" if data.nil?
10
+ def format(data:, tree: false)
11
+ return "" if data.nil? || data.empty?
12
12
 
13
+ if tree
14
+ render_tree(data)
15
+ else
16
+ render_data(data)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def render_data(data)
13
23
  keys = collect_keys(data)
14
24
  values = collect_values(data)
15
25
 
@@ -17,19 +27,34 @@ module JCF
17
27
  table.render(:unicode, resize: true)
18
28
  end
19
29
 
20
- def collect_values(data)
30
+ def render_tree(data)
31
+ TTY::Tree.new(data).render
32
+ end
33
+
34
+ def collect_keys(data)
35
+ return ["Empty result"] if !data || data.empty?
36
+
21
37
  if data.is_a?(Array)
22
- data.map { |d| d.attributes.values.collect(&:to_s) }
38
+ data.first.keys.collect(&:to_s)
39
+ elsif data.is_a?(Hash)
40
+ data.keys.collect(&:to_s) || ["Empty result"]
23
41
  else
24
- [data.attributes.values.collect(&:to_s)]
42
+ data.keys.collect(&:to_s)
25
43
  end
26
44
  end
27
45
 
28
- def collect_keys(data)
46
+ # Hash:
47
+ # values: [["name1", "name2"], ["space1", "space2"]]
48
+ # output: [["name1", "space1"], ["name2", "space2"]]
49
+ def collect_values(data)
50
+ return [["Empty result"]] if !data || data.empty?
51
+
29
52
  if data.is_a?(Array)
30
- data.first&.attributes&.keys || ["Empty result"]
53
+ data.map { |d| d.attributes.values.collect(&:to_s) }
54
+ elsif data.is_a?(Hash)
55
+ data.values.transpose
31
56
  else
32
- data.attributes.keys
57
+ [data.attributes.values.collect(&:to_s)]
33
58
  end
34
59
  end
35
60
  end
data/lib/jcf/cli.rb CHANGED
@@ -14,6 +14,50 @@ module JCF
14
14
  @cache ||= MiniCache::Store.new
15
15
  end
16
16
 
17
+ def self.plugin(plugin)
18
+ plugin = JCF::Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
19
+ validate_plugin!(plugin)
20
+ plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
21
+ plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
22
+ end
23
+
24
+ def self.validate_plugin!(plugin)
25
+ puts "Validating plugin (#{plugin}) implementation conforms to interface" if ENV["DEBUG"]
26
+
27
+ %i[metrics names values].each do |method|
28
+ raise "Plugin does not conform to interface (missing method \"#{method.to_s}\")" \
29
+ unless plugin.new(name: nil).respond_to?(method)
30
+ end
31
+ rescue JCF::CLI::NotLoggedInError => e
32
+ puts e.message
33
+ exit 1
34
+ end
35
+
36
+ module Plugins
37
+ @plugins = {}
38
+
39
+ def self.plugins
40
+ @plugins
41
+ end
42
+
43
+ def self.load_plugin(name)
44
+ return @plugins[name] if @plugins[name]
45
+
46
+ puts "Loading plugin #{name}" if ENV["DEBUG"]
47
+ require "jcf/plugins/#{name}"
48
+ raise "Plugin didn't correctly register itself" unless @plugins[name]
49
+ @plugins[name]
50
+ end
51
+
52
+ # Plugins need to call this method to register themselves:
53
+ #
54
+ # JCF::Plugins.register_plugin :render, Render
55
+ def self.register_plugin(name, mod)
56
+ puts "Registering plugin #{name}" if ENV["DEBUG"]
57
+ @plugins[name] = mod
58
+ end
59
+ end
60
+
17
61
  module CLI
18
62
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
19
63
  def self.loader
@@ -59,7 +103,9 @@ module JCF
59
103
 
60
104
  extend Dry::CLI::Registry
61
105
 
106
+ puts "Loading formatters..." if ENV["DEBUG"]
62
107
  register_formatters!
108
+ puts "Loading commands..." if ENV["DEBUG"]
63
109
  register_commands!
64
110
  end
65
111
  end
data/lib/jcf/metric.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ class Metric
7
+ attr_accessor :name, :value
8
+
9
+ def to_s
10
+ "#{name}: #{value}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-cloudwatch"
4
+ require "aws-sdk-rds"
5
+ require "active_support/core_ext/integer/time"
6
+ require "active_support/isolated_execution_state"
7
+ require "filesize"
8
+
9
+ module JCF
10
+ module Plugins
11
+ class AwsRds
12
+ def initialize(name:)
13
+ @name = name
14
+ @metrics = {}
15
+ check_token!
16
+ end
17
+
18
+ def names
19
+ %w[storage_used storage_allocated storage_free iops cpu]
20
+ end
21
+
22
+ def values
23
+ names.map { |name| send(name.to_sym) }
24
+ end
25
+
26
+ def metrics
27
+ @metrics[:storage_used] = to_gb(storage_used(name: @name || ""))
28
+ @metrics[:storage_allocated] = to_gb(storage_allocated(name: @name) || "")
29
+ @metrics[:storage_free] = to_gb(storage_free(name: @name) || "")
30
+ @metrics[:iops] = (iops(name: @name).to_fs(:rounded, precision: 0) || "")
31
+ @metrics[:cpu] = (cpu(name: @name).to_fs(:rounded, precision: 0) || "")
32
+ @metrics
33
+ end
34
+
35
+ def to_s
36
+ metrics.to_s
37
+ end
38
+
39
+ private
40
+
41
+ def check_token!
42
+ Aws::STS::Client.new.get_caller_identity({})
43
+ rescue Aws::Errors::MissingCredentialsError
44
+ raise JCF::CLI::NotLoggedInError, "Your AWS token has expired. Please log in again."
45
+ end
46
+
47
+ METRICS = {
48
+ free_storage: "FreeStorageSpace",
49
+ allocated_storage: "AllocatedStorage",
50
+ iops: "WriteIOPS",
51
+ cpu: "CPUUtilization"
52
+ }.freeze
53
+
54
+ def storage_free(name:)
55
+ storage_free = cloudwatch(name: name, metric: "FreeStorageSpace")
56
+ storage_free.to_i
57
+ end
58
+
59
+ def storage_allocated(name:)
60
+ rds = Aws::RDS::Client.new
61
+ size = rds
62
+ .describe_db_instances(db_instance_identifier: name)
63
+ .db_instances.first.allocated_storage
64
+ Filesize.from("#{size} GB").to_i
65
+ end
66
+
67
+ def storage_used(name:)
68
+ free = storage_free(name: name)
69
+ allocated = storage_allocated(name: name)
70
+ allocated - free
71
+ end
72
+
73
+ def iops(name:)
74
+ cloudwatch(name: name, metric: :iops)
75
+ end
76
+
77
+ def cpu(name:)
78
+ cloudwatch(name: name, metric: :cpu)
79
+ end
80
+
81
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
82
+ def cloudwatch(name:, metric:)
83
+ cloudwatch = Aws::CloudWatch::Client.new
84
+ res = cloudwatch.get_metric_statistics({
85
+ namespace: "AWS/RDS",
86
+ metric_name: metric,
87
+ dimensions: [{ name: "DBInstanceIdentifier", value: name }],
88
+ start_time: 1.day.ago,
89
+ end_time: Time.now,
90
+ period: 86_400,
91
+ statistics: ["Average"]
92
+ })
93
+
94
+ pp res if ENV["DEBUG"]
95
+
96
+ res.datapoints.first&.average || 0
97
+ rescue Aws::Errors::MissingCredentialsError
98
+ puts "You are not logged in to an AWS shell. 'gds aws <ACCOUNT> -s'"
99
+ end
100
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
101
+
102
+ def to_gb(bytes)
103
+ Filesize.from("#{bytes} b").to("GB").to_fs(:rounded, precision: 2)
104
+ rescue ArgumentError
105
+ 0
106
+ end
107
+ end
108
+
109
+ register_plugin :aws_rds, AwsRds
110
+ end
111
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-s3"
4
+ require "active_support/core_ext/integer/time"
5
+ require "active_support/isolated_execution_state"
6
+ require "filesize"
7
+
8
+ module JCF
9
+ module Plugins
10
+ class AwsS3
11
+ def initialize(name:)
12
+ @name = name
13
+ @metrics = {}
14
+ check_token!
15
+ end
16
+
17
+ def names
18
+ %w[storage_used]
19
+ end
20
+
21
+ def values
22
+ names.map { |name| send(name.to_sym) }
23
+ end
24
+
25
+ def metrics
26
+ @metrics[:storage_used] = to_gb(s3_storage_used(name: @name || ""))
27
+ @metrics
28
+ end
29
+
30
+ def to_s
31
+ metrics.to_s
32
+ end
33
+
34
+ private
35
+
36
+ def s3_storage_used(name:)
37
+ s3 = Aws::S3::Client.new
38
+ total_size = 0
39
+ response = s3.list_objects_v2(bucket: name)
40
+ response.contents.each do |object|
41
+ total_size += object.size
42
+ end
43
+ total_size
44
+ end
45
+
46
+ def check_token!
47
+ Aws::STS::Client.new.get_caller_identity({})
48
+ rescue Aws::Errors::MissingCredentialsError
49
+ raise JCF::CLI::NotLoggedInError, "Your AWS token has expired. Please log in again."
50
+ end
51
+
52
+ def to_gb(bytes)
53
+ Filesize.from("#{bytes} b").to("GB").to_fs(:rounded, precision: 2)
54
+ rescue ArgumentError
55
+ 0
56
+ end
57
+ end
58
+
59
+ register_plugin :aws_s3, AwsS3
60
+ end
61
+ end
data/lib/jcf/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JCF
4
- VERSION = "0.0.10"
4
+ VERSION = "0.0.12"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jcf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamie van Dyke
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-03 00:00:00.000000000 Z
11
+ date: 2023-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -240,6 +240,20 @@ dependencies:
240
240
  - - "~>"
241
241
  - !ruby/object:Gem::Version
242
242
  version: 0.12.0
243
+ - !ruby/object:Gem::Dependency
244
+ name: tty-tree
245
+ requirement: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - "~>"
248
+ - !ruby/object:Gem::Version
249
+ version: 0.4.0
250
+ type: :runtime
251
+ prerelease: false
252
+ version_requirements: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - "~>"
255
+ - !ruby/object:Gem::Version
256
+ version: 0.4.0
243
257
  description:
244
258
  email:
245
259
  - me@fearof.fish
@@ -265,7 +279,6 @@ files:
265
279
  - lib/jcf/cf.rb
266
280
  - lib/jcf/cf/.DS_Store
267
281
  - lib/jcf/cf/base.rb
268
- - lib/jcf/cf/metric.rb
269
282
  - lib/jcf/cf/organization.rb
270
283
  - lib/jcf/cf/quota.rb
271
284
  - lib/jcf/cf/relationships.rb
@@ -273,6 +286,7 @@ files:
273
286
  - lib/jcf/cf/service_instance.rb
274
287
  - lib/jcf/cf/service_offering.rb
275
288
  - lib/jcf/cf/service_plan.rb
289
+ - lib/jcf/cf/services.rb
276
290
  - lib/jcf/cf/space.rb
277
291
  - lib/jcf/cf/user.rb
278
292
  - lib/jcf/cli.rb
@@ -286,14 +300,19 @@ files:
286
300
  - lib/jcf/cli/commands/cf/service_instances.rb
287
301
  - lib/jcf/cli/commands/cf/service_offerings.rb
288
302
  - lib/jcf/cli/commands/cf/service_plans.rb
303
+ - lib/jcf/cli/commands/cf/services.rb
289
304
  - lib/jcf/cli/commands/cf/spaces.rb
290
305
  - lib/jcf/cli/commands/cf/users.rb
306
+ - lib/jcf/cli/commands/plugins.rb
291
307
  - lib/jcf/cli/commands/version.rb
292
308
  - lib/jcf/cli/errors.rb
293
309
  - lib/jcf/cli/output_formatters.rb
294
310
  - lib/jcf/cli/output_formatters/csv.rb
295
311
  - lib/jcf/cli/output_formatters/json.rb
296
312
  - lib/jcf/cli/output_formatters/text.rb
313
+ - lib/jcf/metric.rb
314
+ - lib/jcf/plugins/aws_rds.rb
315
+ - lib/jcf/plugins/aws_s3.rb
297
316
  - lib/jcf/version.rb
298
317
  - sig/jcf.rbs
299
318
  homepage: https://github.com/fearoffish/paas-org-metric-gathering-gem
@@ -320,7 +339,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
320
339
  - !ruby/object:Gem::Version
321
340
  version: '0'
322
341
  requirements: []
323
- rubygems_version: 3.4.19
342
+ rubygems_version: 3.4.20
324
343
  signing_key:
325
344
  specification_version: 4
326
345
  summary: Gather metrics from AWS for CloudFoundry installations
data/lib/jcf/cf/metric.rb DELETED
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base"
4
-
5
- module JCF
6
- module CF
7
- class Metric < Base
8
- attr_accessor :name, :region, :deploy_env, :organization, :organization_guid, :space, :space_guid,
9
- :service_broker_name, :service_broker_guid, :service_offering, :service_plan,
10
- :storage_allocated, :storage_used, :storage_free, :iops, :cpu
11
-
12
- # rubocop:disable Metrics/MethodLength
13
- def attributes
14
- {
15
- name: name,
16
- region: region,
17
- organization: organization,
18
- organization_guid: organization_guid,
19
- space: space,
20
- space_guid: space_guid,
21
- service_broker_name: service_broker_name,
22
- service_broker_guid: service_broker_guid,
23
- service_offering: service_offering,
24
- service_plan: service_plan,
25
- storage_used: storage_used,
26
- storage_allocated: storage_allocated,
27
- storage_free: storage_free,
28
- iops: iops,
29
- cpu: cpu
30
- }
31
- end
32
- # rubocop:enable Metrics/MethodLength
33
-
34
- def to_s
35
- "#{name}: used(#{storage_used}) allocated(#{storage_allocated}) free(#{storage_free}) iops(#{iops}) cpu(#{cpu})"
36
- end
37
- end
38
- end
39
- end