#!/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, json
import tempfile, os
#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


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:
            # skip downed interfaces
            pass
    return adds

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

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(object):
    """
    Sharing common static configurations.
    """
    config = OrderedDict()

    config_name = ""
    ceph_conf = ""
    hostname = ""

    @staticmethod
    def assign(sections):
        """
        Map sections keys into Common.config
        """
        Common.config['auth'] = sections["authentications"].authentications
        Common.config['targets'] = sections["targets"].targets
        Common.config['portals'] = sections["portals"].portals
        Common.config['pools'] = sections["pools"].pools

class Runtime(object):
    """
    Sharing common runtime state.
    """
    config = {}
    target = "/sys/kernel/config/target"
    fabric = "iscsi"

    @staticmethod
    def tpg(*args):
        """
        Returns target portal group for specified target and entry if portal 
        exists, otherwise defaults to 1. Also returns the target portal group
        for specified target, image and portal.
        """
        target = args[0]
        if len(args) == 2:
            entry = args[1] 
            if 'portal' in entry:
                return Runtime.config['portals'][target][entry['image']][entry['portal']]
            else:
                return 1
        else:
            image = args[1]
            portal = args[2] 
            return Runtime.config['portals'][target][image][portal]

    @staticmethod
    def core(pathname):
        """
        Returns composed core path
        """
        return glob.glob("{}/{}/{}".format(Runtime.target, "core", pathname))

    @staticmethod
    def path(pathname):
        """
        Returns composed path
        """
        return glob.glob("{}/{}/{}".format(Runtime.target,
                                           Runtime.fabric,
                                           pathname))
    @staticmethod
    def targets():
        """
        Returns targets
        """
        return Runtime.config['portals'].keys()

    @staticmethod
    def images(target):
        """
        Returns images
        """
        return Runtime.config['portals'][target].keys()

    @staticmethod
    def portals(target, image):
        """
        Returns portals
        """
        return Runtime.config['portals'][target][image].keys()

#    @staticmethod
#    def tpg(target, image, portal):
#        """
#        Returns target portal group
#        """
#        return Runtime.config['portals'][target][image][portal]


class InitialContents(object):
    """
    Provides the initial contents for the editor
    """
    def __init__(self):
        if (not Common.config['auth'] and
                not Common.config['targets'] and
                not Common.config['pools']):
            self.text = self._instructions()
        else:
            self.text = json.dumps(Common.config, indent=2)

    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.redhat: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.redhat:01:abcdefghijkl"
                          }
                        ]
                      }
                    ]
                  }
                ]
              }


            #\n""")

    def text(self):
        return self.text

class Content(object):
    """
    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 = {}
        self.initial_content = ""
        self.submitted = ""

    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
        program = editor if editor else os.environ.get('EDITOR',
                                                       '/usr/bin/vim')
        self.initial_content = InitialContents()

        with tempfile.NamedTemporaryFile(suffix=".tmp") as tmpfile:
            tmpfile.write(self.initial_content.text)
            tmpfile.flush()

            valid = False
            while not valid:
                call([program, tmpfile.name])
                valid, self.submitted = self._check(tmpfile.name)
                if not valid:
                    try:
                        raw_input("Press enter to edit or Ctrl-C to quit ")
                    except KeyboardInterrupt:
                        raise SystemExit("\nBye")


    def read(self, filename):
        """
        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(filename):
            raise IOError("file '{}' does not exist".format(filename))
        valid, self.submitted = self._check(filename)
        if not valid:
            raise RuntimeError("file {} failed validation".format(filename))

    def _check(self, filename):
        """
        Returns a tuple of whether the file provided is valid and a json
        structure of its contents

            filename - a string
        """
        submitted = strip_comments(open(filename).read())
        valid = (self.validate(submitted) and
                 self.verify_mandatory_keys(submitted))
        if valid:
            text = json.loads(submitted, object_pairs_hook=OrderedDict)
        else:
            text = ""
        return(valid, text)


    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:
            json.loads(text)
        except ValueError, error:
            logging.error(error)
            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, conn):
        """
        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...")
            with conn as cluster:
                self.attr = Attributes(cluster)
                self._remove_deleted()
                self._update_submitted()


    def _remove_deleted(self):
        """
        Remove no longer needed extended attributes
        """
        self._remove_absent_entry()
        self._remove_absent_auth()
        self._remove_absent_auth_entry()


    def _update_submitted(self):
        """
        Create or update extended attributes for each section
        """
        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']] = []
                self._add_current_gateways(pool, hosts)
            for pool in self.submitted['pools']:
                self._subtract_submitted_gateways(pool, hosts)
                self._remove_difference(pool, hosts)

    def _add_current_gateways(self, pool, hosts):
        """
        Adds gateways from current configuration
        """
        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'])

    def _subtract_submitted_gateways(self, pool, hosts):
        """
        Subtracts gateways in submitted configuration
        """
        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'])

    def _remove_difference(self, pool, hosts):
        """
        Removes the differences between the current and submitted
        """
        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']))

    def _remove_absent_auth_entry(self):
        """
        Remove entry from the auth section
        """
        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(object):
    """
    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:
            raise IOError("check for missing keyring")
        return self.cluster

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

class Ioctx(object):
    """
    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:
            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(object):
    """
    Methods for updating and removing extended attributes within Ceph.
    """

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

    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(object):
    """
    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(object):
    """
    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):
        """
        Remove target entries
        """
        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(object):
    """
    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(object):
    """
    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):
        """
        Removes authentication entries for other hosts
        """
        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(object):
    """
    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, conn, sections, gateways):
        """
        Scan all configuration objects and build a structure containing
        all gateway hosts.  Merge pools, auth, portals and targets into
        Common.config
        """
        with conn as cluster:
            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
                    sections["pools"].add(pool)
                    sections["targets"].add(self._get_optional(ioctx, self.config_name, 'targets'))
                    sections["portals"].add(self._get_optional(ioctx, self.config_name, 'portals'))

                    gateways.find_portals()
                    attrs = ioctx.get_xattrs(self.config_name)
                    for key, value in attrs:
                        if key == "targets" or key == "portals":
                            continue
                        elif key[0] == "_":
                            sections["authentications"].add(
                                json.loads(value, object_pairs_hook=OrderedDict))
                        else:
                            gateways.add(key, value, self.hostname)
        gateways.purge()
        Common.assign(sections)

    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, error:
            # 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, error:
            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, conn):
        """
        Remove configuration objects from all pools
        """
        #conn = Cluster()
        with conn as cluster:
            pools = cluster.list_pools()
            for pool in pools:
                conn = Ioctx(cluster, pool)
                with conn as ioctx:
                    try:
                        ioctx.remove_object(self.config_name)
                        logging.debug("Removing {} from pool {}".
                                      format(self.config_name, pool))
                    except rados.ObjectNotFound, error:
                        logging.info("No object {} to remove from pool {}".
                                      format(self.config_name, pool))

    def clear(self):
        """
        Reset any targetcli configuration.
        """
        cmds = [["targetcli", "clearconfig confirm=true"]]
        for cmd in cmds:
            popen(cmd)

class Gateways(object):
    """
    Adds the gateways to the pools section.  If host_only is true, then
    only targets, authentications and portals referencing the host are
    kept.
    """

    def __init__(self, sections):
        """
        Keeps a running track of portals, initializes host_only

            sections = dict of configuration
        """
        self.portals = []
        self.sections = sections
        self.host_only = False

    def hostonly(self):
        """
        Enable host_only
        """
        self.host_only = True

    def find_portals(self):
        """
        Search for all portals listed in the targets section.  This is
        a no-op for the global configuration.
        """
        if self.host_only:
            self.targets = self.sections["targets"].list()
            for portal in self.sections["targets"].portals():
                self.portals.append(portal)

    def add(self, key, value, hostname):
        """
        Add a gateway specifically for a host or for all hosts
        """
        if self.host_only:
            if key in self.targets or key == hostname:
                content = json.loads(value,
                                     object_pairs_hook=OrderedDict)
                self.sections["pools"].append('gateways', content)
                for entry in content['tpg']:
                    if 'portal' in entry:
                        self.portals.append(entry['portal'])
        else:
            self.sections["pools"].append('gateways',
                json.loads(value, object_pairs_hook=OrderedDict))


    def purge(self):
        """
        Removes configuration not containing host.  No-op for global
        configuration.
        """
        if self.host_only:
            self.sections["targets"].purge()
            self.sections["authentications"].purge()
            self.sections["portals"].purge(self.portals)

##########################################################################
# 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(object):
    """
    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(object):
    """
    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._load_modules()
        self._cmd()
        # targetcli uses backstore/block, but LIO sysfs uses
        # /sys/kernel/config .... /iblock
        if self.selected == "block":
            Runtime.config['backstore'] = "iblock"
        else:
            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 = Runtime.core("*/{}".format(entry['image']))
            if existing:
                self.selected = re.split("[/_]", existing[0])[6]

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

    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 = Runtime.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 = ["iscsi_target_mod", "target_core_iblock"]
        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 = Runtime.core("rbd_*/{}/enable".format(entry['image']))
            for filename in files:
                enabled = open(filename).read().rstrip('\n')
                if enabled == "0":
                    with open(filename, "w") as enable:
                        enable.write("1")
                        logging.debug("Enabling {}".format(filename))


class Iscsi(object):
    """
    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 = []

        self._arrange()
        self._gen_wwn()
        self._assign_vendor()

        base = ["targetcli", "/{}".format(Runtime.fabric), "create"]

        if self.iqns:
            for iqn in self.iqns:
                path = Runtime.path("{}".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 _arrange(self):
        """
        Keep the host entry target at the front of the list
        """
        for entry in Common.config['targets']:
            if 'host' in entry:
                self.iqns.insert(0, entry['target'])
            else:
                self.iqns.append(entry['target'])


    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 = Runtime.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:
                        # Already in use
                        pass

    def _assign_vendor(self):
        """
        Add branding
        """
        for pentry, gentry, entry in entries():
            path = Runtime.core("{}_*/{}/wwn/vendor_id".format(Runtime.config['backstore'],
                                                               entry['image']))
            if path and os.path.isfile("/etc/redhat-release"):
                try:
                    vendor = open(path[0], "w")
                    vendor.write("RedHat\n")
                    vendor.close()
                except IOError:
                    # 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 = Runtime.path("iqn*")
            Common.config['iqns'] = [basename(path[0])]
        logging.debug("Common.config['iqns']: {}".format(Common.config['iqns']))


class TPGs(object):
    """
    Creates any additional TPGs needed.
    """

    def __init__(self, tpg_counter, portal_index):
        """
        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_counter = tpg_counter
        self.portal_index = portal_index

        self._add()
        Runtime.config['portals'] = self.portal_index.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)
            self.tpg_counter.add(target)
            self.portal_index.add(target, entry['image'])

            if 'target' in gentry:
                #self._add_target(target, entry['image'])
                self._add_target(target)
            else:
                self._add_host(entry, target)

    def _add_target(self, target):
        """
        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.portal_index.list():
                        self.portal_index.index(hentry['portal'])
                        self._add_command(target)
                        self.tpg_counter.next()

    def _add_command(self, target):
        """
        Append command with current counter
        """
        self.portal_index.tpg(self.tpg_counter.value())
        self.cmds.append(self._cmd(target, self.tpg_counter.value()))
        logging.debug("Adding TPG {} for target {}".
                      format(self.tpg_counter.value(), 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.portal_index.find(entry['portal'])
            self.portal_index.index(entry['portal'])
            if allocated_tpg:
                self.portal_index.tpg(allocated_tpg)

            if not entry['portal'] in self.portal_index.list():
                self._check_portal(entry['portal'])
                self._add_command(target)
                self.tpg_counter.next()
        else:
            self.portal_index.index = 'default'
            self._add_command(target)
            self.tpg_counter.next()

    #def _search_allocated_portals(self, name, target, image):
    #    """
    #    Return an already allocated tpg if it exists
    #    """
    #    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, image, portal, tpg in self.portal_index.entries():
            for entry in Common.config['portals']:
                if portal == entry['name']:
                    for address in entry['addresses']:
                        addr = re.split(" ", address)[0]
                        if not addr in Runtime.config['addresses']:
                            self._disable_tpg(target, tpg)


    def _disable_tpg(self, target, tpg):
        """
        Disable TPG and disable tpg_enabled_sendtargets.
        """
        path = Runtime.path("{}/tpgt_{}/attrib/tpg_enabled_sendtargets".
                            format(target, tpg))[0]
        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 = Runtime.path("{}/tpgt_{}/enable".format(target, tpg))[0]
        enabled = open(tpg_path).read().rstrip('\n')
        if enabled == "1":
            popen(["targetcli", "/{}/{}/tpg{}".
                   format(Runtime.fabric, target, tpg), "disable"])


    def _cmd(self, target, tpg):
        """
        Return targetcli command if configfs entry is not present
        """
        path = Runtime.path("{}/tpgt_{}".format(target, tpg))
        if not path:
            return ["targetcli", "/{}/{}".format(Runtime.fabric, 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 TPGCounter(object):
    """
    """

    def __init__(self):
        """
        Track counter
        """
        self.tpg = {}
        self.target = None

    def add(self, target):
        """
        Initializing
        """
        #if not target in self.tpg or not 'initiator' in entry:
        if not target in self.tpg:
            self.tpg[target] = 1
            self.target = target
            logging.debug("Initializing target {}".format(target))

    def value(self):
        """
        Return tpg value
        """
        return self.tpg[self.target]

    def next(self):
        """
        Increment for next target
        """
        self.tpg[self.target] += 1

class PortalIndex(object):
    """
    """

    def __init__(self):
        """
        """
        self.portals = {}

        self.target = None
        self.image = None
        self.portal = None

    def add(self, target, image):
        """
        """
        if not target in self.portals:
            self.portals[target] = {}
            self.target = target
        if not image in self.portals[target]:
            self.portals[target][image] = {}
            self.image = image

    def list(self):
        """
        """
        return self.portals[self.target][self.image]

    def index(self, value):
        """
        """
        self.portal = value

    def tpg(self, value):
        """
        """
        self.portals[self.target][self.image][self.portal] = value

    def find(self, name):
        """
        """
        for target, image, portal, tpg in self.entries():
            if (name == portal):
                return tpg 
        return False

    def entries(self):
        """
        """
        for target in self.portals.keys():
            for image in self.portals[target].keys():
                for portal in self.portals[target][image].keys():
                    yield(target, image, portal, 
                          self.portals[target][image][portal])

    #def __setattr__(self, name, value):
    #    if name == "portal":
    #        #self.portal = value
    #        object.__setattr__(self, name, value)
    #    if name == "tpg":
    #        self.portals[self.target][self.image][self.portal] = value
    #    if name == "portals":
    #        setattr(self, name, value)


class Portals(object):
    """
    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],
                        self._cmd(target,
                                  Runtime.tpg(target, image, portal),
                                  address)
                        logging.debug("Adding address {} to tpg {} under target {}".format(address, Runtime.tpg(target, image, portal), target))
        else:
            self._cmd(iqn({}), "1", "")


    def _entries(self):
        """
        Generator
        """
        for target in Runtime.targets():
            for image in Runtime.images(target):
                for portal in Runtime.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",
               "/{}/{}/tpg{}/portals".format(Runtime.fabric, target, tpg),
               "create", address]
        portal = Runtime.path("{}/tpgt_{}/np/{}*".
                              format(target, tpg, re.sub(r' ', ':', address)))
        if not portal:
            """
            targetcli creates a default portal that listens on all IPs.
            Delete this if the user has requested the creation of specific
            portals.
            """
            def_portal = Runtime.path("{}/tpgt_{}/np/0.0.0.0:3260".
                              format(target, tpg))
            if def_portal:
                popen(["targetcli",
                       "/{}/{}/tpg{}/portals".format(Runtime.fabric, target, tpg),
                       "delete", "0.0.0.0", "3260"])

            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(object):
    """
    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.images(target):
                    if image == entry['image']:
                        for portal in Runtime.portals(target, image):
                            tpg = str(Runtime.tpg(target, image, portal))

                            self._add_command(target, tpg, entry)
            else:
                tpg = str(Runtime.tpg(target, entry))
                self._add_command(target, tpg, entry)

    def _add_command(self, target, tpg, entry):
        """
        Append to existing commands if necessary
        """
        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 = Runtime.path("{}/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",
                   "/{}/{}/tpg{}/luns".format(Runtime.fabric, target, tpg),
                   "create", "/backstores/rbd/{}".format(image)]
        else:
            cmd = ["targetcli",
                   "/{}/{}/tpg{}/luns".format(Runtime.fabric, target, tpg),
                   "create", "/backstores/block/{}".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(object):
    """
    """

    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 'target' in gentry:
                if self._check_auth(gentry['target']):
                    self._check_initiator(entry, target, gentry['target'])
                    for image in Runtime.images(target):
                        if image == entry['image']:
                            for portal in Runtime.portals(target, image):
                                tpg = Runtime.tpg(target, image, portal)
                                self._add_command(target, tpg, entry)
            else:
                if self._check_auth(gentry['host']):
                    self._check_initiator(entry, target, gentry['host'])
                    tpg = str(Runtime.tpg(target, entry))
                    self._add_command(target, tpg, entry)

    def _check_auth(self, target_auth):
        """
        """
        return(find_auth(target_auth) == "acls" or
               find_auth(target_auth) == "tpg+identified")


    def _check_initiator(self, entry, target, target_auth):
        """
        """
        if not 'initiator' in entry:
            raise RuntimeError("Entry for target {} missing initiator for specified authentication {}".format(target, find_auth(target_auth)))


    def _add_command(self, target, tpg, 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 = Runtime.path("{}/tpgt_{}/lun/lun_*/*".format(target, tpg))
        for path in lun_path:
            if basename(os.path.realpath(path)) == image:
                return re.split("[/_]", path)[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 = Runtime.path("{}/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 = Runtime.path("{}/tpgt_{}/acls/{}/lun_{}".
                            format(target, tpg, initiator, lun))
        if not path:
            self.cmds.append(["targetcli",
                              "/{}/{}/tpg{}/acls/{}".format(Runtime.fabric,
                                                            target, tpg,
                                                            initiator),
                              "create", lun, lun])

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


class Acls(object):
    """
    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 self._check_auth(gentry['target']):
                    self._check_initiator(entry, target, gentry['target'])
                    for image in Runtime.images(target):
                        if image == entry['image']:
                            for portal in Runtime.portals(target, image):
                                tpg = str(Runtime.tpg(target, image, portal))
                                self._add_command(target, tpg, entry)
            else:
                if self._check_auth(gentry['host']):
                    self._check_initiator(entry, target, gentry['host'])
                    tpg = str(Runtime.tpg(target, entry))
                    self._add_command(target, tpg, entry)

    def _check_auth(self, target_auth):
        """
        """
        return(find_auth(target_auth) == "acls" or
               find_auth(target_auth) == "tpg+identified")

    def _check_initiator(self, entry, target, target_auth):
        """
        """
        if not 'initiator' in entry:
            raise RuntimeError("Entry for target " +
                               "{} ".format(target) +
                               "missing initiator for specified " +
                               "authentication " +
                               "{}".format(find_auth(target_auth)))

    def _add_command(self, target, tpg, 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 = Runtime.path("{}/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",
               "/{}/{}/tpg{}/acls".format(Runtime.fabric, 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(object):
    """
    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.targets():
                    if target == iqn(auth):
                        self.target = target
                        for image in Runtime.images(target):
                            for portal in Runtime.portals(target, image):
                                self.tpg = str(Runtime.tpg(target, image, portal))
                                self.auth = auth
                                self.select_auth()
                                self.cmds.extend(self.select_discovery())
        else:
            for target in Runtime.targets():
                self.target = target
                for image in Runtime.images(target):
                    for portal in Runtime.portals(target, image):
                        self.tpg = str(Runtime.tpg(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, tpg+identified, 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 = Runtime.path("{}/tpgt_{}/attrib".format(self.target, self.tpg))[0]
        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",
               "/{}/{}/tpg{}".format(Runtime.fabric, 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 = Runtime.path("discovery_auth")[0]
        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", "/{}".format(Runtime.fabric),
               "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 = Runtime.path("discovery_auth")[0]
        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", "/{}".format(Runtime.fabric), "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 = Runtime.path("discovery_auth")[0]
        enforce = open(path + "/enforce_discovery_auth").read().rstrip('\n')
        if enforce == "0":
            return []

        cmd = ["targetcli", "/{}".format(Runtime.fabric), "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 = Runtime.path("{}/tpgt_{}/auth".format(self.target, self.tpg))[0]
        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",
               "/{}/{}/tpg{}".format(Runtime.fabric, 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 = Runtime.path("{}/tpgt_{}/auth".format(self.target, self.tpg))[0]
        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",
               "/{}/{}/tpg{}".format(Runtime.fabric, 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 = Runtime.path("{}/tpgt_{}/attrib".format(self.target, self.tpg))[0]
        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",
                    "/{}/{}/tpg{}".format(Runtime.fabric,
                                          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",
                    "/{}/{}/tpg{}".format(Runtime.fabric, 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 = Runtime.path("{}/tpgt_{}/acls/{}/auth".
                            format(self.target,
                                   self.tpg, self.acl['initiator']))[0]
        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",
               "/{}/{}/tpg{}/acls/{}".format(Runtime.fabric, 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 = Runtime.path("{}/tpgt_{}/acls/{}/auth".
                            format(self.target, self.tpg,
                                   self.acl['initiator']))[0]
        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",
               "/{}/{}/tpg{}/acls/{}".format(Runtime.fabric, 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 = Runtime.path("{}/tpgt_{}/attrib".format(self.target, self.tpg))[0]
        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",
                "/{}/{}/tpg{}".format(Runtime.fabric, 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(Cluster())
    elif args.clear:
        configs.clear()
        if args.unmap:
            images = Images()
            images.unmap()
    elif args.unmap:
        images = Images()
        images.unmap()
    elif args.file:
        conn = Cluster()
        configs.wipe(conn)
        content = Content()
        content.read(args.file)
        content.save(conn)
    elif args.add:
        content = Content()
        content.read(args.add)
        content.save(Cluster())
    else:
        sections = {"pools": Pools(),
                    "portals": PortalSection(),
                    "targets": Targets(),
                    "authentications": Authentications()}
        gateways = Gateways(sections)
        if args.output:
            configs.retrieve(Cluster(), sections, gateways)
            configs.display()
        elif args.edit:
            conn = Cluster()
            configs.retrieve(conn, sections, gateways)
            content = Content()
            content.edit(args.editor)
            content.save(conn)
        elif args.local:
            gateways.hostonly()
            configs.retrieve(Cluster(), sections, gateways)
            configs.display()
        else:
            gateways.hostonly()
            configs.retrieve(Cluster(), sections, gateways)
            images = Images()
            images.map()
            backstores = Backstores("block")
            backstores.create()
            iscsi = Iscsi()
            iscsi.create()
            tpgs = TPGs(TPGCounter(), PortalIndex())
            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('-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 error:
            print error
            exit()
        except Exception as error:
            logging.error(error)
            exit(1)

