class HAConfigFrontend(FullFeaturedScrollArea): COMPONENT = 'ha' LABEL = tr('High Availability') REQUIREMENTS = ('ha',) ICON = ':/icons/picto_ha.png' if EDENWALL: LINK_STATE = { NOT_REGISTERED: tr('Not registered'), NOT_CONNECTED: tr('Not connected'), CONNECTED: tr('Connected'), } NODE_STATE = { NOT_REGISTERED: tr('Not registered'), ACTIVE: tr('Active'), INACTIVE: tr('Inactive'), } STATE_DESCRIPTIONS = { ENOHA: span(tr('High availability is not configured.')), PENDING_PRIMARY: span(tr('Primary; click "Join" to <br/>complete high availability configuration.')), PRIMARY: span(tr('Primary, in function.')), SECONDARY: span(tr('Secondary, in function.')), PENDING_SECONDARY: span(tr('Secondary; connect EAS to the primary server to <br/>complete high availability configuration.')), } def __init__(self, client, parent): self.auth_page = None self.group_page = None self.auth_configs = {} self.group_configs = {} self.mainwindow = parent self.node_status_label = QLabel() # status of current node (active / inactive) self.link_status_label = QLabel() # status of dedicaced link self.interface_label = QLabel() self.activity_label = QLabel() self.last_error_text = QTextEdit() self.last_error_text.setReadOnly(True) self.last_error_text.setMaximumHeight(100) self.type_label = QLabel() self.join = None self.link_state = None self.ha_last_date = None self.version = self.mainwindow.init_call('ha', 'getComponentVersion') FullFeaturedScrollArea.__init__(self, client, parent) self.missing_upgrades = [] # create timer only if HA activated if self.__ha_type() != ENOHA: self.timer = Timer( self.setViewData, REFRESH_INTERVAL_MILLISECONDS, self.mainwindow.keep_alive.thread, self ) else: self.timer = None self.force_join = QAction(QIcon(":/icons/force_join"), tr("Force joining secondary"), self) self.connect(self.force_join, SIGNAL('triggered(bool)'), self.joinSecondary) self.force_takeover = QAction(QIcon(":/icons/ha_takeover"), tr("Force to become active"), self) self.connect(self.force_takeover, SIGNAL('triggered(bool)'), self.takeover) buttons = [self.force_join, self.force_takeover] self.contextual_toolbar = ToolBar(buttons, name=tr("High Availability")) @staticmethod def get_calls(): """ services called by initial multicall """ return (('ha', 'getState'), ('ha', 'getFullState'), ('ha', 'getComponentVersion')) def __ha_type(self): config = QHAObject.getInstance().cfg if config is None: return ENOHA return config.ha_type def buildInterface(self): frame = QFrame() self.setWidget(frame) self.setWidgetResizable(True) layout = QGridLayout(frame) title = u'<h1>%s</h1>' % self.tr('High Availability Configuration') layout.addWidget(QLabel(title), 0, 0, 1, -1) configure = QPushButton(QIcon(":/icons/configurationha.png"), tr('Configure')) self.mainwindow.writeAccessNeeded(configure) layout.addWidget(configure, 1, 3) self.join = QPushButton(QIcon(":/icons/joinha.png"), tr('Join Secondary')) layout.addWidget(self.join, 2, 3) if "1.1" == self.version: layout.addWidget(QLabel(tr('Appliance status')), 3, 0) layout.addWidget(self.node_status_label, 3, 1) row = 4 else: row = 3 self.last_error_title = QLabel(tr('Last error')) self.missing_upgrade_text_label = QLabel() self.missing_upgrade_nums_label = QLabel() widgets = [ (QLabel(tr('Link status')), self.link_status_label), (QLabel(tr('Type')),self.type_label), (QLabel(tr('Interface')),self.interface_label), (QLabel(tr('Last activity')), self.activity_label), (self.last_error_title, self.last_error_text), (self.missing_upgrade_text_label, self.missing_upgrade_nums_label), ] for index, (label, widget) in enumerate(widgets): layout.addWidget(label, row+index, 0) layout.addWidget(widget, row+index, 1) # syncUpgrades_button = QPushButton(tr('Synchronize upgrades')) # layout.addWidget(syncUpgrades_button, 7, 2) # self.connect(syncUpgrades_button, SIGNAL('clicked()'), self.syncUpgrades) layout.setColumnStretch(0, 1) layout.setColumnStretch(1, 5) layout.setColumnStretch(2, 10) layout.setColumnStretch(3, 1) layout.setRowStretch(row+6, row+7) self.connect(configure, SIGNAL("clicked()"), self.displayConfig) self.connect(self.join, SIGNAL("clicked()"), self.joinSecondary) def displayConfig(self): config = QHAObject.getInstance().hacfg if config.ha_type == PRIMARY: QMessageBox.warning( self, tr('High availability already configured'), tr('High availability status disallows editing the configuration')) return ha_wizard = ConfigWizard(self) ret = ha_wizard.exec_() if ret != QDialog.Accepted: return False qhaobject = QHAObject.getInstance() qhaobject.pre_modify() config = qhaobject.hacfg qnetobject = QNetObject.getInstance() qnetobject.pre_modify() net_cfg = qnetobject.netcfg old_type = config.ha_type new_config = ha_wizard.getData() config.ha_type = new_config.ha_type config.interface_id = new_config.interface_id config.interface_name = new_config.interface_name if config.ha_type in (PENDING_PRIMARY, PENDING_SECONDARY): iface = net_cfg.getIfaceByHardLabel(config.interface_id) configureHA(net_cfg, iface) network_modified = True elif config.ha_type != old_type: deconfigureHA(net_cfg) network_modified = True # XXX should not reconfigure now ? else: network_modified = False valid = qnetobject.post_modify() if not valid: qhaobject.revert() else: # FIXME: use post_modify() result? qhaobject.post_modify() self.setModified(True) if network_modified: network = self.mainwindow.getPage('network') network.setModified(True) dhcp = self.mainwindow.getPage('dhcp') dhcp.dhcp_widget.setModified(True) dhcp.dhcp_widget.fillView() self.setViewData() return True def hide_last_error(self): self.last_error_text.clear() self.last_error_text.hide() self.last_error_title.hide() def show_last_error(self, last_error): if last_error: self.last_error_text.setText(last_error) self.last_error_title.show() self.last_error_text.show() else: self.hide_last_error() def syncUpgrades(self): # First, update the list of missing upgrades: defer = self.setViewData() if defer: defer.addCallback(self._syncUpgrades) def _syncUpgrades(self): pass def fetchConfig(self): # we use QHAObject pass def __disable(self): self.close() raise NuConfModuleDisabled("Disabling high availability interface") def setViewData(self): config = QHAObject.getInstance().hacfg if "1.0" == self.version: raw_state = self.mainwindow.init_call('ha', 'getState') if raw_state is None: self.__disable() return self.link_state, self.ha_last_date, last_error = raw_state self.link_status_label.setText(self.LINK_STATE.get(self.link_state, tr('(unknown)'))) else: raw_state = self.mainwindow.init_call('ha', 'getFullState') if raw_state is None: self.__disable() return node_state = raw_state['node_state'] self.link_state = raw_state['link_state'] self.ha_last_date = raw_state['seen_other'] last_error = raw_state['last_error'] self.link_status_label.setText(self.LINK_STATE.get(self.link_state, tr('(unknown)'))) self.node_status_label.setText(self.NODE_STATE.get(node_state, tr('(unknown)'))) # TEMP : use compatibility instead try: try: if raw_state.get('link_state', None) == CONNECTED: self.join.setEnabled(False) except Exception: if isinstance(raw_state, list) and len(raw_state) > 0: if raw_state[0] == CONNECTED: self.join.setEnabled(False) except TypeError: pass ha_type = self.__ha_type() self.type_label.setText( HAConfigFrontend.STATE_DESCRIPTIONS[ha_type] ) if ha_type != ENOHA: if self.ha_last_date not in (0, None): fmt = '%Y-%m-%d %H:%M:%S' seen = time.strftime(fmt, time.localtime(self.ha_last_date)) self.activity_label.setText(unicode(seen)) if config.interface_id is not None: qnetobject = QNetObject.getInstance() iface = qnetobject.netcfg.getIfaceByHardLabel(config.interface_id) self.interface_label.setText(iface.fullName()) try: last_error = self.client.call('ha', 'getLastError') self.show_last_error(last_error) except Exception: self.hide_last_error() else: self.interface_label.clear() self.activity_label.clear() self.hide_last_error() if ha_type == PRIMARY: async = self.client.async() async.call('ha', 'getMissingUpgradeNums', callback=self._get_missing_upgrade_nums, errback=self.writeError) self.mainwindow.writeAccessNeeded(self.join) if self.join.isEnabled(): self.join.setEnabled(PENDING_PRIMARY == ha_type) def _get_missing_upgrade_nums(self, missing_upgrade_nums): try: self.missing_upgrade_nums = missing_upgrade_nums if type(missing_upgrade_nums) != type([]): self.missing_upgrade_nums_label.setText(tr('N/A')) elif not missing_upgrade_nums: self.missing_upgrade_nums_label.setText(tr('None ')) else: sample = sorted(missing_upgrade_nums) if len(sample) > MAX_MISSING_UPGRADE_NUMS: sample = sample[:MAX_MISSING_UPGRADE_NUMS] + ['...'] self.missing_upgrade_nums_label.setText( ', '.join([unicode(num) for num in sample])) self.missing_upgrade_text_label.setText( tr('Missing upgrades on the secondary')) except Exception: pass def sendConfig(self, message): """ Save HA config """ serialized = QHAObject.getInstance().hacfg.serialize() self.client.call('ha', 'configureHA', serialized, message) def joinSecondary(self): self.mainwindow.addToInfoArea( tr("Attempting to enslave the secondary, please wait...") ) self.splash = SplashScreen() self.splash.setText(tr("Attempting to enslave the secondary...")) self.splash.show() async = self.client.async() async.call('ha', 'startHA', callback = self.successJoin, errback = self.errorJoin ) def successJoin(self, ok): self.splash.hide() self.timer = Timer( self.setViewData, REFRESH_INTERVAL_MILLISECONDS, self.mainwindow.keep_alive.thread, self ) self.join.setEnabled(False) self.mainwindow.addToInfoArea(tr("Joining secondary: success"), category=COLOR_SUCCESS) def errorJoin(self, error): self.splash.hide() self.mainwindow.addToInfoArea(tr('Joining secondary: fail'), category=COLOR_ERROR) warning = QMessageBox(self) warning.setWindowTitle(tr('Joining secondary: fail')) warning.setText(tr('An error was encountered while joining secondary.')) errmsg = exceptionAsUnicode(error) if "current state=ENOHA" in errmsg: errmsg = tr( "Can not join yet: the appliance is still not configured for " "high availability. Did you save and apply your changes?" ) warning.setDetailedText(errmsg) warning.setIcon(QMessageBox.Warning) warning.exec_() def takeover(self): try: self.client.call('ha', 'takeover') except Exception, err: self.mainwindow.exception(err) return self.mainwindow.addToInfoArea(tr('Take over request sent.'), category=COLOR_SUCCESS)
class Applier(object): def __init__(self, mainwindow): self.mainwindow = mainwindow self._interval = _BASE_INTERVAL self.addToInfoArea = mainwindow.addToInfoArea self.client = mainwindow.client self.splash = SplashScreen() self._component_to_name = ComponentToName() self.reset() def setPhase(self, phase): self._last_fired_component = '' self._last_fired_component_formatted = '' def reset(self): self._last_read_index = 0 self._apply_error = None self._applied_component_list = [] self._rolled_back_component_list = [] self._rollback_occurred = False self._component_counter = 0 self._final_success_msg = '' self._final_error_msg = '' self._rollback_error_msg = '' self.setPhase('') def rollback(self): self._component_counter = 0 self._rollback_occurred = True def start(self): self.__start_info() async = self.client.async() async.call('config', 'applyStart', callback=self._start_ok, errback=self._start_err ) def __force_start(self): async = self.client.async() async.call('config', 'forceApplyStart', callback=self._start_ok, errback=self._start_err ) def __start_info(self): # Log an empty line self.mainwindow.addHTMLToInfoArea(BR) self.splash.setText(tr("Applying the new configuration")) #TODO: disable hiding on clic. self.splash.show() def __end_all(self): self.splash.hide() if self.mainwindow._standalone != STANDALONE: self.mainwindow.eas_window.setModified(False) # Log an empty line self.mainwindow.addHTMLToInfoArea(BR) if self._rollback_occurred: message = tr("The application of the new configuration failed: the previous configuration has been restored.") if self._apply_error: message = message + BR + BR + self._apply_error message = unicode(message) QMessageBox.critical( self.mainwindow, tr("Application failed"), message ) else: self.mainwindow.apply_done() def component_label(self, comp_name): return self._component_to_name.display_name(comp_name) def __handle_SessionError(self, err): """ Only acts if action is relevant. You safely can pass any error to this function. """ if isinstance(err, SessionError): QMessageBox.critical( self.mainwindow, tr("Disconnected!"), tr("You have been disconnected. " "Your changes are lost. You may log in again.") ) #FIXME: exit? def __handle_errors(self, err): debug = self.mainwindow.debug if debug: html = Html(formatException(err, debug), escape=False) + BR self.mainwindow.addHTMLToInfoArea(html, category=COLOR_WARNING) else: text = exceptionAsUnicode(err) self.addToInfoArea(text, category=COLOR_WARNING) self.__handle_SessionError(err) def logWithTimestamp(self, timestamp, html, prefix=None): html = Html(html, escape=False) + BR timestamp = formatTimestamp(timestamp, True) if prefix is not None: timestamp = timestamp + prefix self.mainwindow.addHTMLToInfoArea(html, prefix=timestamp) def __process_logs(self, logs): """ Does everything that has to be done with logs -Write them on the console. -take any relevant action returns True if there are other messages to process False if all messages have been processed """ has_more = True if len(logs) == 0: if self._interval < _MAX_INTERVAL: self._interval *= 2 for log in logs: self._interval = _BASE_INTERVAL self._last_read_index += 1 timestamp, message_type, content = log timestamp = parseDatetime(timestamp) if message_type == PHASE_CHANGE: has_more = self._change_phase(timestamp, content) elif message_type in ERRORS: self._display_err(timestamp, content) elif message_type in (GLOBAL_ERROR, GLOBAL_WARNING): colors = {GLOBAL_ERROR:COLOR_ERROR ,GLOBAL_WARNING:COLOR_WARNING} format, substitutions = content message = tr(format) % substitutions self.display_message(timestamp, message, color=colors[message_type]) elif message_type in COMPONENT_LISTS: self._process_component_list(timestamp, message_type, content) elif message_type in COMPONENT_FIRED: self._set_last_fired_component(content) message = tr("%(COMPONENT_NAME)s is being configured") % { "COMPONENT_NAME": self._last_fired_component_formatted} splash_message = self._last_fired_component self._component_counter += 1 if self._rollback_occurred: components = self._rolled_back_component_list else: components = self._applied_component_list if components: progress = " (%s/%s)" % (self._component_counter, len(components)) message += progress splash_message += progress self.logWithTimestamp(timestamp, message) self.splash.setText(splash_message) elif COMPONENT_MESSAGE == message_type: format, substitutions = content message = tr(format) % substitutions message = htmlColor(message, COLOR_VERBOSE) message = tr("%s: %s") % ( self._last_fired_component, unicode(message)) self.logWithTimestamp(timestamp, message) else: # unknow message type message = tr("%s (unknown message type: '%s')") message = message % (content, message_type) message = htmlColor(message, COLOR_INVALID) self.logWithTimestamp(timestamp, message) return has_more def _set_last_fired_component(self, component_name): translated = self.component_label(component_name) self._last_fired_component = translated self._last_fired_component_formatted = htmlBold( htmlColor(translated, COLOR_EMPHASIZED) ) def _display_err(self, timestamp, err): name = self._last_fired_component html = tr("%s triggered the following error") % name html = htmlColor(html, COLOR_ERROR) err = Html(err) # content is an error html = tr("%s: %s") % (unicode(html), unicode(err)) if not self._rollback_occurred: self._apply_error = Html(html, escape=False) self.logWithTimestamp(timestamp, html) def display_message(self, timestamp, message, color=None, prefix=None): if color is not None: html = htmlColor(message, color) else: html = Html(message) self.logWithTimestamp(timestamp, html) def _change_phase(self, timestamp, phase): """ returns True if there are other messages to process False if all messages have been processed """ self.setPhase(phase) has_more = True hexacolor = None if phase == GLOBAL_DONE: if self._rollback_occurred: return False hexacolor = COLOR_SUCCESS message = tr("The new configuration has been successfully applied") if self._final_success_msg: message = self._final_success_msg message = htmlBold(message) has_more = False elif phase == GLOBAL_APPLY_SKIPPED: hexacolor = COLOR_SUCCESS message = tr("Application skipped") message = htmlBold(message) has_more = False elif phase == GLOBAL: hexacolor = COLOR_EMPHASIZED message = tr("Applying the new configuration") message = htmlBold(message) elif phase == APPLYING: hexacolor = COLOR_EMPHASIZED message = tr("Normal application phase started") elif phase == APPLYING_DONE: hexacolor = COLOR_EMPHASIZED message = tr("Normal application phase completed") elif phase == ROLLING_BACK: self.rollback() hexacolor = COLOR_ERROR message = tr("The application of the new configuration failed: " "restoring the previous configuration") if self._rollback_error_msg: message = self._rollback_error_msg message = htmlBold(message) elif phase == ROLLING_BACK_DONE: self._rollback_occurred = True hexacolor = COLOR_ERROR message = tr("The application of the new configuration failed: " "the previous configuration has been restored") if self._final_error_msg: message = self._final_error_msg message = htmlBold(message) else: message = Html(phase) if self.mainwindow.debug \ or phase not in (APPLYING, APPLYING_DONE): prefix = formatTimestamp(timestamp, True) html = message + BR self.mainwindow.addHTMLToInfoArea(html, hexacolor, prefix) return has_more def _process_component_list(self, timestamp, message_type, component_list): displayed_list = map(self.component_label, component_list) displayed_list = u', '.join(displayed_list) if message_type == APPLIED_COMPONENT_LIST: format = tr("Applying the new configuration of components: %s") self._applied_component_list = component_list elif message_type == ROLLED_BACK_COMPONENT_LIST: format = tr("Restoring the previous configuration of components: %s") self._rolled_back_component_list = component_list else: return html = Html(format % displayed_list, escape=False) self.logWithTimestamp(timestamp, html) def _start_err(self, err): """ called when we could not start the poll dance """ if not(isinstance(err, RpcdError) and err.type == "CoreError"): self.__handle_errors(err) self.__end_all() return # old ufwi_conf backend version: falling back to old protocol async = self.client.async() async.call( 'config', 'apply', callback=self._apply_ok, errback=self._apply_err ) def __ask_if_force(self, title, message): ok = QMessageBox.question( self.mainwindow, title, message, QMessageBox.Ok | QMessageBox.Cancel ) if ok == QMessageBox.Ok: self.__force_start() return self.addToInfoArea(tr("Apply operation cancelled.")) self.__end_all() def _start_ok(self, value): """ the poll dance protocol is implemented on the server, go on! """ if value == "trigger reboot? then use force=True": self.__ask_if_force( tr("Trigger a reboot?"), tr( "Applying now (after a restoration) will trigger a reboot of the appliance. " "You can cancel now or proceed." ) ) #Anyway, return (also with cancel) return elif "then use force=True" in value: self.__ask_if_force( RESTORATION_WARNING_TITLE, RESTORATION_WARNING ) #Anyway, return (also with cancel) return if value == "reboot": self.addToInfoArea( tr("This will trigger a reboot of the appliance (after restoration)"), category=COLOR_CRITICAL ) QMessageBox.information( self.mainwindow, tr("EdenWall reboot scheduled"), tr("First application after restoration: EdenWall will reboot afterwards.") ) #reset the last read index self.reset() self._poll() # FIXME: why is this callback never called? def _apply_ok(self, arg): """ End of the apply process. """ self.addToInfoArea(tr("Application successful"), category=COLOR_FANCY) self.__end_all() def _apply_err(self, err): """ End of the apply process. """ self.addToInfoArea(tr("Application failed"), category=COLOR_WARNING) self.__end_all() self.__handle_errors(err) def _poll(self): async = self.client.async() async.call( 'config', 'applyLog', self._last_read_index, callback=self._poll_ok, errback=self._poll_err ) def _poll_ok(self, logs): has_more = self.__process_logs(logs) if has_more: #loop QTimer.singleShot(self._interval, self._poll) else: self.__end_all() def _poll_err(self, err): """ A problem in the apply log dance. After that, we'll rely on a future feature: the getState dance, to know more about the server """ self.__end_all() self.__handle_errors(err) def start_polling( self, final_success_message='', final_error_message='', rollback_error_message='', ): self.reset() self._final_success_msg = final_success_message self._final_error_message = final_error_message self._rollback_error_msg = rollback_error_message self._poll()
class SaveRestoreWidget(QGroupBox): last_save_position = expanduser("~") def __init__(self, mainwindow, parent=None): QGroupBox.__init__(self, parent) self.mainwindow = mainwindow self.buildGui() def buildGui(self): self.__splash = SplashScreen() vbox = QHBoxLayout(self) self.setTitle(tr("Appliance restoration system")) save = QPushButton(QIcon(":/icons/down"), tr("Download appliance configuration"), self) restore = QPushButton(QIcon(":/icons/up"), tr("Restore a previously saved configuration"), self) for item in save, restore: vbox.addWidget(item) self.mainwindow.writeAccessNeeded(restore) self.connect(restore, SIGNAL("clicked()"), self.upload_file) self.connect(save, SIGNAL("clicked()"), self.download_file) # restore.setEnabled(False) def _start_splash(self, message): self.__splash.setText(tr("Please wait...")) self.mainwindow.addToInfoArea(message, category=COLOR_FANCY) self.__splash.show() def _stop_splash(self): self.__splash.hide() def upload_file(self): dialog = RestoreConfirmDialog() accept = dialog.exec_() if not accept: return dialog = UploadDialog( selector_label=tr("Select an EdenWall archive"), filter=tr("EdenWall archive (*.tar.gz *)") ) accepted = dialog.exec_() if accepted != QDialog.Accepted: return filename = dialog.filename if not filename: return with open(filename, "rb") as fd: content = fd.read() content = encodeFileContent(content) self.mainwindow.addToInfoArea(tr("Uploading of an archive file to restore the appliance")) async = self.mainwindow.client.async() async.call("nurestore", "restore", content, callback=self.success_upload, errback=self.error_upload) self._start_splash(tr("Uploading EdenWall restoration archive...")) def success_upload(self, value): self._stop_splash() message = tr("Successfully uploaded the archive file.") self.mainwindow.addToInfoArea(message, COLOR_SUCCESS) restoration_restart(self.mainwindow) def error_upload(self, value): self._stop_splash() message = tr("Error restoring appliance: ") self.mainwindow.addToInfoArea(message + unicode(value), COLOR_ERROR) def download_file(self): async = self.mainwindow.client.async() async.call("nurestore", "export", callback=self.success_download, errback=self.error_download) self._start_splash(tr("Downloading EdenWall restoration archive...")) def success_download(self, value): self._stop_splash() encoded, components = value self.mainwindow.addToInfoArea(tr("Downloaded EdenWall configuration")) extension = "*.tar.gz" archive_description = tr("Edenwall archive file") filter = "%s (%s)" % (archive_description, extension) date = datetime.now().strftime("%c") date = toUnicode(date) date = date.replace(".", "") date = date.replace(" ", "_") date = date.replace(":", "-") host = self.mainwindow.client.host host = host.replace(".", "_") host = host.replace(":", "-") suggestion = u"edenwall-config-%s-%s.tar.gz" % (host, date) filename = QFileDialog.getSaveFileName( self, tr("Choose a filename to save under"), join(SaveRestoreWidget.last_save_position, suggestion), filter ) filename = unicode(filename) if not filename: self.mainwindow.addToInfoArea(tr("EdenWall configuration save cancelled")) return SaveRestoreWidget.last_save_position = split(filename)[0] try: with open(filename, "wb") as fd: fd.write(decodeFileContent(encoded)) except IOError, err: message_vars = (tr("An error occured while saving EdenWall configuration:"), toUnicode(strerror(err.errno))) text_message = "%s\n%s" % message_vars self.mainwindow.addToInfoArea(text_message, category=COLOR_ERROR) html_message = "<span>%s<br/><b>%s</b></span>" % message_vars QMessageBox.critical(self, tr("Save error"), html_message) return self.mainwindow.addToInfoArea(tr("Saved EdenWall configuration as %(filename)s") % {"filename": filename})