""" Copyright (c) 2015-2017 Wind River Systems, Inc. SPDX-License-Identifier: Apache-2.0 """ from collections import OrderedDict from six.moves import configparser import wx import wx.wizard as wiz import wx.lib.dialogs import wx.lib.scrolledpanel from common.configobjects import REGION_CONFIG, DEFAULT_CONFIG from common.exceptions import ValidateFail from common.guicomponents import Field, TYPES, prepare_fields, on_change, \ debug, set_icons, TEXT_WIDTH, VGAP, HGAP from common.validator import ConfigValidator, TiS_VERSION PADDING = 5 CONFIG_TYPE = DEFAULT_CONFIG LINK_SPEED_1G = '1000' LINK_SPEED_10G = '10000' LINK_SPEED_25G = '25000' # Config parser to hold current configuration filename = None filedir = None config = configparser.RawConfigParser() config.optionxform = str def print_config(conf=config): debug('======CONFIG CONTENTS======') debug(get_config(config)) debug('======END CONFIG======') def get_config(conf=config): result = "" for section in conf.sections(): result += "\n[" + section + "]" + "\n" for option in config.options(section): result += option + "=" + config.get(section, option) + "\n" return result def get_opt(section, option): if config.has_section(section): if config.has_option(section, option): return config.get(section, option) return None class ConfigWizard(wx.wizard.Wizard): """Titanium Cloud configuration wizard, contains pages and more specifically ConfigPages, which have a structure for populating/processing configuration fields (questions) """ def __init__(self): wx.wizard.Wizard.__init__(self, None, -1, "Titanium Cloud Configuration File " "Creator v" + TiS_VERSION) set_icons(self) self.pages = [] # Catch wizard events self.Bind(wiz.EVT_WIZARD_PAGE_CHANGED, self.on_page_changed) self.Bind(wiz.EVT_WIZARD_PAGE_CHANGING, self.on_page_changing) self.Bind(wiz.EVT_WIZARD_CANCEL, self.on_cancel) self.Bind(wiz.EVT_WIZARD_FINISHED, self.on_finished) self.add_page(STARTPage(self)) self.add_page(REGIONPage(self)) self.add_page(SHAREDSERVICESPage(self)) self.add_page(REG2SERVICESPage(self)) self.add_page(REG2SERVICESPage2(self)) self.add_page(SYSTEMPage(self)) self.add_page(PXEBootPage(self)) self.add_page(MGMTPage(self)) self.add_page(INFRAPage(self)) self.add_page(OAMPage(self)) self.add_page(AUTHPage(self)) self.add_page(ENDPage(self)) size = self.GetBestSize() # Deprecated, from before scroll panel # for page in self.pages: # if issubclass(type(page), ConfigPage): # # Must create fields for the page and show them all # # to get max possible size # page.load() # page.GetSizer().ShowItems(True) # page_size = page.GetBestSize() # if page_size.GetHeight() > size.GetHeight(): # size.SetHeight(page_size.GetHeight()) # if page_size.GetWidth() > size.GetWidth(): # size.SetWidth(page_size.GetWidth()) # page.DestroyChildren() size.SetWidth(560) size.SetHeight(530) self.SetPageSize(size) self.GetSizer().Layout() def add_page(self, page): """Add a new page""" if self.pages: previous_page = self.pages[-1] page.SetPrev(previous_page) previous_page.SetNext(page) self.pages.append(page) def run(self): """Start the wizard""" self.RunWizard(self.pages[0]) def on_page_changed(self, evt): """Executed after the page has changed.""" page = evt.GetPage() if evt.GetDirection(): page.DestroyChildren() page.load() def on_page_changing(self, evt): """Executed before the page changes, can be blocked (vetoed)""" page = evt.GetPage() # Perform the page validation if evt.GetDirection(): try: page.validate_page() except Exception as ex: dlg = wx.MessageDialog( # ScrolledMessageDialog( self, ex.message, "Error on page") dlg.ShowModal() # Do not allow progress if errors were raised evt.Veto() # raise ex def on_cancel(self, evt): """On cancel button press, not used for now""" pass def on_finished(self, evt): """On finish button press, not used for now""" pass def skip_page(self, page, skip): for p in self.pages: if p.__class__.__name__ == page.__name__: p.skip = skip class WizardPage(wiz.PyWizardPage): """ An extended panel obj with a few methods to keep track of its siblings. This should be modified and added to the wizard. Season to taste.""" def __init__(self, parent): wx.wizard.PyWizardPage.__init__(self, parent) self.parent = parent self.title = "" self.next = self.prev = None self.sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self.sizer) self.skip = False def set_title(self, title_text): title = wx.StaticText(self, -1, title_text) title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)) self.sizer.AddWindow(title, 0, wx.ALIGN_LEFT | wx.ALL, PADDING) self.add_line() def add_content(self, content, proportion=0): """Add aditional widgets to the bottom of the page""" self.sizer.Add(content, proportion, wx.EXPAND | wx.ALL, PADDING) def add_line(self): self.sizer.AddWindow(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.ALL, PADDING) def SetNext(self, next): """Set the next page""" self.next = next def SetPrev(self, prev): """Set the previous page""" self.prev = prev def GetNext(self): """Return the next page""" if self.next and self.next.skip: return self.next.GetNext() return self.next def GetPrev(self): """Return the previous page""" if self.prev and self.prev.skip: return self.prev.GetPrev() return self.prev def load(self): # Run every time a page is visited (from prev or next page) pass def validate_page(self): # Validate the config related to this specific page before advancing pass class ConfigPage(WizardPage): """ A Page of the wizard with questions/answers """ def __init__(self, *args, **kwargs): super(ConfigPage, self).__init__(*args, **kwargs) # Section header to put in the INI file self.section = "" # Methods of the config_validator to be called for this section self.validator_methods = [] self.title = "" self.help_text = "" self.fields = OrderedDict() def load(self): self.title = "" self.sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self.sizer) self.section = "" self.title = "" self.help_text = "" for field in self.fields.values(): field.destroy() self.fields = OrderedDict() def do_setup(self): # Reset page, in case fields have changed self.sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self.sizer) # Set up title and help text self.set_title(self.title) if self.help_text: help_text = wx.StaticText(self, -1, self.help_text) help_text.Wrap(TEXT_WIDTH) self.add_content(help_text) self.add_line() self.spanel = wx.lib.scrolledpanel.ScrolledPanel(self, -1) # to view spanel: , style=wx.SIMPLE_BORDER) self.add_content(self.spanel, 3) # Add fields to page # gridSizer = wx.FlexGridSizer(rows=6, cols=2, vgap=10, # hgap=10) self.gridSizer = wx.GridBagSizer(vgap=VGAP, hgap=HGAP) # gridSizer.SetFlexibleDirection(wx.VERTICAL) # gridSizer.SetFlexibleDirection(wx.BOTH) self.spanel.SetSizer(self.gridSizer) self.spanel.SetupScrolling() # self.add_content(gridSizer) prepare_fields(self.spanel, self.fields, self.gridSizer, self.on_change) self.Layout() self.spanel.Layout() def on_change(self, event): on_change(self, self.fields, event) def validate_page(self): # Gets the config from the current page, then sends to the validator self.get_config() print_config(config) validator = ConfigValidator(config, None, CONFIG_TYPE, True) mode = get_opt('SYSTEM', 'SYSTEM_MODE') if mode: validator.set_system_mode(mode) dc_role = get_opt('SYSTEM', 'DISTRIBUTED_CLOUD_ROLE') if dc_role: validator.set_system_dc_role(dc_role) for method in self.validator_methods: getattr(validator, method)() def get_config(self): # Removes possibly out-dated config section so it can be over-written if config.has_section(self.section): config.remove_section(self.section) self.add_fields() def add_fields(self): # Adds the page's section to the config object if necessary if not config.has_section(self.section): config.add_section(self.section) # Add all of the non-transient fields (straight-forward mapping) for name, field in self.fields.items(): if not field.transient and field.get_value(): config.set(self.section, name, field.get_value()) def bind_events(self): pass class STARTPage(WizardPage): def load(self): super(STARTPage, self).load() self.set_title("Start") help_text = wx.StaticText( self, -1, "Welcome to the Titanium Cloud Configuration File " "Creator.\n\n" "This wizard will walk you through the steps of creating a " "configuration file which can be used to automate the " "installation of Titanium Cloud. Note this utility can only be " "used to create configuration files compatible with version " + TiS_VERSION + " of Titanium Cloud.\n\n" "NOTE: Moving backwards in the wizard will result in loss of the " "current page's configuration and will need to be reentered\n\n" "Press next to begin.\n\n\n\n") help_text.Wrap(TEXT_WIDTH) self.add_content(help_text) # self.add_line() # To implement this, would need special mapping for every page... # (from config to control) # putting this on the long(er)-term todo list for now # self.add_content(wx.StaticText( # self, -1, # 'You may optionally pre-populate this utility by reading in an ' # 'existing Titanium Cloud configuration file')) # self.load_button = wx.Button(self, -1, "Load Configuration File " # "(Optional)") # self.Bind(wx.EVT_BUTTON, self.on_read, self.load_button) # self.add_content(self.load_button) def on_read(self, event): reader = wx.FileDialog( self, "Open Existing Titanium Cloud Configuration File", "", "", "INI file (*.ini)|*.ini", wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) if reader.ShowModal() == wx.ID_CANCEL: return # Read in the config file global filename, filedir, config try: config.read(reader.GetPath()) filename = reader.GetFilename() filedir = reader.GetDirectory() except Exception as ex: wx.LogError("Cannot parse configuration file, Error: %s." % ex) config = configparser.RawConfigParser() config.optionxform = str return # todo tsmith # Do validation of the imported file class REGIONPage(ConfigPage): def load(self): super(REGIONPage, self).load() # Header in INI file self.section = "SHARED_SERVICES" self.validator_methods = [] self.title = "Region Configuration" self.help_text = ( "Configuring this system in region mode provides the ability to " "operate as a secondary independent region to an existing " "Openstack cloud deployment (Certain restrictions apply, refer to " "system documentation).\n\n" "Keystone (and optionally Glance) " "services can be configured as shared services, which " "prevents them from being configured on the secondary region and " "instead those services already configured in the primary region " "will be accessed.") self.set_fields() self.do_setup() self.bind_events() # Skip region pages by default self.skip_region(True) def set_fields(self): self.fields['is_region'] = Field( text="Configure as a secondary region", type=TYPES.checkbox, transient=True, shows=["REGION_NAME", "ADMIN_TENANT_NAME", "ADMIN_USER_NAME", "ADMIN_PASSWORD", "SERVICE_TENANT_NAME", "keystone_help", "KEYSTONE_ADMINURL", "sep1", "keystone_note", ] ) self.fields['REGION_NAME'] = Field( text="Name of the primary region", type=TYPES.string, initial="RegionOne" ) self.fields["sep1"] = Field(type=TYPES.separator) self.fields['keystone_help'] = Field( text="Primary Keystone Configuration\n\nThis information " "is needed for the primary " "region in order to validate or create the shared " "services.", type=TYPES.help, ) self.fields['SERVICE_TENANT_NAME'] = Field( text="Name of the service tenant", type=TYPES.string, initial="RegionTwo_services" ) self.fields['ADMIN_TENANT_NAME'] = Field( text="Name of the admin tenant", type=TYPES.string, initial="admin" ) self.fields['ADMIN_USER_NAME'] = Field( text="Username of the keystone admin account", type=TYPES.string, initial="admin" ) self.fields['ADMIN_PASSWORD'] = Field( text="Password of the keystone admin account", type=TYPES.string, initial="" ) self.fields['KEYSTONE_ADMINURL'] = Field( text="Authentication URL of the keystone service", type=TYPES.string, initial="http://192.168.204.2:5000/v3" ) self.fields['keystone_note'] = Field( text="NOTE: If 'Automatically configure shared keystone' " "is checked in the upcoming 'Secondary Region Services' page," " then the service tenant (above) will be created " "if not present.", type=TYPES.help, ) def validate_page(self): super(REGIONPage, self).validate_page() # Do page specific validation here if self.fields['is_region'].get_value() == 'Y' and \ not config.has_option(self.section, "ADMIN_PASSWORD"): raise ValidateFail("The keystone admin password is mandatory") def get_config(self): super(REGIONPage, self).get_config() if len(config.items(self.section)) == 0: config.remove_section(self.section) config.remove_section("REGION_2_SERVICES") config.remove_section("REGION2_PXEBOOT_NETWORK") else: # Add service name which doesn't change config.set(self.section, "KEYSTONE_SERVICE_NAME", "keystone") config.set(self.section, "KEYSTONE_SERVICE_TYPE", "identity") def bind_events(self): self.fields['is_region'].input.Bind(wx.EVT_CHECKBOX, self.on_region) def on_region(self, event): # Set the region pages to be skipped or not self.skip_region(event.GetInt() == 0) event.Skip() def skip_region(self, skip): debug("Setting region skips to %s" % skip) self.next.skip = skip self.next.next.skip = skip self.next.next.next.skip = skip self.parent.skip_page(AUTHPage, not skip) # Set the config type appropriately global CONFIG_TYPE if skip: CONFIG_TYPE = DEFAULT_CONFIG else: CONFIG_TYPE = REGION_CONFIG # Remove any sections that aren't handled in region-config mode config.remove_section("PXEBOOT_NETWORK") config.remove_section("AUTHENTICATION") class SHAREDSERVICESPage(ConfigPage): def load(self): super(SHAREDSERVICESPage, self).load() self.section = "SHARED_SERVICES" self.validator_methods = [] self.title = "Regions - Shared Services" self.help_text = ( "Keystone is always configured as a shared service. " "Glance may also optionally be configured as " "shared services.") self.set_fields() self.do_setup() self.bind_events() def set_fields(self): # GLANCE self.fields['share_glance'] = Field( text="Share the primary region's glance service", type=TYPES.checkbox, transient=True ) def validate_page(self): # do previous pages validation as well to refresh config, since they # share a section self.prev.validate_page() super(SHAREDSERVICESPage, self).validate_page() # Do page specific validation here def get_config(self): # Skip the parent get_config so the section isn't removed # (since it's shared we want the old info) self.add_fields() # Add Static service types if self.fields['share_glance'].get_value() == 'Y': config.set(self.section, "GLANCE_SERVICE_NAME", "glance") config.set(self.section, "GLANCE_SERVICE_TYPE", "image") class REG2SERVICESPage(ConfigPage): def load(self): super(REG2SERVICESPage, self).load() self.section = "REGION_2_SERVICES" # Validation is only done on last of region pages self.validator_methods = [] self.title = "Secondary Region Services (1/2)" self.help_text = ( "Secondary region services are not shared with the primary " "region, during installation they will be configured to run " "in this region.") self.set_fields() self.do_setup() self.bind_events() def set_fields(self): self.fields['create_help'] = Field( text="During configuration, the Primary Region's Keystone " "can be automatically " "provisioned to accommodate this region, including if " "necessary the services tenant, users, services, " "and endpoints. If this is not " "enabled, manual configuration of the Primary Region's " "Keystone must be done and " "only validation will be performed during this secondary " "region's configuration.\n\n" "Note: passwords are optional if this option is selected.", type=TYPES.help, ) self.fields['CREATE'] = Field( text="Automatically configure shared keystone", type=TYPES.checkbox, initial='Y', ) self.fields['REGION_NAME'] = Field( text="Name for this system's region", type=TYPES.string, initial="RegionTwo" ) self.fields['sep1'] = Field(type=TYPES.separator) if not config.has_option('SHARED_SERVICES', 'GLANCE_SERVICE_NAME'): # GLANCE self.fields['GLANCE_USER_NAME'] = Field( text="Glance username", type=TYPES.string, initial="glance") self.fields['GLANCE_PASSWORD'] = Field( text="Glance user password", type=TYPES.string, initial="") self.fields['sep2'] = Field(type=TYPES.separator) self.fields['NOVA_USER_NAME'] = Field( text="Nova username", type=TYPES.string, initial="nova") self.fields['NOVA_PASSWORD'] = Field( text="Nova user password", type=TYPES.string, initial="") def validate_page(self): super(REG2SERVICESPage, self).validate_page() if self.fields['CREATE'].get_value() == 'N': if (('GLANCE_PASSWORD' in self.fields and not self.fields['GLANCE_PASSWORD'].get_value()) or not self.fields['NOVA_PASSWORD'].get_value()): raise ValidateFail("Passwords are mandatory when automatic " "keystone configuration is not enabled.") def get_config(self): super(REG2SERVICESPage, self).get_config() class REG2SERVICESPage2(ConfigPage): def load(self): super(REG2SERVICESPage2, self).load() self.section = "REGION_2_SERVICES" # Validation is only done on last page self.validator_methods = ["validate_network", "validate_region"] self.title = "Secondary Region Services (2/2)" self.set_fields() self.do_setup() self.bind_events() def set_fields(self): self.fields['NEUTRON_USER_NAME'] = Field( text="Neutron username", type=TYPES.string, initial="neutron") self.fields['NEUTRON_PASSWORD'] = Field( text="Neutron user password", type=TYPES.string, initial="") self.fields['SYSINV_USER_NAME'] = Field( text="Sysinv username", type=TYPES.string, initial="sysinv") self.fields['SYSINV_PASSWORD'] = Field( text="Sysinv user password", type=TYPES.string, initial="") self.fields['PATCHING_USER_NAME'] = Field( text="Patching username", type=TYPES.string, initial="patching") self.fields['PATCHING_PASSWORD'] = Field( text="Patching user password", type=TYPES.string, initial="") self.fields['HEAT_USER_NAME'] = Field( text="Heat username", type=TYPES.string, initial="heat") self.fields['HEAT_PASSWORD'] = Field( text="Heat user password", type=TYPES.string, initial="") self.fields['HEAT_ADMIN_DOMAIN'] = Field( text="Heat admin domain", type=TYPES.string, initial="heat") self.fields['HEAT_ADMIN_USER_NAME'] = Field( text="Heat admin username", type=TYPES.string, initial="heat_stack_admin") self.fields['HEAT_ADMIN_PASSWORD'] = Field( text="Password of the heat admin user", type=TYPES.string, initial="") self.fields['CEILOMETER_USER_NAME'] = Field( text="Ceilometer username", type=TYPES.string, initial="ceilometer") self.fields['CEILOMETER_PASSWORD'] = Field( text="Ceilometer user password", type=TYPES.string, initial="") self.fields['AODH_USER_NAME'] = Field( text="Aodh username", type=TYPES.string, initial="aodh") self.fields['AODH_PASSWORD'] = Field( text="Aodh user password", type=TYPES.string, initial="") self.fields['NFV_USER_NAME'] = Field( text="NFV username", type=TYPES.string, initial="vim") self.fields['NFV_PASSWORD'] = Field( text="NFV user password", type=TYPES.string, initial="") self.fields['MTCE_USER_NAME'] = Field( text="MTCE username", type=TYPES.string, initial="mtce") self.fields['MTCE_PASSWORD'] = Field( text="MTCE user password", type=TYPES.string, initial="") self.fields['PANKO_USER_NAME'] = Field( text="PANKO username", type=TYPES.string, initial="panko") self.fields['PANKO_PASSWORD'] = Field( text="PANKO user password", type=TYPES.string, initial="") self.fields['PLACEMENT_USER_NAME'] = Field( text="Placement username", type=TYPES.string, initial="placement") self.fields['PLACEMENT_PASSWORD'] = Field( text="Placement user password", type=TYPES.string, initial="") self.fields['GNOCCHI_USER_NAME'] = Field( text="GNOCCHI username", type=TYPES.string, initial="gnocchi") self.fields['GNOCCHI_PASSWORD'] = Field( text="GNOCCHI user password", type=TYPES.string, initial="") self.fields['FM_USER_NAME'] = Field( text="FM username", type=TYPES.string, initial="fm") self.fields['FM_PASSWORD'] = Field( text="FM user password", type=TYPES.string, initial="") self.fields['BARBICAN_USER_NAME'] = Field( text="Barbican username", type=TYPES.string, initial="barbican") self.fields['BARBICAN_PASSWORD'] = Field( text="Barbican user password", type=TYPES.string, initial="") def validate_page(self): self.prev.validate_page() super(REG2SERVICESPage2, self).validate_page() def get_config(self): # Special handling for all region sections is done here self.add_fields() class SYSTEMPage(ConfigPage): def load(self): super(SYSTEMPage, self).load() self.section = "SYSTEM" self.validator_methods = [] self.title = "System" self.help_text = ( "All-in-one System Mode Configuration\n\nAvailable options are: \n" "duplex-direct: two node redundant configuration. Management and " "infrastructure networks are directly connected to peer ports\n" "duplex: two node redundant configuration\n" "simplex: single node non-redundant configuration") self.system_mode = ['duplex-direct', 'duplex', 'simplex'] self.set_fields() self.do_setup() self.bind_events() self.skip_not_required_pages(False) def set_fields(self): self.fields['use_mode'] = Field( text="Configure as an All-in-one system", type=TYPES.checkbox, transient=True, shows=["SYSTEM_MODE"] ) self.fields['SYSTEM_MODE'] = Field( text="System redundant configuration", type=TYPES.radio, choices=self.system_mode, ) def validate_page(self): super(SYSTEMPage, self).validate_page() def get_config(self): super(SYSTEMPage, self).get_config() if len(config.items(self.section)) == 0: config.remove_section(self.section) else: config.set(self.section, 'SYSTEM_TYPE', 'All-in-one') def bind_events(self): self.fields['SYSTEM_MODE'].input.Bind(wx.EVT_RADIOBOX, self.on_mode) self.fields['use_mode'].input.Bind(wx.EVT_CHECKBOX, self.on_use_mode) def on_mode(self, event): # Set the pages to be skipped or not self.skip_not_required_pages( self.system_mode[event.GetInt()] == 'simplex') event.Skip() def on_use_mode(self, event): # Set the pages to be skipped or not if event.GetInt() == 0: # If set to not in use, ensure the pages are not skipped self.skip_not_required_pages(False) # And reset to the default selection self.fields['SYSTEM_MODE'].set_value('duplex-direct') event.Skip() def skip_not_required_pages(self, skip): # Skip PXEBOOT, BMC and INFRA pages self.parent.skip_page(PXEBootPage, skip) self.parent.skip_page(INFRAPage, skip) # Remove the sections that are not required config.remove_section("PXEBOOT_NETWORK") config.remove_section("BOARD_MANAGEMENT_NETWORK") config.remove_section("INFRA_NETWORK") class PXEBootPage(ConfigPage): def load(self): super(PXEBootPage, self).load() self.section = "PXEBOOT_NETWORK" self.validator_methods = ["validate_pxeboot"] self.title = "PXEBoot Network" self.help_text = ( "The PXEBoot network is used for initial booting and installation " "of each node. IP addresses on this network are reachable only " "within the data center.\n\n" "The default configuration combines the PXEBoot network and the " "management network. If a separate PXEBoot network is used, it " "will share the management interface, which requires the " "management network to be placed on a VLAN.") self.set_fields() self.do_setup() self.bind_events() def set_fields(self): if config.has_section("REGION_2_SERVICES"): self.fields['mandatory'] = Field( text="A PXEBoot network is mandatory for secondary" " region deployments.", type=TYPES.help ) self.section = "REGION2_PXEBOOT_NETWORK" else: self.fields['use_pxe'] = Field( text="Configure a separate PXEBoot network", type=TYPES.checkbox, transient=True, shows=["PXEBOOT_CIDR"] ) self.fields['PXEBOOT_CIDR'] = Field( text="PXEBoot subnet", type=TYPES.string, initial="192.168.202.0/24" ) def get_config(self): super(PXEBootPage, self).get_config() if len(config.items(self.section)) == 0: config.remove_section(self.section) if config.has_section("REGION_2_SERVICES"): raise ValidateFail( "Must configure a PXEBoot network when in region mode") def validate_page(self): super(PXEBootPage, self).validate_page() # Do page specific validation here class MGMTPage(ConfigPage): def load(self): super(MGMTPage, self).load() # Preserve order plus allow mapping back to raw value if get_opt('SYSTEM', 'SYSTEM_MODE') == 'duplex-direct': self.lag_choices = OrderedDict([ ('802.3ad (LACP) policy', '4'), ]) else: self.lag_choices = OrderedDict([ ('Active-backup policy', '1'), ('802.3ad (LACP) policy', '4'), ]) self.mgmt_speed_choices = [LINK_SPEED_1G, LINK_SPEED_10G, LINK_SPEED_25G] self.section = "MGMT_NETWORK" if get_opt('SYSTEM', 'SYSTEM_MODE') != 'simplex': self.validator_methods = ["validate_pxeboot", "validate_mgmt"] self.help_text = ( "The management network is used for internal communication " "between platform components. IP addresses on this network " "are reachable only within the data center.") else: self.validator_methods = ["validate_aio_simplex_mgmt"] self.help_text = ( "The management network is used for internal communication " "between platform components. IP addresses on this network " "are reachable only within the host.") self.title = "Management Network" self.set_fields() self.do_setup() self.bind_events() def set_fields(self): if get_opt('SYSTEM', 'SYSTEM_MODE') != 'simplex': self.fields['mgmt_port1'] = Field( text="Management interface", type=TYPES.string, initial="enp0s8", transient=True ) self.fields['lag_help'] = Field( text="A management bond interface provides redundant " "connections for the management network. When selected, " "the field above specifies the first member of the bond.", type=TYPES.help, ) self.fields['LAG_INTERFACE'] = Field( text="Use management interface link aggregation", type=TYPES.checkbox, shows=["LAG_MODE", "mgmt_port2"], transient=True ) self.fields['LAG_MODE'] = Field( text="Management interface bonding policy", type=TYPES.choice, choices=self.lag_choices.keys(), transient=True ) self.fields['mgmt_port2'] = Field( text="Second management interface member", type=TYPES.string, initial="", transient=True ) self.fields['INTERFACE_MTU'] = Field( text="Management interface MTU", type=TYPES.int, initial="1500", transient=True ) self.fields['INTERFACE_LINK_CAPACITY'] = Field( text="Management interface link capacity Mbps", type=TYPES.choice, choices=self.mgmt_speed_choices, initial=self.mgmt_speed_choices[0], transient=True ) if config.has_option('PXEBOOT_NETWORK', 'PXEBOOT_CIDR') or \ config.has_option('REGION2_PXEBOOT_NETWORK', 'PXEBOOT_CIDR'): self.fields['vlan_help'] = Field( text=("A management VLAN is required because a separate " "PXEBoot network was configured on the management " "interface."), type=TYPES.help ) self.fields['VLAN'] = Field( text="Management VLAN Identifier", type=TYPES.int, initial="", ) self.fields['CIDR'] = Field( text="Management subnet", type=TYPES.string, initial="192.168.204.0/24", ) self.fields['MULTICAST_CIDR'] = Field( text="Management multicast subnet", type=TYPES.string, initial='239.1.1.0/28' ) # Start/end ranges self.fields['use_entire_subnet'] = Field( text="Restrict management subnet address range", type=TYPES.checkbox, shows=["IP_START_ADDRESS", "IP_END_ADDRESS"], transient=True ) self.fields['IP_START_ADDRESS'] = Field( text="Management network start address", type=TYPES.string, initial="192.168.204.2", ) self.fields['IP_END_ADDRESS'] = Field( text="Management network end address", type=TYPES.string, initial="192.168.204.254", ) # Dynamic addressing self.fields['dynamic_help'] = Field( text=( "IP addresses can be assigned to hosts dynamically or " "a static IP address can be specified for each host. " "Note: This choice applies to both the management network " "and infrastructure network."), type=TYPES.help, ) self.fields['DYNAMIC_ALLOCATION'] = Field( text="Use dynamic IP address allocation", type=TYPES.checkbox, initial='Y' ) else: self.fields['CIDR'] = Field( text="Management subnet", type=TYPES.string, initial="192.168.204.0/28", ) def validate_page(self): super(MGMTPage, self).validate_page() # Do page specific validation here def get_config(self): super(MGMTPage, self).get_config() if get_opt('SYSTEM', 'SYSTEM_MODE') != 'simplex': # Add logical interface ports = self.fields['mgmt_port1'].get_value() if self.fields['mgmt_port2'].get_value(): ports += "," + self.fields['mgmt_port2'].get_value() li = create_li( lag=self.fields['LAG_INTERFACE'].get_value(), mode=self.lag_choices.get(self.fields['LAG_MODE'].get_value()), mtu=self.fields['INTERFACE_MTU'].get_value(), link_capacity=self.fields[ 'INTERFACE_LINK_CAPACITY'].get_value(), ports=ports ) config.set(self.section, 'LOGICAL_INTERFACE', li) clean_lis() class INFRAPage(ConfigPage): def load(self): super(INFRAPage, self).load() # Preserve order plus allow mapping back to raw value self.lag_choices = OrderedDict([ ('Active-backup policy', '1'), ('Balanced XOR policy', '2'), ('802.3ad (LACP) policy', '4'), ]) self.infra_speed_choices = [LINK_SPEED_1G, LINK_SPEED_10G, LINK_SPEED_25G] self.section = "INFRA_NETWORK" self.validator_methods = ["validate_storage", "validate_pxeboot", "validate_mgmt", "validate_infra"] self.title = "Infrastructure Network" self.help_text = ( "The infrastructure network is used for internal communication " "between platform components to offload the management network " "of high bandwidth services. " "IP addresses on this network are reachable only within the data " "center.\n\n" "If a separate infrastructure interface is not configured the " "management network will be used.") self.set_fields() self.do_setup() self.bind_events() def set_fields(self): self.fields['use_infra'] = Field( text="Configure an infrastructure interface", type=TYPES.checkbox, transient=True ) self.fields['infra_port1'] = Field( text="Infrastructure interface", type=TYPES.string, initial="", transient=True ) self.fields['lag_help'] = Field( text="An infrastructure bond interface provides redundant " "connections for the infrastructure network. When selected, " "the field above specifies the first member of the bond.", type=TYPES.help, ) self.fields['LAG_INTERFACE'] = Field( text="Use infrastructure interface link aggregation", type=TYPES.checkbox, shows=["LAG_MODE", "infra_port2"], transient=True ) self.fields['LAG_MODE'] = Field( text="Infrastructure interface bonding policy", type=TYPES.choice, choices=self.lag_choices.keys(), transient=True ) self.fields['infra_port2'] = Field( text="Second infrastructure interface member", type=TYPES.string, initial="", transient=True ) self.fields['INTERFACE_MTU'] = Field( text="Infrastructure interface MTU", type=TYPES.int, initial="1500", transient=True ) self.fields['INTERFACE_LINK_CAPACITY'] = Field( text="Infrastructure interface link capacity Mbps", type=TYPES.choice, choices=self.infra_speed_choices, initial=self.infra_speed_choices[-1], transient=True ) # VLAN self.fields['use_vlan'] = Field( text="Configure an infrastructure VLAN", type=TYPES.checkbox, shows=["VLAN"], transient=True ) self.fields['VLAN'] = Field( text="Infrastructure VLAN Identifier", type=TYPES.int, initial="", ) self.fields['CIDR'] = Field( text="Infrastructure subnet", type=TYPES.string, initial="192.168.205.0/24", ) # Start/end ranges self.fields['use_entire_subnet'] = Field( text="Restrict infrastructure subnet address range", type=TYPES.checkbox, shows=["IP_START_ADDRESS", "IP_END_ADDRESS"], transient=True ) self.fields['IP_START_ADDRESS'] = Field( text="Infrastructure network start address", type=TYPES.string, initial="192.168.205.2", ) self.fields['IP_END_ADDRESS'] = Field( text="Infrastructure network end address", type=TYPES.string, initial="192.168.205.254", ) # This field show/hides all other fields self.fields['use_infra'].shows = [field for field in self.fields.keys() if field is not 'use_infra'] def validate_page(self): super(INFRAPage, self).validate_page() def get_config(self): if self.fields['use_infra'].get_value() is 'N': if config.has_section(self.section): config.remove_section(self.section) clean_lis() return super(INFRAPage, self).get_config() # Add logical interface ports = self.fields['infra_port1'].get_value() if self.fields['infra_port2'].get_value(): ports += "," + self.fields['infra_port2'].get_value() li = create_li( lag=self.fields['LAG_INTERFACE'].get_value(), mode=self.lag_choices.get(self.fields['LAG_MODE'].get_value()), mtu=self.fields['INTERFACE_MTU'].get_value(), link_capacity=self.fields['INTERFACE_LINK_CAPACITY'].get_value(), ports=ports ) config.set(self.section, 'LOGICAL_INTERFACE', li) clean_lis() if len(config.items(self.section)) == 0: config.remove_section(self.section) class OAMPage(ConfigPage): def load(self): super(OAMPage, self).load() self.lag_choices = OrderedDict([ ('Active-backup policy', '1'), ('Balanced XOR policy', '2'), ('802.3ad (LACP) policy', '4'), ]) self.section = "OAM_NETWORK" if get_opt('SYSTEM', 'SYSTEM_MODE') == 'simplex': self.simplex = True self.validator_methods = ["validate_aio_network"] else: self.simplex = False self.validator_methods = ["validate_pxeboot", "validate_mgmt", "validate_infra", "validate_oam"] self.title = "External OAM Network" self.help_text = ( "The external OAM network is used for management of the " "cloud. It also provides access to the " "platform APIs. IP addresses on this network are reachable " "outside the data center.") self.set_fields() self.do_setup() self.bind_events() def set_fields(self): self.fields['oam_port1'] = Field( text="External OAM interface", type=TYPES.string, initial="enp0s3", transient=True ) self.fields['lag_help'] = Field( text="An external OAM bond interface provides redundant " "connections for the OAM network. When selected, the " "field above specifies the first member of the bond.", type=TYPES.help, ) self.fields['LAG_INTERFACE'] = Field( text="External OAM interface link aggregation", type=TYPES.checkbox, shows=["LAG_MODE", "oam_port2"], transient=True ) self.fields['LAG_MODE'] = Field( text="OAM interface bonding policy", type=TYPES.choice, choices=self.lag_choices.keys(), transient=True ) self.fields['oam_port2'] = Field( text="Second External OAM interface member", type=TYPES.string, initial="", transient=True ) self.fields['INTERFACE_MTU'] = Field( text="External OAM interface MTU", type=TYPES.int, initial="1500", transient=True ) # VLAN self.fields['use_vlan'] = Field( text="Configure an External OAM VLAN", type=TYPES.checkbox, shows=["VLAN"], transient=True ) self.fields['VLAN'] = Field( text="External OAM VLAN Identifier", type=TYPES.int, initial="", ) self.fields['CIDR'] = Field( text="External OAM subnet", type=TYPES.string, initial="10.10.10.0/24", ) self.fields['GATEWAY'] = Field( text="External OAM gateway address", type=TYPES.string, initial="10.10.10.1", ) if not self.simplex: self.fields['IP_FLOATING_ADDRESS'] = Field( text="External OAM floating address", type=TYPES.string, initial="10.10.10.2", ) self.fields['IP_UNIT_0_ADDRESS'] = Field( text="External OAM address for first controller node", type=TYPES.string, initial="10.10.10.3", ) self.fields['IP_UNIT_1_ADDRESS'] = Field( text="External OAM address for second controller node", type=TYPES.string, initial="10.10.10.4", ) else: self.fields['IP_ADDRESS'] = Field( text="External OAM address", type=TYPES.string, initial="10.10.10.2", ) def get_config(self): super(OAMPage, self).get_config() # Add logical interface ports = self.fields['oam_port1'].get_value() if self.fields['oam_port2'].get_value(): ports += "," + self.fields['oam_port2'].get_value() li = create_li( lag=self.fields['LAG_INTERFACE'].get_value(), mode=self.lag_choices.get(self.fields['LAG_MODE'].get_value()), mtu=self.fields['INTERFACE_MTU'].get_value(), ports=ports ) config.set(self.section, 'LOGICAL_INTERFACE', li) clean_lis() def validate_page(self): super(OAMPage, self).validate_page() # Do page specific validation here class AUTHPage(ConfigPage): def load(self): super(AUTHPage, self).load() self.section = "AUTHENTICATION" self.validator_methods = ["validate_authentication"] self.title = "Authentication" self.help_text = ( "Create the admin user password.\n" "It must have a minimum length of 7 characters, and must " "contain at least 1 upper case, 1 lower case, 1 digit, " "and 1 special character.\n\n" "Note: This password will be stored as plaintext in the generated " "INI file.") self.set_fields() self.do_setup() self.bind_events() def set_fields(self): self.fields['ADMIN_PASSWORD'] = Field( text="Password", type=TYPES.string, ) def get_config(self): super(AUTHPage, self).get_config() def validate_page(self): super(AUTHPage, self).validate_page() # Do page specific validation here class ENDPage(WizardPage): # Final page for file saving def load(self): super(ENDPage, self).load() # Must ensure fields are destroyed/don't exist before adding to # prevent double-loading self.sizer.Clear(True) self.set_title("Configuration Complete") self.add_content( wx.StaticText(self, -1, 'Titanium Cloud Configuration is ' 'complete, configuration file may now be ' 'saved.')) self.write_button = wx.Button(self, -1, "Save Configuration File") self.Bind(wx.EVT_BUTTON, self.on_save, self.write_button) self.add_content(self.write_button) # Add the version to the config if not config.has_section("VERSION"): config.add_section("VERSION") config.set("VERSION", "RELEASE", TiS_VERSION) self.preview = wx.TextCtrl(self, -1, value=get_config(), style=wx.TE_MULTILINE | wx.TE_READONLY) self.add_content(self.preview, 3) def on_save(self, event): writer = wx.FileDialog(self, message="Save Configuration File", defaultDir=filedir or "", defaultFile=filename or "TiC_config.ini", wildcard="INI file (*.ini)|*.ini", style=wx.FD_SAVE, ) if writer.ShowModal() == wx.ID_CANCEL: return # Write the configuration to disk try: with open(writer.GetPath(), "wb") as f: config.write(f) except IOError: wx.LogError("Error writing configuration file '%s'." % writer.GetPath()) # todo tsmith include a 'reformat' to shuffle numbers down? def clean_lis(): # Remove unreferenced Logical Interfaces in the config referenced = [] for sec in config.sections(): if config.has_option(sec, 'LOGICAL_INTERFACE'): referenced.append(config.get(sec, 'LOGICAL_INTERFACE')) for sec in config.sections(): if "LOGICAL_INTERFACE_" in sec and sec not in referenced: config.remove_section(sec) def create_li(lag='N', mode=None, mtu=1500, link_capacity=None, ports=None): # todo more graceful matching to an existing LI for number in range(1, len(config.sections())): if config.has_section("LOGICAL_INTERFACE_" + str(number)): debug("Found interface " + str(number) + " with ports " + config.get("LOGICAL_INTERFACE_" + str(number), 'INTERFACE_PORTS') + ". Searching for ports: " + ports) if config.get("LOGICAL_INTERFACE_" + str(number), 'INTERFACE_PORTS') == ports: debug("Matched to LI: " + str(number)) # This logical interface already exists, # so use that but update any values name = "LOGICAL_INTERFACE_" + str(number) config.set(name, 'LAG_INTERFACE', lag) if mode: config.set(name, 'LAG_MODE', mode) config.set(name, 'INTERFACE_MTU', mtu) if link_capacity: config.set(name, 'INTERFACE_LINK_CAPACITY', link_capacity) return name # Get unused LI number number = 1 while config.has_section("LOGICAL_INTERFACE_" + str(number)): number += 1 # LI doesnt exist so create it with the given values name = "LOGICAL_INTERFACE_" + str(number) config.add_section(name) config.set(name, 'LAG_INTERFACE', lag) if mode: config.set(name, 'LAG_MODE', mode) config.set(name, 'INTERFACE_MTU', mtu) if link_capacity: config.set(name, 'INTERFACE_LINK_CAPACITY', link_capacity) config.set(name, 'INTERFACE_PORTS', ports) return name def main(): app = wx.App(0) # Start the application # Create wizard and add the pages to it conf_wizard = ConfigWizard() # Start the wizard conf_wizard.run() # Cleanup conf_wizard.Destroy() app.MainLoop() if __name__ == '__main__': main()