aws-edges 0.8

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,163 @@
1
+ # aws-edges
2
+
3
+ ## Overview
4
+
5
+ **aws-edges** allows you to created high level graphviz digraphs of your AWS components. Currently this does not cover everything returned from the AWS Ruby SDK, only select components so far.
6
+
7
+ ## Installation
8
+
9
+ ```
10
+ $ gem install aws-edges
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```
16
+ $ ./aws-edges -h
17
+ Usage: aws-edges [options]
18
+ -c, --config [CONFIG_FILE] Config file
19
+ -f, --config-format [yaml|json] Config file format (yaml or json)
20
+ -a [ACCESS_KEY_ID], AWS Access Key ID
21
+ --access-key-id
22
+ -s [SECRET_ACCESS_KEY], AWS Secret Access Key
23
+ --secret-key-id
24
+ -r, --region [REGION] AWS Region (ie: us-east-1)
25
+ -C, --list-colors Prints out a list of supported colors
26
+ -S, --list-shapes Prints out a list of supported shapes
27
+ -?, --help Shows this message
28
+ ```
29
+
30
+ ## Configuration Example
31
+
32
+ There are some examples located in the 'examples' directory. Below is one of them. The *sources* section tells the *aws-edges* script what to pull from AWS. And the rest is pretty self explanatory.
33
+
34
+ The basic structure is show below. The `name` key provides the output name of the graph generated. Which will generate both a *.dot* and *.png* file by default.
35
+
36
+ ```
37
+ ---
38
+ name: "my graph name"
39
+ rotate: true
40
+ sources:
41
+ - "ec2"
42
+ - "subnet"
43
+ edges:
44
+ -
45
+ from: "ec2_hypervisor"
46
+ from_color: "orange"
47
+ from_shape: "invhouse"
48
+ to: "ec2_virtualization_type"
49
+ to_color: "coral"
50
+ to_shape: "diamond"
51
+ -
52
+ from: "ec2_virtualization_type"
53
+ to: "ec2_instance_id"
54
+ -
55
+ from: "ec2_instance_id"
56
+ from_color: "dodgerblue"
57
+ to: "ec2_subnet_id"
58
+ to_color: "darkturquoise"
59
+ -
60
+ from: "subnet_cidr_block"
61
+ to: "subnet_availability_zone"
62
+ to_color: "crimson"
63
+ cluster:
64
+ label: "Subnet Legend"
65
+ edges:
66
+ -
67
+ from: "subnet_subnet_id"
68
+ to: "subnet_cidr_block"
69
+ to_color: "firebrick"
70
+ ```
71
+ ### Specifying output format
72
+
73
+ If you like to save the graph in a different format other than the default `png`. You can specify the `save_as: "pdf"` or some other image format such as tiff.
74
+
75
+ ### Mapping a 'many' node
76
+
77
+ What is a 'many' node? Simply put, it is a object containing many entries or an Hash of an Array of Hashes.
78
+
79
+ The syntax for mapping a 'many' node is `"redshift_cluster_nodes-private_ip_address"`. The `-` character indicating that `redshift_cluster_nodes` is an Array and you want all the `private_ip_address` objects/property.
80
+
81
+ ### Grouping edges
82
+
83
+ Sometime you would like to visually group a set or sets of edges together so they appear in a container. This can help distinguish the relation between edge objects. You will need to group your objects using the `cluster` key and can label it using something like; `label: "My VPCs"`. Below is an example config snippet:
84
+
85
+ ```
86
+ ---
87
+ name: "example"
88
+ rotate: true
89
+ sources:
90
+ - "vpc"
91
+ cluster:
92
+ label: "Virtual Private Clouds"
93
+ edges:
94
+ -
95
+ to: "vpc_cidr_block"
96
+ from: "vpc_vpc_id"
97
+ ```
98
+
99
+ ### Changing the layout
100
+
101
+ By default graphs are generated in a horizontal top down structure. If you like a vertical left to right representation you will need to set the `rotate: true` property in your config like so:
102
+
103
+ ```
104
+ $ cat myconfig.yml
105
+ ---
106
+ name: "example"
107
+ rotate: true
108
+ sources:
109
+ - "vpc"
110
+ - "ec2"
111
+ - "subnet"
112
+ ```
113
+
114
+ ### Adding edge colors
115
+
116
+ In order to make it easier to identify groups of like edges, coloring (fill) support has been added. Being that this utilizes the `graph` gem it supports all of the colors supported by it.
117
+
118
+ [Visit Graphviz Colors to see what they look like](http://www.graphviz.org/content/color-names)
119
+
120
+ To see what colors are available run the following command:
121
+
122
+ ```
123
+ $ aws-edges -C
124
+ ```
125
+
126
+ or
127
+
128
+ ```
129
+ $ aws-edges --list-colors
130
+ ```
131
+
132
+ To use colors, in your config simply add either `to_color: "orange"` or `from_color: "brown"` to the `edges` section of the config. (See example above)
133
+
134
+ ### Adding edges shapes
135
+
136
+ In order to make it easier to identify *like* edges, shape support has been added. As with colors, it supports the shapes available to the `graph` gem on which the gem requires.
137
+
138
+ [Visit Graphviz Shapes to see what the look like](http://www.graphviz.org/content/node-shapes)
139
+
140
+ To list the supported shapes, run the following command:
141
+
142
+ ```
143
+ $ aws-edges -S
144
+ ```
145
+
146
+ or
147
+
148
+ ```
149
+ $ aws-edges --list-shapes
150
+ ```
151
+
152
+ To use shapes, in your config simply add either `to_shape: "triangle"` or `from_shape: "egg"` to the `edges` section of the config. (See example above)
153
+
154
+ ## Hacking and Contributing
155
+
156
+ Fork it and start hacking. Below is a brief summary of the layout of the code.
157
+
158
+ Inside 'lib/aws-edges' are the classes used. These are kind of categorized by the AWS Ruby SDK "describe*" methods and client types. For example, *subnet.rb* only parses the EC2 *describe-subnets* output. Each class file contains an *initialize* method and a *supported_fields* public method.
159
+
160
+ The 'config.rb' is used to parse the configuration file (which can be either yaml or json). It uses the *supported_fields* method to validate what is entered in the config. It also contains a *valid_keys*, *valid_sources*, *valid_prefixes*.
161
+
162
+ 'graph.rb' is what creates the graphs.
163
+
File without changes
data/SHA256 ADDED
@@ -0,0 +1 @@
1
+ 9076d60c640f2c8a55ca30f030897529861201eb9454fc8de39c8ef16d2e1281 aws-edges-0.7.gem
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+ require 'aws-edges/version'
3
+
4
+ Gem::Specification.new 'aws-edges', AWSEdges::VERSION do |s|
5
+ s.license = 'GPL-3.0'
6
+ s.date = '2016-04-07'
7
+ s.summary = 'Simple AWS architecture charts'
8
+ s.description = 'AWS Edges is used to chart out your AWS environments'
9
+ s.files = `git ls-files`.split("\n") - %w[]
10
+ s.author = 'Rick Briganti'
11
+ s.email = 'jeviolle@newaliases.org'
12
+ s.homepage = 'http://github.com/jeviolle/aws-edges'
13
+ s.add_runtime_dependency 'aws-sdk', '~> 1.34', '>=1.34.0'
14
+ s.add_runtime_dependency 'graph', '~> 2.6', '>=2.6.0'
15
+ s.bindir = 'bin'
16
+ s.executables << 'aws-edges'
17
+ end
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ $LOAD_PATH.push(File.absolute_path(File.dirname(__FILE__)) + '/../lib')
5
+
6
+ require 'aws-sdk'
7
+ require 'aws-edges'
8
+ require 'optparse'
9
+ require 'pp'
10
+
11
+ options = {}
12
+ OptionParser.new do |opts|
13
+ opts.on('-c','--config [CONFIG_FILE]', 'Config file') do |v|
14
+ options[:config] = v
15
+ end
16
+ opts.on('-a','--access-key-id [ACCESS_KEY_ID]', 'AWS Access Key ID') do |v|
17
+ options[:access_key_id] = v
18
+ end
19
+ opts.on('-s','--secret-key-id [SECRET_ACCESS_KEY]', 'AWS Secret Access Key') do |v|
20
+ options[:secret_access_key] = v
21
+ end
22
+ opts.on('-r','--region [REGION]', 'AWS Region (ie: us-east-1)') do |v|
23
+ options[:region] = v
24
+ end
25
+ opts.on('-C','--list-colors', 'Prints out a list of supported colors') do |v|
26
+ options[:colors] = v
27
+ end
28
+ opts.on('-S','--list-shapes', 'Prints out a list of supported shapes') do |v|
29
+ options[:shapes] = v
30
+ end
31
+ opts.on('-?','--help','Shows this message') do
32
+ puts opts
33
+ exit 255
34
+ end
35
+ end.parse!
36
+
37
+ class Array
38
+ def longest_word
39
+ group_by(&:size).max.last
40
+ end
41
+ end
42
+
43
+ def display_supported(type)
44
+ display_types = []
45
+
46
+ case type
47
+ when "colors"
48
+ puts
49
+ puts "http://graphviz.org/content/color-names"
50
+ display_types = digraph.class::BOLD_COLORS + digraph.class::LIGHT_COLORS
51
+ when "shapes"
52
+ puts
53
+ puts "http://graphviz.org/content/node-shapes"
54
+ display_types = digraph.class::SHAPES
55
+ end
56
+
57
+ maxlen = display_types.longest_word[0].length
58
+ puts
59
+ puts "Supported #{type.capitalize}"
60
+ puts "----------------"
61
+ display_types.each_with_index do |obj, idx|
62
+ if idx.to_i.even?
63
+ print obj
64
+ (maxlen - obj.length).times{print " "}
65
+ print "\t"
66
+ end
67
+ puts "#{obj}" if idx.to_i.odd?
68
+ end
69
+ puts
70
+ end
71
+
72
+ if options[:colors] and options[:shapes]
73
+ %w/colors shapes/.each{|i| display_supported i}
74
+ exit 255
75
+ elsif options[:colors]
76
+ display_supported "colors"
77
+ exit 255
78
+ elsif options[:shapes]
79
+ display_supported "shapes"
80
+ exit 255
81
+ end
82
+
83
+ unless options[:access_key_id] and options[:secret_access_key] and options[:region] and options[:config]
84
+ puts "[ERROR] Missing required option, please run with '--help' for more info"
85
+ exit 1
86
+ end
87
+
88
+ AWS.config({
89
+ :access_key_id => options[:access_key_id],
90
+ :secret_access_key => options[:secret_access_key],
91
+ :region => options[:region]
92
+ })
93
+
94
+ # parse config
95
+ config = AWSEdges::Config.new
96
+ config_data = config.parse(options[:config])
97
+
98
+ aws_sources = config_data.delete('sources')
99
+ aws_data = {
100
+ :vpc => {}, :subnet => {}, :ec2 => {},
101
+ :rds => {}, :redshift => {}, :iam => {}
102
+ }
103
+ # run aws clients to describe and retrieve data
104
+ aws_data[:iam] = AWSEdges::IAM.new(AWS::IAM::Client.new).nodes if aws_sources.include?("iam")
105
+ aws_data[:vpc] = AWSEdges::VPC.new(AWS::EC2::Client.new.describe_vpcs.data).nodes if aws_sources.include?("vpc")
106
+ aws_data[:subnet] = AWSEdges::Subnet.new(AWS::EC2::Client.new.describe_subnets.data).nodes if aws_sources.include?("subnet")
107
+ aws_data[:ec2] = AWSEdges::EC2.new(AWS::EC2::Client.new.describe_instances.data).nodes if aws_sources.include?("ec2")
108
+ aws_data[:rds] = AWSEdges::RDS.new(AWS::RDS::Client.new.describe_db_instances.data).nodes if aws_sources.include?("rds")
109
+ aws_data[:redshift] = AWSEdges::Redshift.new(AWS::Redshift::Client.new.describe_clusters.data).nodes if aws_sources.include?("redshift")
110
+
111
+ # graph a graph based on the config and inputs
112
+ grapher = AWSEdges::Graph.new(config_data, aws_data)
113
+ grapher.create_graph()
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: "my graph name"
3
+ rotate: true
4
+ sources:
5
+ - "ec2"
6
+ - "subnet"
7
+ edges:
8
+ -
9
+ from: "ec2_hypervisor"
10
+ from_color: "orange"
11
+ from_shape: "invhouse"
12
+ to: "ec2_virtualization_type"
13
+ to_color: "coral"
14
+ to_shape: "diamond"
15
+ -
16
+ from: "ec2_virtualization_type"
17
+ to: "ec2_instance_id"
18
+ -
19
+ from: "ec2_instance_id"
20
+ from_color: "dodgerblue"
21
+ to: "ec2_subnet_id"
22
+ to_color: "darkturquoise"
23
+ -
24
+ from: "subnet_cidr_block"
25
+ to: "subnet_availability_zone"
26
+ to_color: "crimson"
27
+ cluster:
28
+ label: "Subnet Legend"
29
+ edges:
30
+ -
31
+ from: "subnet_subnet_id"
32
+ to: "subnet_cidr_block"
33
+ to_color: "firebrick"
34
+
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: "iam_structure"
3
+ rotate: True
4
+ sources:
5
+ - "iam"
6
+ edges:
7
+ -
8
+ from_color: "lightseagreen"
9
+ from: "iam_user_name"
10
+ to_shape: "note"
11
+ to_color: "peru"
12
+ to: "iam_user_policies-policy_name"
13
+ -
14
+ from_color: "lightseagreen"
15
+ from: "iam_users-user_name"
16
+ to: "iam_group_name"
17
+ -
18
+ from_shape: "folder"
19
+ from_color: "orange"
20
+ from: "iam_group_name"
21
+ to: "iam_group_policies-policy_name"
22
+ to_color: "peru"
23
+ to_shape: "note"
@@ -0,0 +1,19 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require 'graph'
4
+ require 'aws-edges/vpc.rb'
5
+ require 'aws-edges/subnet.rb'
6
+ require 'aws-edges/iam.rb'
7
+ require 'aws-edges/ec2.rb'
8
+ require 'aws-edges/rds.rb'
9
+ require 'aws-edges/redshift.rb'
10
+ require 'aws-edges/version.rb'
11
+ require 'aws-edges/config.rb'
12
+ require 'aws-edges/graph.rb'
13
+
14
+ ##
15
+ # AWS Edges creates a simple chart of your
16
+ # current AWS environment.
17
+ #
18
+ module AWSEdges
19
+ end
@@ -0,0 +1,166 @@
1
+ module AWSEdges
2
+ class Config
3
+ @@valid_keys = ["name", "sources", "cluster", "label", "edges",
4
+ "from", "from_shape", "from_color",
5
+ "to", "to_shape", "to_color" ,"rotate", "save_as"]
6
+ @@valid_sources = ["VPC","EC2","RDS","Redshift","Subnet","IAM"]
7
+ @@valid_prefixes = @@valid_sources
8
+
9
+ def initialize
10
+ @node_types = @@valid_sources.map{|s|
11
+ eval "AWSEdges::#{s}.supported_fields.map{|f| s.downcase + '_' + f}"
12
+ }.flatten
13
+ end
14
+
15
+ def msg_and_exit(msg)
16
+ puts msg
17
+ exit 1
18
+ end
19
+
20
+ ##
21
+ # Validate the source section of the config file
22
+ #
23
+ def validate_sources(sources)
24
+ msg_and_exit("No sources specified in the config") if sources.nil?
25
+ sources.each do |source|
26
+ unless @@valid_sources.map{|i| i.downcase}.include?(source)
27
+ msg_and_exit("Invalid source detected in config: #{source}")
28
+ end
29
+ end
30
+ end
31
+
32
+ ##
33
+ # Verify that a graph name is specified in the config
34
+ #
35
+ def validate_name(graph_name)
36
+ msg_and_exit("No graph name specified in the config") if graph_name.nil?
37
+ end
38
+
39
+ ##
40
+ # Confirm that from and to edges are specified properly and are
41
+ # not a many-to-many relationship
42
+ def validate_nodes(hashed_data)
43
+ nodes = []
44
+ hashed_data.each do |k,v|
45
+ if k == "edges"
46
+ v.each{|e|
47
+ if e["from"].include?('-') and e["to"].include?('-')
48
+ msg_and_exit("Error: from many to many edges detected in config:\n (#{e['from']}) -> (#{e['to']})")
49
+ end
50
+ nodes.push e["from"]
51
+ nodes.push e["to"]
52
+ }
53
+ end
54
+ nodes.push(validate_nodes(v)) if v.class == Hash
55
+ end
56
+ nodes.flatten.uniq.each do |u|
57
+ unless @node_types.include?(u)
58
+ msg_and_exit("Invalid edge node specified: #{u}")
59
+ end
60
+ end
61
+ end
62
+
63
+ ##
64
+ # Internal method for testing a color
65
+ private def test_color(color)
66
+ colors = digraph.class::BOLD_COLORS + digraph.class::LIGHT_COLORS
67
+ msg_and_exit("Invalid color specified: #{color}") unless colors.include?(color)
68
+ end
69
+
70
+ ##
71
+ # Verify that the colors defined are supported by the
72
+ # underlying Graph gem
73
+ def validate_colors(hashed_data)
74
+ hashed_data.each do |k,v|
75
+ if k == "edges"
76
+ v.each{|e|
77
+ if e["from_color"]
78
+ test_color(e["from_color"])
79
+ end
80
+
81
+ if e["to_color"]
82
+ test_color(e["to_color"])
83
+ end
84
+ }
85
+ end
86
+ end
87
+ end
88
+
89
+ ##
90
+ # Internal method for testing a shape
91
+ private def test_shape(shape)
92
+ shapes = digraph.class::SHAPES
93
+ msg_and_exit("Invalid shape specified: #{shape}") unless shapes.include?(shape)
94
+ end
95
+
96
+ ##
97
+ # Verify that the shapes defined are supported by the
98
+ # underlying Graph gem
99
+ def validate_shapes(hashed_data)
100
+ hashed_data.each do |k,v|
101
+ if k == "edges"
102
+ v.each{|e|
103
+ if e["from_shape"]
104
+ test_shape(e["from_shape"])
105
+ end
106
+
107
+ if e["to_shape"]
108
+ test_shape(e["to_shape"])
109
+ end
110
+ }
111
+ end
112
+ end
113
+ end
114
+
115
+ ##
116
+ # Validate that the 'keys' are valid in the config
117
+ def validate_keys(hashed_data)
118
+ keys = []
119
+ hashed_data.each do |k,v|
120
+ unless k == "edges"
121
+ keys.push(k)
122
+ keys.push(validate_keys(v)) if v.class == Hash
123
+ else
124
+ v.each{|e|
125
+ keys.push(validate_keys(e))
126
+ }
127
+ end
128
+ end
129
+ keys.flatten.uniq.each do |u|
130
+ unless @@valid_keys.include?(u)
131
+ msg_and_exit("Invalid key found in config: #{u}")
132
+ end
133
+ end
134
+ end
135
+
136
+ ##
137
+ # Attempt to parse the json/yaml config and run through
138
+ # validation
139
+ def parse(config_file)
140
+ begin
141
+ contents = File.read(config_file)
142
+ rescue Exception => e
143
+ msg_and_exit("Failed to read config: #{e.message}")
144
+ end
145
+
146
+ begin
147
+ config = YAML.load(contents)
148
+ rescue Exception => e
149
+ config = JSON.parse(contents)
150
+ rescue Exception => e
151
+ msg_and_exit("Failed to parse config: #{e.message}")
152
+ end
153
+
154
+ # run through validation checks
155
+ validate_keys(config)
156
+ validate_colors(config)
157
+ validate_shapes(config)
158
+ validate_sources(config['sources'])
159
+ validate_nodes(config)
160
+ validate_name(config['name'])
161
+
162
+ return config
163
+ end
164
+
165
+ end
166
+ end