github.com/noirx94/tendermintmp@v0.0.1/networks/remote/ansible/inventory/digital_ocean.py (about) 1 #!/usr/bin/env python 2 3 ''' 4 DigitalOcean external inventory script 5 ====================================== 6 7 Generates Ansible inventory of DigitalOcean Droplets. 8 9 In addition to the --list and --host options used by Ansible, there are options 10 for generating JSON of other DigitalOcean data. This is useful when creating 11 droplets. For example, --regions will return all the DigitalOcean Regions. 12 This information can also be easily found in the cache file, whose default 13 location is /tmp/ansible-digital_ocean.cache). 14 15 The --pretty (-p) option pretty-prints the output for better human readability. 16 17 ---- 18 Although the cache stores all the information received from DigitalOcean, 19 the cache is not used for current droplet information (in --list, --host, 20 --all, and --droplets). This is so that accurate droplet information is always 21 found. You can force this script to use the cache with --force-cache. 22 23 ---- 24 Configuration is read from `digital_ocean.ini`, then from environment variables, 25 then and command-line arguments. 26 27 Most notably, the DigitalOcean API Token must be specified. It can be specified 28 in the INI file or with the following environment variables: 29 export DO_API_TOKEN='abc123' or 30 export DO_API_KEY='abc123' 31 32 Alternatively, it can be passed on the command-line with --api-token. 33 34 If you specify DigitalOcean credentials in the INI file, a handy way to 35 get them into your environment (e.g., to use the digital_ocean module) 36 is to use the output of the --env option with export: 37 export $(digital_ocean.py --env) 38 39 ---- 40 The following groups are generated from --list: 41 - ID (droplet ID) 42 - NAME (droplet NAME) 43 - image_ID 44 - image_NAME 45 - distro_NAME (distribution NAME from image) 46 - region_NAME 47 - size_NAME 48 - status_STATUS 49 50 For each host, the following variables are registered: 51 - do_backup_ids 52 - do_created_at 53 - do_disk 54 - do_features - list 55 - do_id 56 - do_image - object 57 - do_ip_address 58 - do_private_ip_address 59 - do_kernel - object 60 - do_locked 61 - do_memory 62 - do_name 63 - do_networks - object 64 - do_next_backup_window 65 - do_region - object 66 - do_size - object 67 - do_size_slug 68 - do_snapshot_ids - list 69 - do_status 70 - do_tags 71 - do_vcpus 72 - do_volume_ids 73 74 ----- 75 ``` 76 usage: digital_ocean.py [-h] [--list] [--host HOST] [--all] 77 [--droplets] [--regions] [--images] [--sizes] 78 [--ssh-keys] [--domains] [--pretty] 79 [--cache-path CACHE_PATH] 80 [--cache-max_age CACHE_MAX_AGE] 81 [--force-cache] 82 [--refresh-cache] 83 [--api-token API_TOKEN] 84 85 Produce an Ansible Inventory file based on DigitalOcean credentials 86 87 optional arguments: 88 -h, --help show this help message and exit 89 --list List all active Droplets as Ansible inventory 90 (default: True) 91 --host HOST Get all Ansible inventory variables about a specific 92 Droplet 93 --all List all DigitalOcean information as JSON 94 --droplets List Droplets as JSON 95 --regions List Regions as JSON 96 --images List Images as JSON 97 --sizes List Sizes as JSON 98 --ssh-keys List SSH keys as JSON 99 --domains List Domains as JSON 100 --pretty, -p Pretty-print results 101 --cache-path CACHE_PATH 102 Path to the cache files (default: .) 103 --cache-max_age CACHE_MAX_AGE 104 Maximum age of the cached items (default: 0) 105 --force-cache Only use data from the cache 106 --refresh-cache Force refresh of cache by making API requests to 107 DigitalOcean (default: False - use cache files) 108 --api-token API_TOKEN, -a API_TOKEN 109 DigitalOcean API Token 110 ``` 111 112 ''' 113 114 # (c) 2013, Evan Wies <evan@neomantra.net> 115 # 116 # Inspired by the EC2 inventory plugin: 117 # https://github.com/ansible/ansible/blob/devel/contrib/inventory/ec2.py 118 # 119 # This file is part of Ansible, 120 # 121 # Ansible is free software: you can redistribute it and/or modify 122 # it under the terms of the GNU General Public License as published by 123 # the Free Software Foundation, either version 3 of the License, or 124 # (at your option) any later version. 125 # 126 # Ansible is distributed in the hope that it will be useful, 127 # but WITHOUT ANY WARRANTY; without even the implied warranty of 128 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 129 # GNU General Public License for more details. 130 # 131 # You should have received a copy of the GNU General Public License 132 # along with Ansible. If not, see <http://www.gnu.org/licenses/>. 133 134 ###################################################################### 135 136 import os 137 import sys 138 import re 139 import argparse 140 from time import time 141 import ConfigParser 142 import ast 143 144 try: 145 import json 146 except ImportError: 147 import simplejson as json 148 149 try: 150 from dopy.manager import DoManager 151 except ImportError as e: 152 sys.exit("failed=True msg='`dopy` library required for this script'") 153 154 155 class DigitalOceanInventory(object): 156 157 ########################################################################### 158 # Main execution path 159 ########################################################################### 160 161 def __init__(self): 162 ''' Main execution path ''' 163 164 # DigitalOceanInventory data 165 self.data = {} # All DigitalOcean data 166 self.inventory = {} # Ansible Inventory 167 168 # Define defaults 169 self.cache_path = '.' 170 self.cache_max_age = 0 171 self.use_private_network = False 172 self.group_variables = {} 173 174 # Read settings, environment variables, and CLI arguments 175 self.read_settings() 176 self.read_environment() 177 self.read_cli_args() 178 179 # Verify credentials were set 180 if not hasattr(self, 'api_token'): 181 sys.stderr.write('''Could not find values for DigitalOcean api_token. 182 They must be specified via either ini file, command line argument (--api-token), 183 or environment variables (DO_API_TOKEN)\n''') 184 sys.exit(-1) 185 186 # env command, show DigitalOcean credentials 187 if self.args.env: 188 print("DO_API_TOKEN=%s" % self.api_token) 189 sys.exit(0) 190 191 # Manage cache 192 self.cache_filename = self.cache_path + "/ansible-digital_ocean.cache" 193 self.cache_refreshed = False 194 195 if self.is_cache_valid(): 196 self.load_from_cache() 197 if len(self.data) == 0: 198 if self.args.force_cache: 199 sys.stderr.write('''Cache is empty and --force-cache was specified\n''') 200 sys.exit(-1) 201 202 self.manager = DoManager(None, self.api_token, api_version=2) 203 204 # Pick the json_data to print based on the CLI command 205 if self.args.droplets: 206 self.load_from_digital_ocean('droplets') 207 json_data = {'droplets': self.data['droplets']} 208 elif self.args.regions: 209 self.load_from_digital_ocean('regions') 210 json_data = {'regions': self.data['regions']} 211 elif self.args.images: 212 self.load_from_digital_ocean('images') 213 json_data = {'images': self.data['images']} 214 elif self.args.sizes: 215 self.load_from_digital_ocean('sizes') 216 json_data = {'sizes': self.data['sizes']} 217 elif self.args.ssh_keys: 218 self.load_from_digital_ocean('ssh_keys') 219 json_data = {'ssh_keys': self.data['ssh_keys']} 220 elif self.args.domains: 221 self.load_from_digital_ocean('domains') 222 json_data = {'domains': self.data['domains']} 223 elif self.args.all: 224 self.load_from_digital_ocean() 225 json_data = self.data 226 elif self.args.host: 227 json_data = self.load_droplet_variables_for_host() 228 else: # '--list' this is last to make it default 229 self.load_from_digital_ocean('droplets') 230 self.build_inventory() 231 json_data = self.inventory 232 233 if self.cache_refreshed: 234 self.write_to_cache() 235 236 if self.args.pretty: 237 print(json.dumps(json_data, sort_keys=True, indent=2)) 238 else: 239 print(json.dumps(json_data)) 240 # That's all she wrote... 241 242 ########################################################################### 243 # Script configuration 244 ########################################################################### 245 246 def read_settings(self): 247 ''' Reads the settings from the digital_ocean.ini file ''' 248 config = ConfigParser.SafeConfigParser() 249 config.read(os.path.dirname(os.path.realpath(__file__)) + '/digital_ocean.ini') 250 251 # Credentials 252 if config.has_option('digital_ocean', 'api_token'): 253 self.api_token = config.get('digital_ocean', 'api_token') 254 255 # Cache related 256 if config.has_option('digital_ocean', 'cache_path'): 257 self.cache_path = config.get('digital_ocean', 'cache_path') 258 if config.has_option('digital_ocean', 'cache_max_age'): 259 self.cache_max_age = config.getint('digital_ocean', 'cache_max_age') 260 261 # Private IP Address 262 if config.has_option('digital_ocean', 'use_private_network'): 263 self.use_private_network = config.getboolean('digital_ocean', 'use_private_network') 264 265 # Group variables 266 if config.has_option('digital_ocean', 'group_variables'): 267 self.group_variables = ast.literal_eval(config.get('digital_ocean', 'group_variables')) 268 269 def read_environment(self): 270 ''' Reads the settings from environment variables ''' 271 # Setup credentials 272 if os.getenv("DO_API_TOKEN"): 273 self.api_token = os.getenv("DO_API_TOKEN") 274 if os.getenv("DO_API_KEY"): 275 self.api_token = os.getenv("DO_API_KEY") 276 277 def read_cli_args(self): 278 ''' Command line argument processing ''' 279 parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on DigitalOcean credentials') 280 281 parser.add_argument('--list', action='store_true', help='List all active Droplets as Ansible inventory (default: True)') 282 parser.add_argument('--host', action='store', help='Get all Ansible inventory variables about a specific Droplet') 283 284 parser.add_argument('--all', action='store_true', help='List all DigitalOcean information as JSON') 285 parser.add_argument('--droplets', '-d', action='store_true', help='List Droplets as JSON') 286 parser.add_argument('--regions', action='store_true', help='List Regions as JSON') 287 parser.add_argument('--images', action='store_true', help='List Images as JSON') 288 parser.add_argument('--sizes', action='store_true', help='List Sizes as JSON') 289 parser.add_argument('--ssh-keys', action='store_true', help='List SSH keys as JSON') 290 parser.add_argument('--domains', action='store_true', help='List Domains as JSON') 291 292 parser.add_argument('--pretty', '-p', action='store_true', help='Pretty-print results') 293 294 parser.add_argument('--cache-path', action='store', help='Path to the cache files (default: .)') 295 parser.add_argument('--cache-max_age', action='store', help='Maximum age of the cached items (default: 0)') 296 parser.add_argument('--force-cache', action='store_true', default=False, help='Only use data from the cache') 297 parser.add_argument('--refresh-cache', '-r', action='store_true', default=False, 298 help='Force refresh of cache by making API requests to DigitalOcean (default: False - use cache files)') 299 300 parser.add_argument('--env', '-e', action='store_true', help='Display DO_API_TOKEN') 301 parser.add_argument('--api-token', '-a', action='store', help='DigitalOcean API Token') 302 303 self.args = parser.parse_args() 304 305 if self.args.api_token: 306 self.api_token = self.args.api_token 307 308 # Make --list default if none of the other commands are specified 309 if (not self.args.droplets and not self.args.regions and 310 not self.args.images and not self.args.sizes and 311 not self.args.ssh_keys and not self.args.domains and 312 not self.args.all and not self.args.host): 313 self.args.list = True 314 315 ########################################################################### 316 # Data Management 317 ########################################################################### 318 319 def load_from_digital_ocean(self, resource=None): 320 '''Get JSON from DigitalOcean API''' 321 if self.args.force_cache and os.path.isfile(self.cache_filename): 322 return 323 # We always get fresh droplets 324 if self.is_cache_valid() and not (resource == 'droplets' or resource is None): 325 return 326 if self.args.refresh_cache: 327 resource = None 328 329 if resource == 'droplets' or resource is None: 330 self.data['droplets'] = self.manager.all_active_droplets() 331 self.cache_refreshed = True 332 if resource == 'regions' or resource is None: 333 self.data['regions'] = self.manager.all_regions() 334 self.cache_refreshed = True 335 if resource == 'images' or resource is None: 336 self.data['images'] = self.manager.all_images(filter=None) 337 self.cache_refreshed = True 338 if resource == 'sizes' or resource is None: 339 self.data['sizes'] = self.manager.sizes() 340 self.cache_refreshed = True 341 if resource == 'ssh_keys' or resource is None: 342 self.data['ssh_keys'] = self.manager.all_ssh_keys() 343 self.cache_refreshed = True 344 if resource == 'domains' or resource is None: 345 self.data['domains'] = self.manager.all_domains() 346 self.cache_refreshed = True 347 348 def build_inventory(self): 349 '''Build Ansible inventory of droplets''' 350 self.inventory = { 351 'all': { 352 'hosts': [], 353 'vars': self.group_variables 354 }, 355 '_meta': {'hostvars': {}} 356 } 357 358 # add all droplets by id and name 359 for droplet in self.data['droplets']: 360 # when using private_networking, the API reports the private one in "ip_address". 361 if 'private_networking' in droplet['features'] and not self.use_private_network: 362 for net in droplet['networks']['v4']: 363 if net['type'] == 'public': 364 dest = net['ip_address'] 365 else: 366 continue 367 else: 368 dest = droplet['ip_address'] 369 370 self.inventory['all']['hosts'].append(dest) 371 372 self.inventory[droplet['id']] = [dest] 373 self.inventory[droplet['name']] = [dest] 374 375 # groups that are always present 376 for group in ('region_' + droplet['region']['slug'], 377 'image_' + str(droplet['image']['id']), 378 'size_' + droplet['size']['slug'], 379 'distro_' + self.to_safe(droplet['image']['distribution']), 380 'status_' + droplet['status']): 381 if group not in self.inventory: 382 self.inventory[group] = {'hosts': [], 'vars': {}} 383 self.inventory[group]['hosts'].append(dest) 384 385 # groups that are not always present 386 for group in (droplet['image']['slug'], 387 droplet['image']['name']): 388 if group: 389 image = 'image_' + self.to_safe(group) 390 if image not in self.inventory: 391 self.inventory[image] = {'hosts': [], 'vars': {}} 392 self.inventory[image]['hosts'].append(dest) 393 394 if droplet['tags']: 395 for tag in droplet['tags']: 396 if tag not in self.inventory: 397 self.inventory[tag] = {'hosts': [], 'vars': {}} 398 self.inventory[tag]['hosts'].append(dest) 399 400 # hostvars 401 info = self.do_namespace(droplet) 402 self.inventory['_meta']['hostvars'][dest] = info 403 404 def load_droplet_variables_for_host(self): 405 '''Generate a JSON response to a --host call''' 406 host = int(self.args.host) 407 droplet = self.manager.show_droplet(host) 408 info = self.do_namespace(droplet) 409 return {'droplet': info} 410 411 ########################################################################### 412 # Cache Management 413 ########################################################################### 414 415 def is_cache_valid(self): 416 ''' Determines if the cache files have expired, or if it is still valid ''' 417 if os.path.isfile(self.cache_filename): 418 mod_time = os.path.getmtime(self.cache_filename) 419 current_time = time() 420 if (mod_time + self.cache_max_age) > current_time: 421 return True 422 return False 423 424 def load_from_cache(self): 425 ''' Reads the data from the cache file and assigns it to member variables as Python Objects''' 426 try: 427 cache = open(self.cache_filename, 'r') 428 json_data = cache.read() 429 cache.close() 430 data = json.loads(json_data) 431 except IOError: 432 data = {'data': {}, 'inventory': {}} 433 434 self.data = data['data'] 435 self.inventory = data['inventory'] 436 437 def write_to_cache(self): 438 ''' Writes data in JSON format to a file ''' 439 data = {'data': self.data, 'inventory': self.inventory} 440 json_data = json.dumps(data, sort_keys=True, indent=2) 441 442 cache = open(self.cache_filename, 'w') 443 cache.write(json_data) 444 cache.close() 445 446 ########################################################################### 447 # Utilities 448 ########################################################################### 449 450 def push(self, my_dict, key, element): 451 ''' Pushed an element onto an array that may not have been defined in the dict ''' 452 if key in my_dict: 453 my_dict[key].append(element) 454 else: 455 my_dict[key] = [element] 456 457 def to_safe(self, word): 458 ''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups ''' 459 return re.sub("[^A-Za-z0-9\-\.]", "_", word) 460 461 def do_namespace(self, data): 462 ''' Returns a copy of the dictionary with all the keys put in a 'do_' namespace ''' 463 info = {} 464 for k, v in data.items(): 465 info['do_' + k] = v 466 return info 467 468 469 ########################################################################### 470 # Run the script 471 DigitalOceanInventory()