text: #!/usr/bin/env python3
  
  # Copyright 2018 Canonical Ltd.
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  """Module for interacting with juju."""
  import os
  from pathlib import Path
  import yaml
  
  from zaza import (
      model,
      controller,
  )
  from zaza.openstack.utilities import generic as generic_utils
  
  
  def get_application_status(application=None, unit=None, model_name=None):
      """Return the juju status for an application.
  
      :param application: Application name
      :type application: string
      :param unit: Specific unit
      :type unit: string
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: Juju status output for an application
      :rtype: dict
      """
      status = get_full_juju_status()
  
      if unit and not application:
          application = unit.split("/")[0]
  
      if application:
          status = status.applications.get(application)
      if unit:
          status = status.get("units").get(unit)
      return status
  
  
  def get_application_ip(application):
      """Get the application's IP address.
  
      :param application: Application name
      :type application: str
      :returns: Application's IP address
      :rtype: str
      """
      try:
          app_config = model.get_application_config(application)
      except KeyError:
          return ''
      vip = app_config.get("vip").get("value")
      if vip:
          ip = vip
      else:
          unit = model.get_units(application)[0]
          ip = unit.public_address
      return ip
  
  
  def get_cloud_configs(cloud=None):
      """Get cloud configuration from local clouds.yaml.
  
      libjuju does not yet have cloud information implemented.
      Use libjuju as soon as possible.
  
      :param cloud: Name of specific cloud
      :type remote_cmd: string
      :returns: Dictionary of cloud configuration
      :rtype: dict
      """
      home = str(Path.home())
      cloud_config = os.path.join(home, ".local", "share", "juju", "clouds.yaml")
      if cloud:
          return generic_utils.get_yaml_config(cloud_config)["clouds"].get(cloud)
      else:
          return generic_utils.get_yaml_config(cloud_config)
  
  
  def get_full_juju_status(model_name=None):
      """Return the full juju status output.
  
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: Full juju status output
      :rtype: dict
      """
      status = model.get_status(model_name=model_name)
      return status
  
  
  def get_machines_for_application(application, model_name=None):
      """Return machines for a given application.
  
      :param application: Application name
      :type application: string
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: machines for an application
      :rtype: Iterator[str]
      """
      status = get_application_status(application, model_name=model_name)
      if not status:
          return
  
      # libjuju juju status no longer has units for subordinate charms
      # Use the application it is subordinate-to to find machines
      if not status.get("units") and status.get("subordinate-to"):
          status = get_application_status(status.get("subordinate-to")[0],
                                          model_name=model_name)
  
      for unit in status.get("units").keys():
          yield status.get("units").get(unit).get("machine")
  
  
  def get_unit_name_from_host_name(host_name, application, model_name=None):
      """Return the juju unit name corresponding to a hostname.
  
      :param host_name: Host name to map to unit name.
      :type host_name: string
      :param application: Application name
      :type application: string
      :param model_name: Name of model to query.
      :type model_name: str
      """
      # Assume that a juju managed hostname always ends in the machine number and
      # remove the domain name if it present.
      machine_number = host_name.split('-')[-1].split('.')[0]
      unit_names = [
          u.entity_id
          for u in model.get_units(application_name=application,
                                   model_name=model_name)
          if int(u.data['machine-id']) == int(machine_number)]
      return unit_names[0]
  
  
  def get_machine_status(machine, key=None, model_name=None):
      """Return the juju status for a machine.
  
      :param machine: Machine number
      :type machine: string
      :param key: Key option requested
      :type key: string
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: Juju status output for a machine
      :rtype: dict
      """
      status = get_full_juju_status(model_name=model_name)
      if "lxd" in machine:
          host = machine.split('/')[0]
          status = status.machines.get(host)['containers'][machine]
      else:
          status = status.machines.get(machine)
      if key:
          status = status.get(key)
      return status
  
  
  def get_machine_series(machine, model_name=None):
      """Return the juju series for a machine.
  
      :param machine: Machine number
      :type machine: string
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: Juju series
      :rtype: string
      """
      return get_machine_status(
          machine=machine,
          key='series',
          model_name=model_name
      )
  
  
  def get_machine_uuids_for_application(application, model_name=None):
      """Return machine uuids for a given application.
  
      :param application: Application name
      :type application: string
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: machine uuuids for an application
      :rtype: Iterator[str]
      """
      for machine in get_machines_for_application(application,
                                                  model_name=model_name):
          yield get_machine_status(machine, key="instance-id",
                                   model_name=model_name)
  
  
  def get_provider_type():
      """Get the type of the undercloud.
  
      :returns: Name of the undercloud type
      :rtype: string
      """
      cloud = controller.get_cloud()
      if cloud:
          # If the controller was deployed from this system with
          # the cloud configured in ~/.local/share/juju/clouds.yaml
          # Determine the cloud type directly
          return get_cloud_configs(cloud)["type"]
      else:
          # If the controller was deployed elsewhere
          # For now assume openstack
          return "openstack"
  
  
  def remote_run(unit, remote_cmd, timeout=None, fatal=None, model_name=None):
      """Run command on unit and return the output.
  
      NOTE: This function is pre-deprecated. As soon as libjuju unit.run is able
      to return output this functionality should move to model.run_on_unit.
  
      :param remote_cmd: Command to execute on unit
      :type remote_cmd: string
      :param timeout: Timeout value for the command
      :type arg: int
      :param fatal: Command failure condidered fatal or not
      :type fatal: boolean
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: Juju run output
      :rtype: string
      :raises: model.CommandRunFailed
      """
      if fatal is None:
          fatal = True
      result = model.run_on_unit(
          unit,
          remote_cmd,
          timeout=timeout,
          model_name=model_name)
      if result:
          if int(result.get("Code")) == 0:
              return result.get("Stdout")
          else:
              if fatal:
                  raise model.CommandRunFailed(remote_cmd, result)
              return result.get("Stderr")
  
  
  def _get_unit_names(names, model_name=None):
      """Resolve given application names to first unit name of said application.
  
      Helper function that resolves application names to first unit name of
      said application.  Any already resolved unit names are returned as-is.
  
      :param names: List of units/applications to translate
      :type names: list(str)
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: List of units
      :rtype: list(str)
      """
      result = []
      for name in names:
          if '/' in name:
              result.append(name)
          else:
              result.append(model.get_first_unit_name(
                  name,
                  model_name=model_name))
      return result
  
  
  def get_relation_from_unit(entity, remote_entity, remote_interface_name,
                             model_name=None):
      """Get relation data passed between two units.
  
      Get relation data for relation with `remote_interface_name` between
      `entity` and `remote_entity` from the perspective of `entity`.
  
      `entity` and `remote_entity` may refer to either a application or a
      specific unit. If application name is given first unit is found in model.
  
      :param entity: Application or unit to get relation data from
      :type entity: str
      :param remote_entity: Application or Unit in the other end of the relation
                            we want to query
      :type remote_entity: str
      :param remote_interface_name: Name of interface to query on remote end of
                                    relation
      :type remote_interface_name: str
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: dict with relation data
      :rtype: dict
      :raises: model.CommandRunFailed
      """
      application = entity.split('/')[0]
      remote_application = remote_entity.split('/')[0]
      rid = model.get_relation_id(application, remote_application,
                                  remote_interface_name=remote_interface_name,
                                  model_name=model_name)
      (unit, remote_unit) = _get_unit_names(
          [entity, remote_entity],
          model_name=model_name)
      cmd = 'relation-get --format=yaml -r "{}" - "{}"' .format(rid, remote_unit)
      result = model.run_on_unit(unit, cmd, model_name=model_name)
      if result and int(result.get('Code')) == 0:
          return yaml.safe_load(result.get('Stdout'))
      else:
          raise model.CommandRunFailed(cmd, result)
  
  
  def leader_get(application, key='', model_name=None):
      """Get leader settings from leader unit of named application.
  
      :param application: Application to get leader settings from.
      :type application: str
      :param key: Key option requested
      :type key: string
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: dict with leader settings
      :rtype: dict
      :raises: model.CommandRunFailed
      """
      cmd = 'leader-get --format=yaml {}'.format(key)
      result = model.run_on_leader(application, cmd, model_name=model_name)
      if result and int(result.get('Code')) == 0:
          return yaml.safe_load(result.get('Stdout'))
      else:
          raise model.CommandRunFailed(cmd, result)
  
  
  def get_subordinate_units(unit_list, charm_name=None, status=None,
                            model_name=None):
      """Get a list of all subordinate units associated with units in unit_list.
  
      Get a list of all subordinate units associated with units in unit_list.
      Subordinate can be filtered by using 'charm_name' which will only return
      subordinate units which have 'charm_name' in the name of the charm e.g.
  
          get_subordinate_units(
              ['cinder/1']) would return ['cinder-hacluster/1',
                                          'cinder-ceph/2'])
      where as
  
          get_subordinate_units(
              ['cinder/1'], charm_name='hac') would return ['cinder-hacluster/1']
  
      NOTE: The charm_name match is against the name of the charm not the
            application name.
  
      :param charm_name: List of unit names
      :type unit_list: []
      :param charm_name: charm_name to match against, can be a sub-string.
      :type charm_name: str
      :param status: Juju status to query against,
      :type status: juju.client._definitions.FullStatus
      :param model_name: Name of model to query.
      :type model_name: str
      :returns: List of matching unit names.
      :rtype: []
      """
      if not status:
          status = model.get_status(model_name=model_name)
      sub_units = []
      for unit_name in unit_list:
          app_name = unit_name.split('/')[0]
          subs = status.applications[app_name]['units'][unit_name].get(
              'subordinates') or {}
          if charm_name:
              for unit_name, unit_data in subs.items():
                  if charm_name in unit_data['charm']:
                      sub_units.append(unit_name)
          else:
              sub_units.extend([n for n in subs.keys()])
      return sub_units
  