""" 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() + "\n" # Special Fields if self.fieldgroup[1]['power_on'].get_value() is 'Y': attrs += "\t\t\n" if self.fieldgroup[1]['uses_bm'].get_value() is 'Y': attrs += "\t\tbmc\n" return "\t\n" + attrs + "\t\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 = "\n" \ "\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 += "" 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()