├── .env ├── .gitignore ├── LICENSE ├── README.md ├── explorer.py ├── images ├── ASG.png ├── Database-mysql.png ├── Database.png ├── Instance.png ├── InternetGateway.png ├── LoadBalancer.png ├── Subnet.png └── VPC.png ├── mapall.py └── requirements.txt /.env: -------------------------------------------------------------------------------- 1 | use_env aws_map 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .Python 3 | bin/* 4 | include/* 5 | lib/* 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Daniel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aws-map 2 | ------------ 3 | Generate basic graphviz/dot maps of your AWS deployments. 4 | 5 | installation 6 | ------------ 7 | 8 | Debian 9 | ``` 10 | $ pip install -r requirements.txt 11 | $ sudo apt-get install graphviz 12 | ``` 13 | 14 | OSX 15 | ``` 16 | $ pip install -r requirements.txt 17 | $ brew install graphviz 18 | ``` 19 | 20 | windows 21 | ``` 22 | https://www.debian.org 23 | ``` 24 | 25 | running 26 | ------- 27 | 28 | ``` 29 | $ ./mapall.py --region us-east-1 | dot -Tpng > aws-map.png 30 | ``` 31 | 32 | viewing the imaage on linux 33 | ``` 34 | $ eog aws-map.png 35 | ``` 36 | 37 | viewing the image on OSX 38 | ``` 39 | $ open aws-map.png 40 | ``` 41 | 42 | viewing the image on windows 43 | ``` 44 | https://www.debian.org 45 | ``` 46 | 47 | Options include specifying just one VPC to draw with: 48 | ./mapall.py --vpc vpc_123456 49 | 50 | Or specifying a subnet to draw with: 51 | ./mapall.py --subnet subnet_123456 52 | 53 | If you want to use [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/): 54 | 55 | ``` 56 | $ sudo apt-get install -y python-setuptools 57 | $ virtualenv -p /usr/bin/python2.7 venv 58 | $ source venv/bin/activate 59 | $ pip install -r requirements.txt 60 | $ ./mapall.py --region us-east-1 | dot -Tpng > aws-map.png 61 | 62 | # And to leave the virtual environment: 63 | $ deactivate 64 | ``` 65 | 66 | Iterating 67 | --------- 68 | You can generate a map of each vpc or subnet individually. This is 69 | very useful if you have a large and complex setup where putting it 70 | all on a single page becomes spaghetti. 71 | 72 | ``` 73 | $ ./mapall.py --iterate vpc 74 | $ ./mapall.py --iterate subnet 75 | ``` 76 | 77 | Security Groups 78 | --------------- 79 | Normally security groups get in the way and obscure what you want 80 | to see so they aren't included. You can add them back with --security. 81 | Note that if you only want to map a single subnet you shouldn't 82 | turn security groups on as there is no easy way to determine which 83 | subnet a security group operates on - so it draws them all - leading 84 | to potentially huge, unusable maps. 85 | 86 | Cacheing 87 | -------- 88 | The program will write the results of the aws query to a .cache 89 | directory and use that unless you specify --nocache. Cacheing is 90 | much faster than querying AWS everytime but obviously won't react 91 | to changes that are made. 92 | 93 | Region 94 | ------ 95 | You must indicate a region for the queries. This can be through the 96 | --region CLI option, or the AWS_DEFAULT_REGION environment variable. 97 | If both are set, the CLI opton takes precedence. 98 | 99 | 100 | Thanks 101 | ---------- 102 | 103 | With the effort of everyone below this project would not be possible. 104 | 105 | * @dwagon 106 | * @justinholmes 107 | * @joerayme 108 | * @hposca 109 | * @bjorand 110 | * @ngfw 111 | 112 | -------------------------------------------------------------------------------- /explorer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import boto.ec2 3 | import boto.vpc 4 | from local_settings import AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 5 | import pprint 6 | 7 | def region_connect(region_name): 8 | vpc_conn = boto.vpc.connect_to_region(region_name, 9 | aws_access_key_id=AWS_ACCESS_KEY_ID, 10 | aws_secret_access_key=AWS_SECRET_ACCESS_KEY) 11 | ec2_conn = boto.ec2.connect_to_region(region_name, 12 | aws_access_key_id=AWS_ACCESS_KEY_ID, 13 | aws_secret_access_key=AWS_SECRET_ACCESS_KEY) 14 | 15 | return vpc_conn 16 | 17 | def get_all_routetables(vpc_conn, filters={}): 18 | raw_route_tables = vpc_conn.get_all_route_tables(filters=filters) 19 | for rt in raw_route_tables: 20 | #pprint.pprint(rt.__dict__) 21 | for a in rt.associations: 22 | if not a.subnet_id: 23 | continue 24 | pprint.pprint(a.__dict__) 25 | for r in rt.routes: 26 | gateway = r.gateway_id 27 | if r.instance_id: 28 | gateway = r.instance_id 29 | print "%-20s -> %s" % (r.destination_cidr_block, gateway) 30 | print "==" 31 | 32 | def get_all_subnets(vpc_conn, filters={}): 33 | raw_subnet_list = vpc_conn.get_all_subnets() 34 | for s in raw_subnet_list: 35 | get_all_routetables(vpc_conn, filters={'vpc_id': s.vpc_id}) 36 | #get_all_internet_gateways(vpc_conn) 37 | 38 | def get_all_internet_gateways(vpc_conn, filters={}): 39 | raw_igw_list = vpc_conn.get_all_internet_gateways(filters=filters) 40 | for igw in raw_igw_list: 41 | print igw 42 | 43 | def main(): 44 | "Main" 45 | 46 | vpc_conn = region_connect('ap-southeast-2') 47 | get_all_subnets(vpc_conn) 48 | 49 | 50 | if __name__ == '__main__': 51 | main() 52 | -------------------------------------------------------------------------------- /images/ASG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniellawrence/aws-map/6717c64407bed41c8f4f09bf0df3f32f470faa10/images/ASG.png -------------------------------------------------------------------------------- /images/Database-mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniellawrence/aws-map/6717c64407bed41c8f4f09bf0df3f32f470faa10/images/Database-mysql.png -------------------------------------------------------------------------------- /images/Database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniellawrence/aws-map/6717c64407bed41c8f4f09bf0df3f32f470faa10/images/Database.png -------------------------------------------------------------------------------- /images/Instance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniellawrence/aws-map/6717c64407bed41c8f4f09bf0df3f32f470faa10/images/Instance.png -------------------------------------------------------------------------------- /images/InternetGateway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniellawrence/aws-map/6717c64407bed41c8f4f09bf0df3f32f470faa10/images/InternetGateway.png -------------------------------------------------------------------------------- /images/LoadBalancer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniellawrence/aws-map/6717c64407bed41c8f4f09bf0df3f32f470faa10/images/LoadBalancer.png -------------------------------------------------------------------------------- /images/Subnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniellawrence/aws-map/6717c64407bed41c8f4f09bf0df3f32f470faa10/images/Subnet.png -------------------------------------------------------------------------------- /images/VPC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniellawrence/aws-map/6717c64407bed41c8f4f09bf0df3f32f470faa10/images/VPC.png -------------------------------------------------------------------------------- /mapall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Map AWS setup 4 | # Images are available from http://aws.amazon.com/architecture/icons/ 5 | import argparse 6 | import time 7 | import os 8 | import sys 9 | import boto 10 | import netaddr 11 | 12 | objects = {} 13 | clusternum = 0 14 | awsflags = [] 15 | nocache = False 16 | secGrpToDraw = set() 17 | 18 | colours = ['azure', 'coral', 'wheat', 'deepskyblue', 'firebrick', 'gold', 'green', 'plum', 'salmon', 'sienna'] 19 | 20 | 21 | class RetryLimitExceeded(Exception): 22 | def __init__(self, exception, retries): 23 | self.exception = exception 24 | self.tries = retries 25 | 26 | def __str__(self): 27 | return repr( 28 | "Throttling retry limit exceeded, no_of_tries(%s), last exception: %s" % (self.tries, self.exception)) 29 | 30 | 31 | def get_api_error_code(exception): 32 | if hasattr(exception, "body"): 33 | if exception.body is not None and hasattr(exception.body, "split"): 34 | code = exception.body.split("")[1] 35 | code = code.split("")[0] 36 | return code 37 | else: 38 | return "" 39 | else: 40 | return "" 41 | 42 | 43 | def paginate_boto_response(api_call, *args, **kwargs): 44 | resultset = [] 45 | tries = 0 46 | retry_interval = 2 47 | retry = 10 48 | while True: 49 | 50 | tries += 1 51 | try: 52 | results = api_call(*args, **kwargs) 53 | if results: 54 | resultset += results 55 | if results.next_token: 56 | kwargs['next_token'] = results.next_token 57 | else: 58 | break 59 | else: 60 | break 61 | except Exception, e: 62 | last_exception = e 63 | code = get_api_error_code(e) 64 | if retry <= 0: 65 | raise RetryLimitExceeded(last_exception, tries) 66 | elif retry > 0 and (code == "Throttling" or code == "RequestLimitExceeded"): 67 | retry -= 1 68 | retry_interval += 1 69 | time.sleep(retry_interval) 70 | else: 71 | raise e 72 | 73 | return resultset 74 | 75 | ############################################################################### 76 | ############################################################################### 77 | ############################################################################### 78 | class Dot(object): 79 | def __init__(self, data, args): 80 | self.data = data 81 | self.args = args 82 | 83 | ########################################################################## 84 | def __getitem__(self, key): 85 | return self.data.get(key, None) 86 | 87 | ########################################################################## 88 | def draw(self, fh): 89 | fh.write('%s [label="%s:%s" %s];\n' % (self.mn(self.name), self.__class__.__name__, self.name, self.image())) 90 | 91 | ########################################################################## 92 | def mn(self, s=None): 93 | """ Munge name to be dottable """ 94 | if not s: 95 | s = self.name 96 | try: 97 | s = s.replace('-', '_') 98 | s = s.replace("'", '"') 99 | return '"' + s + '"' 100 | except AttributeError as e: 101 | return 'NoName' 102 | 103 | ########################################################################## 104 | def partOfInstance(self, instid): 105 | return False 106 | 107 | ########################################################################## 108 | def inSubnet(self, subnet): 109 | return True 110 | 111 | ########################################################################## 112 | def drawSec(self, fh): 113 | sys.stderr.write("%s.drawSec() undefined\n" % self.__class__.__name__) 114 | 115 | ########################################################################## 116 | def connect(self, fh, a, b, **kwargs): 117 | blockstr = '' 118 | for kk, kv in kwargs.items(): 119 | blockstr += '%s=%s ' % (kk, kv) 120 | if blockstr: 121 | blockstr = '[ %s ]' % blockstr 122 | fh.write("%s -> %s %s;\n" % (self.mn(a), self.mn(b), blockstr)) 123 | 124 | ########################################################################## 125 | def tags(self, key=None): 126 | tagd = {} 127 | if 'Tags' not in self.data: 128 | return None 129 | for t in self['Tags']: 130 | tagd[t['Key']] = t['Value'] 131 | if key: 132 | return tagd.get(key, None) 133 | else: 134 | return tagd 135 | 136 | ########################################################################## 137 | def inVpc(self, vpc): 138 | return False 139 | 140 | ########################################################################## 141 | def relevent_to_ip(self, ip): 142 | return False 143 | 144 | ########################################################################## 145 | def rank(self, fh): 146 | fh.write(self.mn()) 147 | 148 | ########################################################################## 149 | def image(self, names=[], shape='box', style='solid'): 150 | if not names: 151 | names = [self.__class__.__name__] 152 | 153 | for name in names: 154 | imgfile = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'images', '%s.png' % name) 155 | 156 | if os.path.exists(imgfile): 157 | imagestr = ', image="%s", style=%s, shape=%s ' % (imgfile, style, shape) 158 | break 159 | else: 160 | imagestr = ', style=%s, shape=%s' % (style, shape) 161 | return imagestr 162 | 163 | 164 | ############################################################################### 165 | ############################################################################### 166 | ############################################################################### 167 | class NetworkAcl(Dot): 168 | """ 169 | { 170 | "Associations": [ 171 | { 172 | "SubnetId": "subnet-XXXXXXXX", 173 | "NetworkAclId": "acl-XXXXXXXX", 174 | "NetworkAclAssociationId": "aclassoc-XXXXXXXX" 175 | }, 176 | ], 177 | "NetworkAclId": "acl-XXXXXXXX", 178 | "VpcId": "vpc-XXXXXXXX", 179 | "Tags": [], 180 | "Entries": [ { 181 | "CidrBlock": "0.0.0.0/0", 182 | "RuleNumber": 1, 183 | "Protocol": "-1", 184 | "Egress": true, 185 | "RuleAction": "allow" 186 | }, ], 187 | "IsDefault": true 188 | } 189 | """ 190 | 191 | def __init__(self, instance, args): 192 | self.data = instance 193 | self.name = instance.id 194 | self.args = args 195 | 196 | def inVpc(self, vpc): 197 | if vpc and self.data.vpc_id != vpc: 198 | return False 199 | return True 200 | 201 | def inSubnet(self, subnet=None): 202 | if subnet: 203 | for assoc in self['Associations']: 204 | if assoc['SubnetId'] == subnet: 205 | return True 206 | return False 207 | return True 208 | 209 | def draw(self, fh): 210 | fh.write("// NACL %s\n" % self.name) 211 | 212 | def drawSec(self, fh): 213 | fh.write("// NACL %s\n" % self.name) 214 | fh.write('%s [shape="box", label="%s"];\n' % (self.mn(), self.name)) 215 | self.genRuleBlock('ingress', fh) 216 | fh.write("%s -> %s_ingress_rules\n" % (self.mn(), self.mn())) 217 | self.genRuleBlock('egress', fh) 218 | fh.write("%s_egress_rules -> %s\n" % (self.mn(), self.mn())) 219 | 220 | def genRuleBlock(self, direct, fh): 221 | fh.write("// NACL %s\n" % self.name) 222 | fh.write('%s_%s_rules [ shape="Mrecord" label=<' % (self.mn(), direct)) 223 | fh.write('\n' % (self.name, direct)) 224 | fh.write('%s %s %s\n' % (header("Rule"), header("CIDR"), header("Ports"))) 225 | for e in self['Entries']: 226 | if direct == 'ingress' and e['Egress']: 227 | continue 228 | if direct == 'egress' and not e['Egress']: 229 | continue 230 | col = "green" if e['RuleAction'] == 'allow' else "red" 231 | protocol = {'6': 'tcp', '17': 'udp'}.get(e['Protocol'], e['Protocol']) 232 | if 'PortRange' in e: 233 | if e['PortRange']['From'] == e['PortRange']['To']: 234 | portrange = "%s/%s" % (e['PortRange']['From'], protocol) 235 | else: 236 | portrange = "%s-%s/%s" % (e['PortRange']['From'], e['PortRange']['To'], protocol) 237 | else: 238 | portrange = '' 239 | fh.write("\n") 240 | fh.write('' % (col, e['RuleNumber'])) 241 | fh.write("" % e['CidrBlock']) 242 | fh.write("\n" % portrange) 243 | fh.write("\n") 244 | fh.write("
%s %s
%s%s%s
>\n") 245 | fh.write("];\n") 246 | 247 | def relevent_to_ip(self, ip): 248 | for e in self['Entries']: 249 | if netaddr.IPAddress(ip) in netaddr.IPNetwork(e['CidrBlock']): 250 | print "NACL %s - ip %s is relevent to %s" % (self.name, ip, e['CidrBlock']) 251 | return True 252 | return False 253 | 254 | 255 | ############################################################################### 256 | ############################################################################### 257 | ############################################################################### 258 | class Instance(Dot): 259 | """ 260 | u'AmiLaunchIndex': 0 261 | u'Architecture': u'x86_64', 262 | u'BlockDeviceMappings': [ 263 | {u'DeviceName': u'/dev/sda1', 264 | u'Ebs': {u'Status': u'attached', u'DeleteOnTermination': True, u'VolumeId': u'vol-XXXXXXXX', u'AttachTime': u'2000-01-01T01:00:00.000Z'} 265 | }], 266 | u'ClientToken': u'stuff', 267 | u'EbsOptimized': False, 268 | u'Hypervisor': u'xen', 269 | u'ImageId': u'ami-XXXXXXXX', 270 | u'InstanceId': u'i-XXXXXXXX', 271 | u'InstanceType': u't1.micro', 272 | u'KernelId': u'aki-XXXXXXXX', 273 | u'KeyName': u'KeyName', 274 | u'LaunchTime': u'2000-01-01T01:00:00.000Z', 275 | u'Monitoring': {u'State': u'disabled'}, 276 | u'NetworkInterfaces': [...], 277 | u'Placement': {u'GroupName': None, u'Tenancy': u'default', u'AvailabilityZone': u'ap-southeast-2a'}, 278 | u'PrivateDnsName': u'ip-10-1-2-3.ap-southeast-2.compute.internal', 279 | u'PrivateIpAddress': u'10.1.2.3', 280 | u'ProductCodes': [], 281 | u'PublicDnsName': u'ec2-54-1-2-3.ap-southeast-2.compute.amazonaws.com', 282 | u'PublicIpAddress': u'54.1.2.3', 283 | u'RootDeviceName': u'/dev/sda1', 284 | u'RootDeviceType': u'ebs', 285 | u'SecurityGroups': [{u'GroupName': u'XXX_GroupName_XXX', u'GroupId': u'sg-XXXXXXXX'}, ... 286 | u'SourceDestCheck': True, 287 | u'State': {u'Code': 16, u'Name': u'running'}, 288 | u'StateTransitionReason': None, 289 | u'SubnetId': u'subnet-XXXXXXXX', 290 | u'Tags': [{u'Key': u'aws:cloudformation:stack-id', u'Value': u'Stuff'}, 291 | {u'Key': u'aws:cloudformation:stack-name', u'Value': u'Stuff'}, 292 | {u'Key': u'Name', u'Value': u'Stuff'}, 293 | {u'Key': u'aws:cloudformation:logical-id', u'Value': u'JumpHost'}], 294 | u'VirtualizationType': u'paravirtual', 295 | u'VpcId': u'vpc-XXXXXXXX', 296 | """ 297 | 298 | def __init__(self, instance, args): 299 | self.data = instance 300 | self.name = instance.id 301 | self.args = args 302 | 303 | def inSubnet(self, subnet=None): 304 | if subnet and self['SubnetId'] != subnet: 305 | return False 306 | return True 307 | 308 | def inVpc(self, vpc=None): 309 | if vpc and self.data.vpc_id != vpc: 310 | return False 311 | return True 312 | 313 | def rank(self, fh): 314 | if self.inVpc(self.args.vpc) and self.inSubnet(self.args.subnet): 315 | fh.write("%s;" % self.mn()) 316 | 317 | def drawSec(self, fh): 318 | fh.write('// Instance %s\n' % self.name) 319 | label = "%s\\n%s\\n%s" % (self.tags('Name'), self.name, self['PrivateIpAddress']) 320 | fh.write('%s [label="%s" %s];\n' % (self.mn(self.name), label, self.image())) 321 | for sg in self['SecurityGroups']: 322 | self.connect(fh, self.name, sg['GroupId']) 323 | if self['SubnetId']: 324 | self.connect(fh, self.name, self['SubnetId']) 325 | 326 | def draw(self, fh): 327 | global clusternum 328 | if not self.inVpc(self.args.vpc) or not self.inSubnet(self.args.subnet): 329 | return 330 | fh.write('// Instance %s\n' % self.name) 331 | fh.write('subgraph cluster_%d {\n' % clusternum) 332 | 333 | if 'Name' in self.data.tags: 334 | label = self.data.tags['Name'] + "\n(" + self.name +")" 335 | else: 336 | label = self.name 337 | 338 | Style='solid' 339 | if self.data.state != 'running': 340 | Style = 'dashed' 341 | 342 | fh.write('%s [label="%s" %s];\n' % (self.mn(self.name), label, self.image(style=Style))) 343 | 344 | extraconns = [] 345 | for o in objects.values(): 346 | if o.partOfInstance(self.name): 347 | self.connect(fh, self.name, o.name) 348 | extraconns = o.subclusterDraw(fh) 349 | fh.write('graph [style=dotted]\n') 350 | fh.write('}\n') # End subgraph cluster 351 | if self.data.subnet_id: 352 | self.connect(fh, self.name, self.data.subnet_id) 353 | for ic, ec in extraconns: 354 | self.connect(fh, ic, ec) 355 | clusternum += 1 356 | if self.args.security: 357 | for sg in self.data.groups: 358 | self.connect(fh, self.name, sg.id) 359 | 360 | 361 | ############################################################################### 362 | ############################################################################### 363 | ############################################################################### 364 | class Subnet(Dot): 365 | """ 366 | u'AvailabilityZone': u'ap-southeast-2a', 367 | u'AvailableIpAddressCount': 10, 368 | u'CidrBlock': u'10.1.2.3/28' 369 | u'DefaultForAz': False, 370 | u'MapPublicIpOnLaunch': False, 371 | u'State': u'available', 372 | u'SubnetId': u'subnet-XXXXXXXX', 373 | u'Tags': [{u'Key': u'aws:cloudformation:stack-id', 374 | u'Value': u'arn:aws:cloudformation:ap-southeast-2:XXXXXXXXXXXX:stack/Stuff'}, 375 | {u'Key': u'aws:cloudformation:stack-name', u'Value': u'Stuff'}, 376 | {u'Key': u'aws:cloudformation:logical-id', u'Value': u'SubnetA3'}], 377 | u'VpcId': u'vpc-XXXXXXXX', 378 | """ 379 | 380 | def __init__(self, subnet, args): 381 | self.data = subnet 382 | self.name = subnet.id 383 | self.args = args 384 | 385 | def inVpc(self, vpc): 386 | if vpc and self.data.vpc_id != vpc: 387 | return False 388 | return True 389 | 390 | def relevent_to_ip(self, ip): 391 | if netaddr.IPAddress(ip) in netaddr.IPNetwork(self.data.cidr_block): 392 | print "Subnet %s - ip %s is relevent to %s" % (self.name, ip, self.data.cidr_block) 393 | return True 394 | return False 395 | 396 | def inSubnet(self, subnet=None): 397 | if subnet and self['SubnetId'] != subnet: 398 | return False 399 | return True 400 | 401 | def rank(self, fh): 402 | if self.inVpc(self.args.vpc) and self.inSubnet(self.args.subnet): 403 | fh.write("%s;" % self.mn()) 404 | 405 | def drawSec(self, fh): 406 | fh.write('// Subnet %s\n' % self.name) 407 | fh.write('%s [label="%s\\n%s" %s];\n' % (self.mn(self.name), self.name, self.data.cidr_block, self.image())) 408 | self.connect(fh, self.name, self.data.vpc_id) 409 | 410 | def draw(self, fh): 411 | if not self.inVpc(self.args.vpc) or not self.inSubnet(self.args.subnet): 412 | return 413 | if 'Name' in self.data.tags: 414 | label = self.data.tags['Name'] 415 | else: 416 | label = self.name 417 | 418 | fh.write('// Subnet %s\n' % self.name) 419 | fh.write('%s [label="%s\\n%s" %s];\n' % (self.mn(self.name), label, self.data.cidr_block, self.image())) 420 | self.connect(fh, self.name, self.data.vpc_id) 421 | 422 | 423 | ############################################################################### 424 | ############################################################################### 425 | ############################################################################### 426 | class Volume(Dot): 427 | """ 428 | u'Attachments': [ 429 | {u'AttachTime': u'2000-01-01T01:00:00.000Z', u'InstanceId': u'i-XXXXXXXX', 430 | u'VolumeId': u'vol-XXXXXXXX', u'State': u'attached', 431 | u'DeleteOnTermination': True, u'Device': u'/dev/sda1'}], 432 | u'AvailabilityZone': u'ap-southeast-2b', 433 | u'CreateTime': u'2000-01-01T01:00:00.000Z', 434 | u'Size': 6 435 | u'SnapshotId': u'snap-XXXXXXXX', 436 | u'State': u'in-use', 437 | u'VolumeId': u'vol-XXXXXXXX', 438 | u'VolumeType': u'standard', 439 | """ 440 | 441 | def __init__(self, vol, args): 442 | self.data = vol 443 | self.name = vol['VolumeId'] 444 | self.args = args 445 | 446 | def partOfInstance(self, instid): 447 | for a in self['Attachments']: 448 | if a['InstanceId'] == instid: 449 | return True 450 | return False 451 | 452 | def drawSec(self, fh): 453 | return 454 | 455 | def draw(self, fh): 456 | if self['State'] not in ('in-use',): 457 | if self.args.vpc: 458 | return 459 | if self.args.subnet or self.args.vpc: 460 | return 461 | fh.write('%s [label="Unattached Volume:%s\\n%s Gb" %s];\n' % ( 462 | self.mn(self.name), self.name, self['Size'], self.image())) 463 | 464 | def subclusterDraw(self, fh): 465 | fh.write('%s [shape=box, label="%s\\n%s Gb"];\n' % (self.mn(self.name), self.name, self['Size'])) 466 | return [] 467 | 468 | 469 | ############################################################################### 470 | ############################################################################### 471 | ############################################################################### 472 | class SecurityGroup(Dot): 473 | """ 474 | u'Description': u'SG Description', 475 | u'GroupId': u'sg-XXXXXXXX' 476 | u'GroupName': u'XXX_GroupName_XXX', 477 | u'IpPermissions': [ 478 | {u'ToPort': 443, u'IpProtocol': u'tcp', 479 | u'IpRanges': [{u'CidrIp': u'0.0.0.0/0'}], 480 | u'UserIdGroupPairs': [], u'FromPort': 443}], 481 | u'IpPermissionsEgress': [ 482 | {u'ToPort': 4502, u'IpProtocol': u'tcp', 483 | u'IpRanges': [{u'CidrIp': u'0.0.0.0/0'}], 484 | u'UserIdGroupPairs': [], u'FromPort': 4502}], 485 | u'OwnerId': u'XXXXXXXXXXXX', 486 | u'Tags': [{u'Key': u'Key', u'Value': u'Value'}, ... 487 | u'VpcId': u'vpc-XXXXXXXX', 488 | """ 489 | 490 | def __init__(self, sg, args): 491 | self.data = sg 492 | self.name = sg.id 493 | self.args = args 494 | self.drawn = False 495 | 496 | def draw(self, fh): 497 | if self.args.vpc and self.data.vpc_id != self.args.vpc: 498 | return 499 | 500 | portstr = self.permstring(fh, self.data.rules) 501 | eportstr = self.permstring(fh, self.data.rules_egress) 502 | 503 | tportstr = [] 504 | if portstr: 505 | tportstr.append("Ingress: %s" % portstr) 506 | if eportstr: 507 | tportstr.append("Egress: %s" % eportstr) 508 | desc = "\\n".join(chunkstring(self.data.description, 20)) 509 | fh.write('%s [label="SG: %s\\n%s\\n%s" %s];\n' % ( 510 | self.mn(self.name), self.name, desc, "\\n".join(tportstr), self.image())) 511 | 512 | def drawSec(self, fh): 513 | global clusternum 514 | self.extraRules = [] 515 | fh.write("// SG %s\n" % self.name) 516 | fh.write('subgraph cluster_%d {\n' % clusternum) 517 | fh.write('style=filled; color="grey90";\n') 518 | fh.write('node [style=filled, color="%s"];\n' % colours[clusternum]) 519 | desc = "\\n".join(chunkstring(self['Description'], 20)) 520 | fh.write('%s [shape="rect", label="%s\\n%s"]\n' % (self.mn(), self.name, desc)) 521 | if self['IpPermissions']: 522 | self.genRuleBlock(self['IpPermissions'], 'ingress', fh) 523 | if self['IpPermissionsEgress']: 524 | self.genRuleBlock(self['IpPermissionsEgress'], 'egress', fh) 525 | clusternum += 1 526 | fh.write("}\n") 527 | 528 | if self['IpPermissions']: 529 | fh.write("%s_ingress_rules -> %s [weight=5];\n" % (self.mn(), self.mn())) 530 | if self['IpPermissionsEgress']: 531 | fh.write("%s -> %s_egress_rules [weight=5];\n" % (self.mn(), self.mn())) 532 | for r in self.extraRules: 533 | fh.write(r) 534 | self.drawn = True 535 | 536 | def genRuleBlock(self, struct, direct, fh): 537 | fh.write("// SG %s %s\n" % (self.name, direct)) 538 | for e in struct: 539 | fh.write("// %s\n" % e) 540 | fh.write('%s_%s_rules [ shape="Mrecord" label=<' % (self.mn(), direct)) 541 | fh.write('\n' % (self.name, direct)) 542 | fh.write('%s %s\n' % (header('CIDR'), header('Ports'))) 543 | 544 | for e in struct: 545 | fh.write("\n") 546 | ipranges = [] 547 | for ipr in e['IpRanges']: 548 | if 'CidrIp' in ipr: 549 | ipranges.append(ipr['CidrIp']) 550 | 551 | if ipranges: 552 | if len(ipranges) > 1: 553 | iprangestr = "
%s %s
" 554 | for ipr in ipranges: 555 | iprangestr += "" % ipr 556 | iprangestr += "
%s
" 557 | else: 558 | iprangestr = "%s" % ipranges[0] 559 | else: 560 | iprangestr = "See %s" % e['UserIdGroupPairs'][0]['GroupId'] 561 | fh.write("%s" % iprangestr) 562 | if 'FromPort' in e and e['FromPort']: 563 | fh.write("%s - %s/%s" % (e['FromPort'], e['ToPort'], e['IpProtocol'])) 564 | else: 565 | fh.write("ALL\n") 566 | fh.write("\n") 567 | fh.write(">\n") 568 | fh.write("];\n") 569 | 570 | for e in struct: 571 | if e['UserIdGroupPairs']: 572 | for pair in e['UserIdGroupPairs']: 573 | secGrpToDraw.add(pair['GroupId']) 574 | self.extraRules.append('%s_%s_rules -> %s;\n' % (self.mn(), direct, self.mn(pair['GroupId']))) 575 | 576 | def relevent_to_ip(self, ip): 577 | for i in self['IpPermissions']: 578 | for ipr in i['IpRanges']: 579 | if netaddr.IPAddress(ip) in netaddr.IPNetwork(ipr['CidrIp']): 580 | return True 581 | for i in self['IpPermissionsEgress']: 582 | for ipr in i['IpRanges']: 583 | if netaddr.IPAddress(ip) in netaddr.IPNetwork(ipr['CidrIp']): 584 | return True 585 | return False 586 | 587 | def permstring(self, fh, obj): 588 | """ 589 | Convert the permutations and combinations into a sensible output 590 | """ 591 | ans = [] 592 | if not obj: 593 | return '' 594 | for ip in obj: 595 | if ip.grants.__len__ > 0: 596 | for pair in ip.grants: 597 | self.connect(fh, self.name, pair.group_id) 598 | if ip.from_port is not None: 599 | ipranges = [] 600 | for ipr in ip.grants: 601 | if ipr.cidr_ip is not None: 602 | ipranges.append(ipr.cidr_ip) 603 | iprangestr = ';'.join(ipranges) 604 | ans.append("%s %s->%s/%s" % (iprangestr, ip.from_port, ip.to_port, ip.ip_protocol)) 605 | return " ".join(ans) 606 | 607 | 608 | ############################################################################### 609 | ############################################################################### 610 | ############################################################################### 611 | class VPC(Dot): 612 | """ 613 | u'CidrBlock': u'172.1.2.3/16', 614 | u'DhcpOptionsId': u'dopt-XXXXXXXX', 615 | u'InstanceTenancy': u'default', 616 | u'IsDefault': True, 617 | u'State': u'available', 618 | u'VpcId': u'vpc-XXXXXXXX', 619 | """ 620 | 621 | def __init__(self, vpc, args): 622 | self.data = vpc 623 | self.name = vpc.id 624 | self.args = args 625 | 626 | def inVpc(self, vpc): 627 | if vpc and self.name != vpc: 628 | return False 629 | return True 630 | 631 | def inSubnet(self, subnet): 632 | """ Return True if the subnet is in this VPC""" 633 | if not subnet: 634 | return True 635 | if objects[subnet].inVpc(self.name): 636 | return True 637 | return False 638 | 639 | def relevent_to_ip(self, ip): 640 | if netaddr.IPAddress(ip) in netaddr.IPNetwork(self.data.cidr_block): 641 | print "VPC %s - ip %s is relevent to %s" % (self.name, ip, self.data.cidr_block) 642 | return True 643 | return False 644 | 645 | def rank(self, fh): 646 | if self.inVpc(self.args.vpc) and self.inSubnet(self.args.subnet): 647 | fh.write("%s;" % self.mn()) 648 | 649 | def drawSec(self, fh): 650 | fh.write('%s [label="%s:%s" %s];\n' % (self.mn(self.name), self.__class__.__name__, self.name, self.image())) 651 | 652 | def draw(self, fh): 653 | if not self.inVpc(self.args.vpc) or not self.inSubnet(self.args.subnet): 654 | return 655 | fh.write('%s [label="%s:%s" %s];\n' % (self.mn(self.name), self.__class__.__name__, self.name, self.image())) 656 | 657 | 658 | ############################################################################### 659 | ############################################################################### 660 | ############################################################################### 661 | class RouteTable(Dot): 662 | """ 663 | u'Associations': [{u'SubnetId': u'subnet-XXXXXXXX', u'RouteTableAssociationId': u'rtbassoc-XXXXXXXX', u'RouteTableId': u'rtb-XXXXXXXX'}, ...] 664 | u'PropagatingVgws': [], 665 | u'RouteTableId': u'rtb-XXXXXXXX', 666 | u'Routes': [ 667 | {u'GatewayId': u'local', u'DestinationCidrBlock': u'10.1.2.3/23', 668 | u'State': u'active', u'Origin': u'CreateRouteTable'}, 669 | {u'Origin': u'CreateRoute', u'DestinationCidrBlock': u'0.0.0.0/0', 670 | u'InstanceId': u'i-XXXXXXXX', u'NetworkInterfaceId': u'eni-XXXXXXXX', 671 | u'State': u'active', u'InstanceOwnerId': u'XXXXXXXXXXXX'}] 672 | u'Tags': [{u'Key': u'Key', u'Value': u'Value'}, ... 673 | u'VpcId': u'vpc-XXXXXXXX', 674 | 675 | """ 676 | 677 | def __init__(self, rt, args): 678 | self.data = rt 679 | self.args = args 680 | self.name = rt.id 681 | 682 | def rank(self, fh): 683 | if self.inVpc(self.args.vpc) and self.inSubnet(self.args.subnet): 684 | fh.write("%s;" % self.mn()) 685 | 686 | def inVpc(self, vpc): 687 | if vpc and self.data.vpc_id != vpc: 688 | return False 689 | return True 690 | 691 | def relevent_to_ip(self, ip): 692 | for rt in self['Routes']: 693 | if netaddr.IPAddress(ip) in netaddr.IPNetwork(rt['DestinationCidrBlock']): 694 | print "RT %s - ip %s is relevent to %s" % (self.name, ip, rt['DestinationCidrBlock']) 695 | return True 696 | return False 697 | 698 | def inSubnet(self, subnet): 699 | if not subnet: 700 | return True 701 | for a in self['Associations']: 702 | if subnet == a.get('SubnetId', None): 703 | return True 704 | return False 705 | 706 | def drawSec(self, fh): 707 | routelist = [] 708 | for rt in self['Routes']: 709 | if 'DestinationCidrBlock' in rt: 710 | routelist.append(rt['DestinationCidrBlock']) 711 | fh.write('%s [ shape="Mrecord" label=<' % self.mn()) 712 | fh.write('\n' % self.name) 713 | fh.write('%s %s\n' % (header('Source'), header('Dest'))) 714 | for route in self['Routes']: 715 | colour = 'green' 716 | if route['State'] != 'active': 717 | colour = 'red' 718 | if 'GatewayId' in route: 719 | src = route['GatewayId'] 720 | else: 721 | src = route['InstanceId'] 722 | fh.write('\n' % (colour, src, route['DestinationCidrBlock'])) 723 | fh.write("
%s
%s%s
>];\n") 724 | 725 | def draw(self, fh): 726 | if not self.inVpc(self.args.vpc) or not self.inSubnet(self.args.subnet): 727 | return 728 | routelist = [] 729 | for rt in self.data.routes: 730 | if rt.destination_cidr_block is not None: 731 | routelist.append(rt.destination_cidr_block) 732 | fh.write('%s [label="RT: %s\\n%s" %s];\n' % (self.mn(), self.name, ";".join(routelist), self.image())) 733 | for ass in self.data.associations: 734 | if ass.subnet_id is not None: 735 | if objects[ass.subnet_id].inSubnet(self.args.subnet): 736 | self.connect(fh, self.name, ass.subnet_id) 737 | for rt in self.data.routes: 738 | if rt.instance_id is not None: 739 | if objects[rt.instance_id].inSubnet(self.args.subnet): 740 | self.connect(fh, self.name, rt.instance_id) 741 | elif rt.interface_id is not None: 742 | self.connect(fh, self.name, rt.instance_id) 743 | 744 | 745 | ############################################################################### 746 | ############################################################################### 747 | ############################################################################### 748 | class NetworkInterface(Dot): 749 | """ 750 | u'Association': {u'PublicIp': u'54.1.2.3', u'IpOwnerId': u'amazon'} 751 | u'Attachment': { 752 | u'Status': u'attached', u'DeviceIndex': 0, 753 | u'AttachTime': u'2000-01-01T01:00:00.000Z', u'InstanceId': u'i-XXXXXXXX', 754 | u'DeleteOnTermination': True, u'AttachmentId': u'eni-attach-XXXXXXXX', 755 | u'InstanceOwnerId': u'XXXXXXXXXXXX'}, 756 | u'AvailabilityZone': u'ap-southeast-2b', 757 | u'Description': None, 758 | u'Groups': [{u'GroupName': u'XXX_GroupName_XXX', u'GroupId': u'sg-XXXXXXXX'}], 759 | u'MacAddress': u'aa:bb:cc:dd:ee:ff', 760 | u'NetworkInterfaceId': u'eni-XXXXXXXX', 761 | u'OwnerId': u'XXXXXXXXXXXX', 762 | u'PrivateDnsName': u'ip-172-1-2-3.ap-southeast-2.compute.internal', 763 | u'PrivateIpAddress': u'172.1.2.3', 764 | u'PrivateIpAddresses': [ 765 | {u'PrivateDnsName': u'ip-172-1-2-3.ap-southeast-2.compute.internal', 766 | u'PrivateIpAddress': u'172.1.2.3', u'Primary': True, 767 | u'Association': {u'PublicIp': u'54.1.2.3', u'IpOwnerId': u'amazon'}}], 768 | u'RequesterManaged': False, 769 | u'SourceDestCheck': True, 770 | u'Status': u'in-use', 771 | u'SubnetId': u'subnet-XXXXXXXX', 772 | u'TagSet': [], 773 | u'VpcId': u'vpc-XXXXXXXX', 774 | """ 775 | 776 | def __init__(self, nic, args): 777 | self.data = nic 778 | self.args = args 779 | self.name = nic.id 780 | 781 | def partOfInstance(self, instid): 782 | try: 783 | return self['Attachment'].get('InstanceId', None) == instid 784 | except AttributeError: 785 | return False 786 | 787 | def inSubnet(self, subnet=None): 788 | if subnet and self['SubnetId'] != subnet: 789 | return False 790 | return True 791 | 792 | def draw(self, fh): 793 | pass 794 | 795 | def subclusterDraw(self, fh): 796 | fh.write( 797 | '%s [label="NIC: %s\\n%s" %s];\n' % (self.mn(self.name), self.name, self['PrivateIpAddress'], self.image())) 798 | externallinks = [] 799 | if self.args.security: 800 | for g in self['Groups']: 801 | externallinks.append((self.name, g['GroupId'])) 802 | return externallinks 803 | 804 | 805 | ############################################################################### 806 | ############################################################################### 807 | ############################################################################### 808 | class InternetGateway(Dot): 809 | """ 810 | u'Attachments': [{u'State': u'available', u'VpcId': u'vpc-XXXXXXXX'}], 811 | u'InternetGatewayId': u'igw-3a121a58', 812 | u'Tags': [ 813 | {u'Key': u'aws:cloudformation:stack-id', u'Value': u'arn:aws:cloudformation:ap-southeast-2:XXXXXXXXXXXX:stack/Stuff'}, 814 | {u'Key': u'aws:cloudformation:logical-id', u'Value': u'InternetGateway'}, 815 | {u'Key': u'aws:cloudformation:stack-name', u'Value': u'Stuff'}], 816 | """ 817 | 818 | def __init__(self, igw, args): 819 | self.data = igw 820 | self.name = igw.id 821 | self.conns = [] 822 | for i in igw.attachments: 823 | # print(i) 824 | self.conns.append(i) 825 | self.args = args 826 | 827 | def rank(self, fh): 828 | if self.args.vpc: 829 | for i in self.conns[:]: 830 | if i != self.args.vpc: 831 | self.conns.remove(i) 832 | if self.conns: 833 | fh.write("%s;" % self.mn()) 834 | 835 | def draw(self, fh): 836 | if self.args.vpc: 837 | for i in self.conns[:]: 838 | if i != self.args.vpc: 839 | self.conns.remove(i) 840 | if self.args.subnet: 841 | for i in self.conns[:]: 842 | if not objects[i].inSubnet(self.args.subnet): 843 | self.conns.remove(i) 844 | if self.conns: 845 | fh.write('%s [label="InternetGateway: %s" %s];\n' % (self.mn(self.name), self.name, self.image())) 846 | for i in self.conns: 847 | self.connect(fh, self.name, i.vpc_id) 848 | 849 | 850 | ############################################################################### 851 | ############################################################################### 852 | ############################################################################### 853 | class LoadBalancer(Dot): 854 | """ 855 | u'AvailabilityZones': [u'ap-southeast-2b', u'ap-southeast-2a'], 856 | u'BackendServerDescriptions': [], 857 | u'CanonicalHostedZoneName': u'Stuff', 858 | u'CanonicalHostedZoneNameID': u'XXXXXXXXXXXXXX', 859 | u'CreatedTime': u'2000-01-01T01:00:00.300Z', 860 | u'DNSName': u'Stuff', 861 | u'HealthCheck': {u'HealthyThreshold': 2, u'Interval': 30, u'Target': u'TCP:7990', u'Timeout': 5, u'UnhealthyThreshold': 2}, 862 | u'Instances': [{u'InstanceId': u'i-XXXXXXXX'}], 863 | u'ListenerDescriptions': [ 864 | {u'Listener': { 865 | u'InstancePort': 7990, u'Protocol': u'HTTPS', u'LoadBalancerPort': 443, 866 | u'SSLCertificateId': u'arn:aws:iam::XXXXXXXXXXXX:server-certificate/GenericSSL', 867 | u'InstanceProtocol': u'HTTP'}, u'PolicyNames': [u'ELBSecurityPolicy-2011-08']}, 868 | {u'Listener': { 869 | u'InstancePort': 7999, u'LoadBalancerPort': 7999, u'Protocol': u'TCP', 870 | u'InstanceProtocol': u'TCP'}, u'PolicyNames': []}], 871 | u'LoadBalancerName': u'Stuff', 872 | u'Policies': {u'LBCookieStickinessPolicies': [], u'AppCookieStickinessPolicies': [], u'OtherPolicies': [u'ELBSecurityPolicy-2011-08']}, 873 | u'Scheme': u'internet-facing', 874 | u'SecurityGroups': [u'sg-XXXXXXXX'], 875 | u'SourceSecurityGroup': {u'OwnerAlias': u'XXXXXXXXXXXX', u'GroupName': u'XXX_GroupName_XXX'} 876 | u'Subnets': [u'subnet-XXXXXXXX', u'subnet-XXXXXXXX'], 877 | u'VPCId': u'vpc-XXXXXXXX', 878 | """ 879 | 880 | def __init__(self, lb, args): 881 | self.data = lb 882 | self.name = lb.name 883 | self.args = args 884 | 885 | def inSubnet(self, subnet=None): 886 | if subnet and subnet not in self.data.subnets: 887 | return False 888 | return True 889 | 890 | def inVpc(self, vpc): 891 | if vpc and self.data.vpc_id != vpc: 892 | return False 893 | return True 894 | 895 | def rank(self, fh): 896 | if self.inVpc(self.args.vpc) and self.inSubnet(self.args.subnet): 897 | fh.write("%s;" % self.mn()) 898 | 899 | def draw(self, fh): 900 | if not self.inVpc(self.args.vpc) or not self.inSubnet(self.args.subnet): 901 | return 902 | ports = [] 903 | for l in self.data.listeners: 904 | # x = l['Listener'] 905 | ports.append( 906 | "%s/%s -> %s/%s" % (l.load_balancer_port, l.protocol, l.instance_port, l.instance_protocol)) 907 | 908 | fh.write('%s [label="ELB: %s\\n%s" %s];\n' % (self.mn(self.name), self.name, "\n".join(ports), self.image())) 909 | for i in self.data.instances: 910 | if objects[i.id].inSubnet(self.args.subnet): 911 | self.connect(fh, self.name, i.id) 912 | for s in self.data.subnets: 913 | if self.args.subnet: 914 | if s != self.args.subnet: 915 | continue 916 | self.connect(fh, self.name, s) 917 | if self.args.security: 918 | for sg in self.data.security_groups: 919 | self.connect(fh, self.name, sg) 920 | 921 | 922 | ############################################################################### 923 | ############################################################################### 924 | ############################################################################### 925 | class Database(Dot): 926 | """ 927 | u'AllocatedStorage': 5, 928 | u'AutoMinorVersionUpgrade': True, 929 | u'AvailabilityZone': u'ap-southeast-2a', 930 | u'BackupRetentionPeriod': 0, 931 | u'DBInstanceClass': u'db.t1.micro', 932 | u'DBInstanceIdentifier': u'devapps' 933 | u'DBInstanceStatus': u'available', 934 | u'DBName': u'crowd', 935 | u'DBParameterGroups': [{u'DBParameterGroupName': u'XXX_GroupName_XXX', u'ParameterApplyStatus': u'in-sync'}], 936 | u'DBSecurityGroups': [], 937 | u'DBSubnetGroup': { 938 | u'DBSubnetGroupDescription': u'default', 939 | u'DBSubnetGroupName': u'default', 940 | u'SubnetGroupStatus': u'Complete' 941 | u'Subnets': [ 942 | { 943 | u'SubnetStatus': u'Active', 944 | u'SubnetIdentifier': u'subnet-XXXXXXXX', 945 | u'SubnetAvailabilityZone': {u'Name': u'ap-southeast-2b', u'ProvisionedIopsCapable': False} 946 | }, 947 | ... 948 | ], 949 | u'VpcId': u'vpc-XXXXXXXX', 950 | }, 951 | u'Endpoint': {u'Port': 3306, u'Address': u'devapps.csgxwe0psnca.ap-southeast-2.rds.amazonaws.com'}, 952 | u'Engine': u'mysql', 953 | u'EngineVersion': u'5.6.13', 954 | u'InstanceCreateTime': u'2000-01-01T01:00:00.275Z', 955 | u'LicenseModel': u'general-public-license', 956 | u'MasterUsername': u'rootmaster', 957 | u'MultiAZ': False, 958 | u'OptionGroupMemberships': [{u'Status': u'in-sync', u'OptionGroupName': u'default:mysql-5-6'}], 959 | u'PendingModifiedValues': {}, 960 | u'PreferredBackupWindow': u'18:37-19:07', 961 | u'PreferredMaintenanceWindow': u'sat:15:17-sat:15:47', 962 | u'PubliclyAccessible': True, 963 | u'ReadReplicaDBInstanceIdentifiers': [], 964 | u'VpcSecurityGroups': [{u'Status': u'active', u'VpcSecurityGroupId': u'sg-XXXXXXXX'}], 965 | """ 966 | 967 | def __init__(self, db, args): 968 | self.data = db 969 | self.name = db.id 970 | self.args = args 971 | 972 | def inSubnet(self, subnet=None): 973 | if not subnet: 974 | return True 975 | for snet in self['DBSubnetGroup']['Subnets']: 976 | if subnet == snet['SubnetIdentifier']: 977 | return True 978 | return False 979 | 980 | def inVpc(self, vpc): 981 | if vpc and self.data.subnet_group.vpc_id != vpc: 982 | return False 983 | return True 984 | 985 | def rank(self, fh): 986 | if self.inVpc(self.args.vpc) and self.inSubnet(self.args.subnet): 987 | fh.write("%s;" % self.mn()) 988 | 989 | def drawSec(self, fh): 990 | imgstr = self.image(["Database-%s" % self['Engine'], 'Database']) 991 | fh.write('%s [label="DB: %s\\n%s" %s];\n' % (self.mn(self.name), self.name, self['Engine'], imgstr)) 992 | 993 | def draw(self, fh): 994 | if not self.inVpc(self.args.vpc) or not self.inSubnet(self.args.subnet): 995 | return 996 | fh.write('// Database %s\n' % self.name) 997 | imgstr = self.image(["Database-%s" % self.data.engine, 'Database']) 998 | fh.write('%s [label="DB: %s\\n%s" %s];\n' % (self.mn(self.name), self.name, self.data.engine, imgstr)) 999 | for subnet in self.data.subnet_group.subnet_ids: 1000 | # if subnet.SubnetStatus == 'Active': 1001 | if objects[subnet].inSubnet(self.args.subnet): 1002 | self.connect(fh, self.name, subnet) 1003 | if self.args.security: 1004 | for sg in self.data.vpc_security_groups: 1005 | self.connect(fh, self.name, sg.vpc_group) 1006 | 1007 | 1008 | class ASG(Dot): 1009 | def __init__(self, db, args): 1010 | self.data = db 1011 | self.name = db.name 1012 | self.args = args 1013 | 1014 | """ 1015 | { 1016 | "AutoScalingGroups": [ 1017 | { 1018 | "AutoScalingGroupARN": "arn:aws:autoscaling:us-west-2:803981987763:autoScalingGroup:930d940e-891e-4781-a11a-7b0acd480f03:autoScalingGroupName/my-test-asg", 1019 | "HealthCheckGracePeriod": 0, 1020 | "SuspendedProcesses": [], 1021 | "DesiredCapacity": 1, 1022 | "Tags": [], 1023 | "EnabledMetrics": [], 1024 | "LoadBalancerNames": [], 1025 | "AutoScalingGroupName": "my-test-asg", 1026 | "DefaultCooldown": 300, 1027 | "MinSize": 0, 1028 | "Instances": [ 1029 | { 1030 | "InstanceId": "i-4ba0837f", 1031 | "AvailabilityZone": "us-west-2c", 1032 | "HealthStatus": "Healthy", 1033 | "LifecycleState": "InService", 1034 | "LaunchConfigurationName": "my-test-lc" 1035 | } 1036 | ], 1037 | "MaxSize": 1, 1038 | "VPCZoneIdentifier": null, 1039 | "TerminationPolicies": [ 1040 | "Default" 1041 | ], 1042 | "LaunchConfigurationName": "my-test-lc", 1043 | "CreatedTime": "2013-08-19T20:53:25.584Z", 1044 | "AvailabilityZones": [ 1045 | "us-west-2c" 1046 | ], 1047 | "HealthCheckType": "EC2" 1048 | } 1049 | ] 1050 | } 1051 | 1052 | """ 1053 | 1054 | def image(self, names=[]): 1055 | return super(ASG, self).image(names) 1056 | 1057 | def draw(self, fh): 1058 | if self.inVpc(self.args.vpc) and self.inSubnet(self.args.subnet): 1059 | fh.write('// ASG %s\n' % self.name) 1060 | imgstr = self.image(["ASG-%s" % self.data.name, 'ASG']) 1061 | fh.write('%s [label="ASG: %s\\n%s" %s];\n' % (self.mn(self.name), self.name, '', imgstr)) 1062 | for lb in self.data.load_balancers: 1063 | if objects[lb].inSubnet(self.args.subnet): 1064 | self.connect(fh, self.name, lb) 1065 | 1066 | def rank(self, fh): 1067 | if self.inVpc(self.args.vpc) and self.inSubnet(self.args.subnet): 1068 | fh.write("%s;" % self.mn()) 1069 | 1070 | def inVpc(self, vpc): 1071 | if vpc: 1072 | subnets = self.data.vpc_zone_identifier 1073 | for subnet in subnets.split(','): 1074 | # sys.stderr.write(subnet) 1075 | if vpc and subnet in objects and objects[subnet].data.vpc_id == vpc: 1076 | return True 1077 | return False 1078 | return True 1079 | 1080 | 1081 | ############################################################################### 1082 | 1083 | def header(lbl): 1084 | return '%s' % lbl 1085 | 1086 | 1087 | ############################################################################### 1088 | def chunkstring(strng, length): 1089 | """ Break a string on word boundaries, where each line is up to 1090 | length characters long """ 1091 | ans = [] 1092 | line = [] 1093 | for w in strng.split(): 1094 | if len(w) >= length: 1095 | ans.append(" ".join(line)) 1096 | ans.append(w) 1097 | line = [] 1098 | continue 1099 | if len(" ".join(line)) + len(w) < length: 1100 | line.append(w) 1101 | else: 1102 | ans.append(" ".join(line)) 1103 | line = [] 1104 | ans.append(" ".join(line)) 1105 | return ans 1106 | 1107 | ############################################################################### 1108 | def get_all_internet_gateways(args): 1109 | if args.verbose: 1110 | sys.stderr.write("Getting internet gateways\n") 1111 | # igw_data = ec2cmd("describe-internet-gateways")['InternetGateways'] 1112 | import boto.vpc 1113 | igw_data = paginate_boto_response(boto.vpc.connect_to_region(args.region).get_all_internet_gateways) 1114 | for igw in igw_data: 1115 | g = InternetGateway(igw, args) 1116 | objects[g.name] = g 1117 | 1118 | 1119 | ############################################################################### 1120 | def get_vpc_list(args): 1121 | import boto.vpc 1122 | vpc_data = paginate_boto_response(boto.vpc.connect_to_region(args.region).get_all_vpcs) 1123 | # vpc_data = ec2cmd("describe-vpcs")['Vpcs'] 1124 | for vpc in vpc_data: 1125 | if args.vpc and vpc.id != args.vpc: 1126 | continue 1127 | if args.verbose: 1128 | sys.stderr.write("VPC: %s\n" % vpc.id) 1129 | g = VPC(vpc, args) 1130 | objects[g.name] = g 1131 | 1132 | 1133 | ############################################################################### 1134 | def get_all_instances(args): 1135 | if args.verbose: 1136 | sys.stderr.write("Getting instances\n") 1137 | import boto.ec2 1138 | reservation_list = paginate_boto_response(boto.ec2.connect_to_region(args.region).get_all_reservations) 1139 | # reservation_list = ec2cmd("describe-instances")['Reservations'] 1140 | for reservation in reservation_list: 1141 | for instance in reservation.instances: 1142 | i = Instance(instance, args) 1143 | objects[i.name] = i 1144 | if args.verbose: 1145 | sys.stderr.write("Instance: %s\n" % i.name) 1146 | 1147 | 1148 | ############################################################################### 1149 | def get_all_subnets(args): 1150 | if args.verbose: 1151 | sys.stderr.write("Getting subnets\n") 1152 | import boto.vpc 1153 | subnets = paginate_boto_response(boto.vpc.connect_to_region(args.region).get_all_subnets) 1154 | for subnet in subnets: 1155 | if args.subnet and subnet.id != args.subnet: 1156 | pass 1157 | elif args.verbose: 1158 | sys.stderr.write("Subnet: %s\n" % subnet.id) 1159 | s = Subnet(subnet, args) 1160 | objects[s.name] = s 1161 | 1162 | 1163 | ############################################################################### 1164 | def get_all_volumes(args): 1165 | if args.verbose: 1166 | sys.stderr.write("Getting volumes\n") 1167 | # volumes = ec2cmd("describe-volumes")['Volumes'] 1168 | volumes = paginate_boto_response(boto.ec2.connect_to_region(args.region).get_all_volumes) 1169 | for volume in volumes: 1170 | v = Volume(volume, args) 1171 | objects[v.name] = v 1172 | 1173 | 1174 | ############################################################################### 1175 | def get_all_security_groups(args): 1176 | if args.verbose: 1177 | sys.stderr.write("Getting security groups\n") 1178 | import boto.ec2 1179 | sgs = paginate_boto_response(boto.ec2.connect_to_region(args.region).get_all_security_groups) 1180 | # sgs = ec2cmd("describe-security-groups")['SecurityGroups'] 1181 | for sg in sgs: 1182 | s = SecurityGroup(sg, args) 1183 | objects[s.name] = s 1184 | if args.verbose: 1185 | sys.stderr.write("SG %s\n" % s.name) 1186 | 1187 | 1188 | ############################################################################### 1189 | def get_all_route_tables(args): 1190 | if args.verbose: 1191 | sys.stderr.write("Getting route tables\n") 1192 | # rts = ec2cmd('describe-route-tables')['RouteTables'] 1193 | import boto.vpc 1194 | rts = paginate_boto_response(boto.vpc.connect_to_region(args.region).get_all_route_tables) 1195 | for rt in rts: 1196 | r = RouteTable(rt, args) 1197 | objects[r.name] = r 1198 | 1199 | 1200 | ############################################################################### 1201 | def get_all_network_interfaces(args): 1202 | if args.verbose: 1203 | sys.stderr.write("Getting NICs\n") 1204 | import boto.ec2 1205 | nics = paginate_boto_response(boto.ec2.connect_to_region(args.region).get_all_network_interfaces) 1206 | # nics = ec2cmd('describe-network-interfaces')['NetworkInterfaces'] 1207 | for nic in nics: 1208 | n = NetworkInterface(nic, args) 1209 | objects[n.name] = n 1210 | 1211 | 1212 | ############################################################################### 1213 | def get_all_rds(args): 1214 | if args.verbose: 1215 | sys.stderr.write("Getting Databases\n") 1216 | import boto.rds 1217 | dbs = paginate_boto_response(boto.rds.connect_to_region(args.region).get_all_dbinstances) 1218 | # dbs = rdscmd('describe-db-instances')['DBInstances'] 1219 | for db in dbs: 1220 | rds = Database(db, args) 1221 | objects[rds.name] = rds 1222 | if args.verbose: 1223 | sys.stderr.write("RDS: %s\n" % rds.name) 1224 | 1225 | 1226 | ############################################################################### 1227 | def get_all_elbs(args): 1228 | if args.verbose: 1229 | sys.stderr.write("Getting Load Balancers\n") 1230 | import boto.ec2.elb 1231 | elbs = paginate_boto_response(boto.ec2.elb.connect_to_region(args.region).get_all_load_balancers) 1232 | # elbs = elbcmd('describe-load-balancers')['LoadBalancerDescriptions'] 1233 | for elb in elbs: 1234 | lb = LoadBalancer(elb, args) 1235 | if args.verbose: 1236 | sys.stderr.write("ELBs: %s\n" % lb.name) 1237 | objects[lb.name] = lb 1238 | 1239 | 1240 | ############################################################################### 1241 | def get_all_networkacls(args): 1242 | if args.verbose: 1243 | sys.stderr.write("Getting NACLs\n") 1244 | import boto.vpc 1245 | nacls = paginate_boto_response(boto.vpc.connect_to_region(args.region).get_all_network_acls) 1246 | # nacls = ec2cmd('describe-network-acls')['NetworkAcls'] 1247 | for nacl in nacls: 1248 | nc = NetworkAcl(nacl, args) 1249 | objects[nc.name] = nc 1250 | if args.verbose: 1251 | sys.stderr.write("NACL: %s\n" % nc.name) 1252 | 1253 | 1254 | ############################################################################### 1255 | def get_all_asgs(args): 1256 | if args.verbose: 1257 | sys.stderr.write("Getting ASGs\n") 1258 | # asgs = asgcmd('describe-auto-scaling-groups')['AutoScalingGroups'] 1259 | import boto.ec2.autoscale 1260 | asgs = paginate_boto_response(boto.ec2.autoscale.connect_to_region(args.region).get_all_groups) 1261 | for asg in asgs: 1262 | _asg = ASG(asg, args) 1263 | objects[_asg.name] = _asg 1264 | if args.verbose: 1265 | sys.stderr.write("ASGs: %s\n" % _asg.name) 1266 | 1267 | 1268 | def map_region(args): 1269 | # EC2 1270 | get_vpc_list(args) 1271 | get_all_internet_gateways(args) 1272 | get_all_network_interfaces(args) 1273 | get_all_instances(args) 1274 | get_all_subnets(args) 1275 | if args.volumes: 1276 | get_all_volumes(args) 1277 | get_all_route_tables(args) 1278 | get_all_security_groups(args) 1279 | get_all_networkacls(args) 1280 | 1281 | # RDS 1282 | get_all_rds(args) 1283 | 1284 | # ELB 1285 | get_all_elbs(args) 1286 | 1287 | get_all_asgs(args) 1288 | 1289 | 1290 | ############################################################################### 1291 | def parseArgs(): 1292 | global nocache 1293 | global awsflags 1294 | parser = argparse.ArgumentParser() 1295 | parser.add_argument( 1296 | '--awsflag', default=None, help="Flags to pass to aws calls [None]") 1297 | parser.add_argument( 1298 | '--vpc', default=None, help="Which VPC to examine [all]") 1299 | parser.add_argument( 1300 | '--subnet', default=None, help="Which subnet to examine [all]") 1301 | parser.add_argument( 1302 | '--iterate', default=None, choices=['vpc', 'subnet'], 1303 | help="Create different maps for each vpc or subnet") 1304 | parser.add_argument( 1305 | '--nocache', default=False, action='store_true', 1306 | help="Don't read from cache'd data") 1307 | parser.add_argument( 1308 | '--output', default=sys.stdout, type=argparse.FileType('w'), 1309 | help="Which file to output to [stdout]") 1310 | parser.add_argument( 1311 | '--security', default=False, action='store_true', 1312 | help="Draw in security groups") 1313 | parser.add_argument( 1314 | '--secmap', default=None, 1315 | help="Draw a security map for specified ec2") 1316 | parser.add_argument( 1317 | '--volumes', default=False, 1318 | help="enables volumes") 1319 | parser.add_argument( 1320 | '-v', '--verbose', default=False, action='store_true', 1321 | help="Print some details") 1322 | 1323 | requiredNamed = parser.add_argument_group('required named arguments') 1324 | 1325 | if 'AWS_DEFAULT_REGION' in os.environ: 1326 | default_region=os.environ['AWS_DEFAULT_REGION'] 1327 | region_required = False 1328 | else: 1329 | default_region=False 1330 | region_required = True 1331 | 1332 | requiredNamed.add_argument( 1333 | '--region', default=default_region, required=region_required, 1334 | help="ec2 region") 1335 | 1336 | args = parser.parse_args() 1337 | nocache = args.nocache 1338 | if args.vpc and not args.vpc.startswith('vpc-'): 1339 | args.vpc = "vpc-%s" % args.vpc 1340 | if args.subnet and not args.subnet.startswith('subnet-'): 1341 | args.subnet = "subnet-%s" % args.subnet 1342 | if args.awsflag: 1343 | awsflags = ["--%s" % args.awsflag] 1344 | return args 1345 | 1346 | 1347 | ############################################################################### 1348 | def generateHeader(fh): 1349 | fh.write("digraph G {\n") 1350 | fh.write('overlap=false\n') 1351 | fh.write('ranksep=1.6\n') 1352 | fh.write('splines=ortho\n') 1353 | 1354 | 1355 | ############################################################################### 1356 | def generateFooter(fh): 1357 | fh.write("}\n") 1358 | 1359 | 1360 | ############################################################################### 1361 | def generate_secmap(ec2, fh): 1362 | """ Generate a security map instead """ 1363 | generateHeader(fh) 1364 | subnet = objects[ec2]['SubnetId'] 1365 | vpc = objects[ec2]['VpcId'] 1366 | 1367 | # The ec2 1368 | objects[ec2].drawSec(fh) 1369 | 1370 | # Security groups associated with the ec2 1371 | for sg in objects[ec2]['SecurityGroups']: 1372 | secGrpToDraw.add(sg['GroupId']) 1373 | objects[sg['GroupId']].drawSec(fh) 1374 | 1375 | # Subnet ec2 is on 1376 | subnet = objects[ec2]['SubnetId'] 1377 | objects[subnet].drawSec(fh) 1378 | 1379 | # NACLs and RTs associated with that subnet 1380 | for obj in objects.values(): 1381 | if obj.__class__ in (NetworkAcl, RouteTable): 1382 | for assoc in obj['Associations']: 1383 | if 'SubnetId' in assoc and assoc['SubnetId'] == subnet: 1384 | obj.drawSec(fh) 1385 | fh.write("%s -> %s\n" % (obj.mn(), objects[subnet].mn())) 1386 | continue 1387 | if obj.__class__ in (Database, ): 1388 | for sg in obj['VpcSecurityGroups']: 1389 | if sg['VpcSecurityGroupId'] in secGrpToDraw: 1390 | obj.drawSec(fh) 1391 | 1392 | # VPC that the EC2 is in 1393 | objects[vpc].drawSec(fh) 1394 | 1395 | # Finish any referred to SG 1396 | for sg in list(secGrpToDraw): 1397 | if not objects[sg].drawn: 1398 | objects[sg].drawSec(fh) 1399 | 1400 | generateFooter(fh) 1401 | 1402 | 1403 | ############################################################################### 1404 | def generate_map(fh, args): 1405 | generateHeader(fh) 1406 | 1407 | # Draw all the objects 1408 | for obj in sorted(objects.values()): 1409 | if obj.__class__ == SecurityGroup: 1410 | if not args.security: 1411 | continue 1412 | obj.draw(fh) 1413 | 1414 | # Assign Ranks 1415 | for objtype in [Database, LoadBalancer, Subnet, Instance, VPC, InternetGateway, RouteTable, ASG]: 1416 | fh.write('// Rank %s\n' % objtype.__name__) 1417 | fh.write('rank_%s [style=invisible]\n' % objtype.__name__) 1418 | fh.write('{ rank=same; rank_%s; ' % objtype.__name__) 1419 | for obj in sorted(objects.values()): 1420 | if obj.__class__ == objtype: 1421 | obj.rank(fh) 1422 | fh.write('}\n') 1423 | ranks = ['RouteTable', 'Subnet', 'Database', 'LoadBalancer', 'ASG', 'Instance', 'VPC', 'InternetGateway'] 1424 | strout = " -> ".join(["rank_%s" % x for x in ranks]) 1425 | fh.write("%s [style=invis];\n" % strout) 1426 | 1427 | generateFooter(fh) 1428 | 1429 | 1430 | ############################################################################### 1431 | def main(): 1432 | args = parseArgs() 1433 | map_region(args) 1434 | if args.secmap: 1435 | generate_secmap(args.secmap, args.output) 1436 | return 1437 | if args.iterate: 1438 | for o in objects.keys(): 1439 | if o.startswith(args.iterate): 1440 | f = open('%s.dot' % o, 'w') 1441 | setattr(args, args.iterate, o) 1442 | generate_map(f, args) 1443 | f.close() 1444 | else: 1445 | generate_map(args.output, args) 1446 | 1447 | ############################################################################### 1448 | if __name__ == '__main__': 1449 | main() 1450 | 1451 | # EOF 1452 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | netaddr 2 | argparse 3 | boto 4 | pydot 5 | pyparsing 6 | wsgiref 7 | --------------------------------------------------------------------------------