#!/usr/bin/env python
# -*- mode: python -*-
# Usage: pf-admin-api [--method <method>] <resource>
# Invokes PingFederate admin REST API
# Retries on connection refused so can be used right after the service starts (before it is listening).
# Always prints HTTP status and reason to stderr
# If HTTP status is succesful, prints response to --response file or to stderr if unsuccesful.
#
from __future__ import print_function
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import json
from optparse import OptionParser
import time
import sys
import io
import os

def do_req(retries,method,url,**kwargs):
  """
  do_req: invoke requests.request, handling connection retries.
  """
  timeout = kwargs['timeout'] if 'timeout' in kwargs else 20
  for n in range(retries):
    try:
      response = requests.request(method,url,**kwargs)
      return response
    except requests.exceptions.ConnectionError as e:
      emsg = e
      time.sleep(timeout)

  # only get here if retries is exceeded
  class R:
    status_code = 408
    reason = 'Request Timeout'
    text = json.dumps({'error': 'timed out', 'error_description': 'the API did not respond'})
  return R()


parser = OptionParser(usage='%prog [options] resource')
parser.add_option('-c','--config',
                  type='string',
                  default='<%= @install_dir -%>/local/etc/pf-admin-cfg.json',
                  help='Name of configuration file [default: %default]')
parser.add_option('-m','--method',
                  type='string',
                  default='GET',
                  help='HTTP method, one of GET,PUT,POST,PATCH,DELETE [default: %default]')
parser.add_option('-j','--json',
                  type='string',
                  default=None,
                  help='JSON file to POST')
parser.add_option('-r','--response',
                  type='string',
                  default='-',
                  help='write succesful JSON response to file [default: %default]')
parser.add_option('-i','--id',
                  type='string',
                  default=None,
                  help='write resource id to file [default: %default]')
parser.add_option('-k','--idkey',
                  type='string',
                  default='id',
                  help='resource id primary key [default: %default]')
parser.add_option('--timeout',
                  type='int',
                  default=20,
                  help='Seconds before timeout [default: %default]')
parser.add_option('--retries',
                  type='int',
                  default=5,
                  help='Number of retries [default: %default]')
parser.add_option('--verify',
                  action="store_true",
                  default=False,
                  help='verify SSL/TLS server certificate [default: %default]')
parser.add_option('-s','--subst',
                  type='string',
                  action='append',
                  default=None,
                  help='--subst key=filename. Substitutes the given @@key@@ in the JSON document with the content of the named file. May be repeated to substitute multple keys.')

(opt, arg) = parser.parse_args()

if len(arg) < 1 or opt.method not in ['GET','PUT','POST','PATCH','DELETE']:
  parser.print_usage()
  exit(1)
  
resource = arg[0]

# set up @@key@@ substitutions from "--subst key1=filename1 --subst key2=filename2..."
# these are generally .id files from prior POSTs
substitutions = {}
if opt.subst:
  for arg in opt.subst:
    subs = arg.split('=')
    if len(subs) != 2:
      parser.print_usage()
      exit(1)
    with io.open(subs[1], 'rb') as file:
      substitutions.update({subs[0]: file.read()})

cfile = io.open(opt.config,'rb')
cfg = json.load(cfile)
if 'user' not in cfg or 'pass' not in cfg or 'baseURL' not in cfg:
  print("configuration file %s must contain 'user', 'pass', 'baseURL'"%opt.config,file=sys.stderr)

header = {'X-XSRF-Header': '1'}

if opt.json:
  data = json.load(io.open(opt.json,'rb'))
  dataString = json.dumps(data)
  header.update({'Content-Type': 'application/json'})
else:
  data = dataString = None

if not opt.verify:
  requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  
url = cfg['baseURL']+'/'+resource

# try and make POST idempotent. Either:
# 1) a prior response file must already exist as that's the source of the 'id'.
# 2) the data file contains the 'id'
# if it exists, add the 'id' to the JSON doc and change the POST to a PUT.
if opt.method == 'POST' and opt.response != '-' and data:
  try:
    rfile = io.open(opt.response,'rb')
  except:
    rfile = None
  if opt.idkey in data and data[opt.idkey]:  # for some resources, the ID is provided
    resp = data
  else:  # for others it is generated
    resp = json.load(rfile) if rfile else None
    if rfile:
      rfile.close() 

  if resp and opt.idkey in resp:
    existing = do_req(opt.retries,'GET',
                      url+'/'+resp[opt.idkey],
                      auth=requests.auth.HTTPBasicAuth(cfg['user'],cfg['pass']),
                      headers=header,
                      timeout=opt.timeout,
                      verify=opt.verify)
    if existing:
      data.update({opt.idkey: resp[opt.idkey]})
      opt.method = 'PUT'
      url = url + '/' + resp[opt.idkey]
      print('Resource exists. Changing POST to PUT.',file=sys.stderr)

# do @@key@@ substitutions, if any
if dataString:
  for k in substitutions:
    dataString = dataString.replace('@@'+k+'@@',substitutions[k])

response = do_req(opt.retries,opt.method,
                  url, 
                  auth=requests.auth.HTTPBasicAuth(cfg['user'],cfg['pass']),
                  data=dataString,
                  headers=header,
                  timeout=opt.timeout,
                  verify=opt.verify)
print((response.status_code,response.reason),file=sys.stderr)
try:
  rjson = json.loads(response.text)
except:
  rjson = None
resp = json.dumps(rjson,indent=2) if rjson else 'None' # pretty-print it
if response.status_code < 400:
  output = sys.stdout if opt.response == '-' else open(opt.response,'wb')
  print(resp,file=output)
  if opt.id:
    id_out = open(opt.id,'wb')
    if opt.idkey in rjson:
      print(rjson[opt.idkey],file=id_out,end='')
  exit(0)
else:
  print(resp,file=sys.stderr)
  if opt.json:
    os.rename(opt.json, opt.json+".fail")
  exit(1)
