aws-reporting 0.9.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.
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/aws-reporting', __FILE__)
4
+
5
+ slop = Slop.new(:strict => true) do
6
+ command 'config' do
7
+ banner "Usage: aws-reporting config [-h] [-f] [--access-key-id ACCESS_KEY_ID --secret-access-key SECRET_ACCESS_KEY]"
8
+ on 'h', 'help', "Show this messages"
9
+ on 'f', 'force', "Force output config file"
10
+ on 'access-key-id=', "AWS Access Key ID"
11
+ on 'secret-access-key=', "AWS Secret Access Key"
12
+
13
+ run do |opts, args|
14
+ AwsReporting::Command::Config.run(opts, args)
15
+ end
16
+ end
17
+
18
+ command 'run' do
19
+ banner "Usage: aws-reporting run [-f] REPORT_PATH"
20
+ on 'h', 'help', "Show this messages"
21
+ on 'f', 'force', "Force output report"
22
+
23
+ run do |opts, args|
24
+ AwsReporting::Command::Run.run(opts, args)
25
+ end
26
+ end
27
+
28
+ command 'serve' do
29
+ banner "Usage: aws-reporting serve [-p PORT] REPORT_PATH"
30
+ on 'h', 'help', "Show this messages"
31
+ on 'p=', 'port=', 'Port to listen on'
32
+
33
+ run do |opts, args|
34
+ AwsReporting::Command::Serve.run(opts, args)
35
+ end
36
+ end
37
+
38
+ command 'version' do
39
+ run do |opts, args|
40
+ AwsReporting::Command::Version.run(opts, args)
41
+ end
42
+ end
43
+
44
+ run do |opts, args|
45
+ puts opts.help
46
+ end
47
+ end
48
+
49
+ begin
50
+ slop.parse
51
+ rescue Slop::Error => e
52
+ puts e.message
53
+ puts slop
54
+ end
@@ -0,0 +1,30 @@
1
+ $LOAD_PATH.push File.expand_path('../aws-reporting', __FILE__)
2
+
3
+ # standard libraries
4
+ require 'yaml'
5
+ require 'fileutils'
6
+
7
+ # 3rd party libraries
8
+ require 'slop'
9
+ require 'aws-sdk'
10
+ require 'formatador'
11
+ require 'parallel'
12
+
13
+ # aws-reporting files
14
+ require 'error'
15
+ require 'version'
16
+ require 'server'
17
+ require 'generator'
18
+ require 'plan'
19
+ require 'config'
20
+ require 'command/config'
21
+ require 'command/run'
22
+ require 'command/serve'
23
+ require 'command/version'
24
+ require 'resolvers'
25
+ require 'resolver/ec2'
26
+ require 'resolver/ebs'
27
+ require 'helper'
28
+ require 'alarm'
29
+ require 'statistics'
30
+ require 'store'
@@ -0,0 +1,36 @@
1
+ module AwsReporting
2
+ module Alarm
3
+ def serialize(dimensions)
4
+ dimensions.sort_by{|dimension| dimension[:name]}.map{|dimension| dimension[:name] + "=>" + dimension[:value]}.join(',')
5
+ end
6
+
7
+ def get_alarm_info(region, alarm)
8
+ info = {:name => alarm.name,
9
+ :region => region,
10
+ :namespace => alarm.namespace,
11
+ :dimensions => alarm.dimensions,
12
+ :metric_name => alarm.metric_name,
13
+ :status => get_status(alarm)}
14
+ end
15
+
16
+ def get_status(alarm)
17
+ return :ALARM if alarm.state_value == 'ALARM'
18
+ return :ALARM if alarm.history_items.to_a.select{|history| history.history_item_type == 'StateUpdate'}.length > 0
19
+ return :OK
20
+ end
21
+
22
+ def get_alarms()
23
+ alarms = []
24
+ AWS.regions.each{|r|
25
+ Config.update_region(r.name)
26
+ cw = AWS::CloudWatch.new
27
+ cw.alarms.each do |alarm|
28
+ alarms << get_alarm_info(r.name, alarm)
29
+ end
30
+ }
31
+ alarms.sort_by{|alarm| [alarm[:namespace], serialize(alarm[:dimensions]), alarm[:metric_name], alarm[:name]].join(' ')}
32
+ end
33
+
34
+ module_function :get_alarms
35
+ end
36
+ end
@@ -0,0 +1,75 @@
1
+ module AwsReporting
2
+ module Command
3
+ module Config
4
+ DEFAULT_PATH = '~/.aws-reporting/config.yaml'
5
+ MESSAGE_ALREADY_EXIST = " already exists. If you want to overwrite this, use '-f' option."
6
+
7
+ def run(opts, args)
8
+ Signal.trap(:INT){
9
+ puts
10
+ puts "interrupted."
11
+ exit
12
+ }
13
+
14
+ help = opts['h']
15
+ if help
16
+ puts opts.help
17
+ return
18
+ end
19
+
20
+ begin
21
+ access = opts['access-key-id']
22
+ secret = opts['secret-access-key']
23
+ force = opts['f']
24
+
25
+ if access and secret
26
+ run_batch(access, secret, force)
27
+ elsif !!!access and !!!secret
28
+ run_interactive(force)
29
+ else
30
+ raise CommandArgumentError.new
31
+ end
32
+ rescue AwsReporting::Error::CommandArgumentError
33
+ puts opts.help
34
+ end
35
+ end
36
+
37
+ def run_batch(access, secret, force)
38
+ path = File.expand_path(DEFAULT_PATH)
39
+ if force or !File.exist?(path)
40
+ update_config(access, secret, path)
41
+ puts 'done.'
42
+ else
43
+ puts path + MESSAGE_ALREADY_EXIST
44
+ end
45
+ end
46
+
47
+ def run_interactive(force)
48
+ path = File.expand_path(DEFAULT_PATH)
49
+ if force or !File.exist?(path)
50
+ print 'Access Key ID :'
51
+ access = $stdin.gets.chomp
52
+ print 'Secret Access Key:'
53
+ secret = $stdin.gets.chomp
54
+
55
+ update_config(access, secret, path)
56
+ puts 'done.'
57
+ else
58
+ puts path + MESSAGE_ALREADY_EXIST
59
+ end
60
+ end
61
+
62
+ def update_config(access, secret, path)
63
+ yaml = YAML.dump(:access_key_id => access, :secret_access_key => secret)
64
+
65
+ FileUtils.mkdir_p(File.dirname(path))
66
+
67
+ open(path, 'w'){|f|
68
+ f.print yaml
69
+ }
70
+ end
71
+
72
+ module_function :run, :run_batch, :run_interactive, :update_config
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,35 @@
1
+ module AwsReporting
2
+ module Command
3
+ module Run
4
+ MESSAGE_ALREADY_EXIST = " already exists. If you want to overwrite this, use '-f' option."
5
+ MESSAGE_NOT_CONFIGURED = "Can not access config file. Run `aws-reporting config` command first."
6
+
7
+ def run(opts, args)
8
+ begin
9
+ help = opts['h']
10
+ if help
11
+ puts opts.help
12
+ return
13
+ end
14
+
15
+ force = opts['f']
16
+ raise AwsReporting::Error::CommandArgumentError.new unless args.length == 1
17
+ path = args[0]
18
+
19
+ generator = AwsReporting::Generator.new
20
+ generator.path = path
21
+ generator.force = force
22
+ generator.generate
23
+ rescue AwsReporting::Error::CommandArgumentError
24
+ puts opts.help
25
+ rescue AwsReporting::Error::OverwriteError => e
26
+ puts e.path + MESSAGE_ALREADY_EXIST
27
+ rescue AwsReporting::Error::ConfigFileLoadError
28
+ puts MESSAGE_NOT_CONFIGURED
29
+ end
30
+ end
31
+
32
+ module_function :run
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ module AwsReporting
2
+ module Command
3
+ module Serve
4
+ def run(opts, args)
5
+ begin
6
+ help = opts['h']
7
+ if help
8
+ puts opts.help
9
+ return
10
+ end
11
+
12
+ port = opts['port'] || 23456
13
+ raise AwsReporting::Error::CommandArgumentError.new unless args.length == 1
14
+ path = args[0]
15
+
16
+ server = AwsReporting::Server.new(path, port)
17
+
18
+ Signal.trap(:INT){
19
+ server.stop
20
+ }
21
+
22
+ server.start
23
+ rescue AwsReporting::Error::CommandArgumentError
24
+ puts opts.help
25
+ end
26
+ end
27
+
28
+ module_function :run
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ module AwsReporting
2
+ module Command
3
+ module Version
4
+ def run(opts, args)
5
+ puts AwsReporting::Version.get
6
+ end
7
+
8
+ module_function :run
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ module AwsReporting
2
+ class Config
3
+ def self.config_file_path()
4
+ File.expand_path('~/.aws-reporting/config.yaml')
5
+ end
6
+
7
+ def self.load()
8
+ @@config = YAML.load(open(config_file_path()){|f| f.read})
9
+ rescue
10
+ raise AwsReporting::Error::ConfigFileLoadError.new
11
+ end
12
+
13
+ def self.update_region(region)
14
+ AWS.config(:access_key_id => @@config[:access_key_id],
15
+ :secret_access_key => @@config[:secret_access_key],
16
+ :region => region)
17
+ end
18
+
19
+ def self.get()
20
+ AWS.config(:access_key_id => @@config[:access_key_id],
21
+ :secret_access_key => @@config[:secret_access_key])
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ module AwsReporting
2
+ module Error
3
+ class CommandArgumentError < StandardError; end
4
+ class OverwriteError < StandardError
5
+ attr_accessor :path
6
+ end
7
+ class ConfigFileLoadError < StandardError; end
8
+ end
9
+ end
@@ -0,0 +1,186 @@
1
+ module AwsReporting
2
+ class Generator
3
+ attr_accessor :path, :force
4
+
5
+ def format_path(path)
6
+ path.split('/')[-2..-1]
7
+ .join('/')
8
+ end
9
+
10
+ def transform(metrics, name_resolvers)
11
+ temp = Hash.new{|h, k| h[k] = Hash.new{|h, k| h[k] = [] }}
12
+ metrics.each{|namespace, value|
13
+ value.each{|identification, files|
14
+ element = {:region => identification[:region],
15
+ :namespace => namespace,
16
+ :dimensions => identification[:dimensions],
17
+ :files => files.sort_by{|entry| entry[:info][:metric_name]}.map{|entry| format_path(entry[:path])}}
18
+
19
+ element[:name] = name_resolvers.get_name(element)
20
+
21
+ dimension_type = identification[:dimensions].map{|item| item[:name]}.sort
22
+
23
+ temp[namespace][dimension_type] << element
24
+ }
25
+ }
26
+
27
+ temp2 = Hash.new{|h, k| h[k] = [] }
28
+ temp.each{|namespace, dimension_table|
29
+ dimension_table.each{|dimension_type, elements|
30
+ temp2[namespace] << {:dimension_type => dimension_type,
31
+ :elements => elements}
32
+ }
33
+ }
34
+
35
+ result = []
36
+ temp2.each{|namespace, dimension_table|
37
+ result << {:namespace => namespace, :dimension_table => dimension_table}
38
+ }
39
+
40
+ sort_result!(result)
41
+
42
+ result
43
+ end
44
+
45
+ def sort_result!(result)
46
+ result.sort_by!{|namespace_table| namespace_table[:namespace]}
47
+ result.each{|namespace_table|
48
+ namespace_table[:dimension_table].sort_by!{|dimension_table| dimension_table[:dimension_type].join(' ')}
49
+ }
50
+
51
+ result.each{|namespace_table|
52
+ namespace_table[:dimension_table].each{|dimension_table|
53
+ dimension_type = dimension_table[:dimension_type]
54
+ dimension_table[:elements].sort_by!{|element| expand(dimension_type, element[:dimensions]) }
55
+ }
56
+ }
57
+ end
58
+
59
+ def expand(dimension_type, dimensions)
60
+ dimension_hash = {}
61
+
62
+ dimensions.each{|dimension|
63
+ dimension_hash[dimension[:name]] = dimension[:value]
64
+ }
65
+
66
+ values = []
67
+ dimension_type.each{|dimension_name|
68
+ values << dimension_hash[dimension_name]
69
+ }
70
+ values.join(' ')
71
+ end
72
+
73
+ def merge_status(s0, s1)
74
+ if s0 == nil or s1 == nil
75
+ s0 || s1
76
+ else
77
+ if s0 == :ALARM or s1 == :ALARM
78
+ 'ALARM'
79
+ else
80
+ 'OK'
81
+ end
82
+ end
83
+ end
84
+
85
+ def build_alarm_tree(alarms)
86
+ alarm_tree = {}
87
+ alarms.each{|alarm|
88
+ key = {:region => alarm[:region],
89
+ :namespace => alarm[:namespace],
90
+ :dimensions => alarm[:dimensions],
91
+ :metric_name => alarm[:metric_name]}
92
+ alarm_tree[key] = merge_status(alarm_tree[key], alarm[:status])
93
+ }
94
+ alarm_tree
95
+ end
96
+
97
+ def set_status(data, alarm_tree)
98
+ key = {:region => data[:info][:region],
99
+ :namespace => data[:info][:namespace],
100
+ :dimensions => data[:info][:dimensions],
101
+ :metric_name => data[:info][:metric_name]}
102
+ data[:info][:status] = alarm_tree[key] if alarm_tree[key]
103
+ end
104
+
105
+ def download(base_path, plan, start_time, end_time, period, name_resolvers, timestamp)
106
+ metrics = Hash.new{|h, k| h[k] = Hash.new{|h, k| h[k] = []}}
107
+
108
+ alarms = AwsReporting::Alarm.get_alarms()
109
+ alarm_tree = build_alarm_tree(alarms)
110
+
111
+ mutex = Mutex.new
112
+ started_at = Time.now
113
+ num_of_metrics = plan.length
114
+ num_of_downloaded = 0
115
+ Parallel.each_with_index(plan, :in_threads => 8){|entry, i|
116
+ namespace = entry[:namespace]
117
+ metric_name = entry[:metric_name]
118
+ dimensions = entry[:dimensions]
119
+ statistics = entry[:statistics]
120
+ region = entry[:region]
121
+
122
+ mutex.synchronize do
123
+ num_of_downloaded += 1
124
+ Formatador.redisplay_progressbar(num_of_downloaded, num_of_metrics, {:started_at => started_at})
125
+ end
126
+
127
+ data = AwsReporting::Statistics.get(region, namespace, metric_name, start_time, end_time, period, dimensions, statistics)
128
+ set_status(data, alarm_tree)
129
+ file = AwsReporting::Store.save(base_path, data)
130
+
131
+ identification = {:dimensions => dimensions, :region => region}
132
+ metrics[namespace][identification] << file
133
+ }
134
+
135
+ report_info = {:start_time => start_time.to_s,
136
+ :end_time => end_time.to_s,
137
+ :period => period.to_s,
138
+ :timestamp => timestamp.to_s,
139
+ :num_of_metrics => plan.length.to_s,
140
+ :version => Version.get}
141
+
142
+ open(base_path + '/metrics.json', 'w'){|f|
143
+ f.print JSON.dump({:report_info => report_info, :metrics => transform(metrics, name_resolvers), :alarms => alarms})
144
+ }
145
+ end
146
+
147
+ def copy_template(path, force)
148
+ report_path = File.expand_path(path)
149
+ if force != true and File.exist?(path)
150
+ error = AwsReporting::Error::OverwriteError.new
151
+ error.path = report_path
152
+ raise error
153
+ end
154
+
155
+ template_path = File.expand_path('../../../template', __FILE__)
156
+ template_files = Dir.glob(template_path + '/*')
157
+ FileUtils.mkdir_p(report_path)
158
+ FileUtils.cp_r(template_files, report_path)
159
+ end
160
+
161
+ def generate
162
+ timestamp = Time.now
163
+
164
+ AwsReporting::Config.load()
165
+
166
+ puts 'Report generating started.'
167
+ copy_template(@path, @force)
168
+ data_dir = @path + '/data'
169
+
170
+ puts '(1/3) Planning...'
171
+ plan = Plan.generate
172
+ puts "Planning complete."
173
+ start_time = Time.now - 24 * 60 * 60 * 14
174
+ end_time = Time.now
175
+ period = 60 * 60
176
+ puts '(2/3) Building name tables...'
177
+ name_resolvers = AwsReporting::Resolvers.new
178
+ name_resolvers.init
179
+ puts 'Name tables were builded.'
180
+ puts '(3/3) Downloading metrics...'
181
+ download(data_dir, plan, start_time, end_time, period, name_resolvers, timestamp)
182
+ puts 'Downloading metrics done.'
183
+ puts 'Report generating complete!'
184
+ end
185
+ end
186
+ end