#!/usr/bin/python
# Copyright (C) 2015 Eric Jackson <ejackson@suse.com>

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, see
# <http://www.gnu.org/licenses/>.


import glob, uuid
import argparse
import rados, rbd, json
import sys, tempfile, os
from subprocess import call, Popen, PIPE
import re, socket
import pprint
import os.path
from os.path import basename
import netifaces
from collections import OrderedDict
import logging


# CONFIGFS target path
TARGET="/sys/kernel/config/target"

def popen(cmd):
    """
    Execute a command, print both stdout and stderr, and exit unless 
    successful.
 
        cmd - an array of strings of the command
    """
    print " ".join(cmd)
    proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
    for line in proc.stdout:
        print line.rstrip('\n')
    for line in proc.stderr:
        print line.rstrip('\n')
    proc.wait()
    if (proc.returncode != 0):
        exit(proc.returncode)
    if (logging.getLogger().level <= logging.INFO):
        print ""

def strip_comments(text):
    """
    Remove all entries beginning with # to end of line

        text - a string
    """
    return(re.sub(re.compile("#.*?\n" ) ,"" ,text))
        
def lstrip_spaces(text):
    """
    Remove 12 spaces 

        text - a string
    """
    return(re.sub(re.compile("^ {12}", re.MULTILINE), "", text))

def check_keys(keys, data, description):
    """
    Verify that keys are present in data

        keys - an array of strings
        data - a dict
        description - a string
    """
    for key in keys:
        if not key in data:
            raise ValueError("Missing attribute '{}' in {}".format(key, description))

def compare_settings(keys, current, config):
    """
    Verify that values are identical

        keys - an array of strings
        current - a dict
        config - a dict, possible superset of current
    """
    for key in keys:
        if (current[key] != config[key]):
            return(False)
    return(True)

def iqn(entry):
    """
    Return the target iqn if exists, otherwise default to the first iqn listed
    in the targets section, which is host specific.  

        entry - a dictionary, typically an image entry
    """
    if 'target' in entry:
        return(entry['target'])
    else:
        return(Common.config['iqns'][0])

def addresses():
    """
    Return a list of all ip addresses 
    """
    adds = []
    for interface in netifaces.interfaces():
        addrs = netifaces.ifaddresses(interface)
        try:
            for entry in addrs[netifaces.AF_INET]:
                adds.append(entry['addr'])
            for entry in addrs[netifaces.AF_INET6]:
                # Strip interface
                adds.append(re.split("%", entry['addr'])[0])
        except KeyError, e:
            # skip downed interfaces
            pass
    return(adds)

def uniq(cmds):
    """
    Remove redundant entries from list of lists
    """
    u = {}
    c = []
    for cmd in cmds:
        u[" ".join(cmd)] = ''
    for k in u.keys():
        c.append(k.split())
    return(sorted(c))

def find_auth(key):
    """
    Search for the matching host or target and return the authentication value

        key - string, host or target
    """
    for entry in Common.config['auth']:
        if ('host' in entry and entry['host'] == key):
            return(entry['authentication'])
        if ('target' in entry and entry['target'] == key):
            return(entry['authentication'])
    logging.warning("{} not found in auth".format(key))
    return("")
            
class Common:
    """
    Sharing common static configurations. 
    """
    config = OrderedDict()

class Runtime:
    """
    Sharing common runtime state.
    """
    config = {}

    @staticmethod
    def tpg(target, entry):
        return(Runtime.config['portals'][target][entry['image']][entry['portal']] 
                    if 'portal' in entry else 1)

class Content:
    """
    Contains operations for reading, editing and saving the configuration to
    Ceph. 
    """

    def __init__(self):
        """
        The variable self.current holds the JSON structure of the existing 
        configuration.
        """
        self.current = {}
 
    def edit(self, editor):
        """
        Edit the global configuration in a text editor.  Submitted changes
        are validated.  Errors are displayed after an edit session allowing
        a user to start another edit session or interrupt the program.

            editor - specify another editor, defaults to vim
        """
        self.current = Common.config
        EDITOR = editor if editor else os.environ.get('EDITOR', 
                                                   '/usr/bin/vim')
        if (not self.current['auth'] and 
            not self.current['targets'] and
            not self.current['pools']):
            initial_message = self.instructions()
        else:
            initial_message = json.dumps(self.current, indent=2)

        with tempfile.NamedTemporaryFile(suffix=".tmp") as tmpfile:
            tmpfile.write(initial_message)
            tmpfile.flush()

            valid = False
            while (not valid):
                call([EDITOR, tmpfile.name])
                submitted = strip_comments(open(tmpfile.name).read())
                valid = (self.validate(submitted) and 
                         self.verify_mandatory_keys(submitted))
                if (not valid):
                    try:
                        raw_input("Press enter to edit or Ctrl-C to quit ")
                    except KeyboardInterrupt, e:
                        raise SystemExit("\nBye")
            self.submitted = json.loads(submitted, object_pairs_hook=OrderedDict)

    def instructions(self):
        """
        Initial instructions when no configuration exists
        """
        return(lstrip_spaces("""#
            #
            # lrbd stores an iSCSI configuration in Ceph and applies the 
            # configuration to a host acting as a gateway between Ceph and an 
            # initiator (i.e. iSCSI client)
            #
            # Since no configuration exists, the simplest example is provided 
            # below.  Replace 'rbd', 'igw1', 'archive' and 
            # 'iqn.1996-04.de.suse:01:abcdefghijkl' with your pool, host, 
            # rbd image name and initator iqn.
            #
            # Alternatively, check the samples/ subdirectory.  Select the most 
            # suitable configuration and customize.  Apply your configuration 
            # with 'lrbd -f <filename>'.  For additional options, run 'lrbd -h'
            #
              {
                "pools": [
                  { "pool": "rbd",
                    "gateways": [
                      { "host": "igw1",
                        "tpg": [
                          { "image": "archive",
                            "initiator": "iqn.1996-04.de.suse:01:abcdefghijkl" 
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
             

            #\n"""))

    def read(self, file):
        """
        The counterpart to using an editor, this method reads a file directly
        and runs the same validation.

            file - a text configuration file
        """
        if not os.path.isfile(file):
            raise IOError("file '{}' does not exist".format(file))
        text = strip_comments(open(file).read())
        if (self.validate(text) and self.verify_mandatory_keys(text)):
               self.submitted = json.loads(text, object_pairs_hook=OrderedDict)
        else:
            raise RuntimeError("file {} failed validation".format(file))
       

    def validate(self, text):
        """
        JSON format is finicky about trailing commas and such.  Print
        the errors to stdout.

            text - a string of the entire configuration
        """
        try:
          content = json.loads(text)
        except ValueError, e:
          logging.error(e)
          return False
        return True

    def verify_mandatory_keys(self, text):
        """
        Checks for dictionary keys related to the global data structure.

            text - a string of the entire configuration
        """
        content = json.loads(text)
        if not 'pools' in content:
            raise ValueError("Mandatory key 'pools' is missing")
        if not content['pools']:
            raise ValueError("pools have no entries")
        if not 'gateways' in content['pools'][0]:
            raise ValueError("Mandatory key 'gateways' is missing")
        if not content['pools'][0]['gateways']:
            raise ValueError("gateways have no entries")
        if not ('host' in content['pools'][0]['gateways'][0] or
            'target' in content['pools'][0]['gateways'][0]):
            raise ValueError("Mandatory key 'host' or 'target' is missing")
        if not 'tpg' in content['pools'][0]['gateways'][0]:
            raise ValueError("Mandatory key 'tpg' is missing")

        # Authentication section is optional, but keys are required when present
        if 'auth' in content:
            for entry in content['auth']:
                if not ('host' in entry or 'target' in entry):
                    raise ValueError("Mandatory key 'host' or 'target' is missing from auth")
        return(True)


    def save(self):
        """
        Write the configuration to Ceph.  Remove any entries that were deleted
        from the submission.  Data is subdivided for simpler host retrieval.

        Stores the following attributes:
            targets - static iqn for each gateway host.  Stored on each 
                      configuration object in every pool if it exists.  
            portals - named groups of network addresses
            _<host> - authentication information for gateway host
            _<target> - authentication information for target
            <host>  - pool information for gateway host
            <target>  - pool information for redundant target
        """
        if self.submitted != self.current:
            logging.debug("Saving...")
            conn = Cluster()
            with conn as cluster:
                self.attr = Attributes(cluster) 
                self._remove_absent_entry()
                self._remove_absent_auth()

                        
                for pool in self.submitted['pools']: 
                    if 'gateways' in pool:
                        for gateway in pool['gateways']:
                            self._write_host(pool, gateway)
                            self._write_target(pool, gateway)
                    if 'auth' in self.submitted:
                        self._write_auth(pool)
                    if 'targets' in self.submitted:
                        self._write_targets(pool)
                    if 'portals' in self.submitted:
                        self._write_portals(pool)


    def _remove_absent_entry(self):
        """
        Remove host or target entries that have been deleted from the 
        submitted configuration
        """
        logging.debug("Removing deleted entries")
        hosts = {}
        if ('pools' in self.current and self.current['pools']):
            for pool in self.current['pools']: 
                hosts[pool['pool']] = []
                # Add current gateways
                if 'gateways' in pool:
                    for gateway in pool['gateways']:
                        if 'host' in gateway:
                            hosts[pool['pool']].append(gateway['host'])
                        if 'target' in gateway:
                            hosts[pool['pool']].append(gateway['target'])
            for pool in self.submitted['pools']: 
                # Subtract submitted gateways, skip new entries
                if 'gateways' in pool:
                    for gateway in pool['gateways']:
                        if ('host' in gateway and
                            gateway['host'] in hosts[pool['pool']]):
                            hosts[pool['pool']].remove(gateway['host'])
                        if ('target' in gateway and 
                            gateway['target'] in hosts[pool['pool']]):
                            hosts[pool['pool']].remove(gateway['target'])
                # Remove difference
                for host in hosts[pool['pool']]:
                    self.attr.remove(str(pool['pool']), str(host))
                    logging.debug("Removing host {} from pool {}".format(host, pool))

    def _remove_absent_auth(self):
        """
        Remove auth section that has been deleted from the submitted 
        configuration
        """
        if ('auth' in self.current and self.current['auth'] 
            and not 'auth' in self.submitted):
            for pool in self.submitted['pools']: 
                self.attr.remove_auth(str(pool['pool']))

        if ('auth' in self.current and self.current['auth'] 
            and 'auth' in self.submitted and self.submitted['auth']):
            for old in self.current['auth']:
                found = False
                for key in [ 'host', 'target']:
                    if key in old:
                        for new in self.submitted['auth']:
                            if key in new:
                                if old[key] == new[key]:
                                    found = True
                        if not found:
                            for pool in self.submitted['pools']: 
                                logging.debug("removing {} from {}".format(old[key], pool['pool']))
                                self.attr.remove_auth(str(pool['pool']), old[key])
                    

    def _write_host(self, pool, gateway):
        """
        Write a host entry
        """
        if 'host' in gateway:
            self.attr.write(str(pool['pool']), 
                str(gateway['host']), json.dumps(gateway))

    def _write_target(self, pool, gateway):
        """
        Write a target entry
        """
        if 'target' in gateway:
            self.attr.write(str(pool['pool']), 
                str(gateway['target']), json.dumps(gateway))

    def _write_auth(self, pool):
        """
        Write authentication entry for host or target 
        """
        for entry in self.submitted['auth']: 
            if 'host' in entry:
                self.attr.write(str(pool['pool']), 
                    str('_' + entry['host']), json.dumps(entry))
            elif 'target' in entry:
                self.attr.write(str(pool['pool']), 
                    str('_' + entry['target']), json.dumps(entry))
            else:
                raise ValueError("auth entry must contain either 'host' or 'target'")

    def _write_targets(self, pool):
        """
        Write targets section
        """
        self.attr.write(str(pool['pool']), 
            str('targets'), json.dumps(self.submitted['targets']))

    def _write_portals(self, pool):
        """
        Write portals section
        """
        self.attr.write(str(pool['pool']), 
            str('portals'), json.dumps(self.submitted['portals']))

class Cluster:
    """
    Support 'with' for Rados connections
    """

    def __init__(self):
        """
        Capture pool name
        """
        self.cluster = None

    def __enter__(self):
        """
        Connect to Ceph, return connection
        """
        self.cluster = rados.Rados(conffile=Common.ceph_conf)
        try:
            self.cluster.connect()
        except rados.ObjectNotFound, e:
            raise IOError("check for missing keyring")
        return(self.cluster)

    def __exit__(self, exc_ty, exc_val, tb):
        """
        Close connection
        """
        self.cluster.shutdown()

class Ioctx:
    """
    Support 'with' for pool connections
    """

    def __init__(self, cluster, pool):
        """
        Capture pool name
        """
        self.cluster = cluster
        self.ioctx = None
        self.pool = pool

    def __enter__(self):
        """
        Connect to Ceph, open pool, return connection
        """
        try:
            self.ioctx = self.cluster.open_ioctx(self.pool)
        except rados.ObjectNotFound, e:
            raise RuntimeError("pool '{}' does not exist".format(self.pool))
        return(self.ioctx)

    def __exit__(self, exc_ty, exc_val, tb):
        """
        Close pool
        """
        self.ioctx.close()

class Attributes:
    """
    Methods for updating and removing extended attributes within Ceph.
    """

    def __init__(self, cluster):
        self.cluster = cluster
        pass

    def write(self, pool, key, attrs):
        """
        Write an empty object and set an extended attribute

            pool - a string, name of Ceph pool
            key - a string, name of gateway host or target
            attrs - a string, json format
        """
        conn = Ioctx(self.cluster, pool)
        with conn as ioctx:
            ioctx.write_full(Common.config_name, "")
            ioctx.set_xattr(Common.config_name, key, attrs)
            logging.debug("Writing {} to pool {}".format(key, pool))

    def remove(self, pool, attr):
        """
        Remove a specified attribute.  This is necessary when a host has
        been removed from the list of gateways
  
            pool - a string, name of Ceph pool
            attr - a string, name of gateway host
        """
        conn = Ioctx(self.cluster, pool)
        with conn as ioctx:
            ioctx.rm_xattr(Common.config_name, attr)
            logging.debug("Removing {} from pool {}".format(attr, pool))

    def remove_auth(self, pool, host=""):
        """
        Remove authentication attributes for a pool

            pool - a string, name of Ceph pool
            host - a string, specific host to remove. Empty string matches all.
        """
        conn = Ioctx(self.cluster, pool)
        with conn as ioctx:
            for key, value in ioctx.get_xattrs(Common.config_name):
                if (not host and key[0] == "_") or (key == ("_" + host)):
                    ioctx.rm_xattr(Common.config_name, key)
                    logging.debug("Removing {} from pool {}".format(key, pool))


class Pools:
    """
    Manages the entire structure of pools, gateways, tpg and initiators.  
    All hosts are included.
    """

    def __init__(self):
        """
        A list of pools.  Data structure is label : value throughout. 
        """
        self.pools = []

    def add(self, item):
        """
        Creates another pool entry
        
            item - dict (e.g. "pool": "swimming")
        """
        self.pools.append(OrderedDict())
        self.pools[-1]['pool'] = item

    def append(self, key, item):
        """
        Adds another JSON structure to 'key' in the same named pool above.

            key - a string such as "gateways"
            item - JSON structure of host, tpg and portals
        """
        if not key in self.pools[-1]:
            self.pools[-1][key] = []
        self.pools[-1][key].append(item)

    def display(self):
        """
        Useful for debugging
        """
        pprint.pprint(self.pools)

class PortalSection:
    """
    Manages the portal section of the extended attributes (i.e. all data stored 
    under portals).
    """

    def __init__(self):
        """
        List of portals, entries are name and addresses
        """
        self.portals = []

    def add(self, item):
        """
        Add entire structure, identical copies are stored in each pool so 
        only one is needed.
        """
        if not self.portals and item:
            self.portals.extend(item)

    def purge(self, portals):
        for entry in self.portals:
            if not entry['name'] in portals:
                self.portals.remove(entry)

    def display(self):
        """
        Useful for debugging
        """
        pprint.pprint(self.portals)

class Targets:
    """
    Manages the target section of the extended attributes (i.e. all data stored 
    under targets).
    """

    def __init__(self):
        """
        List of targets, entries are either host and iqn or hosts and iqn
        """
        self.targets = []

    def add(self, item):
        """
        Add entire structure, identical copies are stored in each pool so 
        only one is needed.
        """
        if not self.targets and item:
            self.targets.extend(item)

    def display(self):
        """
        Useful for debugging
        """
        pprint.pprint(self.targets)
     
    def list(self):
        """
        Return only iqn values filtered by hostname
        """
        targets = []
        for entry in self.targets:
            if not 'target' in entry:
                raise RuntimeError("Missing keyword target from entry in targets section.")
            if 'hosts' in entry:
                for hentry in entry['hosts']:
                    if (Common.hostname == hentry['host']):
                        targets.append(entry['target'])
        return(targets)

    def portals(self):
        """
        Return only portal values filtered by hostname
        Return all portal values 
        """
        portals = []
        for entry in self.targets:
            if 'hosts' in entry:
                for hentry in entry['hosts']:
                    portals.append(hentry['portal'])
        return(portals)

    def purge(self):
        """
        Remove all entries that do not match or contain hostname
        """
        for entry in self.targets:
            if 'host' in entry:
                if (Common.hostname != entry['host']):
                       self.targets.remove(entry)
            if 'hosts' in entry:
                found = False
                for hentry in entry['hosts']:
                    if Common.hostname == hentry['host']:
                        found = True
                if not found:
                    self.targets.remove(entry)

class Authentications:
    """
    Manages the authentication section under the extended attribute auth.
    This section is optional, but relates to gateways and targets 
    independently.  Authentication can be none, tpg (common credentials),
    tpg+identified (common credentials, known initiators) or
    acls (host specific credentials).
    """

    def __init__(self):
        """
        List of authentications.  Absent and present but disabled are 
        permitted.
        """
        self.authentications = []

    def add(self, item):
        """
        Add entire structure, identical copies are stored in each pool so 
        only one is needed.
        """
        if not self._exists(item):
            self.authentications.append(item)

    def _exists(self, item):
        """
        helper function for above since "item in list" didn't work for
        list of lists
        """
        present = False
        for entry in self.authentications:
            for attr in [ 'host', 'target' ]:
                if attr in item and attr in entry:
                    if (item[attr] == entry[attr]):
                        present = True
                        break
        return(present)

    def purge(self):
        for entry in self.authentications:
            if 'host' in entry:
                if (Common.hostname != entry['host']):
                    self.authentications.remove(entry)


    def display(self):
        """
        Useful for debugging
        """
        pprint.pprint(self.authentications)


class Configs:
    """
    Read the configuration from Ceph for both global and host only 
    configurations.  Merges pools, targets and authentications into
    larger structures.  Assigns to Common.* for sharing with other
    classes.
    """

    def __init__(self, config_name, ceph_conf, hostname):
        """
        Set initial overrides and assign to Common configuration

            config_name - a string for the name of the configuration object
                          in Ceph
            ceph_conf - an alternative Ceph configuration file
            hostname - specify an alternative gateway host
        """
        self.config_name = config_name if config_name else "lrbd.conf"
        self.ceph_conf = ceph_conf if ceph_conf else "/etc/ceph/ceph.conf"


        if not os.path.isfile(self.ceph_conf):
            raise IOError("{} does not exist".format(self.ceph_conf))

        self.hostname = hostname if hostname else socket.gethostname()

        Common.config_name = self.config_name
        Common.ceph_conf = self.ceph_conf
        Common.hostname = self.hostname

    def retrieve(self, filter=None):
        """
        Scan all configuration objects and build a structure containing
        all gateway hosts.  Merge pools, auth, portals and targets into 
        Common.config 
        """
        conn = Cluster()
        with conn as cluster:
            p = Pools()
            pt = PortalSection()
            t = Targets()
            a = Authentications()

            portals = []
            for pool in cluster.list_pools():
                pool_id = cluster.pool_lookup(pool)
                tier_id = cluster.get_pool_base_tier(pool_id)
                if (pool_id != tier_id):
                    logging.info("Skipping tier cache {}".format(pool))
                    continue
                conn = Ioctx(cluster, pool)
                with conn as ioctx:

                    if self._config_missing(ioctx, self.config_name, pool):
                        continue
                    p.add(pool)
                    t.add(self._get_optional(ioctx, self.config_name, 'targets'))
                    pt.add(self._get_optional(ioctx, self.config_name, 'portals'))
                    
                    if filter:
                        targets = t.list()
                        for portal in t.portals():
                            portals.append(portal)
                        

                    attrs = ioctx.get_xattrs(self.config_name)
                    for key, value in attrs:
                        if key == "targets" or key == "portals":
                            continue
                        elif key[0] == "_":
                            a.add(json.loads(value, 
                                object_pairs_hook=OrderedDict))
                        else:
                            if filter:
                                if (key in targets or key == self.hostname):
                                    content = json.loads(value,
                                        object_pairs_hook=OrderedDict)
                                    p.append('gateways', content)
                                    for entry in content['tpg']:
                                        if 'portal' in entry:
                                            portals.append(entry['portal'])
                            else:
                                p.append('gateways', json.loads(value, 
                                    object_pairs_hook=OrderedDict))

        if filter:
            t.purge()
            a.purge()
            pt.purge(portals)

        Common.config['auth'] = a.authentications
        Common.config['targets'] = t.targets
        Common.config['portals'] = pt.portals
        Common.config['pools'] = p.pools

    def _config_missing(self, ioctx, config_name, pool):
        """
        Check for configuration object

            ioctx - existing pool connection
            config_name - name of the configuration object
            pool - name of pool
        """
        try:
            ioctx.stat(config_name) # Check for object
        except rados.ObjectNotFound, e:
            # No configuration for pool, skipping
            logging.info("No configuration object {} in pool {}".format(self.config_name, pool))
            return(True)
        return(False)

    def _get_optional(self, ioctx, config_name, attr):
        """
        Load value of specified attribute, may not exist

            ioctx - existing pool connection
            config_name - name of the configuration object
            attr - key desired (e.g. 'targets' or 'portals')
        """
        try:
            return(json.loads(ioctx.get_xattr(config_name, attr), object_pairs_hook=OrderedDict))
        except rados.NoData, e:
            pass

    def display(self):
        """
        JSON dump of structure to user.  Keys are sorted which makes the 
        format obnoxious when reviewing.  TODO: custom JSON output with
        keys sorted by significance.
        """
        print json.dumps(Common.config, indent=4)

    def wipe(self):
        """
        Remove configuration objects from all pools
        """
        cluster = rados.Rados(conffile=self.ceph_conf)

        cluster.connect()
        pools = cluster.list_pools()
        for pool in pools:
            ioctx = cluster.open_ioctx(pool)
            try:
                ioctx.remove_object(self.config_name)
                logging.debug("Removing {} from pool {}".format(self.config_name, pool))
            except rados.ObjectNotFound, e:
                logging.info("No object {} to remove from pool {}".format(self.config_name, pool))
            ioctx.close
        cluster.shutdown()

    def clear(self):
        """
        Reset any targetcli configuration.  

        Note: the clearconfig option is missing from the current targetcli
        which would remove the additional dependencies
        """
        cmds = [ [ "/usr/sbin/tcm_fabric", "--unloadall" ],
                 [ "/usr/sbin/lio_node", "--unload" ],
                 [ "/usr/sbin/tcm_node", "--unload" ] ]
        for cmd in cmds:
            popen(cmd)


##########################################################################
# Ideal spot for separating into another file.  All classes and functions
# below change the host system.
##########################################################################
def entries():
    """
    Generator yielding pool, gateway and tpg entries
    """
    for pentry in Common.config['pools']:
        if 'gateways' in pentry:
            for gentry in pentry['gateways']:
                for entry in gentry['tpg']:
                    yield (pentry, gentry, entry)

class Images:
    """
    Manages mapping and unmapping RBD images
    """

    def __init__(self):
        """
        Parse and store 'rbd showmapped'
        """
        self.mounts = {}
        proc = Popen(["rbd", "showmapped"], stdout=PIPE, stderr=PIPE)
        for line in proc.stdout:
                results = re.split(r'\s+', line)
                if (results[0] == 'id'):
                    continue
                self.mounts[ ":".join([ results[1], results[2] ]) ] = results[4]  

    def map(self):
        """
        Create the commands to map each rbd device
        """
        self.map_cmds = []
        
        for pentry, gentry, entry in entries():
           if ":".join([ pentry['pool'], entry['image'] ]) in self.mounts.keys():
               continue
           self.map_cmds.append([ "rbd", "-p", pentry['pool'], "map", entry['image'] ])

        for cmd in self.map_cmds:
            popen(cmd)

    def unmap(self):
        """
        Unmount all rbd images
        """
        for mount in self.mounts.keys():
            popen([ "rbd", "unmap", self.mounts[mount]])


class Backstores:
    """
    Creates the necessary backstores via targetcli for each RBD image.
    """

    def __init__(self, backstore):
        """
        Set selected backstore, load modules for rbd, create command
        """
        self.cmds = []
        if backstore == None:
            self._detect()
        else:
            self.selected = backstore

        # Added to python-rtslib 104105
        #if (self.selected == "rbd"):
        #    self._load_modules()
        self._cmd()
        Runtime.config['backstore'] = self.selected
            

    def _detect(self):
        """
        Check for existing backstores and set selected, otherwise default
        All images will be either iblock or rbd.  Last checked wins.
        """
        for pentry, gentry, entry in entries():
            existing = glob.glob(TARGET + "/core/*/{}".format(entry['image']))
            if existing:
                self.selected = re.split("[/_]", existing[0])[6]

        if not hasattr(self, 'selected'):
            # default
            self.selected = "rbd"

    def _cmd(self):
        """
        Generate the backstore commands, skip existing.
        """
        for pentry, gentry, entry in entries():
           
            cmd = [ "targetcli", "/backstores/{}".format(self.selected), 
                    "create", "name={}".format(entry['image']), 
                    "dev=/dev/rbd/{}/{}".format(pentry['pool'], 
                    entry['image']) ]
            backstore = glob.glob(TARGET + "/core/{}_*/{}".format(self.selected, entry['image']))
            if not backstore: 
                self.cmds.append(cmd)


    def _load_modules(self):
        """
        Same kernel modules as targetcli + target_core_rbd 
        """
        modules = [ "vhost_scsi", "iscsi_target_mod", "tcm_loop", "tcm_fc",
                    "ib_srpt", "tcm_qla2xxx", "target_core_rbd" ]
        for module in modules:
            if not os.path.isdir("/sys/module/{}".format(module)):
                popen([ "modprobe", module ])

    def create(self):
        """
        Execute saved commands
        """
        for cmd in uniq(self.cmds):
            popen(cmd)
        self._enable_rbd()

    def _enable_rbd(self):
        """
        An image in an rbd backstore must be enabled prior to lun creation
        """

        for pentry, gentry, entry in entries():
            files = glob.glob(TARGET + "/core/rbd_*/{}/enable".format(entry['image']))
            for file in files:
                enabled = open(file).read().rstrip('\n')
                if (enabled == "0"):
                    with open(file, "w") as enable:
                        enable.write("1")
                        logging.debug("Enabling {}".format(file))
                

class Iscsi:
    """
    Creates iscsi entries with provided static target iqns or dynamically
    generates one if none are provided.
    """

    def __init__(self):
        """
        Find all target entries in targets.  Append to cmds all that do not 
        exist.  If no targets are provided, set cmds to a single base command.
        """
        self.cmds = []
        self.iqns = []
        for entry in Common.config['targets']:
            # Keep the host entry at the front of the list
            if 'host' in entry:
                self.iqns.insert(0, entry['target'])
            else:
                self.iqns.append(entry['target'])
            if 'hosts' in entry and Runtime.config['backstore'] == "iblock":
                logging.warning("Multiple gateway targets not supported with iblock backend, use rbd backend\n") 

        self._gen_wwn()
        self._assign_vendor()

        base = [ "targetcli", "/iscsi", "create" ]

        if self.iqns:
            for iqn in self.iqns:
                path = glob.glob(TARGET + "/iscsi/{}".format(iqn))
                if not path:
                    cmd = list(base)
                    cmd.append(iqn)
                    self.cmds.append(cmd)
        else:
            cmd = base
            self.cmds.append(cmd)
            logging.warning("No matching host found, generating dynamic target\n") 

    def _gen_wwn(self):
        """
        generate the same wwn for targets on multiple gateways 
        """
        # has to be unique for target and image 
        if 'pools' in Common.config:
            for pentry, gentry, entry in entries():
                if 'target' in gentry:
                    _uuid = uuid.uuid3(uuid.NAMESPACE_DNS, 
                                str(gentry['target'] + entry['image']))
                    logging.debug("For image {} on target {}\nuuid: {}".format(entry['image'], gentry['target'], _uuid))
                    path = glob.glob(TARGET + "/core/{}_*/{}/wwn/vpd_unit_serial".format(Runtime.config['backstore'], entry['image']))
                    try:
                        vus = open(path[0], "w")
                        vus.write(str(_uuid) + "\n")
                        vus.close()
                    except IOError, e:
                        # Already in use
                        pass

    def _assign_vendor(self):
        """
        Add branding 
        """
        for pentry, gentry, entry in entries():
            path = glob.glob(TARGET + "/core/{}_*/{}/wwn/vendor_id".format(Runtime.config['backstore'], entry['image']))
            if (path and os.path.isfile("/etc/SuSE-release")):
                try:
                    vendor = open(path[0], "w")
                    vendor.write("SUSE\n")
                    vendor.close()
                except IOError, e:
                    # Already in use
                    pass


    def create(self):
        """
        Execute commands and assign list of targets to Common.config['iqns']
        """
        for cmd in self.cmds:
            popen(cmd)
        if self.iqns:
            Common.config['iqns'] = self.iqns
        else:
            path = glob.glob(TARGET + "/iscsi/iqn*")
            Common.config['iqns'] = [ basename(path[0]) ]
        logging.debug("Common.config['iqns']: {}".format(Common.config['iqns']))


class TPGs:
    """
    Creates any additional TPGs needed.
    """

    def __init__(self):
        """
        Track several states.  

            self.cmds - final list of commands to be executed
            self.remote - TPG for holding remote gateway portals per target
            self.tpg - running counter of TPG per target
            self.portals - maps each portal to TPG per target
        """
        self.cmds = []
        self.pools = Common.config['pools']
        Runtime.config['addresses'] = addresses()

        self.tpg = {}
        self.portals = {}

        self._add()
        Runtime.config['portals'] = self.portals

    def _add(self):
        """
        Adds a TPG for each portal group.  Since iscsi.create() makes tpg1,
        skips that one naturally.
        """
        for pentry, gentry, entry in entries():
            target = iqn(gentry)
            if (not target in self.tpg or not 'initiator' in entry):
                self.tpg[target] = 1
                logging.debug("Initializing target {}".format(target))
            if not target in self.portals:
                self.portals[target] = {}
            if not entry['image'] in self.portals[target]:
                self.portals[target][entry['image']] = {}
            if 'target' in gentry:
                self._add_target(target, entry['image'])
            else:
                self._add_host(entry, target)

    def _add_target(self, target, image):
        """
        Adds a TPG for each portal on this host.  Effectively multiplies the
        number of defined groups of TPGs by the number of portal groups.  Each
        gateway will create the same ordering so a specific image will be the
        same TPG and index on every gateway.
 
            target - target iqn
        """
        for tentry in Common.config['targets']:
            if target == tentry['target']:
                for hentry in tentry['hosts']:
                    if not hentry['portal'] in self.portals[target][image]:
                        self.portals[target][image][hentry['portal']] = self.tpg[target]
                        self.tpg[target] += 1 
                        self.cmds.append(self._cmd(target, 
                                         self.portals[target][image][hentry['portal']]))
                        logging.debug("Adding TPG {} for target {}".format(self.portals[target][image][hentry['portal']], target))
                    
    def _add_host(self, entry, target):
        """
        Adds a tpg for the specified entry

            entry - a dict containing portal, initiator and image keys 
            target - target iqn 
        """
        if 'portal' in entry:
            allocated_tpg = self._search_allocated_portals(entry['portal'], target, entry['image'])
            if allocated_tpg:
                self.portals[target][entry['image']][entry['portal']] = allocated_tpg

            if not entry['portal'] in self.portals[target][entry['image']]:
                self._check_portal(entry['portal'])
                self.portals[target][entry['image']][entry['portal']] = self.tpg[target]
                self.tpg[target] += 1 
                self.cmds.append(self._cmd(target, 
                                 self.portals[target][entry['image']][entry['portal']]))
                logging.debug("Adding TPG {} for target {}".format(self.portals[target][entry['image']][entry['portal']], target))
        else:
            self.portals[target][entry['image']]['default'] = self.tpg[target]
            self.tpg[target] += 1 
            self.cmds.append(self._cmd(target, 
                             self.portals[target][entry['image']]['default']))
            logging.debug("Adding TPG {} for target {}".format(self.portals[target][entry['image']]['default'], target))

    def _search_allocated_portals(self, name, target, image):
        """
        """
        for image in self.portals[target].keys():
            for portal in self.portals[target][image].keys():
                if name == portal:
                    return(self.portals[target][image][portal])
        return(False)
                    
        

                    
    def _check_portal(self, name):
        """
        Check that the referenced portal is defined in portals
        """
        found = False
        for entry in Common.config['portals']:
            if name == entry['name']:
                found = True
                break
        if not found:
            raise ValueError("portal {} is missing from portals".format(name))

    def disable_remote(self):
        """
        Find non-local portals on each tpg and disable
        """
        for target in self.portals.keys():
            for image in self.portals[target].keys():
                for name in self.portals[target][image].keys():
                    for entry in Common.config['portals']:
                        if name == entry['name']:
                            for address in entry['addresses']:
                                addr = re.split(" ", address)[0]
                                if not addr in Runtime.config['addresses']:
                                    self._disable_tpg(target, 
                                        self.portals[target][image][name])


    def _disable_tpg(self, target, tpg):
        """
        Disable TPG and disable tpg_enabled_sendtargets.
        """
        path = TARGET + "/iscsi/{}/tpgt_{}/attrib/tpg_enabled_sendtargets".format(target, tpg)
        if not os.path.isfile(path):
            raise RuntimeError("tpg_enabled_sendtargets unsupported, upgrade kernel to 3.12.46-102-default or higher")
        tes = open(path, "w")
        tes.write("0")
        tes.close()
        logging.debug("Disabling tpg_enabled_sendtargets for tpg {} under target {}".format(tpg, target))
        tpg_path = TARGET + "/iscsi/{}/tpgt_{}/enable".format(target, tpg)
        enabled = open(tpg_path).read().rstrip('\n')
        if (enabled == "1"):
            popen([ "targetcli", "/iscsi/{}/tpg{}".format(target, tpg), "disable" ])


    def _cmd(self, target, tpg):
        """
        Return targetcli command if configfs entry is not present
        """
        path = glob.glob(TARGET + "/iscsi/{}/tpgt_{}".format(target, tpg))
        if not path:
            return([ "targetcli", "/iscsi/{}".format(target), 
                         "create {}".format(tpg) ])
        return([])

    def create(self):
        """
        Execute commands and assign list of targets to Common.config['iqns']
        """
        for cmd in self.cmds:
            if cmd:
                popen(cmd)
        

class Portals:
    """
    Manage the creation of portals, skipping existing.  If none are provided
    in the configuration, assign the base targetcli command which selects
    a default interface.
    """

    def __init__(self):
        """
        Build portal commands, assign address to correct TPG
        """
        self.cmds = []
        self.luns = []

        if 'portals' in Common.config and Common.config['portals']:
            for target, image, portal, entry in self._entries():
                if entry['name'] == portal:
                    for address in entry['addresses']:
                        self._cmd(target, 
                            Runtime.config['portals'][target][image][portal], 
                            address)
                        logging.debug("Adding address {} to tpg {} under target {}".format(address, Runtime.config['portals'][target][image][portal], target))
        else:
            self._cmd(iqn({}), "1", "")
                        

    def _entries(self):
        """
        Generator
        """
        for target in Runtime.config['portals'].keys():
            for image in Runtime.config['portals'][target]:
                for portal in Runtime.config['portals'][target][image]:
                    self._check(portal)
                    for entry in Common.config['portals']:
                        yield(target, image, portal, entry)


    def _check(self, name):
        """
        Simple verification to check the portal referenced is defined

            name - name of portal
        """
        found = False
        if (name == "default"):
            found = True
        for entry in Common.config['portals']:
            if entry['name'] == name:
                found = True
        
        if not found:
            raise ValueError("portal {} missing from portals section".format(name))
            
         


    def _cmd(self, target, tpg, address):
        """
        Compose targetcli commmand for creating portal if needed. Convert
        address from space to colon delimited, if needed.
        """
        cmd = [ "targetcli", "/iscsi/{}/tpg{}/portals".format(target, tpg), "create", address ] 
        portal = glob.glob(TARGET + "/iscsi/{}/tpgt_{}/np/{}*".format(target, tpg, re.sub(r' ', ':', address)))
        if not portal:
            self.cmds.append(cmd)


    def create(self):
        """
        Execute saved commands.  Skip redundant commands from multiple image
        entries.
        """
        #for cmd in uniq(self.cmds):
        for cmd in self.cmds:
            popen(cmd)

class Luns:
    """
    Manages the creation of luns.  Also, provides method for 
    disabling auto add which is necessary for acls.
    """

    def __init__(self):
        """
        Skips existing luns.  Builds commands for each image under the 
        correct target.
        """
        self.cmds = []
        self.exists = {}
                
        self._find()

        for pentry, gentry, entry in entries():
            target = iqn(gentry)
            if 'target' in gentry:
                for image in Runtime.config['portals'][target].keys():
                    if image == entry['image']:
                        for portal in Runtime.config['portals'][target][image].keys():
                            tpg = str(Runtime.config['portals'][target][image][portal])
                   
                            if not (target in self.exists and 
                                    tpg in self.exists[target] and
                                    entry['image'] in self.exists[target][tpg]):
                                self._cmd(target, tpg, entry['image'])
                                logging.debug("Adding lun for image {} to tpg {} under target {}".format(entry['image'], tpg, target))
            else:
                tpg = str(Runtime.tpg(target, entry))
                if not (target in self.exists and 
                        tpg in self.exists[target] and
                        entry['image'] in self.exists[target][tpg]):
                    self._cmd(target, tpg, entry['image'])
                    logging.debug("Adding lun for image {} to tpg {} under target {}".format(entry['image'], tpg, target))
                
    def _find(self):
        """
        Scan paths for existing luns and save lun name to list
        """
        for pentry, gentry, entry in entries():
            target = iqn(gentry)
            udev_paths = glob.glob(TARGET + "/iscsi/{}/tpgt_*/lun/lun_*/*/udev_path".format(target))
            if not target in self.exists:
                self.exists[target] = {}
            for udev_path in udev_paths:
                contents = open(udev_path).read().rstrip('\n')
                tpg = re.split("[/_]", udev_path)[8]
                if not tpg in self.exists[target]:
                    self.exists[target][tpg] = []
                self.exists[target][tpg].append(basename(contents))
      

    def _cmd(self, target, tpg, image):
        """
        Compose targetcli commmand for creating lun if needed.
        """
        if (Runtime.config['backstore'] == "rbd"):
            cmd = [ "targetcli", "/iscsi/{}/tpg{}/luns".format(target, tpg), "create", "/backstores/rbd/{}".format(image) ] 
        else:
            cmd = [ "targetcli", "/iscsi/{}/tpg{}/luns".format(target, tpg), "create", "/backstores/iblock/{}".format(image) ] 
        self.cmds.append(cmd)

    def create(self):
        """
        Disable auto mapping.  Execute saved commands.
        """
        self.disable_auto_add_mapped_luns()
        for cmd in uniq(self.cmds):
            popen(cmd)

    def disable_auto_add_mapped_luns(self):
        """
        Allow device to initiator mapping by disabling auto mapping.
        """
        proc = Popen(["targetcli", "get", "global", "auto_add_mapped_luns"], stdout=PIPE, stderr=PIPE)
        for line in proc.stdout:
            results = re.split(r'=', line)
            if (results[1].rstrip() != 'false'):
                cmd = [ "targetcli", "set", "global", "auto_add_mapped_luns=false" ]
                popen(cmd)

class Map:

    def __init__(self):
        """
        Creates mapped luns under each initiator.  Skips existing. 
        """
        self.cmds = []
        self.luns = []

        for pentry, gentry, entry in entries():
            target = iqn(gentry)
            #if 'initiator' in entry:
            if 'target' in gentry:
                if (find_auth(gentry['target']) == "acls" or
                    find_auth(gentry['target']) == "tpg+identified"):
                    if not 'initiator' in entry:
                        raise RuntimeError("Entry for target {} missing initiator for specified authentication {}".format(target, find_auth(gentry['target'])))
                    for image in Runtime.config['portals'][target].keys():
                        if image == entry['image']:
                            for portal in Runtime.config['portals'][target][image].keys():
                                tpg = Runtime.config['portals'][target][image][portal]
                                lun = self._lun(target, tpg, entry['image'])
                                self._check(target, tpg, entry['initiator'])
                                self._cmd(target, tpg, entry['initiator'], lun)
                                logging.debug("Mapping lun {} for initiator {} to tpg {} under target {}".format(lun, entry['initiator'], tpg, target))
            else:
                if (find_auth(gentry['host']) == "acls" or
                    find_auth(gentry['host']) == "tpg+identified"):
                    if not 'initiator' in entry:
                        raise RuntimeError("Entry for host {} missing initiator for specified authentication {}".format(gentry['host'], find_auth(gentry['host'])))
                    tpg = Runtime.tpg(target, entry)
                    lun = self._lun(target, tpg, entry['image'])
                    self._check(target, tpg, entry['initiator'])
                    self._cmd(target, tpg, entry['initiator'], lun)
                    logging.debug("Mapping lun {} for initiator {} to tpg {} under target {}".format(lun, entry['initiator'], tpg, target))
              

    def _lun(self, target, tpg, image):
        """
        Return the numeric value of the lun for this image

            image - name of RBD image
        """
        lun_path = glob.glob(TARGET + "/iscsi/{}/tpgt_{}/lun/lun_*/*".format(target, tpg))
        for p in lun_path:
            if (basename(os.path.realpath(p)) == image):
                return(re.split("[/_]", p)[11])
          
        raise ValueError("lun missing from tpg{} under target {}".format(tpg, target))

    def _check(self, target, tpg, initiator):
        """
        Check that acl exists, otherwise, raise exception

            target - iqn of the target
            tpg - number of tpg, most likely "1"
            initiator - iqn of client
        """
        path = glob.glob(TARGET + "/iscsi/{}/tpgt_{}/acls/{}".format(target, tpg, initiator))
        if not path:
            raise ValueError("ERROR: acl missing for initiator {} under tpg {} under target {}".format(initiator, tpg, target))

    def _cmd(self, target, tpg, initiator, lun):
        """
        Compose command to create a mapped lun.  Skip if exists.

            target - iqn of the target
            tpg - number of tpg, most likely "1"
            initiator - iqn of client
            lun - number for block device of RBD image
        """
        path = glob.glob(TARGET + "/iscsi/{}/tpgt_{}/acls/{}/lun_{}".format(target, tpg, initiator, lun))
        if not path:
            self.cmds.append([ "targetcli", "/iscsi/{}/tpg{}/acls/{}".format(target, tpg, initiator), "create", lun, lun ])

    def map(self):
        """
        Execute saved commands.
        """
        for cmd in self.cmds:
            popen(cmd)


class Acls:
    """
    Manage acls for each initiator.  Skip existing entries.  

    """

    def __init__(self):
        """
        Create acl under correct tpg per target.  Skip existing.  
        Scan portal addresses for remote gateways.  Create acl under remote 
        tpg, if necessary.
        """
        self.cmds = []
        self.initiators = []
        self.exists = {}

        self._find()
        for pentry, gentry, entry in entries():
            target = iqn(gentry)
            if 'target' in gentry:
                if (find_auth(gentry['target']) == "acls" or 
                    find_auth(gentry['target']) == "tpg+identified"):
                    if not 'initiator' in entry:
                        raise RuntimeError("Entry for target {} missing initiator for specified authentication {}".format(target, find_auth(gentry['target'])))

                    for image in Runtime.config['portals'][target].keys():
                        if image == entry['image']:
                            for portal in Runtime.config['portals'][target][image].keys():
                                tpg = str(Runtime.config['portals'][target][image][portal])
                                if not (target in self.exists and
                                        tpg in self.exists[target] and
                                        entry['initiator'] in self.exists[target][tpg]):
                                    self._cmd(target, tpg, entry['initiator'])
                                    logging.debug("Adding initiator {} under tpg {} for target {}".format(entry['initiator'], tpg, target))
            else:
                if (find_auth(gentry['host']) == "acls" or
                    find_auth(gentry['host']) == "tpg+identified"):
                    if not 'initiator' in entry:
                        raise RuntimeError("Entry for host {} missing initiator for specified authentication {}".format(gentry['host'], find_auth(gentry['host'])))
                    tpg = str(Runtime.tpg(target, entry))
                    if not (target in self.exists and 
                            tpg in self.exists[target] and
                            entry['initiator'] in self.exists[target][tpg]):
                        self._cmd(target, tpg, entry['initiator'])
                        logging.debug("Adding initiator {} under tpg {} for target {}".format(entry['initiator'], tpg, target))
                    
    def _find(self):
        """
        Add existing initiators to list
        """
        for pentry in Common.config['pools']:
            if 'gateways' in pentry:
                for gentry in pentry['gateways']:
                    target = iqn(gentry)
                    if not target in self.exists:
                        self.exists[target] = {}
                    paths = glob.glob(TARGET + "/iscsi/{}/tpgt_*/acls/*".format(target))
                    for path in paths:
                        self.initiators.append(basename(path))
                        tpg = re.split("[/_]", path)[8]
                        if not tpg in self.exists[target]:
                            self.exists[target][tpg] = []
                        self.exists[target][tpg].append(basename(path))
                        


    def _cmd(self, target, tpg, initiator):
        """
        Compose targetcli command for creating an acl.  Append to list.
        """
        cmd = [ "targetcli", "/iscsi/{}/tpg{}/acls".format(target, tpg), "create", initiator ] 
        self.cmds.append(cmd) 
               
    def create(self):
        """
        Execute unique, saved commands
        """
        for cmd in uniq(self.cmds):
            popen(cmd)


class Auth:
    """
    Manage the authentications for each target.  Each authentication mode
    contains multiple steps.  Delegate creation of the necessary commands.  
    Execute commands.
    """

    def __init__(self):
        """
        Check for existence of the authentication section and current setting.
        Select appropriate delegation.  Note that discovery authentication
        is independent of normal authentication and optional.
        """
        self.cmds = []
        self.tpg = {}

        if 'auth' in Common.config and Common.config['auth']:
            for auth in Common.config['auth']:
                for target in Runtime.config['portals'].keys():
                    if target == iqn(auth):
                        self.target = target
                        for image in Runtime.config['portals'][target].keys():
                            for portal in Runtime.config['portals'][target][image].keys():
                                self.tpg = str(Runtime.config['portals'][target][image][portal])
                                self.auth = auth
                                self.select_auth()
                                self.cmds.extend(self.select_discovery())
        else:
            for target in Runtime.config['portals'].keys():
                self.target = target
                for image in Runtime.config['portals'][target].keys():
                    for portal in Runtime.config['portals'][target][image].keys():
                        self.tpg = str(Runtime.config['portals'][target][image][portal])
                        self.cmds.append(self.set_noauth())
            self.cmds.append(self.set_discovery_off())

    def select_auth(self):
        """
        Delegate authentication
        """
        if self.auth['authentication'] == "none":
            self.cmds.append(self.set_noauth())
        elif self.auth['authentication'] == "tpg":
            self.cmds.extend(self.select_tpg())
        elif self.auth['authentication'] == "tpg+identified":
            self._generate_acls()
            self.cmds.extend(self.select_acls())
        elif self.auth['authentication'] == "acls":
            self.cmds.extend(self.select_acls())
        else:
            raise ValueError("InvalidAuthentication: authentication must be one of tpg, acls or none")

    def _generate_acls(self):
        """
        Create or append to the acls array the common tpg entry for each
        initiator.  This is technically the same as specifying acls with
        the same userid/password/etc.  
        """
        for initiator in self._find_tpg_identified_initiators():
            if not 'acls' in self.auth:
                self.auth['acls'] = []
            entry = {}
            entry['initiator'] = initiator
            entry['userid'] = self.auth['tpg']['userid']
            entry['password'] = self.auth['tpg']['password']

            if 'mutual' in self.auth['tpg']:
                entry['mutual'] = self.auth['tpg']['mutual']
            if 'userid_mutual' in self.auth['tpg']:
                entry['userid_mutual'] = self.auth['tpg']['userid_mutual']
            if 'password_mutual' in self.auth['tpg']:
                entry['password_mutual'] = self.auth['tpg']['password_mutual']
            self.auth['acls'].append(entry)

    def _find_tpg_identified_initiators(self):
        """
        Search for all initiators for current tpg+identified entry
        """
        initiators = []
        for pentry, gentry, entry in entries():
            for key in [ 'target', 'host']:
                if (key in self.auth and key in gentry and
                   self.auth[key] == gentry[key]):
                    initiators.append(entry['initiator'])
        return(initiators)


    def set_noauth(self):
        """
        Disable authentication
        """
        logging.debug("Disable authentication")
        path = TARGET + "/iscsi/{}/tpgt_{}/attrib".format(self.target, self.tpg)
        authentication = open(path + "/authentication").read().rstrip('\n')
        demo_mode_write_protect = open(path + "/demo_mode_write_protect").read().rstrip('\n')
        
        if ((authentication == "0") and
           (demo_mode_write_protect == "0")):
            return([])

        cmd = [ "targetcli", "/iscsi/{}/tpg{}".format(self.target, self.tpg), "set", "attribute", "authentication=0", "demo_mode_write_protect=0", "generate_node_acls=1" ]
        return(cmd)

    def select_discovery(self):
        """
        Discovery is optional, can be completely disabled, have only mutual 
        disabled or be completely enabled.  Delegate appropriately.
        """
        cmds = []
        for auth in Common.config['auth']:
            if "discovery" in auth:
                if auth['discovery']['auth'] == "enable":
                    self.d_auth = auth
                    if "mutual" in auth['discovery']:
                        if auth['discovery']['mutual'] == "enable":
                            cmds.append(self.set_discovery_mutual())
                        else:
                            cmds.append(self.set_discovery())
                    else:
                        cmds.append(self.set_discovery())
                else:
                    cmds.append(self.set_discovery_off())
            else:
                cmds.append(self.set_discovery_off())
            return(cmds)

    def set_discovery(self):
        """
        Call targetcli to only set the discovery userid and password.  Check
        current settings.
        """
        logging.debug("Set discovery authentication")
        keys = [ 'userid', 'password']
        check_keys(keys, self.d_auth['discovery'], "discovery under auth")

        path = TARGET + "/iscsi/discovery_auth"
        current = {}
        current['userid'] = open(path + "/userid").read().rstrip('\n')
        current['password'] = open(path + "/password").read().rstrip('\n')
        
        if compare_settings(keys, current, self.d_auth['discovery']):
            return([])

        cmd = [ "targetcli", "/iscsi", "set", "discovery_auth", "enable=1",
                 "userid={}".format(
                     self.d_auth['discovery']['userid']), 
                 "password={}".format(
                     self.d_auth['discovery']['password']) ] 
        return(cmd)

    def set_discovery_mutual(self):
        """
        Call targetcli to set both normal and mutual discovery authentication.
        Checks current settings.
        """
        logging.debug("Set discovery and mutual authentication")
        keys = [ 'userid', 'password', 'userid_mutual', 'password_mutual']
        check_keys(keys, self.d_auth['discovery'], "discovery under auth")

        path = TARGET + "/iscsi/discovery_auth"
        current = {}
        current['userid'] = open(path + "/userid").read().rstrip('\n')
        current['password'] = open(path + "/password").read().rstrip('\n')
        current['userid_mutual'] = open(path + "/userid_mutual").read().rstrip('\n')
        current['password_mutual'] = open(path + "/password_mutual").read().rstrip('\n')
        
        if compare_settings(keys, current, self.d_auth['discovery']):
            return([])


        cmd = [ "targetcli", "/iscsi", "set", "discovery_auth", "enable=1", 
                 "userid={}".format(
                     self.d_auth['discovery']['userid']), 
                 "password={}".format(
                     self.d_auth['discovery']['password']), 
                 "mutual_userid={}".format(
                     self.d_auth['discovery']['userid_mutual']), 
                 "mutual_password={}".format(
                     self.d_auth['discovery']['password_mutual']) ] 
        return(cmd)

    def set_discovery_off(self):
        """
        Disable discovery
        """
        logging.debug("Disable discovery authentication")
        path = TARGET + "/iscsi/discovery_auth"
        enforce = open(path + "/enforce_discovery_auth").read().rstrip('\n')
        if (enforce == "0"):
            return([])

        cmd = [ "targetcli", "/iscsi", "set", "discovery_auth", "enable=0" ]
        return(cmd)

    def select_tpg(self):
        """
        TPG is optional, can have only mutual disabled or be completely 
        enabled.  Delegate appropriately.  TPG allows a common userid and
        password for all initiators. Works for tpg and tpg+identified.
        """
        cmds = []
        if "mutual" in self.auth['tpg']:
            if self.auth['tpg']['mutual'] == "enable":
                cmds.append(self.set_tpg_mutual())
                cmds.append(self.set_tpg_mode())
            else:    
                cmds.append(self.set_tpg())
                cmds.append(self.set_tpg_mode())
        else:
            cmds.append(self.set_tpg())
            cmds.append(self.set_tpg_mode())
        return(cmds)


    def set_tpg(self):
        """
        Call targetcli to set only the common userid and password.  Check
        current setting.
        """
        logging.debug("Set tpg authentication")
        keys = [ 'userid', 'password']
        check_keys(keys, self.auth['tpg'], "tpg under auth")
                
        path = TARGET + "/iscsi/{}/tpgt_{}/auth".format(self.target, self.tpg)
        current = {}
        current['userid'] = open(path + "/userid").read().rstrip('\n')
        current['password'] = open(path + "/password").read().rstrip('\n')
        
        if compare_settings(keys, current, self.auth['tpg']):
            return([])

        cmd = [ "targetcli", "/iscsi/{}/tpg{}".format(self.target, self.tpg), "set", "auth", 
                 "userid={}".format(
                     self.auth['tpg']['userid']), 
                 "password={}".format(
                     self.auth['tpg']['password']) ] 
        return(cmd)

    def set_tpg_mutual(self):
        """
        Call targetcli to set both the common and mutual userids and passwords.
        Checks current settings.
        """
        logging.debug("Set tpg and mutual authentication")
        keys = [ 'userid', 'password', 'userid_mutual', 'password_mutual']
        check_keys(keys, self.auth['tpg'], "tpg under auth")

        path = TARGET + "/iscsi/{}/tpgt_{}/auth".format(self.target, self.tpg)
        current = {}
        current['userid'] = open(path + "/userid").read().rstrip('\n')
        current['password'] = open(path + "/password").read().rstrip('\n')
        current['userid_mutual'] = open(path + "/userid_mutual").read().rstrip('\n')
        current['password_mutual'] = open(path + "/password_mutual").read().rstrip('\n')
        
        if compare_settings(keys, current, self.auth['tpg']):
            return([])

        cmd = [ "targetcli", "/iscsi/{}/tpg{}".format(self.target, self.tpg), "set", "auth", 
                 "userid={}".format(
                     self.auth['tpg']['userid']), 
                 "password={}".format(
                     self.auth['tpg']['password']), 
                 "userid_mutual={}".format(
                     self.auth['tpg']['userid_mutual']), 
                 "password_mutual={}".format(
                     self.auth['tpg']['password_mutual']) ] 
        return(cmd)

    def set_tpg_mode(self):
        """
        Enable authentication, allow writing and enable acl generation. Checks
        current settings.
        """
        path = TARGET + "/iscsi/{}/tpgt_{}/attrib".format(self.target, self.tpg)
        authentication = open(path + "/authentication").read().rstrip('\n')
        demo_mode_write_protect = open(path + "/demo_mode_write_protect").read().rstrip('\n')
        generate_node_acls = open(path + "/generate_node_acls").read().rstrip('\n')
        
        if (self.auth['authentication'] == "tpg"):
            if ((authentication == "1") and
               (demo_mode_write_protect == "0") and
               (generate_node_acls  == "1")): 
                return([])

            return([ "targetcli", "/iscsi/{}/tpg{}".format(self.target, self.tpg), "set", "attribute", "authentication=1", "demo_mode_write_protect=0", "generate_node_acls=1" ]) 
        else:
            # tpg+identified
            if ((authentication == "1") and
               (demo_mode_write_protect == "0") and
               (generate_node_acls  == "0")): 
                return([])

            return([ "targetcli", "/iscsi/{}/tpg{}".format(self.target, self.tpg), "set", "attribute", "authentication=1", "demo_mode_write_protect=0", "generate_node_acls=0" ]) 

    def select_acls(self):
        """
        ACLs are optional, can have only mutual disabled or be completely 
        enabled for each initiator.  Delegate appropriately.  ACLs allow a 
        unique userid and password for each initiator.
        
        """
        cmds = []
        for acl in self.auth['acls']:
            self.acl = acl
            if "mutual" in acl:
                if acl['mutual'] == "enable":
                    cmds.append(self.set_acls_mutual())
                else:
                    cmds.append(self.set_acls())
            else:
                cmds.append(self.set_acls())
        cmds.append(self.set_acls_mode())
        return(cmds)
 
    def set_acls(self):
        """
        Call targetcli to set a userid and password for a specific initiator.
        Checks current setting.
        """
        logging.debug("Set acl authentication")
        keys = [ 'userid', 'password']
        check_keys(keys, self.acl, "acl")

        path = TARGET + "/iscsi/{}/tpgt_{}/acls/{}/auth".format(self.target, self.tpg, self.acl['initiator'])
        current = {}
        current['userid'] = open(path + "/userid").read().rstrip('\n')
        current['password'] = open(path + "/password").read().rstrip('\n')
        
        if compare_settings(keys, current, self.acl):
            return([])

        cmd = [ "targetcli", "/iscsi/{}/tpg{}/acls/{}".format(self.target, self.tpg, self.acl['initiator']), "set", "auth", 
                 "userid={}".format(self.acl['userid']),
                 "password={}".format(self.acl['password']), ] 
        return(cmd)

    def set_acls_mutual(self):
        """
        Call targetcli to set both a normal and mutual authentication for 
        an initiator.  Checks current settings.
        """
        logging.debug("Set acl and mutual authentication")
        keys = [ 'userid', 'password', 'userid_mutual', 'password_mutual']
        check_keys(keys, self.acl, "acl")

        path = TARGET + "/iscsi/{}/tpgt_{}/acls/{}/auth".format(self.target, self.tpg, self.acl['initiator'])
        current = {}
        current['userid'] = open(path + "/userid").read().rstrip('\n')
        current['password'] = open(path + "/password").read().rstrip('\n')
        current['userid_mutual'] = open(path + "/userid_mutual").read().rstrip('\n')
        current['password_mutual'] = open(path + "/password_mutual").read().rstrip('\n')

        if compare_settings(keys, current, self.acl):
            return([])

        cmd = [ "targetcli", "/iscsi/{}/tpg{}/acls/{}".format(self.target, self.tpg, self.acl['initiator']), "set", "auth", 
                 "userid={}".format(self.acl['userid']),
                 "password={}".format(self.acl['password']), 
                 "userid_mutual={}".format(self.acl['userid_mutual']),
                 "password_mutual={}".format(self.acl['password_mutual']) ] 
        return(cmd)

    def set_acls_mode(self):
        """
        Enable authentication, disable acls generation.  Checks current settings.
        """
        path = TARGET + "/iscsi/{}/tpgt_{}/attrib".format(self.target, self.tpg)
        authentication = open(path + "/authentication").read().rstrip('\n')
        demo_mode_write_protect = open(path + "/demo_mode_write_protect").read().rstrip('\n')
        generate_node_acls = open(path + "/generate_node_acls").read().rstrip('\n')
        
        if ((authentication == "1") and
           (demo_mode_write_protect == "0") and
           (generate_node_acls  == "0")): 
            return([])
        return([ "targetcli", "/iscsi/{}/tpg{}".format(self.target, self.tpg), "set", "attribute", "authentication=1", "demo_mode_write_protect=0", "generate_node_acls=0" ]) 

    def create(self):
        """
        Execute all the authentication commands
        """
        for cmd in self.cmds:
            if cmd:
                popen(cmd)


def main(args):
    """
    Apply stored configuration by default.  Otherwise, execute the alternate
    path from the specified options. 

        args - expects parse_args() result from argparse
    """
    configs = Configs(args.config, args.ceph, args.host)
    logging.basicConfig(format='%(levelname)s: %(message)s')

    if (args.verbose or args.wipe or args.host):
        logging.getLogger().level = logging.INFO

    if (args.debug):
        logging.getLogger().level = logging.DEBUG

    if (args.wipe):
        configs.wipe()
    elif (args.clear):
        configs.clear()
        if (args.unmap):
            images = Images()
            images.unmap()
    elif (args.unmap):
        images = Images()
        images.unmap()
    elif (args.file):
        configs.wipe()
        content = Content()
        content.read(args.file)
        content.save()
    elif (args.add):
        content = Content()
        content.read(args.add)
        content.save()
    elif (args.output):
        configs.retrieve()
        configs.display()
    elif (args.edit):
        configs.retrieve()
        content = Content()
        content.edit(args.editor)
        content.save()
    elif (args.local):
        configs.retrieve("host")
        configs.display()
    else:
        configs.retrieve("host")
        images = Images()
        images.map()
        backstores = Backstores(args.backstore)
        backstores.create()
        iscsi = Iscsi()
        iscsi.create()
        tpgs = TPGs()
        tpgs.create()
        tpgs.disable_remote()
        luns = Luns()
        luns.create()
        portals = Portals()
        portals.create()
        acls = Acls()
        acls.create()
        maps = Map()
        maps.map()
        auth = Auth()
        auth.create()


# Main
if __name__ == "__main__":
    parser = argparse.ArgumentParser()

    parser.add_argument('-e', '--edit', action='store_true', dest='edit', default=False,
                    help='edit the rbd configuration for iSCSI')
    parser.add_argument('-E', '--editor', action='store', dest='editor',
                    help='use editor to edit the rbd configuration for iSCSI', metavar='editor')
    parser.add_argument('-c', '--config', action='store', dest='config',
                    help='use name for object, defaults to "lrbd.conf"', metavar='name')
    parser.add_argument('--ceph', action='store', dest='ceph',
                    help='specify the ceph configuration file', metavar='ceph')
    parser.add_argument('-H', '--host', action='store', dest='host',
                    help='specify the hostname, defaults to "{}"'.format(socket.gethostname()), metavar='host')
    parser.add_argument('-o', '--output', action='store_true', dest='output', 
                    help='display the configuration')
    parser.add_argument('-l', '--local', action='store_true', dest='local', 
                    help='display the host configuration')
    parser.add_argument('-f', '--file', action='store', dest='file', 
                    help='import the configuration from file', metavar='file')
    parser.add_argument('-a', '--add', action='store', dest='add', 
                    help='add the configuration from file', metavar='file')
    parser.add_argument('-u', '--unmap', action='store_true', dest='unmap', 
                    help='unmap the rbd images')
    parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', 
                    help='print INFO messages')
    parser.add_argument('-d', '--debug', action='store_true', dest='debug', 
                    help='print DEBUG messages')
    parser.add_argument('-I', '--iblock', action='store_const', 
                    dest='backstore', const='iblock',
                    help='set the backstore to iblock, defaults to rbd')
    parser.add_argument('-R', '--rbd', action='store_const', dest='backstore', 
                    const='rbd',
                    help='set the backstore to rbd')
    parser.add_argument('-W', '--wipe', action='store_true', dest='wipe', 
                    help='wipe the configuration objects from all pools')
    parser.add_argument('-C', '--clear', action='store_true', dest='clear', 
                    help='clear the targetcli configuration')
    
    args = parser.parse_args()
    
    if (args.editor != None):
        args.edit = True
    
    if args.debug:
        main(args)
    else:
        try:
            main(args)
        except SystemExit as e:
            print e
            exit()
        except Exception as e:
            logging.error(e)
            exit(1)

