Salt PostgreSQL returner for obtaining visualization data and associated Salt module
diff --git a/_modules/saltresource.py b/_modules/saltresource.py
new file mode 100644
index 0000000..f9d0987
--- /dev/null
+++ b/_modules/saltresource.py
@@ -0,0 +1,247 @@
+from __future__ import absolute_import
+# Let's not allow PyLint complain about string substitution
+# pylint: disable=W1321,E1321
+
+# Import python libs
+import logging
+
+# Import Salt libs
+import salt.returners
+
+# Import third party libs
+try:
+    import psycopg2
+    import psycopg2.extras
+    HAS_POSTGRES = True
+except ImportError:
+    HAS_POSTGRES = False
+
+LOG = logging.getLogger(__name__)
+
+
+def __virtual__():
+    if not HAS_POSTGRES:
+        return False, 'Could not import saltresource module; psycopg2 is not installed.'
+    return 'saltresource'
+
+
+def _get_options(ret=None):
+    '''
+    Get the postgres options from salt.
+    '''
+    attrs = {'host': 'host',
+             'user': 'user',
+             'passwd': 'passwd',
+             'db': 'db',
+             'port': 'port'}
+
+    _options = salt.returners.get_returner_options('returner.postgres_graph_db',
+                                                   ret,
+                                                   attrs,
+                                                   __salt__=__salt__,
+                                                   __opts__=__opts__)
+    return _options
+
+
+def _get_conn(ret=None):
+    '''
+    Return a postgres connection.
+    '''
+    _options = _get_options(ret)
+
+    host = _options.get('host')
+    user = _options.get('user')
+    passwd = _options.get('passwd')
+    datab = _options.get('db')
+    port = _options.get('port')
+
+    return psycopg2.connect(
+            host=host,
+            user=user,
+            password=passwd,
+            database=datab,
+            port=port)
+
+
+def _close_conn(conn):
+    '''
+    Close the Postgres connection
+    '''
+    conn.commit()
+    conn.close()
+
+
+def graph_data(*args, **kwargs):
+    '''
+    Returns graph data for visualization app
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt '*' saltresource.graph_data
+ 
+    '''
+    conn = _get_conn()
+    cur_dict = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
+
+    cur_dict.execute('SELECT host, service, status FROM salt_resources')
+    resources_db = [dict(res) for res in cur_dict]
+    db_dict = {}
+
+    for resource in resources_db:
+        host = resource.get('host')
+        service = '.'.join(resource.get('service').split('.')[:2])
+        status = resource.get('status')
+
+        if db_dict.get(host, None):
+            if db_dict[host].get(service, None):
+                service_data = db_dict[host][service]
+                service_data.append(status)
+            else:
+                db_dict[host][service] = [status]
+        else:
+            db_dict[host] = {service: []}
+
+    graph = []
+    for host, services in db_dict.items():
+        for service, statuses in services.items():
+            status = 'unknown'
+            if 'failed' in statuses:
+                status = 'failed'
+            elif 'success' in statuses and not ('failed' in statuses or 'unknown' in statuses):
+                status = 'success'
+            datum = {'host': host, 'service': service, 'status': status}
+            graph.append(datum)
+
+    _close_conn(conn)
+
+    return {'graph': graph}
+
+
+def host_data(host, **kwargs):
+    '''
+    Returns data describing single host
+
+    CLI Examples:
+
+    .. code-block:: bash
+
+        salt-call saltresource.host_data '<minion_id>'
+ 
+    '''
+    conn = _get_conn()
+    cur_dict = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
+
+    sql = 'SELECT host, service, resource_id, last_ret, status FROM salt_resources WHERE host=%s'
+    cur_dict.execute(sql, (host,))
+    resources_db = [dict(res) for res in cur_dict]
+    db_dict = {}
+
+    for resource in resources_db:
+        host = resource.get('host')
+        service = '.'.join(resource.get('service').split('.')[:2])
+        status = resource.get('status')
+
+        if db_dict.get(host, None):
+            if db_dict[host].get(service, None):
+                service_data = db_dict[host][service]
+                service_data.append(status)
+            else:
+                db_dict[host][service] = [status]
+        else:
+            db_dict[host] = {service: []}
+
+    graph = []
+
+    for host, services in db_dict.items():
+        for service, statuses in services.items():
+            status = 'unknown'
+            if 'failed' in statuses:
+                status = 'failed'
+            elif 'success' in statuses and not ('failed' in statuses or 'unknown' in statuses):
+                status = 'success'
+            resources = [{'service': r.get('service', ''), 'resource_id': r.get('resource_id', ''), 'last_ret': r.get('last_ret', None), 'status': r.get('status', '')}
+                         for r
+                         in resources_db
+                         if r.get('service', '').startswith(service)]
+            datum = {'host': host, 'service': service, 'status': status, 'resources': resources}
+            graph.append(datum)
+
+    _close_conn(conn)
+
+    return {'graph': graph}
+
+
+def sync_db(*args, **kwargs):
+    conn = _get_conn()
+    cur = conn.cursor()
+
+    resources_sql = '''
+      CREATE TABLE IF NOT EXISTS salt_resources (
+        id            varchar(255) NOT NULL UNIQUE,
+        resource_id   varchar(255) NOT NULL,
+        host          varchar(255) NOT NULL,
+        service       varchar(255) NOT NULL,
+        module        varchar(50) NOT NULL,
+        fun           varchar(50) NOT NULL,
+        status        varchar(50) NOT NULL,
+        options       json NULL,
+        last_ret      text NULL,
+        alter_time    TIMESTAMP WITH TIME ZONE DEFAULT now()
+      );
+    '''
+    cur.execute(resources_sql)
+    conn.commit()
+
+    resources_meta_sql = '''
+      CREATE TABLE IF NOT EXISTS salt_resources_meta (
+        id           varchar(255) NOT NULL UNIQUE,
+        options      json NULL,
+        alter_time   TIMESTAMP WITH TIME ZONE DEFAULT now()
+      );
+    '''
+    cur.execute(resources_meta_sql)
+    _close_conn(conn)
+
+    return True
+
+
+def flush_db(*args, **kwargs):
+    conn = _get_conn()
+    cur = conn.cursor()
+    result = True
+
+    resources_sql = 'DELETE FROM salt_resources'
+    try:
+        cur.execute(resources_sql)
+        conn.commit()
+    except Exception as e:
+        LOG.warning(repr(e))
+        result = False
+
+    resources_meta_sql = 'DELETE FROM salt_resources_meta'
+    try:
+        cur.execute(resources_meta_sql)
+        _close_conn(conn)
+    except Exception as e:
+        LOG.warning(repr(e))
+        result = False
+
+    return result
+
+
+def destroy_db(*args, **kwargs):
+    conn = _get_conn()
+    cur = conn.cursor()
+
+    resources_sql = 'DROP TABLE IF EXISTS salt_resources;'
+    cur.execute(resources_sql)
+    conn.commit()
+
+    resources_meta_sql = 'DROP TABLE IF EXISTS salt_resources_meta;'
+    cur.execute(resources_meta_sql)
+    _close_conn(conn)
+
+    return True
+