text: # -*- coding: utf-8 -*-
  '''
  :maintainer:    SaltStack
  :maturity:      new
  :platform:      all
  
  Utilities supporting modules for Hashicorp Vault. Configuration instructions are
  documented in the execution module docs.
  '''
  
  from __future__ import absolute_import, print_function, unicode_literals
  import base64
  import logging
  import requests
  
  import salt.crypt
  import salt.exceptions
  import salt.utils.versions
  
  log = logging.getLogger(__name__)
  logging.getLogger("requests").setLevel(logging.WARNING)
  
  
  # Load the __salt__ dunder if not already loaded (when called from utils-module)
  __salt__ = None
  
  
  def __virtual__():  # pylint: disable=expected-2-blank-lines-found-0
      try:
          global __salt__  # pylint: disable=global-statement
          if not __salt__:
              __salt__ = salt.loader.minion_mods(__opts__)
              return True
      except Exception as e:
          log.error("Could not load __salt__: %s", e)
          return False
  
  
  def _get_token_and_url_from_master():
      '''
      Get a token with correct policies for the minion, and the url to the Vault
      service
      '''
      minion_id = __grains__['id']
      pki_dir = __opts__['pki_dir']
  
      # When rendering pillars, the module executes on the master, but the token
      # should be issued for the minion, so that the correct policies are applied
      if __opts__.get('__role', 'minion') == 'minion':
          private_key = '{0}/minion.pem'.format(pki_dir)
          log.debug('Running on minion, signing token request with key %s',
                    private_key)
          signature = base64.b64encode(salt.crypt.sign_message(
              private_key,
              minion_id
          ))
          result = __salt__['publish.runner'](
              'vault.generate_token',
              arg=[minion_id, signature]
          )
      else:
          private_key = '{0}/master.pem'.format(pki_dir)
          log.debug('Running on master, signing token request for %s with key %s',
                    minion_id, private_key)
          signature = base64.b64encode(salt.crypt.sign_message(
              private_key,
              minion_id
          ))
          result = __salt__['saltutil.runner'](
              'vault.generate_token',
              minion_id=minion_id,
              signature=signature,
              impersonated_by_master=True
          )
  
      if not result:
          log.error('Failed to get token from master! No result returned - '
                    'is the peer publish configuration correct?')
          raise salt.exceptions.CommandExecutionError(result)
      if not isinstance(result, dict):
          log.error('Failed to get token from master! '
                    'Response is not a dict: %s', result)
          raise salt.exceptions.CommandExecutionError(result)
      if 'error' in result:
          log.error('Failed to get token from master! '
                    'An error was returned: %s', result['error'])
          raise salt.exceptions.CommandExecutionError(result)
      return {
          'url': result['url'],
          'token': result['token'],
          'verify': result.get('verify', None),
      }
  
  
  def _get_vault_connection():
      '''
      Get the connection details for calling Vault, from local configuration if
      it exists, or from the master otherwise
      '''
      def _use_local_config():
          log.debug('Using Vault connection details from local config')
          try:
              if __opts__['vault']['auth']['method'] == 'approle':
                  verify = __opts__['vault'].get('verify', None)
                  if _selftoken_expired():
                      log.debug('Vault token expired. Recreating one')
                      # Requesting a short ttl token
                      url = '{0}/v1/auth/approle/login'.format(__opts__['vault']['url'])
                      payload = {'role_id': __opts__['vault']['auth']['role_id']}
                      if 'secret_id' in __opts__['vault']['auth']:
                          payload['secret_id'] = __opts__['vault']['auth']['secret_id']
                      response = requests.post(url, json=payload, verify=verify)
                      if response.status_code != 200:
                          errmsg = 'An error occured while getting a token from approle'
                          raise salt.exceptions.CommandExecutionError(errmsg)
                      __opts__['vault']['auth']['token'] = response.json()['auth']['client_token']
              if __opts__['vault']['auth']['method'] == 'wrapped_token':
                  verify = __opts__['vault'].get('verify', None)
                  if _wrapped_token_valid():
                      url = '{0}/v1/sys/wrapping/unwrap'.format(__opts__['vault']['url'])
                      headers = {'X-Vault-Token': __opts__['vault']['auth']['token']}
                      response = requests.post(url, headers=headers, verify=verify)
                      if response.status_code != 200:
                          errmsg = 'An error occured while unwrapping vault token'
                          raise salt.exceptions.CommandExecutionError(errmsg)
                      __opts__['vault']['auth']['token'] = response.json()['auth']['client_token']
              return {
                  'url': __opts__['vault']['url'],
                  'token': __opts__['vault']['auth']['token'],
                  'verify': __opts__['vault'].get('verify', None)
              }
          except KeyError as err:
              errmsg = 'Minion has "vault" config section, but could not find key "{0}" within'.format(err.message)
              raise salt.exceptions.CommandExecutionError(errmsg)
  
      if 'vault' in __opts__ and __opts__.get('__role', 'minion') == 'master':
          return _use_local_config()
      elif any((__opts__['local'], __opts__['file_client'] == 'local', __opts__['master_type'] == 'disable')):
          return _use_local_config()
      else:
          log.debug('Contacting master for Vault connection details')
          return _get_token_and_url_from_master()
  
  
  def make_request(method, resource, token=None, vault_url=None, get_token_url=False, **args):
      '''
      Make a request to Vault
      '''
      if not token or not vault_url:
          connection = _get_vault_connection()
          token, vault_url = connection['token'], connection['url']
          if 'verify' not in args:
              args['verify'] = connection['verify']
  
      url = "{0}/{1}".format(vault_url, resource)
      headers = {'X-Vault-Token': token, 'Content-Type': 'application/json'}
      response = requests.request(method, url, headers=headers, **args)
  
      if get_token_url:
          return response, token, vault_url
      else:
          return response
  
  
  def _selftoken_expired():
      '''
      Validate the current token exists and is still valid
      '''
      try:
          verify = __opts__['vault'].get('verify', None)
          url = '{0}/v1/auth/token/lookup-self'.format(__opts__['vault']['url'])
          if 'token' not in __opts__['vault']['auth']:
              return True
          headers = {'X-Vault-Token': __opts__['vault']['auth']['token']}
          response = requests.get(url, headers=headers, verify=verify)
          if response.status_code != 200:
              return True
          return False
      except Exception as e:
          raise salt.exceptions.CommandExecutionError(
              'Error while looking up self token : {0}'.format(e)
          )
  
  
  def _wrapped_token_valid():
      '''
      Validate the wrapped token exists and is still valid
      '''
      try:
          verify = __opts__['vault'].get('verify', None)
          url = '{0}/v1/sys/wrapping/lookup'.format(__opts__['vault']['url'])
          if 'token' not in __opts__['vault']['auth']:
              return False
          headers = {'X-Vault-Token': __opts__['vault']['auth']['token']}
          response = requests.post(url, headers=headers, verify=verify)
          if response.status_code != 200:
              return False
          return True
      except Exception as e:
          raise salt.exceptions.CommandExecutionError(
              'Error while looking up wrapped token : {0}'.format(e)
          )
  