aws-reporting 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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