text: ''' General utilities for percona '''
  import subprocess
  from subprocess import Popen, PIPE
  import socket
  import tempfile
  import os
  import shutil
  import uuid
  
  from charmhelpers.core.host import (
      lsb_release
  )
  from charmhelpers.core.hookenv import (
      charm_dir,
      unit_get,
      relation_ids,
      related_units,
      relation_get,
      relation_set,
      local_unit,
      service_name,
      config,
      log,
      DEBUG,
      INFO,
      WARNING,
      ERROR,
      status_set,
      cached,
  )
  from charmhelpers.fetch import (
      apt_install,
      filter_installed_packages
  )
  from charmhelpers.contrib.network.ip import (
      get_ipv6_addr
  )
  from charmhelpers.contrib.database.mysql import (
      MySQLHelper,
  )
  
  # NOTE: python-mysqldb is installed by charmhelpers.contrib.database.mysql so
  # hence why we import here
  from MySQLdb import (
      OperationalError
  )
  
  KEY = "keys/repo.percona.com"
  REPO = """deb http://repo.percona.com/apt {release} main
  deb-src http://repo.percona.com/apt {release} main"""
  SEEDED_MARKER = "{data_dir}/seeded"
  HOSTS_FILE = '/etc/hosts'
  
  
  def determine_packages():
      if lsb_release()['DISTRIB_CODENAME'] > 'utopic':
          # NOTE(beisner): pxc 5.6 client package is not available
          # in Vivid, install mysql 5.6 client instead per
          # https://launchpad.net/bugs/1476845.
          return [
              'percona-xtradb-cluster-server-5.6',
              'mysql-client-5.6'
          ]
      else:
          return [
              'percona-xtradb-cluster-server-5.5',
              'percona-xtradb-cluster-client-5.5',
          ]
  
  
  def seeded():
      ''' Check whether service unit is already seeded '''
      return os.path.exists(SEEDED_MARKER.format(data_dir=resolve_data_dir()))
  
  
  def mark_seeded():
      ''' Mark service unit as seeded '''
      with open(SEEDED_MARKER.format(data_dir=resolve_data_dir()),
                'w') as seeded:
          seeded.write('done')
  
  
  def setup_percona_repo():
      ''' Configure service unit to use percona repositories '''
      with open('/etc/apt/sources.list.d/percona.list', 'w') as sources:
          sources.write(REPO.format(release=lsb_release()['DISTRIB_CODENAME']))
      subprocess.check_call(['apt-key', 'add', KEY])
  
  
  def get_host_ip(hostname=None):
      try:
          import dns.resolver
      except ImportError:
          apt_install(filter_installed_packages(['python-dnspython']),
                      fatal=True)
          import dns.resolver
  
      if config('prefer-ipv6'):
          # Ensure we have a valid ipv6 address configured
          get_ipv6_addr(exc_list=[config('vip')], fatal=True)[0]
          return socket.gethostname()
  
      hostname = hostname or unit_get('private-address')
      try:
          # Test to see if already an IPv4 address
          socket.inet_aton(hostname)
          return hostname
      except socket.error:
          # This may throw an NXDOMAIN exception; in which case
          # things are badly broken so just let it kill the hook
          answers = dns.resolver.query(hostname, 'A')
          if answers:
              return answers[0].address
  
  
  def is_sufficient_peers():
      """If min-cluster-size has been provided, check that we have sufficient
      number of peers to proceed with bootstrapping percona cluster.
      """
      min_size = config('min-cluster-size')
      if min_size:
          size = 0
          for rid in relation_ids('cluster'):
              size = len(related_units(rid))
  
          # Include this unit
          size += 1
          if min_size > size:
              log("Insufficient number of units to configure percona cluster "
                  "(expected=%s, got=%s)" % (min_size, size), level=INFO)
              return False
          else:
              log("Sufficient units available to configure percona cluster "
                  "(>=%s)" % (min_size), level=DEBUG)
  
      return True
  
  
  def get_cluster_hosts():
      hosts_map = {}
      hostname = get_host_ip()
      hosts = [hostname]
      # We need to add this localhost dns name to /etc/hosts along with peer
      # hosts to ensure percona gets consistently resolved addresses.
      if config('prefer-ipv6'):
          addr = get_ipv6_addr(exc_list=[config('vip')], fatal=True)[0]
          hosts_map = {addr: hostname}
  
      for relid in relation_ids('cluster'):
          for unit in related_units(relid):
              rdata = relation_get(unit=unit, rid=relid)
              private_address = rdata.get('private-address')
              if config('prefer-ipv6'):
                  hostname = rdata.get('hostname')
                  if not hostname or hostname in hosts:
                      log("(unit=%s) Ignoring hostname '%s' provided by cluster "
                          "relation for addr %s" %
                          (unit, hostname, private_address), level=DEBUG)
                      continue
                  else:
                      log("(unit=%s) hostname '%s' provided by cluster relation "
                          "for addr %s" % (unit, hostname, private_address),
                          level=DEBUG)
  
                  hosts_map[private_address] = hostname
                  hosts.append(hostname)
              else:
                  hosts.append(get_host_ip(private_address))
  
      if hosts_map:
          update_hosts_file(hosts_map)
  
      return hosts
  
  
  SQL_SST_USER_SETUP = ("GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.* "
                        "TO 'sstuser'@'localhost' IDENTIFIED BY '{}'")
  
  SQL_SST_USER_SETUP_IPV6 = ("GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT "
                             "ON *.* TO 'sstuser'@'ip6-localhost' IDENTIFIED "
                             "BY '{}'")
  
  SQL_SET_ROOTPW = ("UPDATE mysql.user SET Password=PASSWORD('{}') WHERE User='root'")
  
  def get_db_helper():
      return MySQLHelper(rpasswdf_template='/var/lib/charm/%s/mysql.passwd' %
                         (service_name()),
                         upasswdf_template='/var/lib/charm/%s/mysql-{}.passwd' %
                         (service_name()))
  
  
  def configure_sstuser(sst_password):
      m_helper = get_db_helper()
      m_helper.connect(password=m_helper.get_mysql_root_password())
      m_helper.execute(SQL_SST_USER_SETUP.format(sst_password))
      m_helper.execute(SQL_SST_USER_SETUP_IPV6.format(sst_password))
  
  def configure_rootpw(password):
      m_helper = get_db_helper()
      m_helper.connect()
      m_helper.execute(SQL_SET_ROOTPW.format(password))
      m_helper.execute("FLUSH PRIVILEGES")
  
  
  # TODO: mysql charmhelper
  def configure_mysql_root_password(password):
      ''' Configure debconf with root password '''
      dconf = Popen(['debconf-set-selections'], stdin=PIPE)
      # Set both percona and mysql password options to cover
      # both upstream and distro packages.
      packages = ["percona-server-server", "mysql-server"]
      m_helper = get_db_helper()
      root_pass = m_helper.get_mysql_root_password(password)
      for package in packages:
          dconf.stdin.write("%s %s/root_password password %s\n" %
                            (package, package, root_pass))
          dconf.stdin.write("%s %s/root_password_again password %s\n" %
                            (package, package, root_pass))
      dconf.communicate()
      dconf.wait()
  
  
  # TODO: Submit for charmhelper
  def relation_clear(r_id=None):
      ''' Clears any relation data already set on relation r_id '''
      settings = relation_get(rid=r_id,
                              unit=local_unit())
      for setting in settings:
          if setting not in ['public-address', 'private-address']:
              settings[setting] = None
      relation_set(relation_id=r_id,
                   **settings)
  
  
  def update_hosts_file(map):
      """Percona does not currently like ipv6 addresses so we need to use dns
      names instead. In order to make them resolvable we ensure they are  in
      /etc/hosts.
  
      See https://bugs.launchpad.net/galera/+bug/1130595 for some more info.
      """
      with open(HOSTS_FILE, 'r') as hosts:
          lines = hosts.readlines()
  
      log("Updating %s with: %s (current: %s)" % (HOSTS_FILE, map, lines),
          level=DEBUG)
  
      newlines = []
      for ip, hostname in map.items():
          if not ip or not hostname:
              continue
  
          keepers = []
          for line in lines:
              _line = line.split()
              if len(line) < 2 or not (_line[0] == ip or hostname in _line[1:]):
                  keepers.append(line)
              else:
                  log("Marking line '%s' for update or removal" % (line.strip()),
                      level=DEBUG)
  
          lines = keepers
          newlines.append("%s %s\n" % (ip, hostname))
  
      lines += newlines
  
      with tempfile.NamedTemporaryFile(delete=False) as tmpfile:
          with open(tmpfile.name, 'w') as hosts:
              for line in lines:
                  hosts.write(line)
  
      os.rename(tmpfile.name, HOSTS_FILE)
      os.chmod(HOSTS_FILE, 0o644)
  
  
  def assert_charm_supports_ipv6():
      """Check whether we are able to support charms ipv6."""
      if lsb_release()['DISTRIB_CODENAME'].lower() < "trusty":
          raise Exception("IPv6 is not supported in the charms for Ubuntu "
                          "versions less than Trusty 14.04")
  
  
  def unit_sorted(units):
      """Return a sorted list of unit names."""
      return sorted(
          units, lambda a, b: cmp(int(a.split('/')[-1]), int(b.split('/')[-1])))
  
  
  def install_mysql_ocf():
      dest_dir = '/usr/lib/ocf/resource.d/percona/'
      for fname in ['ocf/percona/mysql_monitor']:
          src_file = os.path.join(charm_dir(), fname)
          if not os.path.isdir(dest_dir):
              os.makedirs(dest_dir)
  
          dest_file = os.path.join(dest_dir, os.path.basename(src_file))
          if not os.path.exists(dest_file):
              log('Installing %s' % dest_file, level='INFO')
              shutil.copy(src_file, dest_file)
          else:
              log("'%s' already exists, skipping" % dest_file, level='INFO')
  
  
  def get_wsrep_value(key):
      m_helper = get_db_helper()
      try:
          m_helper.connect(password=m_helper.get_mysql_root_password())
      except OperationalError:
          log("Could not connect to db", DEBUG)
          return None
  
      cursor = m_helper.connection.cursor()
      ret = None
      try:
          cursor.execute("show status like '%s'" % (key))
          ret = cursor.fetchall()
      except:
          log("Failed to get '%s'", ERROR)
          return None
      finally:
          cursor.close()
  
      if ret:
          return ret[0][1]
  
      return None
  
  
  def is_bootstrapped():
      if not is_sufficient_peers():
          return False
  
      uuids = []
      rids = relation_ids('cluster') or []
      for rid in rids:
          units = related_units(rid)
          units.append(local_unit())
          for unit in units:
              id = relation_get('bootstrap-uuid', unit=unit, rid=rid)
              if id:
                  uuids.append(id)
  
      if uuids:
          if len(set(uuids)) > 1:
              log("Found inconsistent bootstrap uuids - %s" % (uuids), WARNING)
  
          return True
  
      return False
  
  
  def notify_bootstrapped(cluster_rid=None, cluster_uuid=None):
      if cluster_rid:
          rids = [cluster_rid]
      else:
          rids = relation_ids('cluster')
          if not rids:
              log("No relation ids found for 'cluster'", level=INFO)
              return
  
      if not cluster_uuid:
          cluster_uuid = get_wsrep_value('wsrep_cluster_state_uuid')
          if not cluster_uuid:
              cluster_uuid = str(uuid.uuid4())
              log("Could not determine cluster uuid so using '%s' instead" %
                  (cluster_uuid), INFO)
  
      log("Notifying peers that percona is bootstrapped (uuid=%s)" %
          (cluster_uuid), DEBUG)
      for rid in rids:
          relation_set(relation_id=rid, **{'bootstrap-uuid': cluster_uuid})
  
  
  def cluster_in_sync():
      '''
      Determines whether the current unit is in sync
      with the rest of the cluster
      '''
      ready = get_wsrep_value('wsrep_ready') or False
      sync_status = get_wsrep_value('wsrep_local_state') or 0
      if ready and int(sync_status) in [2, 4]:
          return True
      return False
  
  
  def assess_status():
      '''Assess the status of the current unit'''
      min_size = config('min-cluster-size')
      # Ensure that number of peers > cluster size configuration
      if not is_sufficient_peers():
          status_set('blocked', 'Insufficient peers to bootstrap cluster')
          return
  
      if min_size and int(min_size) > 1:
          # Once running, ensure that cluster is in sync
          # and has the required peers
          if not is_bootstrapped():
              status_set('waiting', 'Unit waiting for cluster bootstrap')
          elif is_bootstrapped() and cluster_in_sync():
              status_set('active', 'Unit is ready and clustered')
          else:
              status_set('blocked', 'Unit is not in sync')
      else:
          status_set('active', 'Unit is ready')
  
  
  @cached
  def resolve_data_dir():
      if lsb_release()['DISTRIB_CODENAME'] < 'vivid':
          return '/var/lib/mysql'
      else:
          return '/var/lib/percona-xtradb-cluster'
  
  
  @cached
  def resolve_cnf_file():
      if lsb_release()['DISTRIB_CODENAME'] < 'vivid':
          return '/etc/mysql/my.cnf'
      else:
          return '/etc/mysql/percona-xtradb-cluster.conf.d/mysqld.cnf'
  