config/configutilities/configutilities/configutilities/hostfiletool.py

511 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 common import utils, exceptions
from common.guicomponents import Field, TYPES, prepare_fields, on_change, \
set_icons, handle_sub_show
from common.configobjects import HOST_XML_ATTRIBUTES
from 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()