class ClusterDisplay(QGroupBox): def __init__(self, parent, clusterframe, settings): QGroupBox.__init__(self, parent) self._settings = settings self._clusterframe = clusterframe self._imagecontainer = None self._jobid = None self._toggle_state = JOB_CONTROL_SUSPEND self._service = None self._host_url = CecogEnvironment.analyzer_config.get('Cluster', 'host_url') try: self._host_url_fallback = CecogEnvironment.analyzer_config.get(\ 'Cluster', 'host_url_fallback') except: # old config file self._host_url_fallback = self._host_url self.setTitle('ClusterControl') label1 = QLabel('Cluster URL:', self) fixed = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) expanding = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) label1.setSizePolicy(fixed) self._label_hosturl = QLabel('', self) self._label_hosturl.setSizePolicy(expanding) label3 = QLabel('Cluster status:', self) label3.setSizePolicy(fixed) self._label_status = QLabel('', self) self._label_status.setSizePolicy(expanding) label4 = QLabel('Path mappings:', self) label4.setSizePolicy(fixed) self._table_info = QTableWidget(self) self._table_info.setSelectionMode(QTableWidget.NoSelection) labels = ['status', 'your machine', 'on the cluster'] self._table_info.setColumnCount(len(labels)) self._table_info.setHorizontalHeaderLabels(labels) self._table_info.setSizePolicy(QSizePolicy(QSizePolicy.Expanding|QSizePolicy.Maximum, QSizePolicy.MinimumExpanding)) layout = QGridLayout(self) layout.addWidget(label1, 0, 0, Qt.AlignRight) layout.addWidget(self._label_hosturl, 0, 1, 1, 4) layout.addWidget(label3, 1, 0, Qt.AlignRight) layout.addWidget(self._label_status, 1, 1, 1, 4) layout.addWidget(label4, 2, 0, Qt.AlignRight) layout.addWidget(self._table_info, 3, 0, 1, 5) label = QLabel('Mail addresses:', self) layout.addWidget(label, 4, 0, Qt.AlignRight) mails = CecogEnvironment.analyzer_config.get('Cluster', 'mail_adresses') self._txt_mail = QLineEdit(mails, self) layout.addWidget(self._txt_mail, 4, 1, 1, 4) self._btn_submit = QPushButton('Submit job', self) self._btn_submit.setEnabled(False) self._btn_submit.clicked.connect(self._on_submit_job) layout.addWidget(self._btn_submit, 5, 0, 1, 5) line = QFrame(self) line.setFrameShape(QFrame.HLine) layout.addWidget(line, 6, 0, 1, 5) label5 = QLabel('Current job ID:', self) layout.addWidget(label5, 7, 0, Qt.AlignRight) self._txt_jobid = QLineEdit(self._jobid or '', self) regexp = QRegExp('\d+') regexp.setPatternSyntax(QRegExp.RegExp2) self._txt_jobid.textEdited.connect(self._on_jobid_entered) self._txt_jobid.returnPressed.connect(self._on_update_job_status) layout.addWidget(self._txt_jobid, 7, 1) self._btn_update = QPushButton('Update', self) self._btn_update.clicked.connect(self._on_update_job_status) layout.addWidget(self._btn_update, 7, 2) self._btn_toogle = QPushButton(self._toggle_state, self) self._btn_toogle.clicked.connect(self._on_toggle_job) self._btn_toogle.setCheckable(True) layout.addWidget(self._btn_toogle, 7, 3) self._btn_terminate = QPushButton('Terminate', self) self._btn_terminate.clicked.connect(self._on_terminate_job) layout.addWidget(self._btn_terminate, 7, 4) label = QLabel('Job status:', self) layout.addWidget(label, 8, 0, Qt.AlignRight) self._label_jobstatus = QLabel('', self) layout.addWidget(self._label_jobstatus, 8, 1, 1, 4) layout.addItem(QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Expanding|QSizePolicy.Maximum), 10, 0, 1, 5) @property def imagecontainer(self): if self._imagecontainer is None: raise RuntimeError("Image container is not loaded yet") return self._imagecontainer @imagecontainer.deleter def imagecontainer(self): del self._imagecontainer @imagecontainer.setter def imagecontainer(self, imagecontainer): self._imagecontainer = imagecontainer def _on_jobid_entered(self, txt): self._jobid = str(txt) @pyqtSlot() def _on_submit_job(self): self._submit_settings.set_section(SECTION_NAME_GENERAL) if not self._submit_settings.get2('constrain_positions'): positions = [] for plate_id in self.imagecontainer.plates: self.imagecontainer.set_plate(plate_id) meta_data = self.imagecontainer.get_meta_data() positions += ['%s___%s' % (plate_id, p) for p in meta_data.positions] self._submit_settings.set2('positions', ','.join(positions)) nr_items = len(positions) else: positions = self._submit_settings.get2('positions') nr_items = len(positions.split(',')) # FIXME: we need to get the current value for 'position_granularity' settings_dummy = self._clusterframe.get_special_settings(self._settings) position_granularity = settings_dummy.get('Cluster', 'position_granularity') path_out = self._submit_settings.get2('pathout') emails = str(self._txt_mail.text()).split(',') try: self.dlg = ProgressDialog("submitting jobs...", None, 0, 0, self) settings_str = self._submit_settings.to_string() func = lambda: self._service.submit_job('cecog_batch', settings_str, path_out, emails, nr_items, position_granularity, VERSION) self.dlg.exec_(func) jobid = self.dlg.getTargetResult() except Exception as e: exception(self, 'Error on job submission (%s)' %str(e)) else: # FIXME: no idea how DRMAA 1.0 compatible this is if type(jobid) == types.ListType: self._jobid = ','.join(jobid) main_jobid = jobid[0].split('.')[0] else: self._jobid = str(jobid) main_jobid = jobid self._txt_jobid.setText(self._jobid) self._update_job_status() information(self, 'Job submitted successfully', "Job successfully submitted to the cluster.\nJob ID: %s, items: %d" % (main_jobid, nr_items)) @pyqtSlot() def _on_terminate_job(self): try: self.dlg = ProgressDialog("terminating jobs...", None, 0, 0, self) func = lambda: self._service.control_job(self._jobid, JOB_CONTROL_TERMINATE) self.dlg.exec_(func) except Exception as e: exception(self, 'Error on job termination (%s)' %str(e)) else: self._btn_toogle.setChecked(False) self._toggle_state = JOB_CONTROL_SUSPEND self._btn_toogle.setText(self._toggle_state) self._update_job_status() @pyqtSlot() def _on_toggle_job(self): try: self.dlg = ProgressDialog("suspending jobs...", None, 0, 0, self) func = lambda: self._service.control_job(self._jobid, self._toggle_state) self.dlg.exec_(func) except Exception as e: self._toggle_state = JOB_CONTROL_SUSPEND self._btn_toogle.setChecked(False) exception(self, 'Error on toggle job status (%s)' %str(e)) else: if self._toggle_state == JOB_CONTROL_SUSPEND: self._toggle_state = JOB_CONTROL_RESUME else: self._toggle_state = JOB_CONTROL_SUSPEND self._update_job_status() self._btn_toogle.setText(self._toggle_state) @pyqtSlot() def _on_update_job_status(self): txt = self._update_job_status() information(self, 'Cluster update', "Message: '%s'" % txt) def _update_job_status(self): try: self.dlg = ProgressDialog("updating job status...", None, 0, 0, self) func = lambda: self._service.get_job_status(self._jobid) self.dlg.exec_(func) txt = self.dlg.getTargetResult() except Exception as e: exception(self, 'Error on retrieve job status (%s)' %str(e)) else: self._label_jobstatus.setText(txt) return txt def _check_host_url(self): url = urlparse.urlparse(self._host_url) try: test_sock = socket.create_connection((url.hostname, url.port), timeout=1) test_sock.shutdown(2) test_sock.close() except: try: url = urlparse.urlparse(self._host_url_fallback) test_sock = socket.create_connection((url.hostname, url.port), timeout=1) test_sock.shutdown(2) test_sock.close() self._host_url = self._host_url_fallback except: exception(self, 'Error on connecting to cluster control service. Please check your config.ini') def _connect(self): self._check_host_url() success = False msg = 'Error on connecting to cluster control service on %s' % self._host_url try: client = RemotingService(self._host_url) self.dlg = ProgressDialog("connecting to cluster...", None, 0, 0, self) func = lambda: client.getService('clustercontrol') self.dlg.exec_(func) self._service = self.dlg.getTargetResult() except: exception(self, msg) else: try: self.dlg.exec_(self._service.get_cecog_versions) cluster_versions = self.dlg.getTargetResult() except Exception as e: exception(self, msg + '(%s)' %str(e)) else: if not VERSION in set(cluster_versions): warning(self, 'Cecog version %s not supported by the cluster' % VERSION, 'Valid versions are: %s' \ % ', '.join(cluster_versions)) else: success = True return success def _get_mappable_paths(self): ''' Get the paths/filenames which have to be mapped to run on a remote cluster. Whether an option is considered or not might depend on other values/switches, e.g. if classification is not needed there is no need to map the paths. ''' #FIXME: should be done in a better way. results = [] targets = [(('General', 'pathin'), []), (('General', 'pathout'),[]), (('General', 'structure_file_extra_path_name'), [('General', 'structure_file_extra_path')]), (('Classification', 'primary_classification_envpath'), [('Processing', 'primary_classification')]), (('Classification', 'secondary_classification_envpath'), [('General', 'process_secondary'), ('Processing', 'secondary_classification')]), (('Classification', 'tertiary_classification_envpath'), [('General', 'process_tertiary'), ('Processing', 'tertiary_classification')]), (('Classification', 'merged_classification_envpath'), [('General', 'process_merged'), ('Processing', 'merged_classification')]), ] targets.extend([(('ObjectDetection', '%s_flat_field_correction_image_dir' % prefix), [('ObjectDetection', '%s_flat_field_correction' % prefix)]) for prefix in ['primary', 'secondary', 'tertiary']] ) for info, const in targets: passed = reduce(lambda x,y: x and y, map(lambda z: self._settings.get(*z), const), True) if passed: results.append(info) return results def update_display(self, is_active): if self._connect(): self._can_submit = True try: self._submit_settings = self._clusterframe.get_special_settings( \ self._settings, self.imagecontainer.has_timelapse) except: self._submit_settings = self._clusterframe.get_special_settings(self._settings) self._label_hosturl.setText(self._host_url) self._label_status.setText(self._service.get_service_info()) mappable_paths = self._get_mappable_paths() self._table_info.clearContents() self._table_info.setRowCount(len(mappable_paths)) for idx, info in enumerate(mappable_paths): value = self._settings.get(*info) mapped = CecogEnvironment.map_path_to_os( value, target_os='linux', force=False) self._submit_settings.set(info[0], info[1], mapped) status = not mapped is None item = QTableWidgetItem() item.setBackground(QBrush(QColor('green' if status else 'red'))) txt_mapped = str(mapped) if status else \ 'Warning: path can not be mapped on the cluster' self._table_info.setItem(idx, 0, item) self._table_info.setItem(idx, 1, QTableWidgetItem(value)) self._table_info.setItem(idx, 2, QTableWidgetItem(txt_mapped)) self._can_submit &= status self._table_info.resizeColumnsToContents() self._table_info.resizeRowsToContents() self._btn_submit.setEnabled(self._can_submit and is_active) self._btn_terminate.setEnabled(is_active) self._btn_toogle.setEnabled(is_active) self._btn_update.setEnabled(is_active) else: self._btn_submit.setEnabled(False) self._btn_terminate.setEnabled(False) self._btn_toogle.setEnabled(False) self._btn_update.setEnabled(False)
class ClusterDisplay(QGroupBox): def __init__(self, parent, clusterframe, settings): QGroupBox.__init__(self, parent) self._settings = settings self._clusterframe = clusterframe self._imagecontainer = None self._jobid = None self._toggle_state = JOB_CONTROL_SUSPEND self._service = None self._host_url = CecogEnvironment.analyzer_config.get( 'Cluster', 'host_url') try: self._host_url_fallback = CecogEnvironment.analyzer_config.get(\ 'Cluster', 'host_url_fallback') except: # old config file self._host_url_fallback = self._host_url self.setTitle('ClusterControl') label1 = QLabel('Cluster URL:', self) fixed = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) expanding = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) label1.setSizePolicy(fixed) self._label_hosturl = QLabel('', self) self._label_hosturl.setSizePolicy(expanding) label3 = QLabel('Cluster status:', self) label3.setSizePolicy(fixed) self._label_status = QLabel('', self) self._label_status.setSizePolicy(expanding) label4 = QLabel('Path mappings:', self) label4.setSizePolicy(fixed) self._table_info = QTableWidget(self) self._table_info.setSelectionMode(QTableWidget.NoSelection) labels = ['status', 'your machine', 'on the cluster'] self._table_info.setColumnCount(len(labels)) self._table_info.setHorizontalHeaderLabels(labels) self._table_info.setSizePolicy(QSizePolicy(QSizePolicy.Expanding|QSizePolicy.Maximum, QSizePolicy.MinimumExpanding)) layout = QGridLayout(self) layout.addWidget(label1, 0, 0, Qt.AlignRight) layout.addWidget(self._label_hosturl, 0, 1, 1, 4) layout.addWidget(label3, 1, 0, Qt.AlignRight) layout.addWidget(self._label_status, 1, 1, 1, 4) layout.addWidget(label4, 2, 0, Qt.AlignRight) layout.addWidget(self._table_info, 3, 0, 1, 5) label = QLabel('Mail addresses:', self) layout.addWidget(label, 4, 0, Qt.AlignRight) mails = CecogEnvironment.analyzer_config.get('Cluster', 'mail_adresses') self._txt_mail = QLineEdit(mails, self) layout.addWidget(self._txt_mail, 4, 1, 1, 4) self._btn_submit = QPushButton('Submit job', self) self._btn_submit.setEnabled(False) self._btn_submit.clicked.connect(self._on_submit_job) layout.addWidget(self._btn_submit, 5, 0, 1, 5) line = QFrame(self) line.setFrameShape(QFrame.HLine) layout.addWidget(line, 6, 0, 1, 5) label5 = QLabel('Current job ID:', self) layout.addWidget(label5, 7, 0, Qt.AlignRight) self._txt_jobid = QLineEdit(self._jobid or '', self) regexp = QRegExp('\d+') regexp.setPatternSyntax(QRegExp.RegExp2) self._txt_jobid.textEdited.connect(self._on_jobid_entered) self._txt_jobid.returnPressed.connect(self._on_update_job_status) layout.addWidget(self._txt_jobid, 7, 1) self._btn_update = QPushButton('Update', self) self._btn_update.clicked.connect(self._on_update_job_status) layout.addWidget(self._btn_update, 7, 2) self._btn_toogle = QPushButton(self._toggle_state, self) self._btn_toogle.clicked.connect(self._on_toggle_job) self._btn_toogle.setCheckable(True) layout.addWidget(self._btn_toogle, 7, 3) self._btn_terminate = QPushButton('Terminate', self) self._btn_terminate.clicked.connect(self._on_terminate_job) layout.addWidget(self._btn_terminate, 7, 4) label = QLabel('Job status:', self) layout.addWidget(label, 8, 0, Qt.AlignRight) self._label_jobstatus = QLabel('', self) layout.addWidget(self._label_jobstatus, 8, 1, 1, 4) layout.addItem(QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Expanding|QSizePolicy.Maximum), 10, 0, 1, 5) @property def jobIds(self): return self._txt_jobid.text() @jobIds.setter def jobIds(self, jobids): self._txt_jobid.setText(jobids) self._jobid = str(jobids) @property def imagecontainer(self): if self._imagecontainer is None: raise RuntimeError("Image container is not loaded yet") return self._imagecontainer @imagecontainer.deleter def imagecontainer(self): del self._imagecontainer @imagecontainer.setter def imagecontainer(self, imagecontainer): self._imagecontainer = imagecontainer def _on_jobid_entered(self, txt): self._jobid = str(txt) @pyqtSlot() def _on_submit_job(self): self._submit_settings.set_section(SECTION_NAME_GENERAL) if not self._submit_settings.get2('constrain_positions'): positions = [] for plate_id in self.imagecontainer.plates: self.imagecontainer.set_plate(plate_id) meta_data = self.imagecontainer.get_meta_data() positions += ['%s___%s' % (plate_id, p) for p in meta_data.positions] self._submit_settings.set2('positions', ','.join(positions)) nr_items = len(positions) else: positions = self._submit_settings.get2('positions') nr_items = len(positions.split(',')) # FIXME: we need to get the current value for 'position_granularity' settings_dummy = self._clusterframe.get_special_settings(self._settings) position_granularity = settings_dummy.get('Cluster', 'position_granularity') path_out = self._submit_settings.get2('pathout') emails = str(self._txt_mail.text()).split(',') try: self.dlg = ProgressDialog("submitting jobs...", None, 0, 0, self) settings_str = self._submit_settings.to_string() func = lambda: self._service.submit_job('cecog_batch', settings_str, path_out, emails, nr_items, position_granularity, version) self.dlg.exec_(func) jobid = self.dlg.getTargetResult() except Exception as e: exception(self, 'Error on job submission (%s)' %str(e)) else: # FIXME: no idea how DRMAA 1.0 compatible this is if type(jobid) == types.ListType: self._jobid = ','.join(jobid) main_jobid = jobid[0].split('.')[0] else: self._jobid = str(jobid) main_jobid = jobid self._txt_jobid.setText(self._jobid) self._update_job_status() information(self, 'Job submitted successfully', "Job successfully submitted to the cluster.\nJob ID: %s, items: %d" % (main_jobid, nr_items)) @pyqtSlot() def _on_terminate_job(self): try: self.dlg = ProgressDialog("terminating jobs...", None, 0, 0, self) func = lambda: self._service.control_job(self._jobid, JOB_CONTROL_TERMINATE) self.dlg.exec_(func) except Exception as e: exception(self, 'Error on job termination (%s)' %str(e)) else: self._btn_toogle.setChecked(False) self._toggle_state = JOB_CONTROL_SUSPEND self._btn_toogle.setText(self._toggle_state) self._update_job_status() @pyqtSlot() def _on_toggle_job(self): try: self.dlg = ProgressDialog("suspending jobs...", None, 0, 0, self) func = lambda: self._service.control_job(self._jobid, self._toggle_state) self.dlg.exec_(func) except Exception as e: self._toggle_state = JOB_CONTROL_SUSPEND self._btn_toogle.setChecked(False) exception(self, 'Error on toggle job status (%s)' %str(e)) else: if self._toggle_state == JOB_CONTROL_SUSPEND: self._toggle_state = JOB_CONTROL_RESUME else: self._toggle_state = JOB_CONTROL_SUSPEND self._update_job_status() self._btn_toogle.setText(self._toggle_state) @pyqtSlot() def _on_update_job_status(self): txt = self._update_job_status() information(self, 'Cluster update', "Message: '%s'" % txt) def _update_job_status(self): try: self.dlg = ProgressDialog("updating job status...", None, 0, 0, self) func = lambda: self._service.get_job_status(self._jobid) self.dlg.exec_(func) txt = self.dlg.getTargetResult() except Exception as e: exception(self, 'Error on retrieve job status (%s)' %str(e)) else: self._label_jobstatus.setText(txt) return txt def _check_host_url(self): url = urlparse.urlparse(self._host_url) try: test_sock = socket.create_connection((url.hostname, url.port), timeout=1) test_sock.shutdown(2) test_sock.close() except: try: url = urlparse.urlparse(self._host_url_fallback) test_sock = socket.create_connection((url.hostname, url.port), timeout=1) test_sock.shutdown(2) test_sock.close() self._host_url = self._host_url_fallback except: exception(self, 'Error on connecting to cluster control service. Please check your config.ini') def _connect(self): self._check_host_url() success = False msg = 'Error on connecting to cluster control service on %s' % self._host_url try: client = RemotingService(self._host_url) self.dlg = ProgressDialog("connecting to cluster...", None, 0, 0, self) func = lambda: client.getService('clustercontrol') self.dlg.exec_(func) self._service = self.dlg.getTargetResult() except: exception(self, msg) else: try: self.dlg.exec_(self._service.get_cecog_versions) cluster_versions = self.dlg.getTargetResult() except Exception as e: exception(self, msg + '(%s)' %str(e)) else: if not version in set(cluster_versions): warning(self, 'Cecog version %s not supported by the cluster' % version, 'Valid versions are: %s' \ % ', '.join(cluster_versions)) else: success = True return success def _get_mappable_paths(self): ''' Get the paths/filenames which have to be mapped to run on a remote cluster. Whether an option is considered or not might depend on other values/switches, e.g. if classification is not needed there is no need to map the paths. ''' #FIXME: should be done in a better way. results = [] targets = [(('General', 'pathin'), []), (('General', 'pathout'),[]), (('General', 'structure_file_extra_path_name'), [('General', 'structure_file_extra_path')]), (('Classification', 'primary_classification_envpath'), [('Processing', 'primary_classification')]), (('Classification', 'secondary_classification_envpath'), [('General', 'process_secondary'), ('Processing', 'secondary_classification')]), (('Classification', 'tertiary_classification_envpath'), [('General', 'process_tertiary'), ('Processing', 'tertiary_classification')]), (('Classification', 'merged_classification_envpath'), [('General', 'process_merged'), ('Processing', 'merged_classification')]), ] targets.extend([(('ObjectDetection', '%s_flat_field_correction_image_dir' % prefix), [('ObjectDetection', '%s_flat_field_correction' % prefix)]) for prefix in ['primary', 'secondary', 'tertiary']] ) for info, const in targets: passed = reduce(lambda x,y: x and y, map(lambda z: self._settings.get(*z), const), True) if passed: results.append(info) return results def update_display(self, is_active): if self._connect(): self._can_submit = True try: self._submit_settings = self._clusterframe.get_special_settings( \ self._settings, self.imagecontainer.has_timelapse) except: self._submit_settings = self._clusterframe.get_special_settings(self._settings) self._label_hosturl.setText(self._host_url) self._label_status.setText(self._service.get_service_info()) mappable_paths = self._get_mappable_paths() self._table_info.clearContents() self._table_info.setRowCount(len(mappable_paths)) for idx, info in enumerate(mappable_paths): value = self._settings.get(*info) mapped = CecogEnvironment.map_path_to_os( value, target_os='linux', force=False) self._submit_settings.set(info[0], info[1], mapped) status = not mapped is None item = QTableWidgetItem() item.setBackground(QBrush(QColor('green' if status else 'red'))) txt_mapped = str(mapped) if status else \ 'Warning: path can not be mapped on the cluster' self._table_info.setItem(idx, 0, item) self._table_info.setItem(idx, 1, QTableWidgetItem(value)) self._table_info.setItem(idx, 2, QTableWidgetItem(txt_mapped)) self._can_submit &= status self._table_info.resizeColumnsToContents() self._table_info.resizeRowsToContents() self._btn_submit.setEnabled(self._can_submit and is_active) self._btn_terminate.setEnabled(is_active) self._btn_toogle.setEnabled(is_active) self._btn_update.setEnabled(is_active) else: self._btn_submit.setEnabled(False) self._btn_terminate.setEnabled(False) self._btn_toogle.setEnabled(False) self._btn_update.setEnabled(False)
class ClusterDisplay(QGroupBox): MIN_API_VERSION = 2 def __init__(self, parent, clusterframe, settings): super(ClusterDisplay, self).__init__(parent) self._settings = settings self._clusterframe = clusterframe self._imagecontainer = None self._jobid = None self._toggle_state = JOB_CONTROL_SUSPEND self._service = None self._host_url = None self.setTitle('ClusterControl') label1 = QLabel('Cluster URL:', self) fixed = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) expanding = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) label1.setSizePolicy(fixed) self._label_hosturl = QLabel('', self) self._label_hosturl.setSizePolicy(expanding) label3 = QLabel('Cluster status:', self) label3.setSizePolicy(fixed) self._label_status = QLabel('', self) self._label_status.setSizePolicy(expanding) label4 = QLabel('Path mappings:', self) label4.setSizePolicy(fixed) self._table_info = QTableWidget(self) self._table_info.setSelectionMode(QTableWidget.NoSelection) labels = ['Local', 'Remote'] self._table_info.setColumnCount(len(labels)) self._table_info.setHorizontalHeaderLabels(labels) self._table_info.setSizePolicy( QSizePolicy(QSizePolicy.Expanding|QSizePolicy.Maximum, QSizePolicy.MinimumExpanding)) layout = QGridLayout(self) layout.addWidget(label1, 0, 0, Qt.AlignRight) layout.addWidget(self._label_hosturl, 0, 1, 1, 4) layout.addWidget(label3, 1, 0, Qt.AlignRight) layout.addWidget(self._label_status, 1, 1, 1, 4) layout.addWidget(label4, 2, 0, Qt.AlignRight) layout.addWidget(self._table_info, 3, 0, 1, 5) self._btn_submit = QPushButton('Submit job', self) self._btn_submit.setEnabled(False) self._btn_submit.clicked.connect(self._on_submit_job) layout.addWidget(self._btn_submit, 5, 0, 1, 5) line = QFrame(self) line.setFrameShape(QFrame.HLine) layout.addWidget(line, 6, 0, 1, 5) label5 = QLabel('Current job ID:', self) layout.addWidget(label5, 7, 0, Qt.AlignRight) self._txt_jobid = QLineEdit(self._jobid or '', self) regexp = QRegExp('\d+') regexp.setPatternSyntax(QRegExp.RegExp2) self._txt_jobid.textEdited.connect(self._on_jobid_entered) self._txt_jobid.returnPressed.connect(self._on_update_job_status) layout.addWidget(self._txt_jobid, 7, 1) self._btn_update = QPushButton('Update', self) self._btn_update.clicked.connect(self._on_update_job_status) layout.addWidget(self._btn_update, 7, 2) self._btn_toogle = QPushButton(self._toggle_state, self) self._btn_toogle.clicked.connect(self._on_toggle_job) self._btn_toogle.setCheckable(True) layout.addWidget(self._btn_toogle, 7, 3) self._btn_terminate = QPushButton('Terminate', self) self._btn_terminate.clicked.connect(self._on_terminate_job) layout.addWidget(self._btn_terminate, 7, 4) label = QLabel('Job status:', self) layout.addWidget(label, 8, 0, Qt.AlignRight) self._label_jobstatus = QLabel('', self) layout.addWidget(self._label_jobstatus, 8, 1, 1, 4) layout.addItem(QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Expanding|QSizePolicy.Maximum), 10, 0, 1, 5) @property def jobIds(self): jids = self._txt_jobid.text() if not jids: return None else: return jids @jobIds.setter def jobIds(self, jobids): self._txt_jobid.setText(jobids) self._jobid = str(jobids) @property def imagecontainer(self): if self._imagecontainer is None: raise RuntimeError("Image container is not loaded yet") return self._imagecontainer @imagecontainer.deleter def imagecontainer(self): del self._imagecontainer @imagecontainer.setter def imagecontainer(self, imagecontainer): self._imagecontainer = imagecontainer def _on_jobid_entered(self, txt): self._jobid = str(txt) def clear_output_directory(self, directory): """Remove the content of the output directory except the structure file.""" for root, dirs, files in os.walk(directory, topdown=False): for name in files: if not name.endswith(".xml"): try: os.remove(os.path.join(root, name)) except OSError: pass for name in dirs: try: os.rmdir(os.path.join(root, name)) except OSError: pass @pyqtSlot() def _on_submit_job(self): self._submit_settings.set_section(SECTION_NAME_GENERAL) if not self._submit_settings.get2('constrain_positions'): positions = [] for plate_id in self.imagecontainer.plates: self.imagecontainer.set_plate(plate_id) meta_data = self.imagecontainer.get_meta_data() positions += ['%s___%s' % (plate_id, p) for p in meta_data.positions] self._submit_settings.set2('positions', ','.join(positions)) nr_items = len(positions) else: positions = self._submit_settings.get2('positions') nr_items = len(positions.split(',')) settings_dummy = self._clusterframe.get_special_settings(self._settings) apc = AppPreferences() batch_size = apc.batch_size pathout = self._submit_settings.get2('pathout') if not self._submit_settings('General', 'skip_finished'): self.clear_output_directory(self._settings("General", "pathout")) try: self.dlg = ProgressDialog("Submitting Jobs...", None, 0, 0, self) settings_str = self._submit_settings.to_string() func = lambda: self._service.submit_job('cecog_batch', settings_str, pathout, nr_items, batch_size, version) self.dlg.exec_(func) jobid = self.dlg.getTargetResult() except Exception as e: QMessageBox.critical( self, "Error", 'Job submission failed (%s)' %str(e)) else: # FIXME: no idea how DRMAA 1.0 compatible this is if type(jobid) == types.ListType: self._jobid = ','.join(jobid) main_jobid = jobid[0].split('.')[0] else: self._jobid = str(jobid) main_jobid = jobid self._txt_jobid.setText(self._jobid) self._update_job_status() QMessageBox.information( self, "Information", ("Job(s) successfully submitted\n" "Job ID: %s, #jobs: %d" % (main_jobid, nr_items))) @pyqtSlot() def _on_terminate_job(self): if self.jobIds is None: return try: self.dlg = ProgressDialog("Terminating Jobs...", None, 0, 0, self) func = lambda: self._service.control_job(self._jobid, JOB_CONTROL_TERMINATE) self.dlg.exec_(func) except Exception as e: QMessageBox.critical( self, "Error", "Job termination failed (%s)" %str(e)) else: self._btn_toogle.setChecked(False) self._toggle_state = JOB_CONTROL_SUSPEND self._btn_toogle.setText(self._toggle_state) self._update_job_status() @pyqtSlot() def _on_toggle_job(self): if self.jobIds is None: return try: self.dlg = ProgressDialog("Suspending Jobs...", None, 0, 0, self) func = lambda: self._service.control_job(self._jobid, self._toggle_state) self.dlg.exec_(func) except Exception as e: self._toggle_state = JOB_CONTROL_SUSPEND self._btn_toogle.setChecked(False) QMessageBox.critical( self, "Error", "Could not toggle job status (%s)" %str(e)) else: if self._toggle_state == JOB_CONTROL_SUSPEND: self._toggle_state = JOB_CONTROL_RESUME else: self._toggle_state = JOB_CONTROL_SUSPEND self._update_job_status() self._btn_toogle.setText(self._toggle_state) @pyqtSlot() def _on_update_job_status(self): txt = self._update_job_status() if txt is not None: QMessageBox.information(self, "Cluster update", "Message: '%s'" % txt) def _update_job_status(self): if self.jobIds is None: return try: self.dlg = ProgressDialog("Updating Job Status...", None, 0, 0, self) func = lambda: self._service.get_job_status(self._jobid) self.dlg.exec_(func) txt = self.dlg.getTargetResult() except Exception as e: QMessageBox.critical( self, "Error", 'Could not retrieve job status (%s)' %str(e)) else: self._label_jobstatus.setText(txt) return txt def _check_host_url(self): url = urlparse.urlparse(self._host_url) try: test_sock = socket.create_connection((url.hostname, url.port), timeout=1) test_sock.shutdown(2) test_sock.close() except: msg = "Connection failed (%s)" %self._host_url raise ConnectionError(msg) def _check_api_version(self): try: api_version = self._service.api_version() except RemotingError: # this call is not supported by older version of the # cluster service api_version = 1 if api_version < self.MIN_API_VERSION: msg = ("Api version of the cluster services is %d " "but version %d is required.") \ %(self.MIN_API_VERSION, api_version) raise RuntimeError(msg) def _turn_off_cluster_support(self): pref = AppPreferences() pref.cluster_support = False pref.saveSettings() def _connect(self): success = False try: self._check_host_url() client = RemotingService(self._host_url) self.dlg = ProgressDialog("Connecting to Cluster...", None, 0, 0, self) func = lambda: client.getService('clustercontrol') self.dlg.exec_(func) self._service = self.dlg.getTargetResult() self._check_api_version() except ConnectionError as e: msg = ("%s\nDo you want to turn off the cluster support?") %str(e) ret = QMessageBox.question( self, "Error", msg) if ret == QMessageBox.Yes: self._turn_off_cluster_support() except Exception as e: QMessageBox.critical(self, "Error", str(e)) else: try: self.dlg.exec_(self._service.get_cecog_versions) cluster_versions = self.dlg.getTargetResult() except Exception as e: QMessageBox.critical(self, "Error", str(e)) else: if not version in set(cluster_versions): QMessageBox.warning( self, "Warning", ("Cluster does not support %s %s" "Available versions: %s" %(appname, version, ', '.join(cluster_versions)))) else: success = True return success def _get_mappable_paths(self): """Get the paths/filenames which have to be mapped to run on a remote cluster. Whether an option is considered or not might depend on other values/switches, e.g. if classification is not needed there is no need to map the paths. """ # FIXME: should be done in a better way. results = [] targets = [(('General', 'pathin'), []), (('General', 'pathout'),[]), (('General', 'plate_layout'),[]), (('Classification', 'primary_classification_envpath'), [('Processing', 'primary_classification')]), (('Classification', 'secondary_classification_envpath'), [('General', 'process_secondary'), ('Processing', 'secondary_classification')]), (('Classification', 'tertiary_classification_envpath'), [('General', 'process_tertiary'), ('Processing', 'tertiary_classification')]), (('Classification', 'merged_classification_envpath'), [('General', 'process_merged'), ('Processing', 'merged_classification')]), ] targets.extend( [(('ObjectDetection', '%s_flat_field_correction_image_dir' % prefix), [('ObjectDetection', '%s_flat_field_correction' % prefix)]) for prefix in ['primary', 'secondary', 'tertiary']]) for info, const in targets: passed = reduce(lambda x,y: x and y, map(lambda z: self._settings.get(*z), const), True) if passed: results.append(info) return results def check_directories(self): """Check local and remote directories defined in the path mapping table for existence and setup the table view accordingly. """ ndirs = self._table_info.rowCount() remote_dirs = [self._table_info.item(i, 1).text() for i in xrange(ndirs)] remote_state = self._service.check_directory(remote_dirs) local_dirs = [self._table_info.item(i, 0).text() for i in xrange(ndirs)] local_state = [isdir(d) for d in local_dirs] states = [local_state, remote_state] for i in xrange(ndirs): for j in xrange(2): item = self._table_info.item(i, j) item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) if states[j][i]: item.setBackground(QBrush(QColor("green"))) else: if not item.text(): item.setText("Not available") item.setBackground(QBrush(QColor("red"))) def update_display(self, is_active): apc = AppPreferences() self._host_url = apc.url if self._connect(): can_submit = True try: self._submit_settings = self._clusterframe.get_special_settings( \ self._settings, self.imagecontainer.has_timelapse) except: self._submit_settings = self._clusterframe.get_special_settings(self._settings) self._label_hosturl.setText(self._host_url) self._label_status.setText(self._service.get_service_info()) mappable_paths = self._get_mappable_paths() self._table_info.clearContents() self._table_info.setRowCount(len(mappable_paths)) for i, info in enumerate(mappable_paths): value = self._settings.get(*info) mapped = apc.map2platform(value) self._submit_settings.set(info[0], info[1], mapped) if mapped is None: can_submit = False self._table_info.setItem(i, 0, QTableWidgetItem(value)) self._table_info.setItem(i, 1, QTableWidgetItem(mapped)) self.check_directories() self._table_info.resizeColumnsToContents() self._table_info.resizeRowsToContents() self._btn_submit.setEnabled(can_submit and is_active) else: self._btn_submit.setEnabled(False)
class ClusterDisplay(QGroupBox): MIN_API_VERSION = 2 def __init__(self, parent, clusterframe, settings): super(ClusterDisplay, self).__init__(parent) self._settings = settings self._clusterframe = clusterframe self._imagecontainer = None self._jobid = None self._toggle_state = JOB_CONTROL_SUSPEND self._service = None self._host_url = None self.setTitle('ClusterControl') label1 = QLabel('Cluster URL:', self) fixed = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) expanding = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) label1.setSizePolicy(fixed) self._label_hosturl = QLabel('', self) self._label_hosturl.setSizePolicy(expanding) label3 = QLabel('Cluster status:', self) label3.setSizePolicy(fixed) self._label_status = QLabel('', self) self._label_status.setSizePolicy(expanding) label4 = QLabel('Path mappings:', self) label4.setSizePolicy(fixed) self._table_info = QTableWidget(self) self._table_info.setSelectionMode(QTableWidget.NoSelection) labels = ['Local', 'Remote'] self._table_info.setColumnCount(len(labels)) self._table_info.setHorizontalHeaderLabels(labels) self._table_info.setSizePolicy( QSizePolicy(QSizePolicy.Expanding | QSizePolicy.Maximum, QSizePolicy.MinimumExpanding)) layout = QGridLayout(self) layout.addWidget(label1, 0, 0, Qt.AlignRight) layout.addWidget(self._label_hosturl, 0, 1, 1, 4) layout.addWidget(label3, 1, 0, Qt.AlignRight) layout.addWidget(self._label_status, 1, 1, 1, 4) layout.addWidget(label4, 2, 0, Qt.AlignRight) layout.addWidget(self._table_info, 3, 0, 1, 5) self._btn_submit = QPushButton('Submit job', self) self._btn_submit.setEnabled(False) self._btn_submit.clicked.connect(self._on_submit_job) layout.addWidget(self._btn_submit, 5, 0, 1, 5) line = QFrame(self) line.setFrameShape(QFrame.HLine) layout.addWidget(line, 6, 0, 1, 5) label5 = QLabel('Current job ID:', self) layout.addWidget(label5, 7, 0, Qt.AlignRight) self._txt_jobid = QLineEdit(self._jobid or '', self) regexp = QRegExp('\d+') regexp.setPatternSyntax(QRegExp.RegExp2) self._txt_jobid.textEdited.connect(self._on_jobid_entered) self._txt_jobid.returnPressed.connect(self._on_update_job_status) layout.addWidget(self._txt_jobid, 7, 1) self._btn_update = QPushButton('Update', self) self._btn_update.clicked.connect(self._on_update_job_status) layout.addWidget(self._btn_update, 7, 2) self._btn_toogle = QPushButton(self._toggle_state, self) self._btn_toogle.clicked.connect(self._on_toggle_job) self._btn_toogle.setCheckable(True) layout.addWidget(self._btn_toogle, 7, 3) self._btn_terminate = QPushButton('Terminate', self) self._btn_terminate.clicked.connect(self._on_terminate_job) layout.addWidget(self._btn_terminate, 7, 4) label = QLabel('Job status:', self) layout.addWidget(label, 8, 0, Qt.AlignRight) self._label_jobstatus = QLabel('', self) layout.addWidget(self._label_jobstatus, 8, 1, 1, 4) layout.addItem( QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Expanding | QSizePolicy.Maximum), 10, 0, 1, 5) @property def jobIds(self): jids = self._txt_jobid.text() if not jids: return None else: return jids @jobIds.setter def jobIds(self, jobids): self._txt_jobid.setText(jobids) self._jobid = str(jobids) @property def imagecontainer(self): if self._imagecontainer is None: raise RuntimeError("Image container is not loaded yet") return self._imagecontainer @imagecontainer.deleter def imagecontainer(self): del self._imagecontainer @imagecontainer.setter def imagecontainer(self, imagecontainer): self._imagecontainer = imagecontainer def _on_jobid_entered(self, txt): self._jobid = str(txt) @pyqtSlot() def _on_submit_job(self): self._submit_settings.set_section(SECTION_NAME_GENERAL) if not self._submit_settings.get2('constrain_positions'): positions = [] for plate_id in self.imagecontainer.plates: self.imagecontainer.set_plate(plate_id) meta_data = self.imagecontainer.get_meta_data() positions += [ '%s___%s' % (plate_id, p) for p in meta_data.positions ] self._submit_settings.set2('positions', ','.join(positions)) nr_items = len(positions) else: positions = self._submit_settings.get2('positions') nr_items = len(positions.split(',')) settings_dummy = self._clusterframe.get_special_settings( self._settings) apc = AppPreferences() batch_size = apc.batch_size pathout = self._submit_settings.get2('pathout') try: self.dlg = ProgressDialog("Submitting Jobs...", None, 0, 0, self) settings_str = self._submit_settings.to_string() func = lambda: self._service.submit_job( 'cecog_batch', settings_str, pathout, nr_items, batch_size, version) self.dlg.exec_(func) jobid = self.dlg.getTargetResult() except Exception as e: QMessageBox.critical(self, "Error", 'Job submission failed (%s)' % str(e)) else: # FIXME: no idea how DRMAA 1.0 compatible this is if type(jobid) == types.ListType: self._jobid = ','.join(jobid) main_jobid = jobid[0].split('.')[0] else: self._jobid = str(jobid) main_jobid = jobid self._txt_jobid.setText(self._jobid) self._update_job_status() QMessageBox.information(self, "Information", ("Job(s) successfully submitted\n" "Job ID: %s, #jobs: %d" % (main_jobid, nr_items))) @pyqtSlot() def _on_terminate_job(self): if self.jobIds is None: return try: self.dlg = ProgressDialog("Terminating Jobs...", None, 0, 0, self) func = lambda: self._service.control_job(self._jobid, JOB_CONTROL_TERMINATE) self.dlg.exec_(func) except Exception as e: QMessageBox.critical(self, "Error", "Job termination failed (%s)" % str(e)) else: self._btn_toogle.setChecked(False) self._toggle_state = JOB_CONTROL_SUSPEND self._btn_toogle.setText(self._toggle_state) self._update_job_status() @pyqtSlot() def _on_toggle_job(self): if self.jobIds is None: return try: self.dlg = ProgressDialog("Suspending Jobs...", None, 0, 0, self) func = lambda: self._service.control_job(self._jobid, self. _toggle_state) self.dlg.exec_(func) except Exception as e: self._toggle_state = JOB_CONTROL_SUSPEND self._btn_toogle.setChecked(False) QMessageBox.critical(self, "Error", "Could not toggle job status (%s)" % str(e)) else: if self._toggle_state == JOB_CONTROL_SUSPEND: self._toggle_state = JOB_CONTROL_RESUME else: self._toggle_state = JOB_CONTROL_SUSPEND self._update_job_status() self._btn_toogle.setText(self._toggle_state) @pyqtSlot() def _on_update_job_status(self): txt = self._update_job_status() if txt is not None: QMessageBox.information(self, "Cluster update", "Message: '%s'" % txt) def _update_job_status(self): if self.jobIds is None: return try: self.dlg = ProgressDialog("Updating Job Status...", None, 0, 0, self) func = lambda: self._service.get_job_status(self._jobid) self.dlg.exec_(func) txt = self.dlg.getTargetResult() except Exception as e: QMessageBox.critical(self, "Error", 'Could not retrieve job status (%s)' % str(e)) else: self._label_jobstatus.setText(txt) return txt def _check_host_url(self): url = urlparse.urlparse(self._host_url) try: test_sock = socket.create_connection((url.hostname, url.port), timeout=1) test_sock.shutdown(2) test_sock.close() except: msg = "Connection failed (%s)" % self._host_url raise ConnectionError(msg) def _check_api_version(self): try: api_version = self._service.api_version() except RemotingError: # this call is not supported by older version of the # cluster service api_version = 1 if api_version < self.MIN_API_VERSION: msg = ("Api version of the cluster services is %d " "but version %d is required.") \ %(self.MIN_API_VERSION, api_version) raise RuntimeError(msg) def _turn_off_cluster_support(self): pref = AppPreferences() pref.cluster_support = False pref.saveSettings() def _connect(self): success = False try: self._check_host_url() client = RemotingService(self._host_url) self.dlg = ProgressDialog("Connecting to Cluster...", None, 0, 0, self) func = lambda: client.getService('clustercontrol') self.dlg.exec_(func) self._service = self.dlg.getTargetResult() self._check_api_version() except ConnectionError as e: msg = ("%s\nDo you want to turn off the cluster support?") % str(e) ret = QMessageBox.question(self, "Error", msg) if ret == QMessageBox.Yes: self._turn_off_cluster_support() except Exception as e: QMessageBox.critical(self, "Error", str(e)) else: try: self.dlg.exec_(self._service.get_cecog_versions) cluster_versions = self.dlg.getTargetResult() except Exception as e: QMessageBox.critical(self, "Error", str(e)) else: if not version in set(cluster_versions): QMessageBox.warning( self, "Warning", ("Cluster does not support %s %s" "Available versions: %s" % (appname, version, ', '.join(cluster_versions)))) else: success = True return success def _get_mappable_paths(self): """Get the paths/filenames which have to be mapped to run on a remote cluster. Whether an option is considered or not might depend on other values/switches, e.g. if classification is not needed there is no need to map the paths. """ #FIXME: should be done in a better way. results = [] targets = [ (('General', 'pathin'), []), (('General', 'pathout'), []), (('General', 'structure_file_extra_path_name'), [('General', 'structure_file_extra_path')]), (('Classification', 'primary_classification_envpath'), [('Processing', 'primary_classification')]), (('Classification', 'secondary_classification_envpath'), [('General', 'process_secondary'), ('Processing', 'secondary_classification')]), (('Classification', 'tertiary_classification_envpath'), [('General', 'process_tertiary'), ('Processing', 'tertiary_classification')]), (('Classification', 'merged_classification_envpath'), [('General', 'process_merged'), ('Processing', 'merged_classification')]), ] targets.extend([(('ObjectDetection', '%s_flat_field_correction_image_dir' % prefix), [('ObjectDetection', '%s_flat_field_correction' % prefix)]) for prefix in ['primary', 'secondary', 'tertiary']]) for info, const in targets: passed = reduce(lambda x, y: x and y, map(lambda z: self._settings.get(*z), const), True) if passed: results.append(info) return results def check_directories(self): """Check local and remote directories defined in the path mapping table for existence and setup the table view accordingly. """ ndirs = self._table_info.rowCount() remote_dirs = [ self._table_info.item(i, 1).text() for i in xrange(ndirs) ] remote_state = self._service.check_directory(remote_dirs) local_dirs = [ self._table_info.item(i, 0).text() for i in xrange(ndirs) ] local_state = [isdir(d) for d in local_dirs] states = [local_state, remote_state] for i in xrange(ndirs): for j in xrange(2): item = self._table_info.item(i, j) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) if states[j][i]: item.setBackground(QBrush(QColor("green"))) else: if not item.text(): item.setText("Not available") item.setBackground(QBrush(QColor("red"))) def update_display(self, is_active): apc = AppPreferences() self._host_url = apc.url if self._connect(): can_submit = True try: self._submit_settings = self._clusterframe.get_special_settings( \ self._settings, self.imagecontainer.has_timelapse) except: self._submit_settings = self._clusterframe.get_special_settings( self._settings) self._label_hosturl.setText(self._host_url) self._label_status.setText(self._service.get_service_info()) mappable_paths = self._get_mappable_paths() self._table_info.clearContents() self._table_info.setRowCount(len(mappable_paths)) for i, info in enumerate(mappable_paths): value = self._settings.get(*info) mapped = apc.map2platform(value) self._submit_settings.set(info[0], info[1], mapped) if mapped is None: can_submit = False self._table_info.setItem(i, 0, QTableWidgetItem(value)) self._table_info.setItem(i, 1, QTableWidgetItem(mapped)) self.check_directories() self._table_info.resizeColumnsToContents() self._table_info.resizeRowsToContents() self._btn_submit.setEnabled(can_submit and is_active) else: self._btn_submit.setEnabled(False)