From 04d7c31e31270f8200debfffc659da3b635c5442 Mon Sep 17 00:00:00 2001
From: andre keedy <andre.keedy@emc.com>
Date: Tue, 12 Jan 2016 10:51:36 -0500
Subject: [PATCH] first commit

---
 README.md                                |   7 +
 _50_admin_rackhd_panels.py               |  11 +
 install.sh                               |  33 +++
 rackhd/__init__.py                       |   0
 rackhd/forms.py                          | 152 +++++++++++++
 rackhd/json2html.py                      | 136 +++++++++++
 rackhd/panel.py                          |  19 ++
 rackhd/shovel.py                         |  79 +++++++
 rackhd/tables.py                         | 127 +++++++++++
 rackhd/templates/rackhd/_register.html   |  25 ++
 rackhd/templates/rackhd/_unregister.html |  25 ++
 rackhd/templates/rackhd/detail.html      |  11 +
 rackhd/templates/rackhd/events.html      |  12 +
 rackhd/templates/rackhd/index.html       |   7 +
 rackhd/templates/rackhd/register.html    |   7 +
 rackhd/templates/rackhd/unregister.html  |   7 +
 rackhd/tests.py                          |  19 ++
 rackhd/urls.py                           |  28 +++
 rackhd/views.py                          | 276 +++++++++++++++++++++++
 setup_openstack.md                       | 115 ++++++++++
 20 files changed, 1096 insertions(+)
 create mode 100644 README.md
 create mode 100644 _50_admin_rackhd_panels.py
 create mode 100644 install.sh
 create mode 100644 rackhd/__init__.py
 create mode 100644 rackhd/forms.py
 create mode 100644 rackhd/json2html.py
 create mode 100644 rackhd/panel.py
 create mode 100644 rackhd/shovel.py
 create mode 100644 rackhd/tables.py
 create mode 100644 rackhd/templates/rackhd/_register.html
 create mode 100644 rackhd/templates/rackhd/_unregister.html
 create mode 100644 rackhd/templates/rackhd/detail.html
 create mode 100644 rackhd/templates/rackhd/events.html
 create mode 100644 rackhd/templates/rackhd/index.html
 create mode 100644 rackhd/templates/rackhd/register.html
 create mode 100644 rackhd/templates/rackhd/unregister.html
 create mode 100644 rackhd/tests.py
 create mode 100644 rackhd/urls.py
 create mode 100644 rackhd/views.py
 create mode 100644 setup_openstack.md

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..efe3f77
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# RackHD plugin for OpenStack Horizon dashboard
+
+- [Configure Openstack to Boot Baremetal nodes](https://github.com/keedya/Shovel-horizon/blob/master/Horizon/setup_openstack.md)
+- git clone https://github.com/keedya/Shovel-horizon.git
+- cd Shovel-horizon/Horizon
+- sudo ./install.sh --url 'Shovel Url' --location 'Horizon Path'
+- sudo service apache2 restart
diff --git a/_50_admin_rackhd_panels.py b/_50_admin_rackhd_panels.py
new file mode 100644
index 0000000..39dc754
--- /dev/null
+++ b/_50_admin_rackhd_panels.py
@@ -0,0 +1,11 @@
+# The slug of the panel to be added to HORIZON_CONFIG. Required.
+PANEL = 'rackhd'
+# The slug of the dashboard the PANEL associated with. Required.
+PANEL_DASHBOARD = 'admin'
+
+# The slug of the panel group the PANEL is associated with.
+PANEL_GROUP = 'admin'
+
+# Python panel class of the PANEL to be added.
+ADD_PANEL = ('openstack_dashboard.dashboards.admin.'
+             'rackhd.panel.Rackhd')
\ No newline at end of file
diff --git a/install.sh b/install.sh
new file mode 100644
index 0000000..3e6879a
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+TEMP=`getopt -o u:l: --long url: --long location: -- "$@"`
+
+if [ $? != 0 ] ; then echo "Exit" ; exit 1 ; fi
+
+eval set -- "$TEMP"
+SHOVEL_URL=${SHOVEL_URL-}
+FILE_LOC=${FILE_LOC-}
+
+while true ; do
+        case "$1" in
+		-u | --url) SHOVEL_URL=$2 ;shift 2 ;;
+		-l | --location) FILE_LOC=$2;shift 2 ;; 
+		--) shift; break ;;
+		*) echo "Internal error!" ; exit 1 ;;
+        esac 
+done 
+echo "get shovel url: " $SHOVEL_URL 
+echo "get file location: " $FILE_LOC 
+if [ -z "$SHOVEL_URL" -o -z "$FILE_LOC" ] 
+then
+   echo "You must specify Shovel service URL(http://<ipaddr>)using --url <shovel-url>"
+   echo "and horizon location using --location <horizon path>"
+   exit 1
+fi
+
+#replace in shovel.py  SHOVEL_URL with the new url value
+sed -i "s|.*URI = .*|URI = \"$SHOVEL_URL\" + SHOVEL_BASE_API|g" rackhd/shovel.py
+#copy rackhd to horizon admin dashboard
+cp -r rackhd $FILE_LOC/openstack_dashboard/dashboards/admin
+#copy _50_admin_rackhd_panels.py to dashboard enabled
+cp _50_admin_rackhd_panels.py $FILE_LOC/openstack_dashboard/enabled
diff --git a/rackhd/__init__.py b/rackhd/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rackhd/forms.py b/rackhd/forms.py
new file mode 100644
index 0000000..c768d62
--- /dev/null
+++ b/rackhd/forms.py
@@ -0,0 +1,152 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import forms
+from horizon import messages
+
+from openstack_dashboard import api
+from openstack_dashboard.dashboards.admin.rackhd import shovel
+
+LOG = logging.getLogger(__name__)
+
+class RegisterForm(forms.SelfHandlingForm):
+    uuid = forms.Field(label=_('Node ID'), widget=forms.TextInput(attrs={'readonly':'readonly'}))
+    name = forms.CharField(max_length=255, label=_('Name'), required=True)
+    driver = forms.ChoiceField(label=_('Driver'), required=True, 
+              widget=forms.Select(attrs={'class': 'switchable','data-slug': 'driver'}))
+    kernel = forms.ChoiceField(label=_('Deploy Kernel'), required=True, 
+              widget=forms.Select(attrs={'class': 'switchable'}))
+    ramdisk = forms.ChoiceField(label=_('Deploy RAM Disk'), required=True, 
+              widget=forms.Select(attrs={'class': 'switchable'}))
+    port = forms.ChoiceField(label=_('Mac address'), required=True, 
+              widget=forms.Select(attrs={'class': 'switchable'}))
+    ipmihost = forms.CharField(required=False,
+            widget=forms.TextInput(attrs={'class': 'switched','data-switch-on': 'driver','data-driver-pxe_ipmitool': _('IPMI Host Address')}))
+    ipmiuser = forms.CharField(required=False,
+            widget=forms.TextInput(attrs={'class': 'switched','data-switch-on': 'driver','data-driver-pxe_ipmitool': _('IPMI Username')}))
+    ipmipass = forms.CharField(required=False, 
+            widget=forms.PasswordInput(attrs={'class': 'switched','data-switch-on': 'driver','data-driver-pxe_ipmitool': _('IPMI Password')}))
+
+    sshhost = forms.CharField(required=False,
+            widget=forms.TextInput(attrs={'class': 'switched','data-switch-on': 'driver','data-driver-pxe_ssh': _('SSH Host Address')}))
+    sshuser = forms.CharField(required=False,
+            widget=forms.TextInput(attrs={'class': 'switched','data-switch-on': 'driver','data-driver-pxe_ssh': _('SSH Username')}))
+    sshpass = forms.CharField(required=False,
+            widget=forms.PasswordInput(attrs={'class': 'switched','data-switch-on': 'driver','data-driver-pxe_ssh': _('SSH Password')}))
+    sshport = forms.CharField(required=False,
+            widget=forms.TextInput(attrs={'class': 'switched','data-switch-on': 'driver','data-driver-pxe_ssh': _('SSH Port')}))
+
+    failovernode = forms.ChoiceField(label=_("Failover Node"), required=False)
+    enfailover = forms.BooleanField(label=_("Enable Failover"), initial=False, required=False)
+    eventre = forms.CharField(max_length=255, label=_('Event Trigger (regex)'), required=False, initial='')
+
+    def __init__(self, request, *args, **kwargs):
+        super(RegisterForm, self).__init__(request, *args, **kwargs)
+        self._node = kwargs['initial'].get('node', None)
+        if self._node is not None:
+            self._drivers = kwargs['initial']['drivers']
+            self._ramdisk = kwargs['initial']['images']
+            self._macaddress = kwargs['initial']['ports']
+            self.fields['name'].initial = shovel.get_catalog_data_by_source(self._node['id'],'dmi')['System Information']['Product Name']
+            self.fields['uuid'].initial = self._node['id']
+            self.fields['driver'].choices = [ (elem,_(elem)) for elem in self._drivers ]
+            self.fields['kernel'].choices = [ (elem,_(elem)) for elem in self._ramdisk ]
+            self.fields['ramdisk'].choices = [ (elem,_(elem)) for elem in self._ramdisk ]
+            self.fields['port'].choices = [ (elem,_(elem)) for elem in self._macaddress]      
+            # BMC information initials
+            bmc = shovel.get_catalog_data_by_source(self._node['id'], 'bmc')
+            bmcuser = shovel.get_catalog_data_by_source(self._node['id'], 'ipmi-user-list-1')
+            self.fields['ipmihost'].initial = bmc['IP Address']
+            self.fields['ipmiuser'].initial = bmcuser['2']['']
+            
+            # Host network initials
+            host = shovel.get_catalog_data_by_source(self._node['id'], 'ohai')
+            self.fields['sshuser'].initial = host['current_user']
+            self.fields['sshhost'].initial = host['ipaddress']
+            self.fields['sshport'].initial = '22'
+
+            # Failover node initials
+            nodes = shovel.request_nodes_get()
+            self.fields['failovernode'].choices = [ (n['id'],_(n['id'])) for n in nodes if n['id'] != self._node['id'] ]
+        else:
+            redirect = reverse('horizon:admin:rackhd:index')
+            msg = 'Invalid node ID specified'
+            messages.error(request, _(msg))
+            raise ValueError(msg) 
+
+    def _add_new_node(self, request, data):
+        try:
+            # create node with shovel
+            #replace kernal and ramdisk with image id
+            list_images = shovel.get_images_list()['images']
+            for elem in list_images:
+                if data['ramdisk'] == elem['name']:
+                    data['ramdisk'] = elem['id']
+                if data['kernel'] == elem['name']:
+                    data['kernel'] = elem['id']
+            result = shovel.register_node_post(data)
+            if 'error_message' in result:
+                raise Exception(result) 
+            else:
+                msg = _('Registered node {0} ({1})'.format(data['uuid'], data['name']))
+                messages.success(request, msg)
+                return True
+        except Exception:
+            redirect = reverse('horizon:admin:rackhd:index')
+            msg = _('Failed to register baremetal node: {0} ({1})'.format(data['uuid'], data['name']))
+            messages.error(request, msg)
+            return False
+
+    def _check_failover(self, data):
+        if not data['enfailover']:
+            data.pop('failovernode', None)
+
+    def handle(self, request, data):
+        self._check_failover(data)
+        self._add_new_node(request, data)
+        return True
+
+
+class UnregisterForm(forms.SelfHandlingForm):
+    uuid = forms.CharField(max_length=255, label=_("Unregister Node"))
+    
+    def __init__(self, request, *args, **kwargs):
+        super(UnregisterForm, self).__init__(request, *args, **kwargs)
+        self._node = kwargs['initial']['node']
+        self.fields['uuid'].initial = self._node['id']     
+
+    def _remove_current_node(self, request, data):
+        try:            
+            # unregister a node from ironic using shovel
+            result = shovel.unregister_node_del(data['uuid'])
+            if 'result' in result:
+                msg = _('Unregistered node {0}'.format(data['uuid']))
+                messages.success(request, msg)
+                return True              
+            else:
+                raise Exception(result) 
+        except Exception:
+            redirect = reverse('horizon:admin:rackhd:index')
+            msg = _('Failed to unregister baremetal node: {0}'.format(data['uuid']))
+            messages.error(request, msg)
+            return False
+
+    def handle(self, request, data):
+        self._remove_current_node(request, data)
+        return True
+
diff --git a/rackhd/json2html.py b/rackhd/json2html.py
new file mode 100644
index 0000000..64d3b63
--- /dev/null
+++ b/rackhd/json2html.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+
+'''
+JSON 2 HTML convertor
+=====================
+(c) Varun Malhotra 2013
+Source Code: https://github.com/softvar/json2html
+Contributors:
+-------------
+1. Michel M�ller(@muellermichel), patch #2 - https://github.com/softvar/json2html/pull/2
+LICENSE: MIT
+--------
+'''
+
+import json
+import ordereddict
+
+class JSON:
+
+	def convert(self, **args):
+		'''
+		convert json Object to HTML Table format
+		'''
+
+		# table attributes such as class
+		# eg: table_attributes = "class = 'sortable table table-condensed table-bordered table-hover'
+		global table_attributes
+		table_attributes = ''
+
+		if 'table_attributes' in args:
+			table_attributes = args['table_attributes']
+		else:
+			# by default HTML table border
+			table_attributes = 'border="1"'
+
+		if 'json' in args:
+			self.json_input = args['json']
+			try:
+				json.loads(self.json_input)
+			except:
+				self.json_input = json.dumps(self.json_input)
+		else:
+			raise Exception('Can\'t convert NULL!')
+
+
+		ordered_json = json.loads(self.json_input, object_pairs_hook=ordereddict.OrderedDict)
+
+		return self.iterJson(ordered_json)
+
+
+	def columnHeadersFromListOfDicts(self, ordered_json):
+		'''
+		If suppose some key has array of objects and all the keys are same,
+		instead of creating a new row for each such entry, club those values,
+		thus it makes more sense and more readable code.
+		@example:
+			jsonObject = {"sampleData": [ {"a":1, "b":2, "c":3}, {"a":5, "b":6, "c":7} ] }
+			OUTPUT:
+				<table border="1"><tr><th>1</th><td><table border="1"><tr><th>a</th><th>c</th><th>b</th></tr><tr><td>1</td><td>3</td><td>2</td></tr><tr><td>5</td><td>7</td><td>6</td></tr></table></td></tr></table>
+		@contributed by: @muellermichel
+		'''
+
+		if len(ordered_json) < 2:
+			return None
+		if not isinstance(ordered_json[0],dict):
+			return None
+
+		column_headers = ordered_json[0].keys()
+
+		for entry in ordered_json:
+			if not isinstance(entry,dict):
+				return None
+			if len(entry.keys()) != len(column_headers):
+				return None
+			for header in column_headers:
+				if not header in entry:
+					return None
+		return column_headers
+
+
+	def iterJson(self, ordered_json):
+		'''
+		Iterate over the JSON and process it to generate the super awesome HTML Table format
+		'''
+
+		def markup(entry, parent_is_list = False):
+			'''
+			Check for each value corresponding to its key and return accordingly
+			'''
+			if(isinstance(entry,unicode)):
+				return unicode(entry)
+			if(isinstance(entry,int) or isinstance(entry,float)):
+				return str(entry)
+			if(parent_is_list and isinstance(entry,list)==True):
+				#list of lists are not accepted
+				return ''
+			if(isinstance(entry,list)==True) and len(entry) == 0:
+				return ''
+			if(isinstance(entry,list)==True):
+				return '<ul><li>' + '</li><li>'.join([markup(child, parent_is_list=True) for child in entry]) + '</li></ul>'
+			if(isinstance(entry,dict)==True):
+				return self.iterJson(entry)
+
+			#safety: don't do recursion over anything that we don't know about - iteritems() will most probably fail
+			return ''
+
+		convertedOutput = ''
+
+		global table_attributes
+		table_init_markup = "<table %s>" %(table_attributes)
+		convertedOutput = convertedOutput + table_init_markup
+
+		for k,v in ordered_json.iteritems():
+			convertedOutput = convertedOutput + '<tr>'
+			convertedOutput = convertedOutput + '<th>'+ str(k) +'</th>'
+
+			if (v == None):
+				v = unicode("")
+			if(isinstance(v,list)):
+				column_headers = self.columnHeadersFromListOfDicts(v)
+				if column_headers != None:
+					convertedOutput = convertedOutput + '<td>'
+					convertedOutput = convertedOutput + table_init_markup
+					convertedOutput = convertedOutput + '<tr><th>' + '</th><th>'.join(column_headers) + '</th></tr>'
+					for list_entry in v:
+						convertedOutput = convertedOutput + '<tr><td>' + '</td><td>'.join([markup(list_entry[column_header]) for column_header in column_headers]) + '</td></tr>'
+					convertedOutput = convertedOutput + '</table>'
+					convertedOutput = convertedOutput + '</td>'
+					convertedOutput = convertedOutput + '</tr>'
+					continue
+			convertedOutput = convertedOutput + '<td>' + markup(v) + '</td>'
+			convertedOutput = convertedOutput + '</tr>'
+		convertedOutput = convertedOutput + '</table>'
+		return convertedOutput
+
+json2html = JSON()
\ No newline at end of file
diff --git a/rackhd/panel.py b/rackhd/panel.py
new file mode 100644
index 0000000..6ff4dc7
--- /dev/null
+++ b/rackhd/panel.py
@@ -0,0 +1,19 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+import horizon
+class Rackhd(horizon.Panel):
+    name = _("RackHD")
+    slug = "rackhd"
+    permissions = ('openstack.roles.admin',)
\ No newline at end of file
diff --git a/rackhd/shovel.py b/rackhd/shovel.py
new file mode 100644
index 0000000..fd87dde
--- /dev/null
+++ b/rackhd/shovel.py
@@ -0,0 +1,79 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import logging
+import json
+import requests 
+
+
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+
+LOG = logging.getLogger(__name__)
+
+SHOVEL_BASE_API = '/api/1.1'
+URI = SHOVEL_URL + SHOVEL_BASE_API
+
+def get_driver_list():
+    r = requests.get(URI + '/ironic/drivers')
+    return r.json()
+
+def get_images_list():
+    r = requests.get(URI + '/glance/images')
+    return r.json()
+
+def get_ironic_nodes():
+    r = requests.get(URI + '/ironic/nodes')
+    return r.json()
+
+def get_ironic_node(id):
+    r = requests.get(URI + '/ironic/nodes/' + id)
+    return r.json()
+
+def request_node_get(id):
+    r = requests.get(URI + '/nodes/' + id)
+    return r.json()
+
+def request_nodes_get():
+    r = requests.get(URI + '/nodes')
+    return r.json()
+
+def register_node_post(data):
+    headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
+    r = requests.post(URI + '/register',
+            data=json.dumps(data), headers=headers)
+    return r.json()
+
+def unregister_node_del(name):
+    headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
+    data = {}
+    r = requests.delete(URI + '/unregister/' + name,
+                        data=json.dumps(data), headers=headers)
+    return r.json()    
+
+def request_catalog_get(uuid):
+    r = requests.get(URI+ '/catalogs/' + uuid)
+    return r.json()
+
+def get_catalog_data_by_source(id,source):
+    r = requests.get(URI+ '/catalogs/' + id + '/' + source)
+    return r.json()['data']
+
+def get_current_sel_data(id):
+    r = requests.get(URI+ '/nodes/' + id + '/sel')
+    return r.json()
+
+def node_patch(uuid, data):
+    headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
+    r = requests.patch(URI + '/ironic/node/' + uuid,
+            data=json.dumps(data), headers=headers)
+    return r.json()
diff --git a/rackhd/tables.py b/rackhd/tables.py
new file mode 100644
index 0000000..fd416b0
--- /dev/null
+++ b/rackhd/tables.py
@@ -0,0 +1,127 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.core.urlresolvers import reverse
+from django.template import defaultfilters as filters
+from django.utils.translation import pgettext_lazy
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext_lazy
+
+from horizon import tables
+from horizon.utils import filters as utils_filters
+
+from openstack_dashboard import api
+from openstack_dashboard import policy
+
+class RegisterSelectedNodes(tables.LinkAction):
+    name = "register_selected"
+    verbose_name = _("Register Selected")
+    icon = "plus"
+    classes = ("ajax-modal",)
+    url = "horizon:admin:rackhd:register"
+    def get_link_url(self, datum=None, *args, **kwargs):
+        return reverse(self.url)
+
+
+class UnregisterSelectedNodes(tables.LinkAction):
+    name = "unregister_selected"
+    verbose_name = _("Unegister Selected")
+    icon = "minus"
+    classes = ("ajax-modal",)
+    url = "horizon:admin:rackhd:unregister"
+    def get_link_url(self, datum=None, *args, **kwargs):
+        return reverse(self.url)
+
+
+class RegisterNode(tables.LinkAction):
+    name = "register"
+    verbose_name = _("Register")
+    icon = "plus"
+    classes = ("ajax-modal",)
+    url = "horizon:admin:rackhd:register"
+
+
+class Failover(tables.LinkAction):
+    name = "failover"
+    verbose_name = _("Failover")
+    icon = "minus"
+    classes = ("ajax-modal",)
+    url = "horizon:admin:rackhd:failover"
+
+
+class UnregisterNode(tables.LinkAction):
+    name = "unregister"
+    verbose_name = _("Unregister")
+    icon = "minus"
+    classes = ("ajax-modal",)
+    url = "horizon:admin:rackhd:unregister"
+
+
+class BareMetalFilterAction(tables.FilterAction):
+    def filter(self, table, services, filter_string):
+        q = filter_string.lower()
+        return filter(lambda service: q in service.host.lower(), services)
+
+
+class BareMetalDetailsTable(tables.DataTable):
+    catalog = tables.Column("catalog", verbose_name=_("Node Catalog"), filters=[filters.safe])
+    class Meta(object):
+        name = "node_catalog"
+        verbose_name = _("Catalog")
+
+
+class BareMetalLastEventTable(tables.DataTable):
+    date = tables.Column('date',verbose_name=_("Date"))
+    event = tables.Column('event',verbose_name=_("Event"))
+    logid = tables.Column('logid',verbose_name=_("Log ID"))
+    sensor_num = tables.Column('sensor_num',verbose_name=_("Sensor Number"))
+    sensor_type = tables.Column('sensor_type',verbose_name=_("Sensor Type"))
+    time = tables.Column('time',verbose_name=_("Time"))
+    value = tables.Column('value',verbose_name=_("Value"))
+    class Meta(object):
+        name = "lastevent"
+        hidden_title=False
+        verbose_name = _("Last Triggered")
+        row_actions = (Failover,UnregisterNode,)
+
+
+class BareMetalAllEventsTable(tables.DataTable):
+    if False:
+        date = tables.Column('date',verbose_name=_("Date"))
+        event = tables.Column('event',verbose_name=_("Event"))
+        logid = tables.Column('logid',verbose_name=_("Log ID"))
+        sensor_num = tables.Column('sensor_num',verbose_name=_("Sensor Number"))
+        sensor_type = tables.Column('sensor_type',verbose_name=_("Sensor Type"))
+        time = tables.Column('time',verbose_name=_("Time"))
+        value = tables.Column('value',verbose_name=_("Value"))
+    else:
+        html = tables.Column('html',verbose_name=_("System Events"), filters=[filters.safe])
+
+    class Meta(object):
+        name = "allevents"
+        hidden_title=False
+        verbose_name = _("All Events")
+
+
+class BareMetalTable(tables.DataTable):
+    name = tables.Column('name', verbose_name=_('Name'), link="horizon:admin:rackhd:detail", )
+    uuid = tables.Column('uuid', verbose_name=_('Node ID') )
+    hwaddr = tables.Column('hwaddr', verbose_name=_('MAC Address') )
+    events = tables.Column('events', verbose_name=_('Events'), link="horizon:admin:rackhd:events" )
+    state = tables.Column('state', verbose_name=_('State'))
+    class Meta(object):
+        name = "baremetal"
+        verbose_name = _("Baremetal Compute Nodes")
+        table_actions = (BareMetalFilterAction,)
+        multi_select = False
+        row_actions = (RegisterNode, UnregisterNode,)
+
diff --git a/rackhd/templates/rackhd/_register.html b/rackhd/templates/rackhd/_register.html
new file mode 100644
index 0000000..813308e
--- /dev/null
+++ b/rackhd/templates/rackhd/_register.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}register_form{% endblock %}
+{% block form_action %}{% url 'horizon:admin:rackhd:register' baremetal %}{% endblock %}
+
+{% block modal-header %}{% trans "Register Node" %}{% endblock %}
+
+{% block modal-body %}
+<div class="left">
+    <fieldset>
+    {% include "horizon/common/_form_fields.html" %}
+    </fieldset>
+</div>
+<div class="right">
+    <h3>{% trans "Description:" %}</h3>
+    <p>{% trans "Register bare metal node." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+  <input class="btn btn-primary pull-right" type="submit" value="{% trans "Register Node" %}" />
+  <a href="{% url 'horizon:admin:hypervisors:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/rackhd/templates/rackhd/_unregister.html b/rackhd/templates/rackhd/_unregister.html
new file mode 100644
index 0000000..3574f6a
--- /dev/null
+++ b/rackhd/templates/rackhd/_unregister.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}unregister_form{% endblock %}
+{% block form_action %}{% url 'horizon:admin:rackhd:unregister' baremetal %}{% endblock %}
+
+{% block modal-header %}{% trans "Unregister Node" %}{% endblock %}
+
+{% block modal-body %}
+<div class="left">
+    <fieldset>
+    {% include "horizon/common/_form_fields.html" %}
+    </fieldset>
+</div>
+<div class="right">
+    <h3>{% trans "Description:" %}</h3>
+    <p>{% trans "Unegister bare metal node." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+  <input class="btn btn-primary pull-right" type="submit" value="{% trans "Unregister Node" %}" />
+  <a href="{% url 'horizon:admin:hypervisors:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/rackhd/templates/rackhd/detail.html b/rackhd/templates/rackhd/detail.html
new file mode 100644
index 0000000..47d807d
--- /dev/null
+++ b/rackhd/templates/rackhd/detail.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Node Details" %}{% endblock %}
+
+{% block main %}
+<div class="row">
+  <div class="col-sm-12">
+  {{ table.render }}
+  </div>
+</div>
+{% endblock %}
diff --git a/rackhd/templates/rackhd/events.html b/rackhd/templates/rackhd/events.html
new file mode 100644
index 0000000..92431fa
--- /dev/null
+++ b/rackhd/templates/rackhd/events.html
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Node Events" %}{% endblock %}
+
+{% block main %}
+  <div id="last-event">
+      {{ lastevent_table.render }}
+  </div>
+  <div id="all-events">
+      {{ allevents_table.render }}
+  </div>
+{% endblock %}
diff --git a/rackhd/templates/rackhd/index.html b/rackhd/templates/rackhd/index.html
new file mode 100644
index 0000000..593c66a
--- /dev/null
+++ b/rackhd/templates/rackhd/index.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Bare Metal" %}{% endblock %}
+
+{% block main %}
+  {{ table.render }}
+{% endblock %}
diff --git a/rackhd/templates/rackhd/register.html b/rackhd/templates/rackhd/register.html
new file mode 100644
index 0000000..e68ca42
--- /dev/null
+++ b/rackhd/templates/rackhd/register.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Register" %}{% endblock %}
+
+{% block main %}
+  {% include 'admin/rackhd/_register.html' %}
+{% endblock %}
diff --git a/rackhd/templates/rackhd/unregister.html b/rackhd/templates/rackhd/unregister.html
new file mode 100644
index 0000000..5dbd53e
--- /dev/null
+++ b/rackhd/templates/rackhd/unregister.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Unregister" %}{% endblock %}
+
+{% block main %}
+  {% include 'admin/rackhd/_unregister.html' %}
+{% endblock %}
diff --git a/rackhd/tests.py b/rackhd/tests.py
new file mode 100644
index 0000000..b9d7be7
--- /dev/null
+++ b/rackhd/tests.py
@@ -0,0 +1,19 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from horizon.test import helpers as test
+
+
+class RackhdTests(test.TestCase):
+    # Unit tests for rackhd.
+    def test_me(self):
+        self.assertTrue(1 + 1 == 2)
diff --git a/rackhd/urls.py b/rackhd/urls.py
new file mode 100644
index 0000000..990ece9
--- /dev/null
+++ b/rackhd/urls.py
@@ -0,0 +1,28 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls import patterns
+from django.conf.urls import url
+from django.conf.urls import include
+
+from openstack_dashboard.dashboards.admin.rackhd import views
+
+
+urlpatterns = patterns(
+    'openstack_dashboard.dashboards.admin.rackhd.views',
+    url(r'^$', views.IndexView.as_view(), name='index'),
+    url(r'^(?P<baremetal>[^/]+)/register$', views.RegisterView.as_view(), name='register'),
+    url(r'^(?P<baremetal>[^/]+)/unregister$', views.UnregisterView.as_view(), name='unregister'),
+    url(r'^(?P<baremetal>[^/]+)/detail$', views.BareMetalDetailView.as_view(), name='detail'),
+    url(r'^(?P<baremetal>[^/]+)/events$', views.BareMetalEventView.as_view(), name='events'),
+    url(r'^(?P<baremetal>[^/]+)/failover$', views.FailoverView.as_view(), name='failover'),
+)
\ No newline at end of file
diff --git a/rackhd/views.py b/rackhd/views.py
new file mode 100644
index 0000000..9f83b5a
--- /dev/null
+++ b/rackhd/views.py
@@ -0,0 +1,276 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import logging
+import json
+import pprint 
+
+from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import forms
+from horizon import tables
+from horizon import messages
+
+from openstack_dashboard import api
+
+from openstack_dashboard.dashboards.admin.rackhd \
+    import forms as baremetal_forms
+from openstack_dashboard.dashboards.admin.rackhd \
+    import tables as baremetal_tables
+    
+from openstack_dashboard.dashboards.admin.rackhd \
+    import json2html as j2h
+
+from openstack_dashboard.dashboards.admin.rackhd import shovel
+
+LOG = logging.getLogger(__name__)
+
+class IndexView(tables.DataTableView):
+    # A very simple class-based view...
+    table_class = baremetal_tables.BareMetalTable
+    template_name = "admin/rackhd/index.html"
+    page_title = _("Baremetal")
+
+    class NodeData:
+        def __init__(self, uuid, name, hwaddr, events, state):
+            self.id = uuid
+            self.name = name
+            self.uuid = uuid
+            self.hwaddr = hwaddr
+            self.events = events
+            self.state = state
+
+    def get_data(self):
+        data = []
+        try:
+            nodes = shovel.request_nodes_get()
+            i = 0
+            for n in nodes:
+                dmi = shovel.get_catalog_data_by_source(n['id'],'dmi')
+                name = dmi['System Information']['Product Name']
+                hwaddr = n['name'] 
+                id = n['id']
+                events = '0'
+                n = self._find_ironic_node(id)
+                if n is not None:
+                    events = n['extra'].get('eventcnt','0')
+                    state = 'Registered'
+                else:
+                    state = 'Unregistered'
+                i += i +1
+                data.append(self.NodeData(id, name, hwaddr, events, state))
+            return data
+        except  Exception, e:
+            print 
+            LOG.error("Excepton in get_baremetal_data():  {0}".format(e))
+            return data
+    def _find_ironic_node(self, id):
+        # ISSUE: iterating all nodes because query by name (onrack id) isn't working in ironic?
+        nodes = shovel.get_ironic_nodes()
+        for n in nodes['nodes']:
+            if n['extra'].get('nodeid', None) == id:
+                return n
+        return None
+
+class RegisterView(forms.ModalFormView):
+    context_object_name = 'baremetal'
+    template_name = 'admin/rackhd/register.html'
+    form_class = baremetal_forms.RegisterForm
+    success_url = reverse_lazy('horizon:admin:rackhd:index')
+    page_title = _("Register Node")
+
+    def get_context_data(self, **kwargs):
+        context = super(RegisterView, self).get_context_data(**kwargs)
+        context["baremetal"] = self.kwargs['baremetal']
+        return context
+
+    def get_initial(self):
+        id = self.kwargs['baremetal']
+        node = shovel.request_node_get(id)
+        list_drivers = shovel.get_driver_list()['drivers']
+        drivers = [ elem['name'] for elem in list_drivers ]
+        ports = str(node['name']).split(',')
+
+        list_images = shovel.get_images_list()['images']
+        images = [ elem['name'] for elem in list_images ]
+        initial = super(RegisterView, self).get_initial()
+        initial.update({'nodeid': self.kwargs['baremetal'], 'node': node, 'drivers': drivers,'images':images,'ports': ports})
+        return initial
+
+
+class UnregisterView(forms.ModalFormView):
+    context_object_name = 'baremetal'
+    template_name = 'admin/rackhd/unregister.html'
+    form_class = baremetal_forms.UnregisterForm
+    success_url = reverse_lazy('horizon:admin:rackhd:index')
+    page_title = _("Unegister Node")
+
+    def get_context_data(self, **kwargs):
+        context = super(UnregisterView, self).get_context_data(**kwargs)
+        context["baremetal"] = self.kwargs['baremetal']
+        return context
+
+    def get_initial(self):
+        id = self.kwargs['baremetal']
+        node = shovel.request_node_get(id)
+        initial = super(UnregisterView, self).get_initial()
+        initial.update({'nodeid': self.kwargs['baremetal'], 'node': node})
+        return initial
+
+
+class FailoverView(forms.ModalFormView):
+    context_object_name = 'baremetal'
+    template_name = 'admin/rackhd/register.html'
+    form_class = baremetal_forms.RegisterForm
+    success_url = reverse_lazy('horizon:admin:rackhd:index')
+    page_title = _("Failover")
+
+    def _find_ironic_node(self, id):
+        nodes = shovel.get_ironic_nodes()
+        for n in nodes['nodes']:
+            if n['extra'].get('nodeid', None) == id:
+                return n
+
+    def _remove_node(self,id):
+        try:            
+            result = shovel.unregister_node_del(id)
+            return True
+        except Exception:
+            redirect = reverse('horizon:admin:rackhd:index')
+            return False
+
+    def get_context_data(self, **kwargs):
+        context = super(FailoverView, self).get_context_data(**kwargs)
+        context["baremetal"] = self.kwargs['baremetal']
+        return context
+
+    def get_initial(self):
+        initial = super(FailoverView, self).get_initial()
+        current_id = self.kwargs['baremetal']
+        inode = self._find_ironic_node(current_id)
+        try:
+            if inode is not None:
+                failover = inode['extra'].get('failover', None)
+                if failover is not None:
+                    node = shovel.request_node_get(failover)
+                    list_drivers = shovel.get_driver_list()['drivers']
+                    drivers = [ elem['name'] for elem in list_drivers ]
+                    initial.update({'nodeid': self.kwargs['baremetal'], 'node': node, 'drivers': drivers}) 
+                else:
+                    raise ValueError('Failover node not set') 
+            else:
+                raise ValueError('Registered node not found') 
+        except ValueError as e:
+            redirect = reverse('horizon:admin:rackhd:index')
+            messages.error(self.request, _(e.message))
+            raise Exception(e.message)    
+        self._remove_node(current_id)
+        messages.success(self.request, _('Removed node {0}'.format(current_id)))
+        return initial
+
+
+class BareMetalDetailView(tables.DataTableView):
+    table_class = baremetal_tables.BareMetalDetailsTable
+    template_name = 'admin/rackhd/detail.html'
+    page_title = _('Details')
+    
+    class CatalogData:
+        def __init__(self, id, catalog):
+            self.id = id
+            self.catalog = catalog
+        
+    def get_data(self):
+        uuid = self.kwargs['baremetal']
+        dmi = shovel.get_catalog_data_by_source(id = uuid, source = 'dmi')
+        scsi = shovel.get_catalog_data_by_source(id = uuid, source = 'lsscsi')
+        del dmi['Processor Information'] # don't feel like rendering this now
+        dmi.update({'Storage Information' : scsi})
+        
+        catalog = json.dumps(dmi, sort_keys=True, indent=4, separators=(',', ': '))
+        data = [ self.CatalogData(id, j2h.json2html.convert( json = catalog, table_attributes="class=\"table-bordered table\"" )) ]
+        return data
+
+
+class BareMetalEventView(tables.MultiTableView):
+    table_classes = (baremetal_tables.BareMetalLastEventTable, 
+                     baremetal_tables.BareMetalAllEventsTable,)
+    template_name = 'admin/rackhd/events.html'
+    page_title = _('Events')
+    name = _("Events")
+    slug = "events"
+
+    class HTMLData:
+        def __init__(self, id, html):
+            self.id = id
+            self.html = html
+
+    class SELEventData:
+        def __init__(self, id, type, value, logid, number, time, date, event):
+            self.id = id
+            self.date = date
+            self.event = event
+            self.logid = logid
+            self.sensor_num = number
+            self.sensor_type = type
+            self.time = time
+            self.value = value
+    
+    def _find_ironic_node(self, id):
+        nodes = shovel.get_ironic_nodes()
+        for n in nodes['nodes']:
+            if n['extra'].get('nodeid', None) == id:
+                return n
+
+    def get_lastevent_data(self):
+        id = self.kwargs['baremetal']
+        try:
+            node = self._find_ironic_node(id)
+            if node is not None:
+                entry = node['extra']['events']
+                return [ self.SELEventData(id,
+                    entry['sensorType'],
+                    entry['value'], 
+                    str(int(entry['logId'], 16)), 
+                    entry['sensorNumber'],
+                    entry['time'],
+                    entry['date'],
+                    entry['event']) ]
+        except:
+            pass
+        return []
+
+    def get_allevents_data(self):
+        id = self.kwargs['baremetal']
+        try:      
+            sel = shovel.get_current_sel_data(id)[0].get('sel', [])
+        except KeyError as e:
+            redirect = reverse('horizon:admin:rackhd:index')
+            messages.error(self.request, _('No SEL data available, check node {0} poller task'.format(id)))
+            raise KeyError(e.message)
+        data = []
+        if False: # TODO: enable this, view is not iterating the data list correctly
+            for entry in sel:
+                data.append(self.SELEventData(id,
+                            entry['sensorType'],
+                            entry['value'], 
+                            str(int(entry['logId'], 16)), 
+                            entry['sensorNumber'],
+                            entry['time'],
+                            entry['date'],
+                            entry['event'] ))
+        else: # build html
+            rsel = list(reversed(sel))
+            j = json.dumps({"":rsel}, sort_keys=True, indent=4, separators=(',', ': '))
+            return [ self.HTMLData(id, j2h.json2html.convert(json=j, table_attributes="class=\"table\"")) ]
+        return data
\ No newline at end of file
diff --git a/setup_openstack.md b/setup_openstack.md
new file mode 100644
index 0000000..3d2460b
--- /dev/null
+++ b/setup_openstack.md
@@ -0,0 +1,115 @@
+# Configure Openstack to Boot Baremetal nodes Using Devstack
+
+## Download and install OpenStack using DevStack
+- git clone https://github.com/openstack-dev/devstack.git devstack
+- sudo ./devstack/tools/create-stack-user.sh
+- sudo su stack
+- cd ~
+- git clone https://github.com/openstack-dev/devstack.git devstack
+- cd Devstack
+- in Devstack, Create local.conf :
+  ```python
+  
+  [[local|localrc]]
+  #Enable Ironic API and Ironic Conductor
+  enable_service ironic
+  enable_service ir-api
+  enable_service ir-cond
+  #Enable Neutron which is required by Ironic and disable nova-network.
+  disable_service n-net
+  disable_service n-novnc
+  enable_service q-dhcp
+  enable_service q-svc
+  enable_service q-agt
+  enable_service q-l3
+  enable_service q-meta
+  enable_service neutron
+  #Optional, to enable tempest configuration as part of devstack
+  disable_service tempest
+  disable_service heat h-api h-api-cfn h-api-cw h-eng
+  disable_service cinder c-sch c-api c-vol
+  ADMIN_PASSWORD=root
+  DATABASE_PASSWORD=$ADMIN_PASSWORD
+  RABBIT_PASSWORD=$ADMIN_PASSWORD
+  SERVICE_PASSWORD=$ADMIN_PASSWORD
+  SERVICE_TOKEN=$ADMIN_PASSWORD
+  HOST_IP=172.31.128.7
+  #Create 3 virtual machines to pose as Ironic's baremetal nodes.
+  IRONIC_VM_COUNT=3
+  IRONIC_VM_SSH_PORT=22
+  IRONIC_BAREMETAL_BASIC_OPS=True
+  #The parameters below represent the minimum possible values to create
+  #functional nodes.
+  IRONIC_VM_SPECS_RAM=1024
+  IRONIC_VM_SPECS_DISK=10
+  #Size of the ephemeral partition in GB. Use 0 for no ephemeral partition.
+  IRONIC_VM_EPHEMERAL_DISK=0
+  VIRT_DRIVER=ironic
+  #By default, DevStack creates a 10.0.0.0/24 network for instances.
+  #If this overlaps with the hosts network, you may adjust with the
+  #following.
+  NETWORK_GATEWAY=10.1.0.1
+  FIXED_RANGE=10.1.0.0/24
+  FIXED_NETWORK_SIZE=256
+  #Neutron OVS (flat)
+  Q_PLUGIN=ml2
+  Q_AGENT_EXTRA_OVS_OPTS=(network_vlan_ranges=physnet1)
+  OVS_VLAN_RANGE=physnet1
+  PHYSICAL_NETWORK=physnet1
+  OVS_PHYSICAL_BRIDGE=br-eth2
+  #Log all output to files
+  LOGFILE=$HOME/devstack.log
+  SCREEN_LOGDIR=$HOME/logs
+  IRONIC_VM_LOG_DIR=$HOME/ironic-bm-logs
+  ```
+- Configure network Interface (assuming port eth2 is used to connect openstack to rackHD)
+ 
+![alt text](https://github.com/keedya/Shovel-horizon/blob/master/Shovel/snapshot/dev_config.PNG)
+
+- cat>>/etc/network/interfaces
+  ```python
+  
+  auto eth2
+  iface eth2 inet static
+  address 172.31.128.7
+  netmask 255.255.255.0
+  ```
+- Restart network service 
+   - sudo ifdown eth2
+   - sudo ifup eth2
+-  Run ./stack.sh
+
+## Configure Neutron
+
+Once the installation is completed, an external bridge can be setup for Neutron physical network
+
+- Bind eth2 to the external bridge:
+ - ovs-vsctl add-port br-eth2 eth2
+- Enable external network access under nested Open vSwitch
+ - ifconfig br-eth2 promisc up
+- Update external bridge configuration cat>>/etc/network/interfaces
+  ```python
+  
+  auto eth2
+  iface eth2 inet manual
+  auto br-eth2
+  iface br-eth2 inet static
+  address 172.31.128.7
+  netmask 255.255.255.0
+  ```
+- Restart network service 
+   - sudo ifdown br-eth2
+   - sudo ifup br-eth2
+
+- Create Flat netwok:
+  - Source ~/devstack/openrc admin admin
+  - neutron net-create flat-provider-network --shared --provider:network_type flat --  provider:physical_network physnet1
+  - neutron subnet-create --name flat-provider-subnet --gateway 172.31.128.7 --dns-nameserver 172.31.128.254 --allocation-pool start=172.31.128.100,end=172.31.128.150 flat-provider-network 172.31.128.0/24
+
+## Spawn an instance using nova service
+- Login the horizon interface (user:admin,password:root)
+- Use horizon to create new instances
+
+
+  
+