diff --git a/saltinventory.py b/saltinventory.py
new file mode 100755
index 0000000..7471207
--- /dev/null
+++ b/saltinventory.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+
+#
+# Requirements:
+#
+# * salt
+# * python-netaddr
+#
+
+import salt.client
+from netaddr import *
+from jinja2 import Template, BaseLoader, TemplateNotFound, FileSystemLoader, Environment
+
+# settings
+
+config = {
+'base_path': 'www/',
+'host_page_path': 'www/hosts/',
+'network_page_path': 'www/networks/',
+'template_path': 'templates/',
+}
+
+# class definitions
+
+class Inventorizer:
+ config = None
+
+ saltcmd = None
+
+ host_list = {}
+ network_list = {}
+
+ def __init__(self, config):
+ self.config = config
+ self.saltcmd = salt.client.LocalClient()
+ self.process()
+
+ def process(self):
+ self.collectHostData()
+ self.createHostList()
+ self.collectNetworkData()
+ self.createHostPages()
+ self.createNetworkList()
+ self.createNetworkPages()
+
+ def collectHostData(self):
+ self.host_list = self.saltcmd.cmd('*', 'grains.items')
+
+ def collectNetworkData(self):
+ result = self.saltcmd.cmd('*', 'network.interfaces')
+ for hostname, hostdata in result.iteritems():
+ for interface, interfacedata in hostdata.iteritems():
+
+ if interfacedata.has_key('inet'):
+ for netdata in interfacedata['inet']:
+ ip = IPNetwork(netdata['address'] + '/' + netdata['netmask'])
+ host = {'hostname': hostname, 'address': netdata['address'], 'netobj': ip}
+ self.addNetworkHost(ip, host)
+
+ if interfacedata.has_key('secondary'):
+ for netdata in interfacedata['secondary']:
+ ip = IPNetwork(netdata['address'] + '/' + netdata['netmask'])
+ host = {'hostname': hostname, 'address': netdata['address'], 'netobj': ip}
+ self.addNetworkHost(ip, host)
+
+ if interfacedata.has_key('inet6'):
+ for netdata in interfacedata['inet6']:
+ ip = IPNetwork(netdata['address'] + '/' + netdata['prefixlen'])
+ host = {'hostname': hostname, 'address': netdata['address'], 'netobj': ip}
+ self.addNetworkHost(ip, host)
+
+
+ def addNetworkHost(self, ip, host):
+ if self.network_list.has_key(ip.cidr):
+ self.network_list[ip.cidr].append(host)
+ else:
+ self.network_list[ip.cidr] = [host]
+
+ def createHostList(self):
+ fo = open(self.config['base_path'] + "hostlist.html", "wb")
+ env = Environment(loader = FileSystemLoader(config['template_path']))
+ template = env.get_template('hostlist_template.html')
+ fo.write(template.render({'hostlist': self.host_list}))
+ fo.close
+
+ def createHostPages(self):
+ for hostname, hostdata in self.host_list.iteritems():
+ self.createHostPage(hostname, hostdata)
+
+ def createHostPage(self, hostname, hostdata):
+ fo = open(self.config['host_page_path'] + hostname + ".html", "wb")
+ env = Environment(loader = FileSystemLoader(config['template_path']))
+ template = env.get_template('host_template.html')
+ fo.write(template.render(hostdata))
+ fo.close
+
+ def createNetworkList(self):
+ fo = open(self.config['base_path'] + "networklist.html", "wb")
+ env = Environment(loader = FileSystemLoader(config['template_path']))
+ env.filters['len'] = self.lenFilter
+ template = env.get_template('networklist_template.html')
+ fo.write(template.render({'networklist': self.network_list}))
+ fo.close
+
+ def createNetworkPages(self):
+ for network, networkdata in self.network_list.iteritems():
+ self.createNetworkPage(network, networkdata)
+
+ def createNetworkPage(self, network, networkdata):
+ fo = open(self.config['network_page_path'] + str(network.cidr).replace('/', '_') + ".html", "wb")
+ env = Environment(loader = FileSystemLoader(config['template_path']))
+ env.filters['replace'] = self.replaceFilter
+ template = env.get_template('network_template.html')
+ fo.write(template.render({'network': network, 'data': networkdata}))
+ fo.close
+
+ def lenFilter(self, list):
+ return len(list)
+
+ def replaceFilter(self, str, old, new):
+ return str(str).replace(old, new)
+
+# main program
+
+inv = Inventorizer(config)
+
diff --git a/templates/host_template.html b/templates/host_template.html
new file mode 100644
index 0000000..59eb696
--- /dev/null
+++ b/templates/host_template.html
@@ -0,0 +1,59 @@
+
+
+ {{ id }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ System information
+ Generic
+
+ - Operating System: {{ kernel }} ({{ kernelrelease }})
+ - Distribution: {{ lsb_distrib_description }}
+ - Machine type: {% if virtual != "physical" %} Virtual ({{ virtual }}) {% else %} Physical {% endif %}
+ - Architecture: {{ osarch }}
+ - Salt version: {{ saltversion }}
+
+ CPU
+
+ - Numer of CPUs: {{ num_cpus }}
+ - CPU architecture: {{ cpuarch }}
+ - CPU model: {{ cpu_model }}
+
+ Memory
+
+ - Total memory: {{ mem_total }}
+
+ Storage
+
+ Network
+
+
+
+
diff --git a/templates/hostlist_template.html b/templates/hostlist_template.html
new file mode 100644
index 0000000..8002c2f
--- /dev/null
+++ b/templates/hostlist_template.html
@@ -0,0 +1,137 @@
+
+
+
+ Host list
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hosts
+
+
+
+
+
+ Name |
+ OS |
+ RAM (MB) |
+ # CPUs |
+
+
+ Name
+ | OS
+ | RAM
+ | # CPUs
+ |
+
+
+ {% for name, data in hostlist.iteritems() %}
+
+ {{ name }} |
+ {{ data.lsb_distrib_description }} |
+ {{ data.mem_total }} |
+ {{ data.num_cpus }} |
+
+ {% endfor %}
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
diff --git a/templates/network_template.html b/templates/network_template.html
new file mode 100644
index 0000000..04383e7
--- /dev/null
+++ b/templates/network_template.html
@@ -0,0 +1,60 @@
+
+
+ {{ network.cidr }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Adresses
+
+
+
+ Address |
+ Host |
+
+
+ Address
+ | Host
+ |
+
+
+ {% for netdata in data %}
+
+ {{ netdata['address'] }} |
+ {{ netdata['hostname'] }} |
+
+ {% endfor %}
+
+
+
+ |
+ |
+
+
+
+
+
+
diff --git a/templates/networklist_template.html b/templates/networklist_template.html
new file mode 100644
index 0000000..150fa97
--- /dev/null
+++ b/templates/networklist_template.html
@@ -0,0 +1,67 @@
+
+
+
+ Network list
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Networks
+
+
+
+
+ Network |
+ Usage |
+
+
+ Network
+ | Usage
+ |
+
+
+ {% for network, data in networklist.iteritems() %}
+
+ {{ network.cidr }} |
+ {{ data|len }} |
+
+ {% endfor %}
+
+
+
+ |
+ |
+
+
+
+
+
+
+
diff --git a/www/res/jquery.number.min.js b/www/res/jquery.number.min.js
new file mode 100644
index 0000000..4fce02b
--- /dev/null
+++ b/www/res/jquery.number.min.js
@@ -0,0 +1,2 @@
+/*! jQuery number 2.1.5 (c) github.com/teamdf/jquery-number | opensource.teamdf.com/license */
+(function(e){"use strict";function t(e,t){if(this.createTextRange){var n=this.createTextRange();n.collapse(true);n.moveStart("character",e);n.moveEnd("character",t-e);n.select()}else if(this.setSelectionRange){this.focus();this.setSelectionRange(e,t)}}function n(e){var t=this.value.length;e=e.toLowerCase()=="start"?"Start":"End";if(document.selection){var n=document.selection.createRange(),r,i,s;r=n.duplicate();r.expand("textedit");r.setEndPoint("EndToEnd",n);i=r.text.length-n.text.length;s=i+n.text.length;return e=="Start"?i:s}else if(typeof this["selection"+e]!="undefined"){t=this["selection"+e]}return t}var r={codes:{46:127,188:44,109:45,190:46,191:47,192:96,220:92,222:39,221:93,219:91,173:45,187:61,186:59,189:45,110:46},shifts:{96:"~",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",48:")",45:"_",61:"+",91:"{",93:"}",92:"|",59:":",39:'"',44:"<",46:">",47:"?"}};e.fn.number=function(i,s,o,u){u=typeof u==="undefined"?",":u;o=typeof o==="undefined"?".":o;s=typeof s==="undefined"?0:s;var a="\\u"+("0000"+o.charCodeAt(0).toString(16)).slice(-4),f=new RegExp("[^"+a+"0-9]","g"),l=new RegExp(a,"g");if(i===true){if(this.is("input:text")){return this.on({"keydown.format":function(i){var a=e(this),f=a.data("numFormat"),l=i.keyCode?i.keyCode:i.which,c="",h=n.apply(this,["start"]),p=n.apply(this,["end"]),d="",v=false;if(r.codes.hasOwnProperty(l)){l=r.codes[l]}if(!i.shiftKey&&l>=65&&l<=90){l+=32}else if(!i.shiftKey&&l>=69&&l<=105){l-=48}else if(i.shiftKey&&r.shifts.hasOwnProperty(l)){c=r.shifts[l]}if(c=="")c=String.fromCharCode(l);if(l!=8&&l!=45&&l!=127&&c!=o&&!c.match(/[0-9]/)){var m=i.keyCode?i.keyCode:i.which;if(m==46||m==8||m==127||m==9||m==27||m==13||(m==65||m==82||m==80||m==83||m==70||m==72||m==66||m==74||m==84||m==90||m==61||m==173||m==48)&&(i.ctrlKey||i.metaKey)===true||(m==86||m==67||m==88)&&(i.ctrlKey||i.metaKey)===true||m>=35&&m<=39||m>=112&&m<=123){return}i.preventDefault();return false}if(h==0&&p==this.value.length||a.val()==0){if(l==8){h=p=1;this.value="";f.init=s>0?-1:0;f.c=s>0?-(s+1):0;t.apply(this,[0,0])}else if(c==o){h=p=1;this.value="0"+o+(new Array(s+1)).join("0");f.init=s>0?1:0;f.c=s>0?-(s+1):0}else if(l==45){h=p=2;this.value="-0"+o+(new Array(s+1)).join("0");f.init=s>0?1:0;f.c=s>0?-(s+1):0;t.apply(this,[2,2])}else{f.init=s>0?-1:0;f.c=s>0?-s:0}}else{f.c=p-this.value.length}f.isPartialSelection=h==p?false:true;if(s>0&&c==o&&h==this.value.length-s-1){f.c++;f.init=Math.max(0,f.init);i.preventDefault();v=this.value.length+f.c}else if(l==45&&(h!=0||this.value.indexOf("-")==0)){i.preventDefault()}else if(c==o){f.init=Math.max(0,f.init);i.preventDefault()}else if(s>0&&l==127&&h==this.value.length-s-1){i.preventDefault()}else if(s>0&&l==8&&h==this.value.length-s){i.preventDefault();f.c--;v=this.value.length+f.c}else if(s>0&&l==127&&h>this.value.length-s-1){if(this.value==="")return;if(this.value.slice(h,h+1)!="0"){d=this.value.slice(0,h)+"0"+this.value.slice(h+1);a.val(d)}i.preventDefault();v=this.value.length+f.c}else if(s>0&&l==8&&h>this.value.length-s){if(this.value==="")return;if(this.value.slice(h-1,h)!="0"){d=this.value.slice(0,h-1)+"0"+this.value.slice(h);a.val(d)}i.preventDefault();f.c--;v=this.value.length+f.c}else if(l==127&&this.value.slice(h,h+1)==u){i.preventDefault()}else if(l==8&&this.value.slice(h-1,h)==u){i.preventDefault();f.c--;v=this.value.length+f.c}else if(s>0&&h==p&&this.value.length>s+1&&h>this.value.length-s-1&&isFinite(+c)&&!i.metaKey&&!i.ctrlKey&&!i.altKey&&c.length===1){if(p===this.value.length){d=this.value.slice(0,h-1)}else{d=this.value.slice(0,h)+this.value.slice(h+1)}this.value=d;v=h}if(v!==false){t.apply(this,[v,v])}a.data("numFormat",f)},"keyup.format":function(r){var i=e(this),o=i.data("numFormat"),u=r.keyCode?r.keyCode:r.which,a=n.apply(this,["start"]),f=n.apply(this,["end"]),l;if(a===0&&f===0&&(u===189||u===109)){i.val("-"+i.val());a=1;o.c=1-this.value.length;o.init=1;i.data("numFormat",o);l=this.value.length+o.c;t.apply(this,[l,l])}if(this.value===""||(u<48||u>57)&&(u<96||u>105)&&u!==8&&u!==46&&u!==110)return;i.val(i.val());if(s>0){if(o.init<1){a=this.value.length-s-(o.init<0?1:0);o.c=a-this.value.length;o.init=1;i.data("numFormat",o)}else if(a>this.value.length-s&&u!=8){o.c++;i.data("numFormat",o)}}if(u==46&&!o.isPartialSelection){o.c++;i.data("numFormat",o)}l=this.value.length+o.c;t.apply(this,[l,l])},"paste.format":function(t){var n=e(this),r=t.originalEvent,i=null;if(window.clipboardData&&window.clipboardData.getData){i=window.clipboardData.getData("Text")}else if(r.clipboardData&&r.clipboardData.getData){i=r.clipboardData.getData("text/plain")}n.val(i);t.preventDefault();return false}}).each(function(){var t=e(this).data("numFormat",{c:-(s+1),decimals:s,thousands_sep:u,dec_point:o,regex_dec_num:f,regex_dec:l,init:this.value.indexOf(".")?true:false});if(this.value==="")return;t.val(t.val())})}else{return this.each(function(){var t=e(this),n=+t.text().replace(f,"").replace(l,".");t.number(!isFinite(n)?0:+n,s,o,u)})}}return this.text(e.number.apply(window,arguments))};var i=null,s=null;if(e.isPlainObject(e.valHooks.text)){if(e.isFunction(e.valHooks.text.get))i=e.valHooks.text.get;if(e.isFunction(e.valHooks.text.set))s=e.valHooks.text.set}else{e.valHooks.text={}}e.valHooks.text.get=function(t){var n=e(t),r,s,o=n.data("numFormat");if(!o){if(e.isFunction(i)){return i(t)}else{return undefined}}else{if(t.value==="")return"";r=+t.value.replace(o.regex_dec_num,"").replace(o.regex_dec,".");return(t.value.indexOf("-")===0?"-":"")+(isFinite(r)?r:0)}};e.valHooks.text.set=function(t,n){var r=e(t),i=r.data("numFormat");if(!i){if(e.isFunction(s)){return s(t,n)}else{return undefined}}else{var o=e.number(n,i.decimals,i.dec_point,i.thousands_sep);return t.value=o}};e.number=function(e,t,n,r){r=typeof r==="undefined"?",":r;n=typeof n==="undefined"?".":n;t=!isFinite(+t)?0:Math.abs(t);var i="\\u"+("0000"+n.charCodeAt(0).toString(16)).slice(-4);var s="\\u"+("0000"+r.charCodeAt(0).toString(16)).slice(-4);e=(e+"").replace(".",n).replace(new RegExp(s,"g"),"").replace(new RegExp(i,"g"),".").replace(new RegExp("[^0-9+-Ee.]","g"),"");var o=!isFinite(+e)?0:+e,u="",a=function(e,t){var n=Math.pow(10,t);return""+Math.round(e*n)/n};u=(t?a(o,t):""+Math.round(o)).split(".");if(u[0].length>3){u[0]=u[0].replace(/\B(?=(?:\d{3})+(?!\d))/g,r)}if((u[1]||"").length