text: # -*- coding: utf-8 -*-
  #
  # Copyright (c) 2017, F5 Networks Inc.
  # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
  
  from __future__ import absolute_import, division, print_function
  __metaclass__ = type
  
  
  import os
  
  try:
      from StringIO import StringIO
  except ImportError:
      from io import StringIO
  
  try:
      from BytesIO import BytesIO
  except ImportError:
      from io import BytesIO
  
  from ansible.module_utils.urls import urlparse
  from ansible.module_utils.urls import generic_urlparse
  from ansible.module_utils.urls import Request
  
  try:
      import json as _json
  except ImportError:
      import simplejson as _json
  
  try:
      from library.module_utils.network.f5.common import F5ModuleError
  except ImportError:
      from ansible.module_utils.network.f5.common import F5ModuleError
  
  
  """An F5 REST API URI handler.
  
  Use this module to make calls to an F5 REST server. It is influenced by the same
  API that the Python ``requests`` tool uses, but the two are not the same, as the
  library here is **much** more simple and targeted specifically to F5's needs.
  
  The ``requests`` design was chosen due to familiarity with the tool. Internally,
  the classes contained herein use Ansible native libraries.
  
  The means by which you should use it are similar to ``requests`` basic usage.
  
  Authentication is not handled for you automatically by this library, however it *is*
  handled automatically for you in the supporting F5 module_utils code; specifically the
  different product module_util files (bigip.py, bigiq.py, etc).
  
  Internal (non-module) usage of this library looks like this.
  
  ```
  # Create a session instance
  mgmt = iControlRestSession()
  mgmt.verify = False
  
  server = '1.1.1.1'
  port = 443
  
  # Payload used for getting an initial authentication token
  payload = {
    'username': 'admin',
    'password': 'secret',
    'loginProviderName': 'tmos'
  }
  
  # Create URL to call, injecting server and port
  url = f"https://{server}:{port}/mgmt/shared/authn/login"
  
  # Call the API
  resp = session.post(url, json=payload)
  
  # View the response
  print(resp.json())
  
  # Update the session with the authentication token
  session.headers['X-F5-Auth-Token'] = resp.json()['token']['token']
  
  # Create another URL to call, injecting server and port
  url = f"https://{server}:{port}/mgmt/tm/ltm/virtual/~Common~virtual1"
  
  # Call the API
  resp = session.get(url)
  
  # View the details of a virtual payload
  print(resp.json())
  ```
  """
  
  from ansible.module_utils.six.moves.urllib.error import HTTPError
  
  
  class Response(object):
      def __init__(self):
          self._content = None
          self.status = None
          self.headers = dict()
          self.url = None
          self.reason = None
          self.request = None
          self.msg = None
  
      @property
      def content(self):
          return self._content
  
      @property
      def raw_content(self):
          return self._content
  
      def json(self):
          return _json.loads(self._content or 'null')
  
      @property
      def ok(self):
          if self.status is not None and int(self.status) > 400:
              return False
          try:
              response = self.json()
              if 'code' in response and response['code'] > 400:
                  return False
          except ValueError:
              pass
          return True
  
  
  class iControlRestSession(object):
      """Represents a session that communicates with a BigIP.
  
      This acts as a loose wrapper around Ansible's ``Request`` class. We're doing
      this as interim work until we move to the httpapi connector.
      """
      def __init__(self, headers=None, use_proxy=True, force=False, timeout=120,
                   validate_certs=True, url_username=None, url_password=None,
                   http_agent=None, force_basic_auth=False, follow_redirects='urllib2',
                   client_cert=None, client_key=None, cookies=None):
          self.request = Request(
              headers=headers,
              use_proxy=use_proxy,
              force=force,
              timeout=timeout,
              validate_certs=validate_certs,
              url_username=url_username,
              url_password=url_password,
              http_agent=http_agent,
              force_basic_auth=force_basic_auth,
              follow_redirects=follow_redirects,
              client_cert=client_cert,
              client_key=client_key,
              cookies=cookies
          )
          self.last_url = None
  
      def get_headers(self, result):
          try:
              return dict(result.getheaders())
          except AttributeError:
              return result.headers
  
      def update_response(self, response, result):
          response.headers = self.get_headers(result)
          response._content = result.read()
          response.status = result.getcode()
          response.url = result.geturl()
          response.msg = "OK (%s bytes)" % response.headers.get('Content-Length', 'unknown')
  
      def send(self, method, url, **kwargs):
          response = Response()
  
          # Set the last_url called
          #
          # This is used by the object destructor to erase the token when the
          # ModuleManager exits and destroys the iControlRestSession object
          self.last_url = url
  
          body = None
          data = kwargs.pop('data', None)
          json = kwargs.pop('json', None)
  
          if not data and json is not None:
              self.request.headers['Content-Type'] = 'application/json'
              body = _json.dumps(json)
              if not isinstance(body, bytes):
                  body = body.encode('utf-8')
          if data:
              body = data
          if body:
              kwargs['data'] = body
  
          try:
              result = self.request.open(method, url, **kwargs)
          except HTTPError as e:
              # Catch HTTPError delivered from Ansible
              #
              # The structure of this object, in Ansible 2.8 is
              #
              # HttpError {
              #   args
              #   characters_written
              #   close
              #   code
              #   delete
              #   errno
              #   file
              #   filename
              #   filename2
              #   fp
              #   getcode
              #   geturl
              #   hdrs
              #   headers
              #   info
              #   msg
              #   name
              #   reason
              #   strerror
              #   url
              #   with_traceback
              # }
              self.update_response(response, e)
              return response
  
          self.update_response(response, result)
          return response
  
      def delete(self, url, **kwargs):
          return self.send('DELETE', url, **kwargs)
  
      def get(self, url, **kwargs):
          return self.send('GET', url, **kwargs)
  
      def patch(self, url, data=None, **kwargs):
          return self.send('PATCH', url, data=data, **kwargs)
  
      def post(self, url, data=None, **kwargs):
          return self.send('POST', url, data=data, **kwargs)
  
      def put(self, url, data=None, **kwargs):
          return self.send('PUT', url, data=data, **kwargs)
  
      def __del__(self):
          if self.last_url is None:
              return
          token = self.request.headers.get('X-F5-Auth-Token', None)
          if not token:
              return
          try:
              p = generic_urlparse(urlparse(self.last_url))
              uri = "https://{0}:{1}/mgmt/shared/authz/tokens/{2}".format(
                  p['hostname'], p['port'], token
              )
              self.delete(uri)
          except ValueError:
              pass
  
  
  class TransactionContextManager(object):
      def __init__(self, client, validate_only=False):
          self.client = client
          self.validate_only = validate_only
          self.transid = None
  
      def __enter__(self):
          uri = "https://{0}:{1}/mgmt/tm/transaction/".format(
              self.client.provider['server'],
              self.client.provider['server_port']
          )
          resp = self.client.api.post(uri, json={})
          if resp.status not in [200]:
              raise Exception
          try:
              response = resp.json()
          except ValueError as ex:
              raise F5ModuleError(str(ex))
  
          self.transid = response['transId']
          self.client.api.request.headers['X-F5-REST-Coordination-Id'] = self.transid
          return self.client
  
      def __exit__(self, exc_type, exc_value, exc_tb):
          self.client.api.request.headers.pop('X-F5-REST-Coordination-Id')
          if exc_tb is None:
              uri = "https://{0}:{1}/mgmt/tm/transaction/{2}".format(
                  self.client.provider['server'],
                  self.client.provider['server_port'],
                  self.transid
              )
              params = dict(
                  state="VALIDATING",
                  validateOnly=self.validate_only
              )
              resp = self.client.api.patch(uri, json=params)
              if resp.status not in [200]:
                  raise Exception
  
  
  def download_file(client, url, dest):
      """Download a file from the remote device
  
      This method handles the chunking needed to download a file from
      a given URL on the BIG-IP.
  
      Arguments:
          client (object): The F5RestClient connection object.
          url (string): The URL to download.
          dest (string): The location on (Ansible controller) disk to store the file.
  
      Returns:
          bool: True on success. False otherwise.
      """
      with open(dest, 'wb') as fileobj:
          chunk_size = 512 * 1024
          start = 0
          end = chunk_size - 1
          size = 0
          current_bytes = 0
  
          while True:
              content_range = "%s-%s/%s" % (start, end, size)
              headers = {
                  'Content-Range': content_range,
                  'Content-Type': 'application/octet-stream'
              }
              data = {
                  'headers': headers,
                  'verify': False,
                  'stream': False
              }
              response = client.api.get(url, headers=headers, json=data)
              if response.status == 200:
                  # If the size is zero, then this is the first time through
                  # the loop and we don't want to write data because we
                  # haven't yet figured out the total size of the file.
                  if size > 0:
                      current_bytes += chunk_size
                      fileobj.write(response.raw_content)
              # Once we've downloaded the entire file, we can break out of
              # the loop
              if end == size:
                  break
              crange = response.headers['Content-Range']
              # Determine the total number of bytes to read.
              if size == 0:
                  size = int(crange.split('/')[-1]) - 1
                  # If the file is smaller than the chunk_size, the BigIP
                  # will return an HTTP 400. Adjust the chunk_size down to
                  # the total file size...
                  if chunk_size > size:
                      end = size
                  # ...and pass on the rest of the code.
                  continue
              start += chunk_size
              if (current_bytes + chunk_size) > size:
                  end = size
              else:
                  end = start + chunk_size - 1
      return True
  
  
  def upload_file(client, url, src, dest=None):
      """Upload a file to an arbitrary URL.
  
      This method is responsible for correctly chunking an upload request to an
      arbitrary file worker URL.
  
      Arguments:
          client (object): The F5RestClient connection object.
          url (string): The URL to upload a file to.
          src (string): The file to be uploaded.
          dest (string): The file name to create on the remote device.
  
      Examples:
          The ``dest`` may be either an absolute or relative path. The basename
          of the path is used as the remote file name upon upload. For instance,
          in the example below, ``BIGIP-13.1.0.8-0.0.3.iso`` would be the name
          of the remote file.
  
          The specified URL should be the full URL to where you want to upload a
          file. BIG-IP has many different URLs that can be used to handle different
          types of files. This is why a full URL is required.
  
          >>> from ansible.module_utils.network.f5.icontrol import upload_client
          >>> url = 'https://{0}:{1}/mgmt/cm/autodeploy/software-image-uploads'.format(
          ...   self.client.provider['server'],
          ...   self.client.provider['server_port']
          ... )
          >>> dest = '/path/to/BIGIP-13.1.0.8-0.0.3.iso'
          >>> upload_file(self.client, url, dest)
          True
  
      Returns:
          bool: True on success. False otherwise.
  
      Raises:
          F5ModuleError: Raised if ``retries`` limit is exceeded.
      """
      if isinstance(src, StringIO) or isinstance(src, BytesIO):
          fileobj = src
      else:
          fileobj = open(src, 'rb')
  
      try:
          size = os.stat(src).st_size
          is_file = True
      except TypeError:
          src.seek(0, os.SEEK_END)
          size = src.tell()
          src.seek(0)
          is_file = False
  
      # This appears to be the largest chunk size that iControlREST can handle.
      #
      # The trade-off you are making by choosing a chunk size is speed, over size of
      # transmission. A lower chunk size will be slower because a smaller amount of
      # data is read from disk and sent via HTTP. Lots of disk reads are slower and
      # There is overhead in sending the request to the BIG-IP.
      #
      # Larger chunk sizes are faster because more data is read from disk in one
      # go, and therefore more data is transmitted to the BIG-IP in one HTTP request.
      #
      # If you are transmitting over a slow link though, it may be more reliable to
      # transmit many small chunks that fewer large chunks. It will clearly take
      # longer, but it may be more robust.
      chunk_size = 1024 * 7168
      start = 0
      retries = 0
      if dest is None and is_file:
          basename = os.path.basename(src)
      else:
          basename = dest
      url = '{0}/{1}'.format(url.rstrip('/'), basename)
  
      while True:
          if retries == 3:
              # Retries are used here to allow the REST API to recover if you kill
              # an upload mid-transfer.
              #
              # There exists a case where retrying a new upload will result in the
              # API returning the POSTed payload (in bytes) with a non-200 response
              # code.
              #
              # Retrying (after seeking back to 0) seems to resolve this problem.
              raise F5ModuleError(
                  "Failed to upload file too many times."
              )
          try:
              file_slice = fileobj.read(chunk_size)
              if not file_slice:
                  break
  
              current_bytes = len(file_slice)
              if current_bytes < chunk_size:
                  end = size
              else:
                  end = start + current_bytes
              headers = {
                  'Content-Range': '%s-%s/%s' % (start, end - 1, size),
                  'Content-Type': 'application/octet-stream'
              }
  
              # Data should always be sent using the ``data`` keyword and not the
              # ``json`` keyword. This allows bytes to be sent (such as in the case
              # of uploading ISO files.
              response = client.api.post(url, headers=headers, data=file_slice)
  
              if response.status != 200:
                  # When this fails, the output is usually the body of whatever you
                  # POSTed. This is almost always unreadable because it is a series
                  # of bytes.
                  #
                  # Therefore, including an empty exception here.
                  raise F5ModuleError()
              start += current_bytes
          except F5ModuleError:
              # You must seek back to the beginning of the file upon exception.
              #
              # If this is not done, then you risk uploading a partial file.
              fileobj.seek(0)
              retries += 1
      return True
  
  
  def tmos_version(client):
      uri = "https://{0}:{1}/mgmt/tm/sys/".format(
          client.provider['server'],
          client.provider['server_port'],
      )
      resp = client.api.get(uri)
  
      try:
          response = resp.json()
      except ValueError as ex:
          raise F5ModuleError(str(ex))
  
      if 'code' in response and response['code'] in [400, 403]:
          if 'message' in response:
              raise F5ModuleError(response['message'])
          else:
              raise F5ModuleError(resp.content)
  
      to_parse = urlparse(response['selfLink'])
      query = to_parse.query
      version = query.split('=')[1]
      return version
  
  
  def bigiq_version(client):
      uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-shared-all-big-iqs/devices".format(
          client.provider['server'],
          client.provider['server_port'],
      )
      query = "?$select=version"
  
      resp = client.api.get(uri + query)
  
      try:
          response = resp.json()
      except ValueError as ex:
          raise F5ModuleError(str(ex))
  
      if 'code' in response and response['code'] in [400, 403]:
          if 'message' in response:
              raise F5ModuleError(response['message'])
          else:
              raise F5ModuleError(resp.content)
  
      if 'items' in response:
          version = response['items'][0]['version']
          return version
  
      raise F5ModuleError(
          'Failed to retrieve BIGIQ version information.'
      )
  
  
  def module_provisioned(client, module_name):
      provisioned = modules_provisioned(client)
      if module_name in provisioned:
          return True
      return False
  
  
  def modules_provisioned(client):
      """Returns a list of all provisioned modules
  
      Args:
          client: Client connection to the BIG-IP
  
      Returns:
          A list of provisioned modules in their short name for.
          For example, ['afm', 'asm', 'ltm']
      """
      uri = "https://{0}:{1}/mgmt/tm/sys/provision".format(
          client.provider['server'],
          client.provider['server_port']
      )
      resp = client.api.get(uri)
  
      try:
          response = resp.json()
      except ValueError as ex:
          raise F5ModuleError(str(ex))
  
      if 'code' in response and response['code'] in [400, 403]:
          if 'message' in response:
              raise F5ModuleError(response['message'])
          else:
              raise F5ModuleError(resp.content)
      if 'items' not in response:
          return []
      return [x['name'] for x in response['items'] if x['level'] != 'none']
  