Adjust Horizon plugin to register external functionality with a legit way of deployment

This commit is contained in:
keedya 2015-12-07 16:53:05 -05:00
parent 1767b1eb51
commit 9efdd2b4cf
23 changed files with 177 additions and 688 deletions

View File

@ -1,39 +1,6 @@
# OnRack plugin for OpenStack Horizon dashboard
On Shovel system:
- Set monorail:httpHost, ironic:httpHost and keystone:httpHost found in ./shovel/config.json
- Start shovel services:
cd ./shovel ; nodejs index.js
- On Horizon system:
Set SHOVEL_URL in ./horizon-shovel/openstack_dashboard/dashboards/admin/hypervisors/baremetal/shovel.py
Copy horizon-shovel contents to horizon:
cp ./horizon-shovel/* /opt/stack/horizon
- Restart Apache:
sudo service apache2 restart
- Enable OnRack event tasker:
Install Celery:
sudo pip install celery
Start celery beat service:
cd /opt/stack/horizon/ ; python manage.py celery worker -B -E
- Connect to Horizon dashboard URL and login
- Navigate to Admin -> System -> Hypervisors page
- Click on 'Bare Metal' tab
git clone https://github.com/keedya/Shovel-horizon.git
cd Shovel-horizon/Horizon
sudo ./install.sh --url <Shovel IP> --location <Horizon Path>
sudo service apache2 restart

33
Horizon/install.sh Executable file
View File

@ -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"
echo $TEMP
ADDR_IP=${ADDR_IP-}
FILE_LOC=${FILE_LOC-}
while true ; do
case "$1" in
-u | --url) ADDR_IP=$2 ;shift 2 ;;
-l | --location) FILE_LOC=$2;shift 2 ;;
--) shift; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
echo "get shovel url: " $ADDR_IP
echo "get file location: " $FILE_LOC
if [ -z "$ADDR_IP" -o -z "$FILE_LOC" ]
then
echo "You must specify ipaddr of shovel and horizon location"
exit 1
fi
#replace in shovel.py SHOVEL_URL with the new addre value
sed -i "s|.*URI = .*|URI = \"$ADDR_IP\" + 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

View File

@ -1,72 +0,0 @@
# 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
from horizon import tabs
import openstack_dashboard.api
from openstack_dashboard.dashboards.admin.hypervisors.baremetal import tables
from openstack_dashboard.dashboards.admin.hypervisors.baremetal import shovel
LOG = logging.getLogger(__name__)
class OnRackTab(tabs.TableTab):
table_classes = (tables.BareMetalTable,)
name = _("Bare Metal")
slug = "baremetal"
template_name = "horizon/common/_data_table.html"
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 _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
def get_baremetal_data(self):
data = []
try:
nodes = shovel.request_nodes_get()
for n in nodes:
if n['type'] in {'enclosure','switch'}:
continue
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'
data.append(self.NodeData(id, name, hwaddr, events, state))
return data
except Exception, e:
LOG.error("Excepton in get_baremetal_data(): {0}".format(e))
return data

View File

@ -1,46 +0,0 @@
# 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 _
from horizon import exceptions
from horizon import tabs
from openstack_dashboard.api import nova
from openstack_dashboard.dashboards.admin.hypervisors.compute \
import tabs as cmp_tabs
from openstack_dashboard.dashboards.admin.hypervisors.baremetal \
import tabs as baremetal_tabs
from openstack_dashboard.dashboards.admin.hypervisors import tables
class HypervisorTab(tabs.TableTab):
table_classes = (tables.AdminHypervisorsTable,)
name = _("Hypervisor")
slug = "hypervisor"
template_name = "horizon/common/_detail_table.html"
def get_hypervisors_data(self):
hypervisors = []
try:
hypervisors = nova.hypervisor_list(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve hypervisor information.'))
return hypervisors
class HypervisorHostTabs(tabs.TabGroup):
slug = "hypervisor_info"
tabs = (HypervisorTab, cmp_tabs.ComputeHostTab, baremetal_tabs.OnRackTab)
sticky = True

View File

@ -1,35 +0,0 @@
# Copyright 2013 B1 Systems GmbH
#
# 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 include
from django.conf.urls import patterns
from django.conf.urls import url
from openstack_dashboard.dashboards.admin.hypervisors.compute \
import urls as compute_urls
from openstack_dashboard.dashboards.admin.hypervisors.baremetal \
import urls as baremetal_urls
from openstack_dashboard.dashboards.admin.hypervisors import views
urlpatterns = patterns(
'openstack_dashboard.dashboards.admin.hypervisors.views',
url(r'^(?P<hypervisor>[^/]+)/$',
views.AdminDetailView.as_view(),
name='detail'),
url(r'^$', views.AdminIndexView.as_view(), name='index'),
url(r'', include(compute_urls, namespace='compute')),
url(r'', include(baremetal_urls, namespace='baremetal')),
)

View File

@ -1,91 +0,0 @@
from django.utils.translation import ugettext_lazy as _
from openstack_dashboard.dashboards.admin.hypervisors.baremetal import shovel
from openstack_dashboard.api import nova
from horizon import exceptions
from horizon import messages
from celery.decorators import task
from datetime import timedelta
import re
import logging
import json
LOG = logging.getLogger(__name__)
def log_sel_all(sel):
""" Simple SEL logger """
for entry in sel:
LOG.info(
'logId: {0}\n'
'sensorType: {1}\n'
'sensorNumber: {2}\n'
'event: {3}\n'
'asserted: {4}\n'
.format(entry['logId'],
entry['sensorType'],
entry['sensorNumber'],
entry['event'],
entry['value']))
return True
def log_sel_entry(entry):
""" Simple SEL entry logger """
LOG.info(
'logId: {0}\n'
'sensorType: {1}\n'
'sensorNumber: {2}\n'
'event: {3}\n'
'asserted: {4}\n'
.format(entry['logId'],
entry['sensorType'],
entry['sensorNumber'],
entry['event'],
entry['value']))
return True
def find_sel_entry_re(sel, regex):
""" Return a list of searched regex expressions in each SEL entry """
entry_list = []
for entry in sel:
if regex and regex.strip():
match = re.search(r""+regex+"", json.dumps(entry, ensure_ascii=True))
if match:
entry_list.append(entry)
return entry_list
def update_events(events, ecount, entry_list, uuid):
""" Update the ironic nodes extra metadata with new event match """
for entry in entry_list:
# update the latest event
if int(entry['time']) > int(events['time']):
LOG.info('update_event(): updating event for node {0} time:{1}'.format(uuid,entry['time']))
ecount += 1
p = json.loads( '[ {"path": "/extra/events", "value": ' + json.dumps(entry, ensure_ascii=True) + ', "op": "replace"}, {"path": "/extra/eventcnt", "value": ' + str(ecount) + ', "op": "replace"} ]' )
shovel.node_patch(uuid, p)
return True
@task()
def SELPoller():
""" Periodic task to poll for monorail SEL events """
nodes = shovel.get_ironic_nodes()
for n in nodes['nodes']:
extra = n['extra']
nodeid = extra.get('nodeid', None)
name = extra.get('name', None)
events = extra.get('events', None)
ecount = int(extra.get('eventcnt', 0))
if nodeid is not None:
failnode = extra.get('failover', None)
regex = extra.get('eventre', None)
if regex is not None:
sel = shovel.get_current_sel_data(nodeid)[0].get('sel', None)
if sel is not None:
update_events(events, ecount, find_sel_entry_re(sel, regex), n['uuid'])
return True

View File

@ -1,355 +0,0 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# 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 os
import sys
import warnings
import django
from django.utils.translation import ugettext_lazy as _
from openstack_dashboard import exceptions
from openstack_dashboard.static_settings import get_staticfiles_dirs # noqa
import djcelery
from datetime import timedelta
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/opt/stack/horizon/openstack_dashboard/static/db/horizon.db',
}
}
CELERYBEAT_SCHEDULE = {
"runs-every-5-seconds": {
"task": "openstack_dashboard.dashboards.admin.tasks.SELPoller",
"schedule": timedelta(seconds=5)
},
}
CELERY_ALWAYS_EAGER = False
CELERYBEAT_SCHEDULER = "djcelery.schedulers.DatabaseScheduler"
djcelery.setup_loader()
warnings.formatwarning = lambda message, category, *args, **kwargs: \
'%s: %s' % (category.__name__, message)
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
BIN_DIR = os.path.abspath(os.path.join(ROOT_PATH, '..', 'bin'))
if ROOT_PATH not in sys.path:
sys.path.append(ROOT_PATH)
DEBUG = False
TEMPLATE_DEBUG = DEBUG
SITE_BRANDING = 'OpenStack Dashboard'
WEBROOT = '/'
LOGIN_URL = None
LOGOUT_URL = None
LOGIN_REDIRECT_URL = None
ROOT_URLCONF = 'openstack_dashboard.urls'
HORIZON_CONFIG = {
'user_home': 'openstack_dashboard.views.get_user_home',
'ajax_queue_limit': 10,
'auto_fade_alerts': {
'delay': 3000,
'fade_duration': 1500,
'types': ['alert-success', 'alert-info']
},
'help_url': "http://docs.openstack.org",
'exceptions': {'recoverable': exceptions.RECOVERABLE,
'not_found': exceptions.NOT_FOUND,
'unauthorized': exceptions.UNAUTHORIZED},
'angular_modules': [],
'js_files': [],
'js_spec_files': [],
}
# Set to True to allow users to upload images to glance via Horizon server.
# When enabled, a file form field will appear on the create image form.
# See documentation for deployment considerations.
HORIZON_IMAGES_ALLOW_UPLOAD = True
# The OPENSTACK_IMAGE_BACKEND settings can be used to customize features
# in the OpenStack Dashboard related to the Image service, such as the list
# of supported image formats.
OPENSTACK_IMAGE_BACKEND = {
'image_formats': [
('', _('Select format')),
('aki', _('AKI - Amazon Kernel Image')),
('ami', _('AMI - Amazon Machine Image')),
('ari', _('ARI - Amazon Ramdisk Image')),
('docker', _('Docker')),
('iso', _('ISO - Optical Disk Image')),
('ova', _('OVA - Open Virtual Appliance')),
('qcow2', _('QCOW2 - QEMU Emulator')),
('raw', _('Raw')),
('vdi', _('VDI - Virtual Disk Image')),
('vhd', _('VHD - Virtual Hard Disk')),
('vmdk', _('VMDK - Virtual Machine Disk')),
]
}
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
)
if django.VERSION >= (1, 8, 0):
MIDDLEWARE_CLASSES += (
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',)
else:
MIDDLEWARE_CLASSES += ('django.middleware.doc.XViewMiddleware',)
MIDDLEWARE_CLASSES += (
'horizon.middleware.HorizonMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.request',
'django.core.context_processors.media',
'django.core.context_processors.static',
'django.contrib.messages.context_processors.messages',
'horizon.context_processors.horizon',
'openstack_dashboard.context_processors.openstack',
)
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
'horizon.loaders.TemplateLoader',
)
TEMPLATE_DIRS = (
os.path.join(ROOT_PATH, 'templates'),
)
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
COMPRESS_PRECOMPILERS = (
('text/scss', 'django_pyscss.compressor.DjangoScssFilter'),
)
COMPRESS_CSS_FILTERS = (
'compressor.filters.css_default.CssAbsoluteFilter',
)
COMPRESS_ENABLED = True
COMPRESS_OUTPUT_DIR = 'dashboard'
COMPRESS_CSS_HASHING_METHOD = 'hash'
COMPRESS_PARSER = 'compressor.parser.HtmlParser'
INSTALLED_APPS = [
'openstack_dashboard',
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'django_pyscss',
'openstack_dashboard.django_pyscss_fix',
'compressor',
'horizon',
'openstack_auth',
'periodically',
'djcelery',
]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
AUTHENTICATION_URLS = ['openstack_auth.urls']
MESSAGE_STORAGE = 'django.contrib.messages.storage.fallback.FallbackStorage'
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
SESSION_COOKIE_HTTPONLY = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_SECURE = False
SESSION_TIMEOUT = 1800
# A token can be near the end of validity when a page starts loading, and
# invalid during the rendering which can cause errors when a page load.
# TOKEN_TIMEOUT_MARGIN defines a time in seconds we retrieve from token
# validity to avoid this issue. You can adjust this time depending on the
# performance of the infrastructure.
TOKEN_TIMEOUT_MARGIN = 10
# When using cookie-based sessions, log error when the session cookie exceeds
# the following size (common browsers drop cookies above a certain size):
SESSION_COOKIE_MAX_SIZE = 4093
# when doing upgrades, it may be wise to stick to PickleSerializer
# NOTE(berendt): Check during the K-cycle if this variable can be removed.
# https://bugs.launchpad.net/horizon/+bug/1349463
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
LANGUAGES = (
('de', 'German'),
('en', 'English'),
('en-au', 'Australian English'),
('en-gb', 'British English'),
('es', 'Spanish'),
('fr', 'French'),
('hi', 'Hindi'),
('ja', 'Japanese'),
('ko', 'Korean (Korea)'),
('nl', 'Dutch (Netherlands)'),
('pl', 'Polish'),
('pt-br', 'Portuguese (Brazil)'),
('ru', 'Russian'),
('sr', 'Serbian'),
('zh-cn', 'Simplified Chinese'),
('zh-tw', 'Chinese (Taiwan)'),
)
LANGUAGE_CODE = 'en'
LANGUAGE_COOKIE_NAME = 'horizon_language'
USE_I18N = True
USE_L10N = True
USE_TZ = True
OPENSTACK_KEYSTONE_DEFAULT_ROLE = '_member_'
DEFAULT_EXCEPTION_REPORTER_FILTER = 'horizon.exceptions.HorizonReporterFilter'
POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf")
# Map of local copy of service policy files
POLICY_FILES = {
'identity': 'keystone_policy.json',
'compute': 'nova_policy.json',
'volume': 'cinder_policy.json',
'image': 'glance_policy.json',
'orchestration': 'heat_policy.json',
'network': 'neutron_policy.json',
'telemetry': 'ceilometer_policy.json',
}
SECRET_KEY = None
LOCAL_PATH = None
SECURITY_GROUP_RULES = {
'all_tcp': {
'name': _('All TCP'),
'ip_protocol': 'tcp',
'from_port': '1',
'to_port': '65535',
},
'all_udp': {
'name': _('All UDP'),
'ip_protocol': 'udp',
'from_port': '1',
'to_port': '65535',
},
'all_icmp': {
'name': _('All ICMP'),
'ip_protocol': 'icmp',
'from_port': '-1',
'to_port': '-1',
},
}
ADD_INSTALLED_APPS = []
# STATIC directory for custom theme, set as default.
# It can be overridden in local_settings.py
CUSTOM_THEME_PATH = 'static/themes/default'
try:
from local.local_settings import * # noqa
except ImportError:
logging.warning("No local_settings file found.")
if not WEBROOT.endswith('/'):
WEBROOT += '/'
if LOGIN_URL is None:
LOGIN_URL = WEBROOT + 'auth/login/'
if LOGOUT_URL is None:
LOGOUT_URL = WEBROOT + 'auth/logout/'
if LOGIN_REDIRECT_URL is None:
LOGIN_REDIRECT_URL = WEBROOT
MEDIA_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'media'))
MEDIA_URL = WEBROOT + 'media/'
STATIC_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'static'))
STATIC_URL = WEBROOT + 'static/'
STATICFILES_DIRS = get_staticfiles_dirs(WEBROOT)
CUSTOM_THEME = os.path.join(ROOT_PATH, CUSTOM_THEME_PATH)
STATICFILES_DIRS.append(
('custom', CUSTOM_THEME),
)
# Load the pluggable dashboard settings
import openstack_dashboard.enabled
import openstack_dashboard.local.enabled
from openstack_dashboard.utils import settings
INSTALLED_APPS = list(INSTALLED_APPS) # Make sure it's mutable
settings.update_dashboards(
[
openstack_dashboard.enabled,
openstack_dashboard.local.enabled,
],
HORIZON_CONFIG,
INSTALLED_APPS,
)
INSTALLED_APPS[0:0] = ADD_INSTALLED_APPS
# Ensure that we always have a SECRET_KEY set, even when no local_settings.py
# file is present. See local_settings.py.example for full documentation on the
# horizon.utils.secret_key module and its use.
if not SECRET_KEY:
if not LOCAL_PATH:
LOCAL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'local')
from horizon.utils import secret_key
SECRET_KEY = secret_key.generate_or_read_from_file(os.path.join(LOCAL_PATH,
'.secret_key_store'))
from openstack_dashboard import policy_backend
POLICY_CHECK_FUNCTION = policy_backend.check
# Add HORIZON_CONFIG to the context information for offline compression
COMPRESS_OFFLINE_CONTEXT = {
'STATIC_URL': STATIC_URL,
'HORIZON_CONFIG': HORIZON_CONFIG,
}
if DEBUG:
logging.basicConfig(level=logging.DEBUG)
# during django reloads and an active user is logged in, the monkey
# patch below will not otherwise be applied in time - resulting in developers
# appearing to be logged out. In typical production deployments this section
# below may be omitted, though it should not be harmful
from openstack_auth import utils as auth_utils
auth_utils.patch_middleware_get_user()

View File

@ -20,7 +20,7 @@ from horizon import forms
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.hypervisors.baremetal import shovel
from openstack_dashboard.dashboards.admin.rackhd import shovel
LOG = logging.getLogger(__name__)
@ -30,7 +30,7 @@ class RegisterForm(forms.SelfHandlingForm):
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'}))
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,
@ -50,11 +50,11 @@ class RegisterForm(forms.SelfHandlingForm):
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='')
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)
@ -65,7 +65,7 @@ class RegisterForm(forms.SelfHandlingForm):
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['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
@ -84,7 +84,7 @@ class RegisterForm(forms.SelfHandlingForm):
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:hypervisors:index')
redirect = reverse('horizon:admin:rackhd:index')
msg = 'Invalid node ID specified'
messages.error(request, _(msg))
raise ValueError(msg)
@ -107,7 +107,7 @@ class RegisterForm(forms.SelfHandlingForm):
messages.success(request, msg)
return True
except Exception:
redirect = reverse('horizon:admin:hypervisors:index')
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
@ -141,7 +141,7 @@ class UnregisterForm(forms.SelfHandlingForm):
else:
raise Exception(result)
except Exception:
redirect = reverse('horizon:admin:hypervisors:index')
redirect = reverse('horizon:admin:rackhd:index')
msg = _('Failed to unregister baremetal node: {0}'.format(data['uuid']))
messages.error(request, msg)
return False

19
Horizon/rackhd/panel.py Executable file
View File

@ -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',)

View File

@ -20,9 +20,8 @@ from horizon import exceptions
LOG = logging.getLogger(__name__)
SHOVEL_URL = 'http://10.240.19.192:9005'
SHOVEL_BASE_API = '/api/1.1'
URI = SHOVEL_URL + SHOVEL_BASE_API
URI = "http://10.240.19.171:9005" + SHOVEL_BASE_API
def get_driver_list():
r = requests.get(URI + '/ironic/drivers')

View File

@ -27,7 +27,7 @@ class RegisterSelectedNodes(tables.LinkAction):
verbose_name = _("Register Selected")
icon = "plus"
classes = ("ajax-modal",)
url = "horizon:admin:hypervisors:baremetal:register"
url = "horizon:admin:rackhd:register"
def get_link_url(self, datum=None, *args, **kwargs):
return reverse(self.url)
@ -37,7 +37,7 @@ class UnregisterSelectedNodes(tables.LinkAction):
verbose_name = _("Unegister Selected")
icon = "minus"
classes = ("ajax-modal",)
url = "horizon:admin:hypervisors:baremetal:unregister"
url = "horizon:admin:rackhd:unregister"
def get_link_url(self, datum=None, *args, **kwargs):
return reverse(self.url)
@ -47,7 +47,7 @@ class RegisterNode(tables.LinkAction):
verbose_name = _("Register")
icon = "plus"
classes = ("ajax-modal",)
url = "horizon:admin:hypervisors:baremetal:register"
url = "horizon:admin:rackhd:register"
class Failover(tables.LinkAction):
@ -55,7 +55,7 @@ class Failover(tables.LinkAction):
verbose_name = _("Failover")
icon = "minus"
classes = ("ajax-modal",)
url = "horizon:admin:hypervisors:baremetal:failover"
url = "horizon:admin:rackhd:failover"
class UnregisterNode(tables.LinkAction):
@ -63,7 +63,7 @@ class UnregisterNode(tables.LinkAction):
verbose_name = _("Unregister")
icon = "minus"
classes = ("ajax-modal",)
url = "horizon:admin:hypervisors:baremetal:unregister"
url = "horizon:admin:rackhd:unregister"
class BareMetalFilterAction(tables.FilterAction):
@ -113,14 +113,15 @@ class BareMetalAllEventsTable(tables.DataTable):
class BareMetalTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Name'), link="horizon:admin:hypervisors:baremetal:detail", )
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:hypervisors:baremetal:events" )
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 = _("RackHD")
verbose_name = _("Baremetal Compute Nodes")
table_actions = (BareMetalFilterAction,)
multi_select = False
row_actions = (RegisterNode, UnregisterNode,)

View File

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}register_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:hypervisors:baremetal:register' baremetal %}{% endblock %}
{% block form_action %}{% url 'horizon:admin:rackhd:register' baremetal %}{% endblock %}
{% block modal-header %}{% trans "Register Node" %}{% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}unregister_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:hypervisors:baremetal:unregister' baremetal %}{% endblock %}
{% block form_action %}{% url 'horizon:admin:rackhd:unregister' baremetal %}{% endblock %}
{% block modal-header %}{% trans "Unregister Node" %}{% endblock %}

View File

@ -3,10 +3,10 @@
{% block title %}{% trans "Node Events" %}{% endblock %}
{% block main %}
<div id="last-event">
{{ lastevent_table.render }}
</div>
<div id="all-events">
{{ allevents_table.render }}
<div id="last-event">
{{ lastevent_table.render }}
</div>
<div id="all-events">
{{ allevents_table.render }}
</div>
{% endblock %}

View File

@ -3,5 +3,5 @@
{% block title %}{% trans "Register" %}{% endblock %}
{% block main %}
{% include 'admin/hypervisors/baremetal/_register.html' %}
{% include 'admin/rackhd/_register.html' %}
{% endblock %}

View File

@ -3,5 +3,5 @@
{% block title %}{% trans "Unregister" %}{% endblock %}
{% block main %}
{% include 'admin/hypervisors/baremetal/_unregister.html' %}
{% include 'admin/rackhd/_unregister.html' %}
{% endblock %}

19
Horizon/rackhd/tests.py Executable file
View File

@ -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)

View File

@ -2,7 +2,7 @@
# 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
# 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
@ -12,14 +12,17 @@
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
from openstack_dashboard.dashboards.admin.hypervisors.baremetal import views
urlpatterns = patterns(
'openstack_dashboard.dashboards.admin.hypervisors.baremetal.views',
'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'),
)
)

View File

@ -2,14 +2,13 @@
# 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
# 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
@ -24,23 +23,71 @@ from horizon import tables
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.hypervisors.baremetal \
from openstack_dashboard.dashboards.admin.rackhd \
import forms as baremetal_forms
from openstack_dashboard.dashboards.admin.hypervisors.baremetal \
from openstack_dashboard.dashboards.admin.rackhd \
import tables as baremetal_tables
from openstack_dashboard.dashboards.admin.hypervisors.baremetal import shovel
from openstack_dashboard.dashboards.admin.hypervisors.baremetal \
from openstack_dashboard.dashboards.admin.rackhd \
import json2html as j2h
import json
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/hypervisors/baremetal/register.html'
template_name = 'admin/rackhd/register.html'
form_class = baremetal_forms.RegisterForm
success_url = reverse_lazy('horizon:admin:hypervisors:index')
success_url = reverse_lazy('horizon:admin:rackhd:index')
page_title = _("Register Node")
def get_context_data(self, **kwargs):
@ -64,9 +111,9 @@ class RegisterView(forms.ModalFormView):
class UnregisterView(forms.ModalFormView):
context_object_name = 'baremetal'
template_name = 'admin/hypervisors/baremetal/unregister.html'
template_name = 'admin/rackhd/unregister.html'
form_class = baremetal_forms.UnregisterForm
success_url = reverse_lazy('horizon:admin:hypervisors:index')
success_url = reverse_lazy('horizon:admin:rackhd:index')
page_title = _("Unegister Node")
def get_context_data(self, **kwargs):
@ -84,9 +131,9 @@ class UnregisterView(forms.ModalFormView):
class FailoverView(forms.ModalFormView):
context_object_name = 'baremetal'
template_name = 'admin/hypervisors/baremetal/register.html'
template_name = 'admin/rackhd/register.html'
form_class = baremetal_forms.RegisterForm
success_url = reverse_lazy('horizon:admin:hypervisors:index')
success_url = reverse_lazy('horizon:admin:rackhd:index')
page_title = _("Failover")
def _find_ironic_node(self, id):
@ -100,7 +147,7 @@ class FailoverView(forms.ModalFormView):
result = shovel.unregister_node_del(id)
return True
except Exception:
redirect = reverse('horizon:admin:hypervisors:index')
redirect = reverse('horizon:admin:rackhd:index')
return False
def get_context_data(self, **kwargs):
@ -125,7 +172,7 @@ class FailoverView(forms.ModalFormView):
else:
raise ValueError('Registered node not found')
except ValueError as e:
redirect = reverse('horizon:admin:hypervisors:index')
redirect = reverse('horizon:admin:rackhd:index')
messages.error(self.request, _(e.message))
raise Exception(e.message)
self._remove_node(current_id)
@ -135,7 +182,7 @@ class FailoverView(forms.ModalFormView):
class BareMetalDetailView(tables.DataTableView):
table_class = baremetal_tables.BareMetalDetailsTable
template_name = 'admin/hypervisors/baremetal/detail.html'
template_name = 'admin/rackhd/detail.html'
page_title = _('Details')
class CatalogData:
@ -158,7 +205,7 @@ class BareMetalDetailView(tables.DataTableView):
class BareMetalEventView(tables.MultiTableView):
table_classes = (baremetal_tables.BareMetalLastEventTable,
baremetal_tables.BareMetalAllEventsTable,)
template_name = 'admin/hypervisors/baremetal/events.html'
template_name = 'admin/rackhd/events.html'
page_title = _('Events')
name = _("Events")
slug = "events"
@ -208,7 +255,7 @@ class BareMetalEventView(tables.MultiTableView):
try:
sel = shovel.get_current_sel_data(id)[0].get('sel', [])
except KeyError as e:
redirect = reverse('horizon:admin:hypervisors:index')
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 = []