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()