github.com/karalabe/go-ethereum@v0.8.5/tests/files/ansible/ec2.py (about) 1 #!/usr/bin/env python 2 3 ''' 4 EC2 external inventory script 5 ================================= 6 7 Generates inventory that Ansible can understand by making API request to 8 AWS EC2 using the Boto library. 9 10 NOTE: This script assumes Ansible is being executed where the environment 11 variables needed for Boto have already been set: 12 export AWS_ACCESS_KEY_ID='AK123' 13 export AWS_SECRET_ACCESS_KEY='abc123' 14 15 This script also assumes there is an ec2.ini file alongside it. To specify a 16 different path to ec2.ini, define the EC2_INI_PATH environment variable: 17 18 export EC2_INI_PATH=/path/to/my_ec2.ini 19 20 If you're using eucalyptus you need to set the above variables and 21 you need to define: 22 23 export EC2_URL=http://hostname_of_your_cc:port/services/Eucalyptus 24 25 For more details, see: http://docs.pythonboto.org/en/latest/boto_config_tut.html 26 27 When run against a specific host, this script returns the following variables: 28 - ec2_ami_launch_index 29 - ec2_architecture 30 - ec2_association 31 - ec2_attachTime 32 - ec2_attachment 33 - ec2_attachmentId 34 - ec2_client_token 35 - ec2_deleteOnTermination 36 - ec2_description 37 - ec2_deviceIndex 38 - ec2_dns_name 39 - ec2_eventsSet 40 - ec2_group_name 41 - ec2_hypervisor 42 - ec2_id 43 - ec2_image_id 44 - ec2_instanceState 45 - ec2_instance_type 46 - ec2_ipOwnerId 47 - ec2_ip_address 48 - ec2_item 49 - ec2_kernel 50 - ec2_key_name 51 - ec2_launch_time 52 - ec2_monitored 53 - ec2_monitoring 54 - ec2_networkInterfaceId 55 - ec2_ownerId 56 - ec2_persistent 57 - ec2_placement 58 - ec2_platform 59 - ec2_previous_state 60 - ec2_private_dns_name 61 - ec2_private_ip_address 62 - ec2_publicIp 63 - ec2_public_dns_name 64 - ec2_ramdisk 65 - ec2_reason 66 - ec2_region 67 - ec2_requester_id 68 - ec2_root_device_name 69 - ec2_root_device_type 70 - ec2_security_group_ids 71 - ec2_security_group_names 72 - ec2_shutdown_state 73 - ec2_sourceDestCheck 74 - ec2_spot_instance_request_id 75 - ec2_state 76 - ec2_state_code 77 - ec2_state_reason 78 - ec2_status 79 - ec2_subnet_id 80 - ec2_tenancy 81 - ec2_virtualization_type 82 - ec2_vpc_id 83 84 These variables are pulled out of a boto.ec2.instance object. There is a lack of 85 consistency with variable spellings (camelCase and underscores) since this 86 just loops through all variables the object exposes. It is preferred to use the 87 ones with underscores when multiple exist. 88 89 In addition, if an instance has AWS Tags associated with it, each tag is a new 90 variable named: 91 - ec2_tag_[Key] = [Value] 92 93 Security groups are comma-separated in 'ec2_security_group_ids' and 94 'ec2_security_group_names'. 95 ''' 96 97 # (c) 2012, Peter Sankauskas 98 # 99 # This file is part of Ansible, 100 # 101 # Ansible is free software: you can redistribute it and/or modify 102 # it under the terms of the GNU General Public License as published by 103 # the Free Software Foundation, either version 3 of the License, or 104 # (at your option) any later version. 105 # 106 # Ansible is distributed in the hope that it will be useful, 107 # but WITHOUT ANY WARRANTY; without even the implied warranty of 108 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 109 # GNU General Public License for more details. 110 # 111 # You should have received a copy of the GNU General Public License 112 # along with Ansible. If not, see <http://www.gnu.org/licenses/>. 113 114 ###################################################################### 115 116 import sys 117 import os 118 import argparse 119 import re 120 from time import time 121 import boto 122 from boto import ec2 123 from boto import rds 124 from boto import route53 125 import ConfigParser 126 from collections import defaultdict 127 128 try: 129 import json 130 except ImportError: 131 import simplejson as json 132 133 134 class Ec2Inventory(object): 135 def _empty_inventory(self): 136 return {"_meta" : {"hostvars" : {}}} 137 138 def __init__(self): 139 ''' Main execution path ''' 140 141 # Inventory grouped by instance IDs, tags, security groups, regions, 142 # and availability zones 143 self.inventory = self._empty_inventory() 144 145 # Index of hostname (address) to instance ID 146 self.index = {} 147 148 # Read settings and parse CLI arguments 149 self.read_settings() 150 self.parse_cli_args() 151 152 # Cache 153 if self.args.refresh_cache: 154 self.do_api_calls_update_cache() 155 elif not self.is_cache_valid(): 156 self.do_api_calls_update_cache() 157 158 # Data to print 159 if self.args.host: 160 data_to_print = self.get_host_info() 161 162 elif self.args.list: 163 # Display list of instances for inventory 164 if self.inventory == self._empty_inventory(): 165 data_to_print = self.get_inventory_from_cache() 166 else: 167 data_to_print = self.json_format_dict(self.inventory, True) 168 169 print data_to_print 170 171 172 def is_cache_valid(self): 173 ''' Determines if the cache files have expired, or if it is still valid ''' 174 175 if os.path.isfile(self.cache_path_cache): 176 mod_time = os.path.getmtime(self.cache_path_cache) 177 current_time = time() 178 if (mod_time + self.cache_max_age) > current_time: 179 if os.path.isfile(self.cache_path_index): 180 return True 181 182 return False 183 184 185 def read_settings(self): 186 ''' Reads the settings from the ec2.ini file ''' 187 188 config = ConfigParser.SafeConfigParser() 189 ec2_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ec2.ini') 190 ec2_ini_path = os.environ.get('EC2_INI_PATH', ec2_default_ini_path) 191 config.read(ec2_ini_path) 192 193 # is eucalyptus? 194 self.eucalyptus_host = None 195 self.eucalyptus = False 196 if config.has_option('ec2', 'eucalyptus'): 197 self.eucalyptus = config.getboolean('ec2', 'eucalyptus') 198 if self.eucalyptus and config.has_option('ec2', 'eucalyptus_host'): 199 self.eucalyptus_host = config.get('ec2', 'eucalyptus_host') 200 201 # Regions 202 self.regions = [] 203 configRegions = config.get('ec2', 'regions') 204 configRegions_exclude = config.get('ec2', 'regions_exclude') 205 if (configRegions == 'all'): 206 if self.eucalyptus_host: 207 self.regions.append(boto.connect_euca(host=self.eucalyptus_host).region.name) 208 else: 209 for regionInfo in ec2.regions(): 210 if regionInfo.name not in configRegions_exclude: 211 self.regions.append(regionInfo.name) 212 else: 213 self.regions = configRegions.split(",") 214 215 # Destination addresses 216 self.destination_variable = config.get('ec2', 'destination_variable') 217 self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable') 218 219 # Route53 220 self.route53_enabled = config.getboolean('ec2', 'route53') 221 self.route53_excluded_zones = [] 222 if config.has_option('ec2', 'route53_excluded_zones'): 223 self.route53_excluded_zones.extend( 224 config.get('ec2', 'route53_excluded_zones', '').split(',')) 225 226 # Include RDS instances? 227 self.rds_enabled = True 228 if config.has_option('ec2', 'rds'): 229 self.rds_enabled = config.getboolean('ec2', 'rds') 230 231 # Return all EC2 and RDS instances (if RDS is enabled) 232 if config.has_option('ec2', 'all_instances'): 233 self.all_instances = config.getboolean('ec2', 'all_instances') 234 else: 235 self.all_instances = False 236 if config.has_option('ec2', 'all_rds_instances') and self.rds_enabled: 237 self.all_rds_instances = config.getboolean('ec2', 'all_rds_instances') 238 else: 239 self.all_rds_instances = False 240 241 # Cache related 242 cache_dir = os.path.expanduser(config.get('ec2', 'cache_path')) 243 if not os.path.exists(cache_dir): 244 os.makedirs(cache_dir) 245 246 self.cache_path_cache = cache_dir + "/ansible-ec2.cache" 247 self.cache_path_index = cache_dir + "/ansible-ec2.index" 248 self.cache_max_age = config.getint('ec2', 'cache_max_age') 249 250 # Configure nested groups instead of flat namespace. 251 if config.has_option('ec2', 'nested_groups'): 252 self.nested_groups = config.getboolean('ec2', 'nested_groups') 253 else: 254 self.nested_groups = False 255 256 # Do we need to just include hosts that match a pattern? 257 try: 258 pattern_include = config.get('ec2', 'pattern_include') 259 if pattern_include and len(pattern_include) > 0: 260 self.pattern_include = re.compile(pattern_include) 261 else: 262 self.pattern_include = None 263 except ConfigParser.NoOptionError, e: 264 self.pattern_include = None 265 266 # Do we need to exclude hosts that match a pattern? 267 try: 268 pattern_exclude = config.get('ec2', 'pattern_exclude'); 269 if pattern_exclude and len(pattern_exclude) > 0: 270 self.pattern_exclude = re.compile(pattern_exclude) 271 else: 272 self.pattern_exclude = None 273 except ConfigParser.NoOptionError, e: 274 self.pattern_exclude = None 275 276 # Instance filters (see boto and EC2 API docs) 277 self.ec2_instance_filters = defaultdict(list) 278 if config.has_option('ec2', 'instance_filters'): 279 for x in config.get('ec2', 'instance_filters', '').split(','): 280 filter_key, filter_value = x.split('=') 281 self.ec2_instance_filters[filter_key].append(filter_value) 282 283 def parse_cli_args(self): 284 ''' Command line argument processing ''' 285 286 parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2') 287 parser.add_argument('--list', action='store_true', default=True, 288 help='List instances (default: True)') 289 parser.add_argument('--host', action='store', 290 help='Get all the variables about a specific instance') 291 parser.add_argument('--refresh-cache', action='store_true', default=False, 292 help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)') 293 self.args = parser.parse_args() 294 295 296 def do_api_calls_update_cache(self): 297 ''' Do API calls to each region, and save data in cache files ''' 298 299 if self.route53_enabled: 300 self.get_route53_records() 301 302 for region in self.regions: 303 self.get_instances_by_region(region) 304 if self.rds_enabled: 305 self.get_rds_instances_by_region(region) 306 307 self.write_to_cache(self.inventory, self.cache_path_cache) 308 self.write_to_cache(self.index, self.cache_path_index) 309 310 311 def get_instances_by_region(self, region): 312 ''' Makes an AWS EC2 API call to the list of instances in a particular 313 region ''' 314 315 try: 316 if self.eucalyptus: 317 conn = boto.connect_euca(host=self.eucalyptus_host) 318 conn.APIVersion = '2010-08-31' 319 else: 320 conn = ec2.connect_to_region(region) 321 322 # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported 323 if conn is None: 324 print("region name: %s likely not supported, or AWS is down. connection to region failed." % region) 325 sys.exit(1) 326 327 reservations = [] 328 if self.ec2_instance_filters: 329 for filter_key, filter_values in self.ec2_instance_filters.iteritems(): 330 reservations.extend(conn.get_all_instances(filters = { filter_key : filter_values })) 331 else: 332 reservations = conn.get_all_instances() 333 334 for reservation in reservations: 335 for instance in reservation.instances: 336 self.add_instance(instance, region) 337 338 except boto.exception.BotoServerError, e: 339 if not self.eucalyptus: 340 print "Looks like AWS is down again:" 341 print e 342 sys.exit(1) 343 344 def get_rds_instances_by_region(self, region): 345 ''' Makes an AWS API call to the list of RDS instances in a particular 346 region ''' 347 348 try: 349 conn = rds.connect_to_region(region) 350 if conn: 351 instances = conn.get_all_dbinstances() 352 for instance in instances: 353 self.add_rds_instance(instance, region) 354 except boto.exception.BotoServerError, e: 355 if not e.reason == "Forbidden": 356 print "Looks like AWS RDS is down: " 357 print e 358 sys.exit(1) 359 360 def get_instance(self, region, instance_id): 361 ''' Gets details about a specific instance ''' 362 if self.eucalyptus: 363 conn = boto.connect_euca(self.eucalyptus_host) 364 conn.APIVersion = '2010-08-31' 365 else: 366 conn = ec2.connect_to_region(region) 367 368 # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported 369 if conn is None: 370 print("region name: %s likely not supported, or AWS is down. connection to region failed." % region) 371 sys.exit(1) 372 373 reservations = conn.get_all_instances([instance_id]) 374 for reservation in reservations: 375 for instance in reservation.instances: 376 return instance 377 378 def add_instance(self, instance, region): 379 ''' Adds an instance to the inventory and index, as long as it is 380 addressable ''' 381 382 # Only want running instances unless all_instances is True 383 if not self.all_instances and instance.state != 'running': 384 return 385 386 # Select the best destination address 387 if instance.subnet_id: 388 dest = getattr(instance, self.vpc_destination_variable) 389 else: 390 dest = getattr(instance, self.destination_variable) 391 392 if not dest: 393 # Skip instances we cannot address (e.g. private VPC subnet) 394 return 395 396 # if we only want to include hosts that match a pattern, skip those that don't 397 if self.pattern_include and not self.pattern_include.match(dest): 398 return 399 400 # if we need to exclude hosts that match a pattern, skip those 401 if self.pattern_exclude and self.pattern_exclude.match(dest): 402 return 403 404 # Add to index 405 self.index[dest] = [region, instance.id] 406 407 # Inventory: Group by instance ID (always a group of 1) 408 self.inventory[instance.id] = [dest] 409 if self.nested_groups: 410 self.push_group(self.inventory, 'instances', instance.id) 411 412 # Inventory: Group by region 413 if self.nested_groups: 414 self.push_group(self.inventory, 'regions', region) 415 else: 416 self.push(self.inventory, region, dest) 417 418 # Inventory: Group by availability zone 419 self.push(self.inventory, instance.placement, dest) 420 if self.nested_groups: 421 self.push_group(self.inventory, region, instance.placement) 422 423 # Inventory: Group by instance type 424 type_name = self.to_safe('type_' + instance.instance_type) 425 self.push(self.inventory, type_name, dest) 426 if self.nested_groups: 427 self.push_group(self.inventory, 'types', type_name) 428 429 # Inventory: Group by key pair 430 if instance.key_name: 431 key_name = self.to_safe('key_' + instance.key_name) 432 self.push(self.inventory, key_name, dest) 433 if self.nested_groups: 434 self.push_group(self.inventory, 'keys', key_name) 435 436 # Inventory: Group by VPC 437 if instance.vpc_id: 438 self.push(self.inventory, self.to_safe('vpc_id_' + instance.vpc_id), dest) 439 440 # Inventory: Group by security group 441 try: 442 for group in instance.groups: 443 key = self.to_safe("security_group_" + group.name) 444 self.push(self.inventory, key, dest) 445 if self.nested_groups: 446 self.push_group(self.inventory, 'security_groups', key) 447 except AttributeError: 448 print 'Package boto seems a bit older.' 449 print 'Please upgrade boto >= 2.3.0.' 450 sys.exit(1) 451 452 # Inventory: Group by tag keys 453 for k, v in instance.tags.iteritems(): 454 key = self.to_safe("tag_" + k + "=" + v) 455 self.push(self.inventory, key, dest) 456 if self.nested_groups: 457 self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k)) 458 self.push_group(self.inventory, self.to_safe("tag_" + k), key) 459 460 # Inventory: Group by Route53 domain names if enabled 461 if self.route53_enabled: 462 route53_names = self.get_instance_route53_names(instance) 463 for name in route53_names: 464 self.push(self.inventory, name, dest) 465 if self.nested_groups: 466 self.push_group(self.inventory, 'route53', name) 467 468 # Global Tag: instances without tags 469 if len(instance.tags) == 0: 470 self.push(self.inventory, 'tag_none', dest) 471 472 # Global Tag: tag all EC2 instances 473 self.push(self.inventory, 'ec2', dest) 474 475 self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance) 476 477 478 def add_rds_instance(self, instance, region): 479 ''' Adds an RDS instance to the inventory and index, as long as it is 480 addressable ''' 481 482 # Only want available instances unless all_rds_instances is True 483 if not self.all_rds_instances and instance.status != 'available': 484 return 485 486 # Select the best destination address 487 #if instance.subnet_id: 488 #dest = getattr(instance, self.vpc_destination_variable) 489 #else: 490 #dest = getattr(instance, self.destination_variable) 491 dest = instance.endpoint[0] 492 493 if not dest: 494 # Skip instances we cannot address (e.g. private VPC subnet) 495 return 496 497 # Add to index 498 self.index[dest] = [region, instance.id] 499 500 # Inventory: Group by instance ID (always a group of 1) 501 self.inventory[instance.id] = [dest] 502 if self.nested_groups: 503 self.push_group(self.inventory, 'instances', instance.id) 504 505 # Inventory: Group by region 506 if self.nested_groups: 507 self.push_group(self.inventory, 'regions', region) 508 else: 509 self.push(self.inventory, region, dest) 510 511 # Inventory: Group by availability zone 512 self.push(self.inventory, instance.availability_zone, dest) 513 if self.nested_groups: 514 self.push_group(self.inventory, region, instance.availability_zone) 515 516 # Inventory: Group by instance type 517 type_name = self.to_safe('type_' + instance.instance_class) 518 self.push(self.inventory, type_name, dest) 519 if self.nested_groups: 520 self.push_group(self.inventory, 'types', type_name) 521 522 # Inventory: Group by security group 523 try: 524 if instance.security_group: 525 key = self.to_safe("security_group_" + instance.security_group.name) 526 self.push(self.inventory, key, dest) 527 if self.nested_groups: 528 self.push_group(self.inventory, 'security_groups', key) 529 530 except AttributeError: 531 print 'Package boto seems a bit older.' 532 print 'Please upgrade boto >= 2.3.0.' 533 sys.exit(1) 534 535 # Inventory: Group by engine 536 self.push(self.inventory, self.to_safe("rds_" + instance.engine), dest) 537 if self.nested_groups: 538 self.push_group(self.inventory, 'rds_engines', self.to_safe("rds_" + instance.engine)) 539 540 # Inventory: Group by parameter group 541 self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), dest) 542 if self.nested_groups: 543 self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name)) 544 545 # Global Tag: all RDS instances 546 self.push(self.inventory, 'rds', dest) 547 548 self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance) 549 550 551 def get_route53_records(self): 552 ''' Get and store the map of resource records to domain names that 553 point to them. ''' 554 555 r53_conn = route53.Route53Connection() 556 all_zones = r53_conn.get_zones() 557 558 route53_zones = [ zone for zone in all_zones if zone.name[:-1] 559 not in self.route53_excluded_zones ] 560 561 self.route53_records = {} 562 563 for zone in route53_zones: 564 rrsets = r53_conn.get_all_rrsets(zone.id) 565 566 for record_set in rrsets: 567 record_name = record_set.name 568 569 if record_name.endswith('.'): 570 record_name = record_name[:-1] 571 572 for resource in record_set.resource_records: 573 self.route53_records.setdefault(resource, set()) 574 self.route53_records[resource].add(record_name) 575 576 577 def get_instance_route53_names(self, instance): 578 ''' Check if an instance is referenced in the records we have from 579 Route53. If it is, return the list of domain names pointing to said 580 instance. If nothing points to it, return an empty list. ''' 581 582 instance_attributes = [ 'public_dns_name', 'private_dns_name', 583 'ip_address', 'private_ip_address' ] 584 585 name_list = set() 586 587 for attrib in instance_attributes: 588 try: 589 value = getattr(instance, attrib) 590 except AttributeError: 591 continue 592 593 if value in self.route53_records: 594 name_list.update(self.route53_records[value]) 595 596 return list(name_list) 597 598 599 def get_host_info_dict_from_instance(self, instance): 600 instance_vars = {} 601 for key in vars(instance): 602 value = getattr(instance, key) 603 key = self.to_safe('ec2_' + key) 604 605 # Handle complex types 606 # state/previous_state changed to properties in boto in https://github.com/boto/boto/commit/a23c379837f698212252720d2af8dec0325c9518 607 if key == 'ec2__state': 608 instance_vars['ec2_state'] = instance.state or '' 609 instance_vars['ec2_state_code'] = instance.state_code 610 elif key == 'ec2__previous_state': 611 instance_vars['ec2_previous_state'] = instance.previous_state or '' 612 instance_vars['ec2_previous_state_code'] = instance.previous_state_code 613 elif type(value) in [int, bool]: 614 instance_vars[key] = value 615 elif type(value) in [str, unicode]: 616 instance_vars[key] = value.strip() 617 elif type(value) == type(None): 618 instance_vars[key] = '' 619 elif key == 'ec2_region': 620 instance_vars[key] = value.name 621 elif key == 'ec2__placement': 622 instance_vars['ec2_placement'] = value.zone 623 elif key == 'ec2_tags': 624 for k, v in value.iteritems(): 625 key = self.to_safe('ec2_tag_' + k) 626 instance_vars[key] = v 627 elif key == 'ec2_groups': 628 group_ids = [] 629 group_names = [] 630 for group in value: 631 group_ids.append(group.id) 632 group_names.append(group.name) 633 instance_vars["ec2_security_group_ids"] = ','.join([str(i) for i in group_ids]) 634 instance_vars["ec2_security_group_names"] = ','.join([str(i) for i in group_names]) 635 else: 636 pass 637 # TODO Product codes if someone finds them useful 638 #print key 639 #print type(value) 640 #print value 641 642 return instance_vars 643 644 def get_host_info(self): 645 ''' Get variables about a specific host ''' 646 647 if len(self.index) == 0: 648 # Need to load index from cache 649 self.load_index_from_cache() 650 651 if not self.args.host in self.index: 652 # try updating the cache 653 self.do_api_calls_update_cache() 654 if not self.args.host in self.index: 655 # host might not exist anymore 656 return self.json_format_dict({}, True) 657 658 (region, instance_id) = self.index[self.args.host] 659 660 instance = self.get_instance(region, instance_id) 661 return self.json_format_dict(self.get_host_info_dict_from_instance(instance), True) 662 663 def push(self, my_dict, key, element): 664 ''' Push an element onto an array that may not have been defined in 665 the dict ''' 666 group_info = my_dict.setdefault(key, []) 667 if isinstance(group_info, dict): 668 host_list = group_info.setdefault('hosts', []) 669 host_list.append(element) 670 else: 671 group_info.append(element) 672 673 def push_group(self, my_dict, key, element): 674 ''' Push a group as a child of another group. ''' 675 parent_group = my_dict.setdefault(key, {}) 676 if not isinstance(parent_group, dict): 677 parent_group = my_dict[key] = {'hosts': parent_group} 678 child_groups = parent_group.setdefault('children', []) 679 if element not in child_groups: 680 child_groups.append(element) 681 682 def get_inventory_from_cache(self): 683 ''' Reads the inventory from the cache file and returns it as a JSON 684 object ''' 685 686 cache = open(self.cache_path_cache, 'r') 687 json_inventory = cache.read() 688 return json_inventory 689 690 691 def load_index_from_cache(self): 692 ''' Reads the index from the cache file sets self.index ''' 693 694 cache = open(self.cache_path_index, 'r') 695 json_index = cache.read() 696 self.index = json.loads(json_index) 697 698 699 def write_to_cache(self, data, filename): 700 ''' Writes data in JSON format to a file ''' 701 702 json_data = self.json_format_dict(data, True) 703 cache = open(filename, 'w') 704 cache.write(json_data) 705 cache.close() 706 707 708 def to_safe(self, word): 709 ''' Converts 'bad' characters in a string to underscores so they can be 710 used as Ansible groups ''' 711 712 return re.sub("[^A-Za-z0-9\-]", "_", word) 713 714 715 def json_format_dict(self, data, pretty=False): 716 ''' Converts a dict to a JSON object and dumps it as a formatted 717 string ''' 718 719 if pretty: 720 return json.dumps(data, sort_keys=True, indent=2) 721 else: 722 return json.dumps(data) 723 724 725 # Run the script 726 Ec2Inventory() 727