516 lines
17 KiB
Python
Executable File
516 lines
17 KiB
Python
Executable File
"""
|
|
Copyright (c) 2015-2017 Wind River Systems, Inc.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""
|
|
|
|
from collections import OrderedDict
|
|
import netaddr
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import wx
|
|
|
|
from configutilities.common import utils
|
|
from configutilities.common import exceptions
|
|
from configutilities.common.guicomponents import Field
|
|
from configutilities.common.guicomponents import TYPES
|
|
from configutilities.common.guicomponents import prepare_fields
|
|
from configutilities.common.guicomponents import on_change
|
|
from configutilities.common.guicomponents import set_icons
|
|
from configutilities.common.guicomponents import handle_sub_show
|
|
from configutilities.common.configobjects import HOST_XML_ATTRIBUTES
|
|
from configutilities.common.validator import TiS_VERSION
|
|
|
|
PAGE_SIZE = (200, 200)
|
|
WINDOW_SIZE = (570, 700)
|
|
CB_TRUE = True
|
|
CB_FALSE = False
|
|
PADDING = 10
|
|
|
|
IMPORT_ID = 100
|
|
EXPORT_ID = 101
|
|
|
|
INTERNAL_ID = 105
|
|
EXTERNAL_ID = 106
|
|
|
|
filedir = ""
|
|
filename = ""
|
|
|
|
# Globals
|
|
BULK_ADDING = False
|
|
|
|
|
|
class HostPage(wx.Panel):
|
|
def __init__(self, parent):
|
|
wx.Panel.__init__(self, parent=parent)
|
|
|
|
self.parent = parent
|
|
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
|
self.SetSizer(self.sizer)
|
|
self.fieldgroup = []
|
|
self.fieldgroup.append(OrderedDict())
|
|
self.fieldgroup.append(OrderedDict())
|
|
self.fieldgroup.append(OrderedDict())
|
|
|
|
self.fields_sizer1 = wx.GridBagSizer(vgap=10, hgap=10)
|
|
self.fields_sizer2 = wx.GridBagSizer(vgap=10, hgap=10)
|
|
self.fields_sizer3 = wx.GridBagSizer(vgap=10, hgap=10)
|
|
|
|
# Basic Fields
|
|
self.fieldgroup[0]['personality'] = Field(
|
|
text="Personality",
|
|
type=TYPES.choice,
|
|
choices=['compute', 'controller', 'storage'],
|
|
initial='compute'
|
|
)
|
|
self.fieldgroup[0]['hostname'] = Field(
|
|
text="Hostname",
|
|
type=TYPES.string,
|
|
initial=parent.get_next_hostname()
|
|
)
|
|
self.fieldgroup[0]['mgmt_mac'] = Field(
|
|
text="Management MAC Address",
|
|
type=TYPES.string,
|
|
initial=""
|
|
)
|
|
self.fieldgroup[0]['mgmt_ip'] = Field(
|
|
text="Management IP Address",
|
|
type=TYPES.string,
|
|
initial=""
|
|
)
|
|
self.fieldgroup[0]['location'] = Field(
|
|
text="Location",
|
|
type=TYPES.string,
|
|
initial=""
|
|
)
|
|
|
|
# Board Management
|
|
self.fieldgroup[1]['uses_bm'] = Field(
|
|
text="This host uses Board Management",
|
|
type=TYPES.checkbox,
|
|
initial="",
|
|
shows=['bm_ip', 'bm_username',
|
|
'bm_password', 'power_on'],
|
|
transient=True
|
|
)
|
|
self.fieldgroup[1]['bm_ip'] = Field(
|
|
text="Board Management IP Address",
|
|
type=TYPES.string,
|
|
initial=""
|
|
)
|
|
self.fieldgroup[1]['bm_username'] = Field(
|
|
text="Board Management username",
|
|
type=TYPES.string,
|
|
initial=""
|
|
)
|
|
self.fieldgroup[1]['bm_password'] = Field(
|
|
text="Board Management password",
|
|
type=TYPES.string,
|
|
initial=""
|
|
)
|
|
self.fieldgroup[1]['power_on'] = Field(
|
|
text="Power on host",
|
|
type=TYPES.checkbox,
|
|
initial="N",
|
|
transient=True
|
|
)
|
|
|
|
# Installation Parameters
|
|
self.fieldgroup[2]['boot_device'] = Field(
|
|
text="Boot Device",
|
|
type=TYPES.string,
|
|
initial=""
|
|
)
|
|
self.fieldgroup[2]['rootfs_device'] = Field(
|
|
text="Rootfs Device",
|
|
type=TYPES.string,
|
|
initial=""
|
|
)
|
|
self.fieldgroup[2]['install_output'] = Field(
|
|
text="Installation Output",
|
|
type=TYPES.choice,
|
|
choices=['text', 'graphical'],
|
|
initial="text"
|
|
)
|
|
self.fieldgroup[2]['console'] = Field(
|
|
text="Console",
|
|
type=TYPES.string,
|
|
initial=""
|
|
)
|
|
|
|
prepare_fields(self, self.fieldgroup[0], self.fields_sizer1,
|
|
self.on_change)
|
|
prepare_fields(self, self.fieldgroup[1], self.fields_sizer2,
|
|
self.on_change)
|
|
prepare_fields(self, self.fieldgroup[2], self.fields_sizer3,
|
|
self.on_change)
|
|
|
|
# Bind button handlers
|
|
self.Bind(wx.EVT_CHOICE, self.on_personality,
|
|
self.fieldgroup[0]['personality'].input)
|
|
|
|
self.Bind(wx.EVT_TEXT, self.on_hostname,
|
|
self.fieldgroup[0]['hostname'].input)
|
|
|
|
# Control Buttons
|
|
self.button_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
|
|
|
|
self.add = wx.Button(self, -1, "Add a New Host")
|
|
self.Bind(wx.EVT_BUTTON, self.on_add, self.add)
|
|
|
|
self.remove = wx.Button(self, -1, "Remove this Host")
|
|
self.Bind(wx.EVT_BUTTON, self.on_remove, self.remove)
|
|
|
|
self.button_sizer.Add(self.add)
|
|
self.button_sizer.Add(self.remove)
|
|
|
|
# Add fields and spacers
|
|
self.sizer.Add(self.fields_sizer1)
|
|
self.sizer.AddWindow(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.ALL,
|
|
PADDING)
|
|
self.sizer.Add(self.fields_sizer2)
|
|
self.sizer.AddWindow(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.ALL,
|
|
PADDING)
|
|
self.sizer.Add(self.fields_sizer3)
|
|
self.sizer.AddStretchSpacer()
|
|
self.sizer.AddWindow(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.ALL,
|
|
PADDING)
|
|
self.sizer.Add(self.button_sizer, border=10, flag=wx.CENTER)
|
|
|
|
def on_hostname(self, event, string=None):
|
|
"""Update the List entry text to match the new hostname
|
|
"""
|
|
string = string or event.GetString()
|
|
index = self.parent.GetSelection()
|
|
self.parent.SetPageText(index, string)
|
|
self.parent.parent.Layout()
|
|
|
|
def on_personality(self, event, string=None):
|
|
"""Remove hostname field if it's a storage or controller
|
|
"""
|
|
string = string or event.GetString()
|
|
index = self.parent.GetSelection()
|
|
if string == 'compute':
|
|
self.fieldgroup[0]['hostname'].show(True)
|
|
self.parent.SetPageText(index,
|
|
self.fieldgroup[0]['hostname'].get_value())
|
|
return
|
|
elif string == 'controller':
|
|
self.fieldgroup[0]['hostname'].show(False)
|
|
elif string == 'storage':
|
|
self.fieldgroup[0]['hostname'].show(False)
|
|
self.parent.SetPageText(index, string)
|
|
self.parent.Layout()
|
|
|
|
def on_add(self, event):
|
|
try:
|
|
self.validate()
|
|
except Exception as ex:
|
|
wx.LogError("Error on page: " + ex.message)
|
|
return
|
|
|
|
self.parent.new_page()
|
|
|
|
def on_remove(self, event):
|
|
if self.parent.GetPageCount() is 1:
|
|
wx.LogError("Must leave at least one host")
|
|
return
|
|
index = self.parent.GetSelection()
|
|
self.parent.DeletePage(index)
|
|
|
|
def to_xml(self):
|
|
"""Create the XML for this host
|
|
"""
|
|
self.validate()
|
|
|
|
attrs = ""
|
|
# Generic handling
|
|
for fgroup in self.fieldgroup:
|
|
for name, field in fgroup.items():
|
|
if field.transient or not field.get_value():
|
|
continue
|
|
attrs += "\t\t<" + name + ">" + \
|
|
field.get_value() + "</" + name + ">\n"
|
|
|
|
# Special Fields
|
|
if self.fieldgroup[1]['power_on'].get_value() is 'Y':
|
|
attrs += "\t\t<power_on/>\n"
|
|
|
|
if self.fieldgroup[1]['uses_bm'].get_value() is 'Y':
|
|
attrs += "\t\t<bm_type>bmc</bm_type>\n"
|
|
|
|
return "\t<host>\n" + attrs + "\t</host>\n"
|
|
|
|
def validate(self):
|
|
if self.fieldgroup[0]['personality'].get_value() == "compute" and not \
|
|
utils.is_valid_hostname(
|
|
self.fieldgroup[0]['hostname'].get_value()):
|
|
raise exceptions.ValidateFail(
|
|
"Hostname %s is not valid" %
|
|
self.fieldgroup[0]['hostname'].get_value())
|
|
|
|
if not utils.is_valid_mac(self.fieldgroup[0]['mgmt_mac'].get_value()):
|
|
raise exceptions.ValidateFail(
|
|
"Management MAC address %s is not valid" %
|
|
self.fieldgroup[0]['mgmt_mac'].get_value())
|
|
|
|
ip = self.fieldgroup[0]['mgmt_ip'].get_value()
|
|
if ip:
|
|
try:
|
|
netaddr.IPAddress(ip)
|
|
except Exception:
|
|
raise exceptions.ValidateFail(
|
|
"Management IP address %s is not valid" % ip)
|
|
|
|
if self.fieldgroup[1]['uses_bm'].get_value() == 'Y':
|
|
ip = self.fieldgroup[1]['bm_ip'].get_value()
|
|
if ip:
|
|
try:
|
|
netaddr.IPAddress(ip)
|
|
except Exception:
|
|
raise exceptions.ValidateFail(
|
|
"Board Management IP address %s is not valid" % ip)
|
|
|
|
else:
|
|
raise exceptions.ValidateFail(
|
|
"Board Management IP is not specified. "
|
|
"External Board Management Network requires Board "
|
|
"Management IP address.")
|
|
|
|
def on_change(self, event):
|
|
on_change(self, self.fieldgroup[1], event)
|
|
|
|
def set_field(self, name, value):
|
|
for fgroup in self.fieldgroup:
|
|
for fname, field in fgroup.items():
|
|
if fname == name:
|
|
field.set_value(value)
|
|
|
|
|
|
class HostBook(wx.Listbook):
|
|
def __init__(self, parent):
|
|
wx.Listbook.__init__(self, parent, style=wx.BK_DEFAULT)
|
|
|
|
self.parent = parent
|
|
self.Layout()
|
|
# Add a starting host
|
|
self.new_page()
|
|
|
|
self.Bind(wx.EVT_LISTBOOK_PAGE_CHANGED, self.on_changed)
|
|
self.Bind(wx.EVT_LISTBOOK_PAGE_CHANGING, self.on_changing)
|
|
|
|
def on_changed(self, event):
|
|
event.Skip()
|
|
|
|
def on_changing(self, event):
|
|
# Trigger page validation before leaving
|
|
if BULK_ADDING:
|
|
event.Skip()
|
|
return
|
|
index = self.GetSelection()
|
|
try:
|
|
if index != -1:
|
|
self.GetPage(index).validate()
|
|
except Exception as ex:
|
|
wx.LogError("Error on page: " + ex.message)
|
|
event.Veto()
|
|
return
|
|
event.Skip()
|
|
|
|
def new_page(self, hostname=None):
|
|
new_page = HostPage(self)
|
|
self.AddPage(new_page, hostname or self.get_next_hostname())
|
|
self.SetSelection(self.GetPageCount() - 1)
|
|
return new_page
|
|
|
|
def get_next_hostname(self, suggest=None):
|
|
prefix = "compute-"
|
|
new_suggest = suggest or 0
|
|
|
|
for existing in range(self.GetPageCount()):
|
|
if prefix + str(new_suggest) in self.GetPageText(existing):
|
|
new_suggest = self.get_next_hostname(suggest=new_suggest + 1)
|
|
|
|
if suggest:
|
|
prefix = ""
|
|
return prefix + str(new_suggest)
|
|
|
|
def to_xml(self):
|
|
"""Create the complete XML and allow user to save
|
|
"""
|
|
xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" \
|
|
"<hosts version=\"" + TiS_VERSION + "\">\n"
|
|
for index in range(self.GetPageCount()):
|
|
try:
|
|
xml += self.GetPage(index).to_xml()
|
|
except Exception as ex:
|
|
wx.LogError("Error on page number %s: %s" %
|
|
(index + 1, ex.message))
|
|
return
|
|
xml += "</hosts>"
|
|
|
|
writer = wx.FileDialog(self,
|
|
message="Save Host XML File",
|
|
defaultDir=filedir or "",
|
|
defaultFile=filename or "TiS_hosts.xml",
|
|
wildcard="XML file (*.xml)|*.xml",
|
|
style=wx.FD_SAVE,
|
|
)
|
|
|
|
if writer.ShowModal() == wx.ID_CANCEL:
|
|
return
|
|
|
|
# Write the XML file to disk
|
|
try:
|
|
with open(writer.GetPath(), "wb") as f:
|
|
f.write(xml.encode('utf-8'))
|
|
except IOError:
|
|
wx.LogError("Error writing hosts xml file '%s'." %
|
|
writer.GetPath())
|
|
|
|
|
|
class HostGUI(wx.Frame):
|
|
def __init__(self):
|
|
wx.Frame.__init__(self, None, wx.ID_ANY,
|
|
"Titanium Cloud Host File Creator v" + TiS_VERSION,
|
|
size=WINDOW_SIZE)
|
|
self.panel = wx.Panel(self)
|
|
|
|
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
|
self.book = HostBook(self.panel)
|
|
self.sizer.Add(self.book, 1, wx.ALL | wx.EXPAND, 5)
|
|
self.panel.SetSizer(self.sizer)
|
|
set_icons(self)
|
|
|
|
menu_bar = wx.MenuBar()
|
|
|
|
# File
|
|
file_menu = wx.Menu()
|
|
import_item = wx.MenuItem(file_menu, IMPORT_ID, '&Import')
|
|
file_menu.AppendItem(import_item)
|
|
export_item = wx.MenuItem(file_menu, EXPORT_ID, '&Export')
|
|
file_menu.AppendItem(export_item)
|
|
menu_bar.Append(file_menu, '&File')
|
|
self.Bind(wx.EVT_MENU, self.on_import, id=IMPORT_ID)
|
|
self.Bind(wx.EVT_MENU, self.on_export, id=EXPORT_ID)
|
|
|
|
self.SetMenuBar(menu_bar)
|
|
self.Layout()
|
|
self.SetMinSize(WINDOW_SIZE)
|
|
self.Show()
|
|
|
|
def on_import(self, e):
|
|
global BULK_ADDING
|
|
try:
|
|
BULK_ADDING = True
|
|
msg = ""
|
|
|
|
reader = wx.FileDialog(self,
|
|
"Import Existing Titanium Cloud Host File",
|
|
"", "", "XML file (*.xml)|*.xml",
|
|
wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
|
|
|
|
if reader.ShowModal() == wx.ID_CANCEL:
|
|
return
|
|
|
|
# Read in the config file
|
|
try:
|
|
with open(reader.GetPath(), 'rb') as f:
|
|
contents = f.read()
|
|
root = ET.fromstring(contents)
|
|
except Exception as ex:
|
|
wx.LogError("Cannot parse host file, Error: %s." % ex)
|
|
return
|
|
|
|
# Check version of host file
|
|
if root.get('version', "") != TiS_VERSION:
|
|
msg += "Warning: This file was created using tools for a " \
|
|
"different version of Titanium Cloud than this tool " \
|
|
"was designed for (" + TiS_VERSION + ")"
|
|
|
|
for idx, xmlhost in enumerate(root.findall('host')):
|
|
hostname = None
|
|
name_elem = xmlhost.find('hostname')
|
|
if name_elem is not None:
|
|
hostname = name_elem.text
|
|
new_host = self.book.new_page()
|
|
self.book.GetSelection()
|
|
try:
|
|
for attr in HOST_XML_ATTRIBUTES:
|
|
elem = xmlhost.find(attr)
|
|
if elem is not None and elem.text:
|
|
# Enable and display bm section if used
|
|
if attr == 'bm_type' and elem.text:
|
|
new_host.set_field("uses_bm", "Y")
|
|
handle_sub_show(
|
|
new_host.fieldgroup[1],
|
|
new_host.fieldgroup[1]['uses_bm'].shows,
|
|
True)
|
|
new_host.Layout()
|
|
|
|
# Basic field setting
|
|
new_host.set_field(attr, elem.text)
|
|
|
|
# Additional functionality for special fields
|
|
if attr == 'personality':
|
|
# Update hostname visibility and page title
|
|
new_host.on_personality(None, elem.text)
|
|
|
|
# Special handling for presence of power_on element
|
|
if attr == 'power_on' and elem is not None:
|
|
new_host.set_field(attr, "Y")
|
|
|
|
new_host.validate()
|
|
except Exception as ex:
|
|
if msg:
|
|
msg += "\n"
|
|
msg += "Warning: Added host %s has a validation error, " \
|
|
"reason: %s" % \
|
|
(hostname or ("with index " + str(idx)),
|
|
ex.message)
|
|
# No longer delete hosts with validation errors,
|
|
# The user can fix them up before exporting
|
|
# self.book.DeletePage(new_index)
|
|
|
|
if msg:
|
|
wx.LogWarning(msg)
|
|
finally:
|
|
BULK_ADDING = False
|
|
self.Layout()
|
|
|
|
def on_export(self, e):
|
|
# Do a validation of current page first
|
|
index = self.book.GetSelection()
|
|
try:
|
|
if index != -1:
|
|
self.book.GetPage(index).validate()
|
|
except Exception as ex:
|
|
wx.LogError("Error on page: " + ex.message)
|
|
return
|
|
|
|
# Check for hostname conflicts
|
|
hostnames = []
|
|
for existing in range(self.book.GetPageCount()):
|
|
hostname = self.book.GetPage(
|
|
existing).fieldgroup[0]['hostname'].get_value()
|
|
if hostname in hostnames:
|
|
wx.LogError("Cannot export, duplicate hostname '%s'" %
|
|
hostname)
|
|
return
|
|
# Ignore multiple None hostnames
|
|
elif hostname:
|
|
hostnames.append(hostname)
|
|
|
|
self.book.to_xml()
|
|
|
|
|
|
def main():
|
|
app = wx.App(0) # Start the application
|
|
HostGUI()
|
|
app.MainLoop()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|