From 5600056d44f816a9494d7747c77d437e59c00b71 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Sun, 22 Jan 2012 18:45:41 +0100 Subject: [PATCH 01/31] Qubes Manager with more tabular layout --- Makefile | 30 +-- mainwindow.ui | 494 +++++++++++++++++++++++++++++++++++++++++++ qubesmanager/main.py | 483 +++++++++++++++++++++--------------------- 3 files changed, 737 insertions(+), 270 deletions(-) create mode 100644 mainwindow.ui diff --git a/Makefile b/Makefile index 3457e08..4273819 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,10 @@ -RPMS_DIR=rpm/ VERSION := $(shell cat version) -help: - @echo "make rpms -- generate binary rpm packages" - @echo "make res -- compile resources" - @echo "make update-repo-current -- copy newly generated rpms to qubes yum repo" - @echo "make update-repo-unstable -- same, but to -testing repo" - @echo "make update-repo-installer -- copy dom0 rpms to installer repo" - - -rpms: - rpmbuild --define "_rpmdir $(RPMS_DIR)" -bb rpm_spec/qmgr.spec - rpm --addsign $(RPMS_DIR)/x86_64/qubes-manager*$(VERSION)*.rpm res: - pyrcc4 -o qubesmanager/qrc_resources.py resources.qrc + pyrcc4 -o qubesmanager/resources_rc.py resources.qrc + pyuic4 -o qubesmanager/ui_mainwindow.py mainwindow.ui pyuic4 -o qubesmanager/ui_newappvmdlg.py newappvmdlg.ui pyuic4 -o qubesmanager/ui_editfwrulesdlg.py editfwrulesdlg.ui pyuic4 -o qubesmanager/ui_newfwruledlg.py newfwruledlg.ui -update-repo-current: - ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/current/dom0/rpm/ - cd ../yum && ./update_repo.sh - -update-repo-current-testing: - ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/current-testing/dom0/rpm/ - cd ../yum && ./update_repo.sh - -update-repo-unstable: - ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/unstable/dom0/rpm/ - cd ../yum && ./update_repo.sh - -update-repo-installer: - ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../installer/yum/qubes-dom0/rpm/ - clean: diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..9fd66b2 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,494 @@ + + + VmManagerWindow + + + + 0 + 0 + 821 + 600 + + + + + 0 + 0 + + + + MainWindow + + + + true + + + + 1 + 1 + + + + false + + + true + + + + + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + 150 + 50 + + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + Qt::DashLine + + + true + + + false + + + 4 + + + false + + + 150 + + + 150 + + + false + + + false + + + + Nowy wiersz + + + + + + + + Name + + + + 50 + false + + + + + 0 + 0 + 0 + + + + + + Template + + + + + NetVM + + + + + CPU + + + + + CPU Graph + + + + + MEM + + + + + MEM Graph + + + + + Update Info + + + + + Block Devices + + + + + + + + + + 0 + 0 + 821 + 23 + + + + + Options + + + + + View + + + + Columns visibility + + + + + + + + + + + + + + + + + + + toolBar + + + false + + + TopToolBarArea + + + false + + + + + + + + + + + + + + + + + + :/createvm.png:/createvm.png + + + Create AppVM + + + Create a new AppVM + + + + + false + + + + :/removevm.png:/removevm.png + + + remove AppVM + + + Remove an existing AppVM (must be stopped first) + + + + + false + + + + :/resumevm.png:/resumevm.png + + + Start/Resume VM + + + Start/Resume a VM + + + + + false + + + + :/pausevm.png:/pausevm.png + + + Pause VM + + + Pause a running VM + + + + + false + + + + :/shutdownvm.png:/shutdownvm.png + + + Shutdown VM + + + Shutdown a running VM + + + + + false + + + + :/root.png:/root.png + + + Select VM applications + + + Select applications present in menu for this VM + + + + + false + + + + :/updateable.png:/updateable.png + + + Update VM + + + Update VM system + + + + + true + + + true + + + + :/showallvms.png + + + + Show/Hide inactive VMs + + + Show/Hide inactive VMs + + + + + + :/firewall.png:/firewall.png + + + Edit VM Firewall rules + + + Edit VM Firewall rules + + + + + true + + + + :/showcpuload.png:/showcpuload.png + + + Show graphs + + + Show Graphs + + + + + Options + + + + + View + + + + + true + + + true + + + CPU + + + + + true + + + true + + + CPU Graph + + + + + true + + + true + + + MEM + + + + + true + + + true + + + MEM Graph + + + + + true + + + true + + + Template + + + + + true + + + true + + + NetVM + + + + + true + + + true + + + Block Devices + + + + + true + + + true + + + Update Info + + + + + + + + diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 4560b5e..240e632 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -37,6 +37,7 @@ from qubes import qubesutils import qubesmanager.qrc_resources import ui_newappvmdlg +from ui_mainwindow import * from appmenu_select import AppmenuSelectWindow from firewall import EditFwRulesDlg, QubesFirewallRulesModel @@ -102,132 +103,114 @@ class VmInfoWidget (QWidget): def __init__(self, vm, parent = None): super (VmInfoWidget, self).__init__(parent) - layout0 = QHBoxLayout() + layout = QHBoxLayout () self.label_name = QLabel (vm.name) - - self.vm_running = vm.last_power_state - layout0.addWidget(self.label_name, alignment=Qt.AlignLeft) - - layout1 = QHBoxLayout() - - if vm.template_vm is not None: - self.label_tmpl = QLabel ("" + (vm.template_vm.name) + "") - elif vm.is_appvm(): # and vm.template_vm is None - self.label_tmpl = QLabel ("StandaloneVM") - elif vm.is_template(): - self.label_tmpl = QLabel ("TemplateVM") - elif vm.qid == 0: - self.label_tmpl = QLabel ("AdminVM") - elif vm.is_netvm(): - self.label_tmpl = QLabel ("NetVM") - else: - self.label_tmpl = QLabel ("") - - label_icon_networked = self.set_icon(":/networking.png", vm.is_networked()) - layout1.addWidget(label_icon_networked, alignment=Qt.AlignLeft) - - if vm.is_updateable(): - label_icon_updtbl = self.set_icon(":/updateable.png", True) - layout1.addWidget(label_icon_updtbl, alignment=Qt.AlignLeft) - - layout1.addWidget(self.label_tmpl, alignment=Qt.AlignLeft) - - layout1.addStretch() - - layout2 = QVBoxLayout () - layout2.addLayout(layout0) - layout2.addLayout(layout1) - - layout3 = QHBoxLayout () self.vm_icon = VmStatusIcon(vm) - layout3.addWidget(self.vm_icon) - layout3.addSpacing (10) - layout3.addLayout(layout2) - self.setLayout(layout3) - - self.previous_outdated = False - self.previous_update_recommended = False - - def set_icon(self, icon_path, enabled = True): - label_icon = QLabel() - icon = QIcon (icon_path) - icon_sz = QSize (VmManagerWindow.row_height * 0.3, VmManagerWindow.row_height * 0.3) - icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled if not enabled else QIcon.Normal) - label_icon.setPixmap (icon_pixmap) - label_icon.setFixedSize (icon_sz) - return label_icon + layout.addWidget(self.vm_icon) + layout.addSpacing (10) + layout.addWidget(self.label_name, alignment=Qt.AlignLeft) + self.setLayout(layout) + def update_vm_state (self, vm): self.vm_icon.update() - def update_outdated(self, vm): - outdated = vm.is_outdated() - if outdated != self.previous_outdated: - if outdated: - self.label_name.setText(vm.name + " (outdated)") - else: - self.label_name.setText(vm.name) - self.previous_outdated = outdated - if vm.is_updateable(): - update_recommended = self.previous_update_recommended - stat_file = vm.dir_path + '/' + updates_stat_file - if not os.path.exists(stat_file) or \ - time.time() - os.path.getmtime(stat_file) > \ - update_suggestion_interval * 24 * 3600: - update_recommended = True - else: - update_recommended = False - if update_recommended != self.previous_update_recommended: - if update_recommended: - self.label_name.setText(vm.name + " (check updates)") - else: - self.label_name.setText(vm.name) - self.previous_update_recommended = update_recommended + -class VmUsageWidget (QWidget): - def __init__(self, vm, cpu_load = 0, parent = None): - super (VmUsageWidget, self).__init__(parent) - - self.cpu_widget = QProgressBar() - self.mem_widget = QProgressBar() - self.cpu_widget.setMinimum(0) - self.cpu_widget.setMaximum(100) - self.mem_widget.setMinimum(0) - self.mem_widget.setMaximum(qubes_host.memory_total/1024) - self.mem_widget.setFormat ("%v MB"); - self.cpu_label = QLabel("CPU") - self.mem_label = QLabel("MEM") - - layout_cpu = QHBoxLayout() - layout_cpu.addWidget(self.cpu_label) - layout_cpu.addWidget(self.cpu_widget) - - layout_mem = QHBoxLayout() - layout_mem.addWidget(self.mem_label) - layout_mem.addWidget(self.mem_widget) +class VmTemplateWidget (QWidget): + def __init__(self, vm, parent=None): + super(VmTemplateWidget, self).__init__(parent) + layout = QVBoxLayout() - layout.addLayout(layout_cpu) - layout.addLayout(layout_mem) + self.info_label = None + if vm.template_vm is not None: + self.label_tmpl = QLabel ("" + (vm.template_vm.name) + "") + else: + self.label_tmpl = QLabel ("None") + if vm.is_appvm(): # and vm.template_vm is None + self.info_label = QLabel ("StandaloneVM") + elif vm.is_template(): + self.info_label = QLabel ("TemplateVM") + elif vm.qid == 0: + self.info_label = QLabel ("AdminVM") + elif vm.is_netvm(): + self.info_label = QLabel ("NetVM") + else: + self.info_label = QLabel ("---") + + + layout.addWidget(self.label_tmpl, alignment=Qt.AlignHCenter) + if self.info_label != None: + layout.addWidget(self.info_label, alignment=Qt.AlignHCenter) self.setLayout(layout) - self.update_load(vm, cpu_load) - def update_load(self, vm, cpu_load): - self.cpu_load = cpu_load if vm.last_power_state else 0 - self.mem_load = vm.get_mem()/1024 if vm.last_power_state else 0 - self.cpu_widget.setValue(self.cpu_load) - self.mem_widget.setValue(self.mem_load) +class VmIconWidget (QWidget): + def __init__(self, icon_path, enabled=True, parent=None): + super(VmIconWidget, self).__init__(parent) + + label_icon = QLabel() + icon = QIcon (icon_path) + icon_sz = QSize (VmManagerWindow.row_height * 0.8, VmManagerWindow.row_height * 0.3) + icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled if not enabled else QIcon.Normal) + label_icon.setPixmap (icon_pixmap) + label_icon.setFixedSize (icon_sz) + + layout = QVBoxLayout() + layout.addWidget(label_icon) + self.setLayout(layout) + + +class VmNetvmWidget (QWidget): + def __init__(self, vm, parent=None): + super(VmNetvmWidget, self).__init__(parent) + + layout = QHBoxLayout() + self.icon = VmIconWidget(":/networking.png", vm.is_networked()) + + if vm.is_netvm(): + self.label_nvm = QLabel ("self") + elif vm.netvm_vm is not None: + self.label_nvm = QLabel ("" + (vm.netvm_vm.name) + "") + else: + self.label_nvm = QLabel ("None") + + layout.addWidget(self.icon, alignment=Qt.AlignLeft) + layout.addWidget(self.label_nvm, alignment=Qt.AlignHCenter) + self.setLayout(layout) + + + +class VmUsageBarWidget (QWidget): + def __init__(self, min, max, format, label, update_func, vm, load, parent = None): + super (VmUsageBarWidget, self).__init__(parent) + + self.min = min + self.max = max + self.update_func = update_func + + self.widget = QProgressBar() + self.widget.setMinimum(min) + self.widget.setMaximum(max) + self.widget.setFormat(format); + self.label = QLabel(label) + + layout = QHBoxLayout() + layout.addWidget(self.label) + layout.addWidget(self.widget) + + self.setLayout(layout) + + self.update_load(vm, load) + + def update_load(self, vm, load): + self.widget.setValue(self.update_func(vm, load)) - def resizeEvent(self, Event = None): - label_width = max(self.mem_label.width(), self.cpu_label.width()) - self.mem_label.setMinimumWidth(label_width) - self.cpu_label.setMinimumWidth(label_width) - super (VmUsageWidget, self).resizeEvent(Event) class LoadChartWidget (QWidget): @@ -313,6 +296,54 @@ class MemChartWidget (QWidget): p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100) +class VmUpdateInfoWidget(QWidget): + + def __init__(self, vm, parent = None): + super (VmUpdateInfoWidget, self).__init__(parent) + layout = QVBoxLayout () + self.label = QLabel("") + layout.addWidget(self.label) + if vm.is_updateable(): + self.updateable_widget = VmIconWidget(":/updateable.png", True) + layout.addWidget(self.updateable_widget, alignment=Qt.AlignHCenter) + self.setLayout(layout) + + self.previous_outdated = False + self.previous_update_recommended = False + + def update_outdated(self, vm): + outdated = vm.is_outdated() + if outdated and not self.previous_outdated: + self.label.setText(" (outdated)") + + self.previous_outdated = outdated + if vm.is_updateable(): + update_recommended = self.previous_update_recommended + stat_file = vm.dir_path + '/' + updates_stat_file + if not os.path.exists(stat_file) or \ + time.time() - os.path.getmtime(stat_file) > \ + update_suggestion_interval * 24 * 3600: + update_recommended = True + else: + update_recommended = False + if update_recommended and not self.previous_update_recommended: + self.label.setText(" (check updates)") + self.previous_update_recommended = update_recommended + + +class VmBlockDevicesWidget(QWidget): + def __init__(self, vm, parent=None): + super(VmBlockDevicesWidget, self).__init__(parent) + + combo = QComboBox() + combo.addItem("USB dummy1") + combo.addItem("USB dummy2") + combo.addItem("USB dummy3") + + layout = QVBoxLayout() + layout.addWidget(combo) + self.setLayout(layout) + class VmRowInTable(object): def __init__(self, vm, row_no, table): @@ -324,23 +355,41 @@ class VmRowInTable(object): self.info_widget = VmInfoWidget(vm) table.setCellWidget(row_no, 0, self.info_widget) - self.usage_widget = VmUsageWidget(vm) - table.setCellWidget(row_no, 1, self.usage_widget) + self.template_widget = VmTemplateWidget(vm) + table.setCellWidget(row_no, 1, self.template_widget) + + self.netvm_widget = VmNetvmWidget(vm) + table.setCellWidget(row_no, 2, self.netvm_widget) + + self.cpu_usage_widget = VmUsageBarWidget(0, 100, "", "CPU", + lambda vm, val: val if vm.last_power_state else 0, vm, 0) + table.setCellWidget(row_no, 3, self.cpu_usage_widget) self.load_widget = LoadChartWidget(vm) - table.setCellWidget(row_no, 2, self.load_widget) + table.setCellWidget(row_no, 4, self.load_widget) + + self.mem_usage_widget = VmUsageBarWidget(0, qubes_host.memory_total/1024, "%v MB", "MEM", + lambda vm, val: vm.get_mem()/1024 if vm.last_power_state else 0, vm, 0) + table.setCellWidget(row_no, 5, self.mem_usage_widget) self.mem_widget = MemChartWidget(vm) - table.setCellWidget(row_no, 3, self.mem_widget) + table.setCellWidget(row_no, 6, self.mem_widget) + + self.updateinfo_widget = VmUpdateInfoWidget(vm) + table.setCellWidget(row_no, 7, self.updateinfo_widget) + + self.blockdevices_widget = VmBlockDevicesWidget(vm) + table.setCellWidget(row_no, 8, self.blockdevices_widget) def update(self, counter, cpu_load = None): self.info_widget.update_vm_state(self.vm) if cpu_load is not None: - self.usage_widget.update_load(self.vm, cpu_load) + self.cpu_usage_widget.update_load(self.vm, cpu_load) + self.mem_usage_widget.update_load(self.vm, None) self.load_widget.update_load(self.vm, cpu_load) self.mem_widget.update_load(self.vm) - self.info_widget.update_outdated(self.vm) + self.updateinfo_widget.update_outdated(self.vm) class NewAppVmDlg (QDialog, ui_newappvmdlg.Ui_NewAppVMDlg): def __init__(self, parent = None): @@ -388,100 +437,40 @@ class ThreadMonitor(QObject): self.event_finished.set() -class VmManagerWindow(QMainWindow): - columns_widths = [250, 200, 150, 150] +class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): row_height = 50 max_visible_rows = 14 update_interval = 1000 # in msec show_inactive_vms = True columns_states = { 0: [0, 1], 1: [0, 2, 3] } + columns_indices = { "Name": 0, + "Template": 1, + "NetVM": 2, + "CPU": 3, + "CPU Graph": 4, + "MEM": 5, + "MEM Graph": 6, + "Update Info": 7, + "Block Device": 8 } + + def __init__(self, parent=None): - super(VmManagerWindow, self).__init__(parent) - - - self.action_createvm = self.createAction ("Create AppVM", slot=self.create_appvm, - icon="createvm", tip="Create a new AppVM") - - self.action_removevm = self.createAction ("Remove AppVM", slot=self.remove_appvm, - icon="removevm", tip="Remove an existing AppVM (must be stopped first)") - - self.action_resumevm = self.createAction ("Start/Resume VM", slot=self.resume_vm, - icon="resumevm", tip="Start/Resume a VM") - - self.action_pausevm = self.createAction ("Pause VM", slot=self.pause_vm, - icon="pausevm", tip="Pause a running VM") - - self.action_shutdownvm = self.createAction ("Shutdown VM", slot=self.shutdown_vm, - icon="shutdownvm", tip="Shutdown a running VM") - - self.action_appmenus = self.createAction ("Select VM applications", slot=self.appmenus_select, - icon="root", tip="Select applications present in menu for this VM") - - self.action_updatevm = self.createAction ("Update VM", slot=self.update_vm, - icon="updateable", tip="Update VM system") - - self.action_showallvms = self.createAction ("Show/Hide Inactive VMs", slot=self.toggle_inactive_view, checkable=True, - icon="showallvms", tip="Show/Hide Inactive VMs") - - self.action_showcpuload = self.createAction ("Show/Hide CPU Load chart", slot=self.showcpuload, checkable=True, - icon="showcpuload", tip="Show/Hide CPU Load chart") - - self.action_editfwrules = self.createAction ("Edit VM Firewall rules", slot=self.edit_fw_rules, - icon="firewall", tip="Edit VM Firewall rules") - - - self.action_removevm.setDisabled(True) - self.action_resumevm.setDisabled(True) - self.action_pausevm.setDisabled(True) - self.action_shutdownvm.setDisabled(True) - self.action_appmenus.setDisabled(True) - self.action_updatevm.setDisabled(True) - - self.action_showallvms.setChecked(self.show_inactive_vms) - - self.toolbar = self.addToolBar ("Toolbar") - self.toolbar.setFloatable(False) - self.addActions (self.toolbar, (self.action_createvm, self.action_removevm, - None, - self.action_resumevm, self.action_shutdownvm, - self.action_editfwrules, self.action_appmenus, - self.action_updatevm, - None, - self.action_showcpuload, - self.action_showallvms, - )) - - self.table = QTableWidget() - self.setCentralWidget(self.table) - self.table.clear() - self.table.setColumnCount(len(VmManagerWindow.columns_widths)) - for (col, width) in enumerate (VmManagerWindow.columns_widths): - self.table.setColumnWidth (col, width) - - self.table.horizontalHeader().setResizeMode(QHeaderView.Stretch) - self.table.horizontalHeader().setResizeMode(0, QHeaderView.Fixed) - self.table.setAlternatingRowColors(True) - self.table.verticalHeader().hide() - self.table.horizontalHeader().hide() - self.table.setGridStyle(Qt.NoPen) - self.table.setSortingEnabled(False) - self.table.setSelectionBehavior(QTableWidget.SelectRows) - self.table.setSelectionMode(QTableWidget.SingleSelection) - - self.__cpugraphs = self.action_showcpuload.isChecked() - self.update_table_columns() - + super(VmManagerWindow, self).__init__() + self.setupUi(self) + self.toolbar = self.toolBar + self.qvm_collection = QubesVmCollection() - self.setWindowTitle("Qubes VM Manager") - + self.connect(self.table, SIGNAL("itemSelectionChanged()"), self.table_selection_changed) - + cur_pos = self.pos() - self.setFixedWidth (self.get_minimum_table_width()) + self.table.setColumnWidth(0, 200) self.fill_table() self.move(cur_pos) - + + self.update_table_columns() + self.counter = 0 self.shutdown_monitor = {} self.last_measure_results = {} @@ -490,41 +479,25 @@ class VmManagerWindow(QMainWindow): def set_table_geom_height(self): # TODO: '6' -- WTF?! - tbl_H = self.toolbar.height() + 6 + \ - self.table.horizontalHeader().height() + 6 + tbl_H = self.toolbar.height() + \ + self.table.horizontalHeader().height() + \ + self.centralwidget.layout().contentsMargins().top() +\ + self.centralwidget.layout().contentsMargins().bottom() n = self.table.rowCount(); - if n > VmManagerWindow.max_visible_rows: - n = VmManagerWindow.max_visible_rows + + if n > 6: + for i in range(0,n-1): + tbl_H += self.table.rowHeight(i) + else: + tbl_H += self.table.verticalHeader().height() + """ + if n > self.max_visible_rows: + n = self.max_visible_rows for i in range (0, n): - tbl_H += self.table.rowHeight(i) + tbl_H += self.table.rowHeight(i) """ - self.setFixedHeight(tbl_H) - - - def addActions(self, target, actions): - for action in actions: - if action is None: - target.addSeparator() - else: - target.addAction(action) - - - def createAction(self, text, slot=None, shortcut=None, icon=None, - tip=None, checkable=False, signal="triggered()"): - action = QAction(text, self) - if icon is not None: - action.setIcon(QIcon(":/%s.png" % icon)) - if shortcut is not None: - action.setShortcut(shortcut) - if tip is not None: - action.setToolTip(tip) - action.setStatusTip(tip) - if slot is not None: - self.connect(action, SIGNAL(signal), slot) - if checkable: - action.setCheckable(True) - return action + self.setMinimumHeight(tbl_H) def get_vms_list(self): @@ -559,7 +532,7 @@ class VmManagerWindow(QMainWindow): return vms_to_display def fill_table(self): - self.table.clear() + #self.table.clear() vms_list = self.get_vms_list() self.table.setRowCount(len(vms_list)) @@ -622,14 +595,17 @@ class VmManagerWindow(QMainWindow): QTimer.singleShot (self.update_interval, self.update_table) def update_table_columns(self): - state = 1 if self.__cpugraphs else 0 - columns = self.columns_states[state] + #for i in range(0, self.table.columnCount()): + #TODO make elegant column visibility actions + #self.table.setColumnHidden(i, False) - for i in range(0, self.table.columnCount()): - enabled = columns.count(i) > 0 - self.table.setColumnHidden(i, not enabled) + width = self.table.horizontalHeader().length() +\ + self.table.verticalScrollBar().width() +\ + self.centralwidget.layout().contentsMargins().left() +\ + self.centralwidget.layout().contentsMargins().right() - self.setMinimumWidth(self.get_minimum_table_width()) + self.table.setFixedWidth( width ) + self.setFixedWidth( width) def table_selection_changed (self): vm = self.get_selected_vm() @@ -644,13 +620,6 @@ class VmManagerWindow(QMainWindow): self.action_editfwrules.setEnabled(vm.is_networked() and not (vm.is_netvm() and not vm.is_proxyvm())) self.action_updatevm.setEnabled(vm.is_updateable() or vm.qid == 0) - def get_minimum_table_width(self): - tbl_W = 0 - for (col, w) in enumerate(VmManagerWindow.columns_widths): - if not self.table.isColumnHidden(col): - tbl_W += w - - return tbl_W def closeEvent (self, event): if event.spontaneous(): # There is something borked in Qt, as the logic here is inverted on X11 @@ -962,6 +931,36 @@ class VmManagerWindow(QMainWindow): if dialog.exec_(): model.apply_rules() + + def showhide_collumn(self, col_num, show): + self.table.setColumnHidden( col_num, not show) + self.update_table_columns() + + def on_actionTemplate_toggled(self, checked): + self.showhide_collumn( 1, checked) + + def on_actionNetVM_toggled(self, checked): + self.showhide_collumn( 2, checked) + + def on_actionCPU_toggled(self, checked): + self.showhide_collumn( 3, checked) + + def on_actionCPU_Graph_toggled(self, checked): + self.showhide_collumn( 4, checked) + + def on_actionMEM_toggled(self, checked): + self.showhide_collumn( 5, checked) + + def on_actionMEM_Graph_toggled(self, checked): + self.showhide_collumn( 6, checked) + + def on_actionUpdate_Info_toggled(self, checked): + self.showhide_collumn( 7, checked) + + def on_actionBlock_Devices_toggled(self, checked): + self.showhide_collumn( 8, checked) + + class QubesTrayIcon(QSystemTrayIcon): def __init__(self, icon): QSystemTrayIcon.__init__(self, icon) From 145eecdfce468f2a5d52f7196abf666a571a0a36 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Mon, 23 Jan 2012 19:04:58 +0100 Subject: [PATCH 02/31] All mainwindow actions connected --- mainwindow.ui | 3 +-- qubesmanager/main.py | 36 +++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index 9fd66b2..fd4a711 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -17,7 +17,7 @@ - MainWindow + Qubes VM Manager @@ -238,7 +238,6 @@ - diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 240e632..d363fba 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -478,7 +478,6 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): QTimer.singleShot (self.update_interval, self.update_table) def set_table_geom_height(self): - # TODO: '6' -- WTF?! tbl_H = self.toolbar.height() + \ self.table.horizontalHeader().height() + \ self.centralwidget.layout().contentsMargins().top() +\ @@ -486,6 +485,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): n = self.table.rowCount(); + """ if n > 6: for i in range(0,n-1): tbl_H += self.table.rowHeight(i) @@ -495,7 +495,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): if n > self.max_visible_rows: n = self.max_visible_rows for i in range (0, n): - tbl_H += self.table.rowHeight(i) """ + tbl_H += self.table.rowHeight(i) self.setMinimumHeight(tbl_H) @@ -626,9 +626,11 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.hide() event.ignore() - def create_appvm(self): + @pyqtSlot(name='on_action_createvm_triggered') + def action_createvm_triggered(self): dialog = NewAppVmDlg() + print "Create VM triggered!\n" # Theoretically we should be locking for writing here and unlock # only after the VM creation finished. But the code would be more messy... @@ -720,7 +722,8 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): vm = self.vms_in_table[row_index].vm return vm - def remove_appvm(self): + @pyqtSlot(name='on_action_removevm_triggered') + def action_removevm_triggered(self): vm = self.get_selected_vm() assert not vm.is_running() assert not vm.installed_by_rpm @@ -789,7 +792,8 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): thread_monitor.set_finished() - def resume_vm(self): + @pyqtSlot(name='on_action_resumevm_triggered') + def action_resumevm_triggered(self): vm = self.get_selected_vm() assert not vm.is_running() @@ -831,7 +835,8 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): thread_monitor.set_finished() - def pause_vm(self): + @pyqtSlot(name='on_action_pausevm_triggered') + def action_pausevm_triggered(self): vm = self.get_selected_vm() assert vm.is_running() try: @@ -840,7 +845,8 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): QMessageBox.warning (None, "Error pausing VM!", "ERROR: {0}".format(ex)) return - def shutdown_vm(self): + @pyqtSlot(name='on_action_shutdownvm_triggered') + def action_shutdownvm_triggered(self): vm = self.get_selected_vm() assert vm.is_running() @@ -862,12 +868,14 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.shutdown_monitor[vm.qid] = VmShutdownMonitor (vm) QTimer.singleShot (vm_shutdown_timeout, self.shutdown_monitor[vm.qid].check_if_vm_has_shutdown) - def appmenus_select(self): + @pyqtSlot(name='on_action_appmenus_triggered') + def action_appmenus_triggered(self): vm = self.get_selected_vm() select_window = AppmenuSelectWindow(vm) select_window.exec_() - def update_vm(self): + @pyqtSlot(name='on_action_updatevm_triggered') + def action_updatevm_triggered(self): vm = self.get_selected_vm() if not vm.is_running(): @@ -908,16 +916,14 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): return thread_monitor.set_finished() - def showcpuload(self): - self.__cpugraphs = self.action_showcpuload.isChecked() - self.update_table_columns() - - def toggle_inactive_view(self): + @pyqtSlot(name='on_action_showallvms_triggered') + def action_showallvms_triggered(self): self.show_inactive_vms = self.action_showallvms.isChecked() self.mark_table_for_update() self.update_table(out_of_schedule = True) - def edit_fw_rules(self): + @pyqtSlot(name='on_action_editfwrules_triggered') + def action_editfwrules_triggered(self): vm = self.get_selected_vm() dialog = EditFwRulesDlg() model = QubesFirewallRulesModel() From 6b8a2279d155dae0d982a353d24d1623f4332209 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 24 Jan 2012 01:18:32 +0100 Subject: [PATCH 03/31] Added multiselectwidget for future use in multiple windows --- Makefile | 2 +- multiselectwidget.ui | 97 ++++++++++ qubesmanager/multiselectwidget.py | 39 ++++ settingsdlg.ui | 290 ++++++++++++++++++++++++++++++ 4 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 multiselectwidget.ui create mode 100644 qubesmanager/multiselectwidget.py create mode 100644 settingsdlg.ui diff --git a/Makefile b/Makefile index 4273819..ac2f310 100644 --- a/Makefile +++ b/Makefile @@ -6,5 +6,5 @@ res: pyuic4 -o qubesmanager/ui_newappvmdlg.py newappvmdlg.ui pyuic4 -o qubesmanager/ui_editfwrulesdlg.py editfwrulesdlg.ui pyuic4 -o qubesmanager/ui_newfwruledlg.py newfwruledlg.ui - + pyuic4 -o qubesmanager/ui_multiselectwidget.py multiselectwidget.ui clean: diff --git a/multiselectwidget.ui b/multiselectwidget.ui new file mode 100644 index 0000000..4109c9f --- /dev/null +++ b/multiselectwidget.ui @@ -0,0 +1,97 @@ + + + MultiSelectWidget + + + + 0 + 0 + 602 + 300 + + + + Form + + + + + + Qt::Horizontal + + + + 10 + 20 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + > + + + + + + + < + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Qt::Horizontal + + + + 10 + 20 + + + + + + + + + diff --git a/qubesmanager/multiselectwidget.py b/qubesmanager/multiselectwidget.py new file mode 100644 index 0000000..f5850df --- /dev/null +++ b/qubesmanager/multiselectwidget.py @@ -0,0 +1,39 @@ +import sys +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from ui_multiselectwidget import * + +class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): + + def __init__(self, parent=None): + super(MultiSelectWidget, self).__init__() + self.setupUi(self); + self.add_selected_button.clicked.connect(self.add_selected) + self.remove_selected_button.clicked.connect(self.remove_selected) + + def switch_selected(self, src, dst): + selected = src.selectedItems() + + for s in selected: + row = src.indexFromItem(s).row() + item = src.takeItem(row) + dst.addItem(item) + + + def add_selected(self): + print "Add selected triggered!" + self.switch_selected(self.available_list, self.selected_list) + + + def remove_selected(self): + print "Remove selected triggered!" + self.switch_selected(self.selected_list, self.available_list) + + + + +if __name__ == "__main__": + app = QtGui.QApplication(sys.argv) + ui = MultiSelectWidget() + ui.show() + sys.exit(app.exec_()) diff --git a/settingsdlg.ui b/settingsdlg.ui new file mode 100644 index 0000000..ec81baa --- /dev/null +++ b/settingsdlg.ui @@ -0,0 +1,290 @@ + + + Dialog + + + + 0 + 0 + 672 + 351 + + + + Settings + + + + + + + + + + + 3 + + + + + + + Basic + + + + + + Name & label: + + + + + + + myappvm + + + + + + + true + + + + + + + Use this template: + + + + + + + + + + Allow networking + + + true + + + + + + + Qt::Vertical + + + + 20 + 157 + + + + + + + + + Advanced + + + + + + Disk storage + + + + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 10000 + + + 2 + + + + + + + false + + + Allow to grow + + + + + + + GB + + + + + + + Private storage max. size + + + + + + + false + + + Include in backups + + + true + + + + + + + + + + Memory/CPU + + + + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 10000 + + + 100 + + + 400 + + + + + + + MB + + + + + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + + + + + VCPUs + + + + + + + + + + Qt::Vertical + + + + 20 + 124 + + + + + + + + + Devices + + + + + + Applications + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From d37c79e2c4cbfab2d3f6bcf1242514c3f28365a5 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 24 Jan 2012 15:04:28 +0100 Subject: [PATCH 04/31] Appmenu_select window with a multiselect widget instead of a table with checkboxes. Plus a sketch of settings dialog. --- multiselectwidget.ui | 90 +++++++++++++++++++++- qubesmanager/appmenu_select.py | 123 +++++++++--------------------- qubesmanager/multiselectwidget.py | 13 +++- 3 files changed, 132 insertions(+), 94 deletions(-) diff --git a/multiselectwidget.ui b/multiselectwidget.ui index 4109c9f..db9cb70 100644 --- a/multiselectwidget.ui +++ b/multiselectwidget.ui @@ -7,13 +7,13 @@ 0 0 602 - 300 + 459 Form - + @@ -28,7 +28,48 @@ - + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Available + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + @@ -75,7 +116,48 @@ - + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Selected + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + diff --git a/qubesmanager/appmenu_select.py b/qubesmanager/appmenu_select.py index f0800b3..6ad82d7 100755 --- a/qubesmanager/appmenu_select.py +++ b/qubesmanager/appmenu_select.py @@ -42,21 +42,14 @@ import time import threading from operator import itemgetter +from multiselectwidget import * + whitelisted_filename = 'whitelisted-appmenus.list' -class AppRowInTable(object): - def __init__(self, filename, name, row_no, table): +class AppListWidgetItem(QListWidgetItem): + def __init__(self, name, filename, parent = None): + super(AppListWidgetItem, self).__init__(name, parent) self.filename = filename - self.row_no = row_no - - table.setRowHeight (row_no, AppmenuSelectWindow.row_height) - - self.name_widget = QTableWidgetItem(name) - self.name_widget.setFlags (Qt.ItemIsSelectable | Qt.ItemIsEnabled ) - table.setItem(row_no, 0, self.name_widget) - - self.appvm_widget = QCheckBox() - table.setCellWidget(row_no, 1, self.appvm_widget) class ThreadMonitor(QObject): def __init__(self): @@ -89,23 +82,10 @@ class AppmenuSelectWindow(QDialog): self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply) self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject) - self.table = QTableWidget(self) - self.table.clear() - self.table.setColumnCount(2) - self.table.setColumnWidth (0, 200) - self.table.setColumnWidth (1, 40) + self.app_list = MultiSelectWidget(self) - self.table.horizontalHeader().setResizeMode(QHeaderView.Stretch) - self.table.horizontalHeader().setResizeMode(1, QHeaderView.Fixed) - self.table.setAlternatingRowColors(True) - self.table.verticalHeader().hide() - self.table.horizontalHeader().show() - self.table.setGridStyle(Qt.NoPen) - self.table.setSortingEnabled(True) - self.table.setSelectionBehavior(QTableWidget.SelectRows) - self.table.setSelectionMode(QTableWidget.SingleSelection) - - self.gridLayout.addWidget(self.table, 0, 0, 1, 1) + + self.gridLayout.addWidget(self.app_list, 0, 0, 1, 1) self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) self.vm = vm @@ -114,86 +94,57 @@ class AppmenuSelectWindow(QDialog): else: self.source_vm = self.vm self.setWindowTitle("Qubes Appmenus for %s" % vm.name) - self.resize(250,500) + self.resize(600,600) - self.fill_table() - self.load_list_of_selected() + self.fill_apps_list() def reject(self): self.done(0) - - def addActions(self, target, actions): - for action in actions: - if action is None: - target.addSeparator() - else: - target.addAction(action) - - def createAction(self, text, slot=None, shortcut=None, icon=None, - tip=None, checkable=False, signal="triggered()"): - action = QAction(text, self) - if icon is not None: - action.setIcon(QIcon(":/%s.png" % icon)) - if shortcut is not None: - action.setShortcut(shortcut) - if tip is not None: - action.setToolTip(tip) - action.setStatusTip(tip) - if slot is not None: - self.connect(action, SIGNAL(signal), slot) - if checkable: - action.setCheckable(True) - return action - - def fill_table(self): + + def fill_apps_list(self): template_dir = self.source_vm.appmenus_templates_dir template_file_list = os.listdir(template_dir) - self.table.clear() - self.table.setHorizontalHeaderLabels(['Name', 'VM']) - self.table.setRowCount(len(template_file_list)) + whitelisted = [] + if os.path.exists(self.vm.dir_path + '/' + whitelisted_filename): + f = open(self.vm.dir_path + '/' + whitelisted_filename, 'r') + whitelisted = [item.strip() for item in f] + f.close() - row_no = 0 - appmenus = [] + self.app_list.clear() + + + available_appmenus = [] for template_file in template_file_list: desktop_template = open(template_dir + '/' + template_file, 'r') for line in desktop_template: if line.startswith("Name=%VMNAME%: "): desktop_name = line.partition('Name=%VMNAME%: ')[2].strip() - row = AppRowInTable (template_file, desktop_name, row_no, self.table) - appmenus.append(row) - row_no += 1 + available_appmenus.append( (template_file, desktop_name) ) break desktop_template.close() - self.table.setRowCount(row_no) - self.appmenus = appmenus - self.table.sortItems(0) + whitelisted_appmenus = [a for a in available_appmenus if a[0] in whitelisted] + available_appmenus = [a for a in available_appmenus if a[0] not in whitelisted] + + for a in available_appmenus: + self.app_list.available_list.addItem( AppListWidgetItem(a[1], a[0])) - def load_list_of_selected(self): - if not os.path.exists(self.vm.dir_path + '/' + whitelisted_filename): - # select none - for row in self.appmenus: - row.appvm_widget.setCheckState(Qt.Unchecked) - return - - f = open(self.vm.dir_path + '/' + whitelisted_filename, 'r') - whitelisted = [item.strip() for item in f] - f.close() - for row in self.appmenus: - if row.filename in whitelisted: - row.appvm_widget.setCheckState(Qt.Checked) - else: - row.appvm_widget.setCheckState(Qt.Unchecked) + for a in whitelisted_appmenus: + self.app_list.selected_list.addItem( AppListWidgetItem(a[1], a[0])) + + self.app_list.available_list.sortItems() + self.app_list.selected_list.sortItems() def save_list_of_selected(self): whitelisted = open(self.vm.dir_path + '/' + whitelisted_filename, 'w') - for row in self.appmenus: - if row.appvm_widget.checkState() == Qt.Checked: - whitelisted.write(row.filename + '\n') - whitelisted.close() + for i in range(self.app_list.selected_list.count()): + item = self.app_list.selected_list.item(i) + whitelisted.write(item.filename + '\n') + whitelisted.close() + def save_and_apply(self): self.save_list_of_selected() diff --git a/qubesmanager/multiselectwidget.py b/qubesmanager/multiselectwidget.py index f5850df..f0c0c23 100644 --- a/qubesmanager/multiselectwidget.py +++ b/qubesmanager/multiselectwidget.py @@ -10,6 +10,8 @@ class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): self.setupUi(self); self.add_selected_button.clicked.connect(self.add_selected) self.remove_selected_button.clicked.connect(self.remove_selected) + self.available_list.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.selected_list.setSelectionMode(QAbstractItemView.ExtendedSelection) def switch_selected(self, src, dst): selected = src.selectedItems() @@ -19,15 +21,18 @@ class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): item = src.takeItem(row) dst.addItem(item) - def add_selected(self): - print "Add selected triggered!" - self.switch_selected(self.available_list, self.selected_list) + self.switch_selected(self.available_list, self.selected_list) + self.selected_list.sortItems() def remove_selected(self): - print "Remove selected triggered!" self.switch_selected(self.selected_list, self.available_list) + self.available_list.sortItems() + + def clear(self): + self.available_list.clear() + self.selected_list.clear() From 601fbd1863d6670ec63fa69f33354a22bbc648cc Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 24 Jan 2012 15:50:21 +0100 Subject: [PATCH 05/31] Restored makefile --- Makefile | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Makefile b/Makefile index ac2f310..6a93e59 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,16 @@ +RPMS_DIR=rpm/ VERSION := $(shell cat version) +help: + @echo "make rpms -- generate binary rpm packages" + @echo "make res -- compile resources" + @echo "make update-repo-current -- copy newly generated rpms to qubes yum repo" + @echo "make update-repo-unstable -- same, but to -testing repo" + @echo "make update-repo-installer -- copy dom0 rpms to installer repo" + + +rpms: + rpmbuild --define "_rpmdir $(RPMS_DIR)" -bb rpm_spec/qmgr.spec + rpm --addsign $(RPMS_DIR)/x86_64/qubes-manager*$(VERSION)*.rpm res: pyrcc4 -o qubesmanager/resources_rc.py resources.qrc @@ -7,4 +19,20 @@ res: pyuic4 -o qubesmanager/ui_editfwrulesdlg.py editfwrulesdlg.ui pyuic4 -o qubesmanager/ui_newfwruledlg.py newfwruledlg.ui pyuic4 -o qubesmanager/ui_multiselectwidget.py multiselectwidget.ui + +update-repo-current: + ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/current/dom0/rpm/ + cd ../yum && ./update_repo.sh + +update-repo-current-testing: + ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/current-testing/dom0/rpm/ + cd ../yum && ./update_repo.sh + +update-repo-unstable: + ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/unstable/dom0/rpm/ + cd ../yum && ./update_repo.sh + +update-repo-installer: + ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../installer/yum/qubes-dom0/rpm/ + clean: From 949f997a381b1c86307f29cdb2e39c2c3bd7b288 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 24 Jan 2012 16:49:14 +0100 Subject: [PATCH 06/31] Fixed resources imports. --- qubesmanager/appmenu_select.py | 2 +- qubesmanager/main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qubesmanager/appmenu_select.py b/qubesmanager/appmenu_select.py index 6ad82d7..9c56d47 100755 --- a/qubesmanager/appmenu_select.py +++ b/qubesmanager/appmenu_select.py @@ -33,7 +33,7 @@ from qubes.qubes import QubesDaemonPidfile from qubes.qubes import QubesHost from qubes.qubes import qrexec_client_path -import qubesmanager.qrc_resources +import qubesmanager.resources_rc from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent diff --git a/qubesmanager/main.py b/qubesmanager/main.py index d363fba..7f456f5 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -35,7 +35,7 @@ from qubes.qubes import QubesDaemonPidfile from qubes.qubes import QubesHost from qubes import qubesutils -import qubesmanager.qrc_resources +import qubesmanager.resources_rc import ui_newappvmdlg from ui_mainwindow import * from appmenu_select import AppmenuSelectWindow From fdf6f893d8701f6a1986ec1719b4edfc43df6c74 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 24 Jan 2012 20:59:44 +0100 Subject: [PATCH 07/31] Main window size-improved. Table rows shorter, main window fit to contents, resizes by row height. --- mainwindow.ui | 34 ++++++++++------ qubesmanager/main.py | 94 ++++++++++++++++++++++++-------------------- 2 files changed, 74 insertions(+), 54 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index fd4a711..c349a8c 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -24,9 +24,9 @@ true - - 1 - 1 + + 0 + 0 @@ -38,14 +38,11 @@ - - - 0 - - + + - + 0 0 @@ -64,10 +61,13 @@ - 150 - 50 + 200 + 30 + + 0 + true @@ -213,7 +213,17 @@ - + + + + 0 + 0 + + + + false + + toolBar diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 7f456f5..76abb11 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -125,26 +125,22 @@ class VmTemplateWidget (QWidget): super(VmTemplateWidget, self).__init__(parent) layout = QVBoxLayout() - self.info_label = None if vm.template_vm is not None: self.label_tmpl = QLabel ("" + (vm.template_vm.name) + "") else: - self.label_tmpl = QLabel ("None") if vm.is_appvm(): # and vm.template_vm is None - self.info_label = QLabel ("StandaloneVM") + self.label_tmpl = QLabel ("StandaloneVM") elif vm.is_template(): - self.info_label = QLabel ("TemplateVM") + self.label_tmpl = QLabel ("TemplateVM") elif vm.qid == 0: - self.info_label = QLabel ("AdminVM") + self.label_tmpl = QLabel ("AdminVM") elif vm.is_netvm(): - self.info_label = QLabel ("NetVM") + self.label_tmpl = QLabel ("NetVM") else: - self.info_label = QLabel ("---") + self.label_tmpl = QLabel ("---") layout.addWidget(self.label_tmpl, alignment=Qt.AlignHCenter) - if self.info_label != None: - layout.addWidget(self.info_label, alignment=Qt.AlignHCenter) self.setLayout(layout) @@ -300,12 +296,9 @@ class VmUpdateInfoWidget(QWidget): def __init__(self, vm, parent = None): super (VmUpdateInfoWidget, self).__init__(parent) - layout = QVBoxLayout () - self.label = QLabel("") - layout.addWidget(self.label) - if vm.is_updateable(): - self.updateable_widget = VmIconWidget(":/updateable.png", True) - layout.addWidget(self.updateable_widget, alignment=Qt.AlignHCenter) + layout = QHBoxLayout () + self.label = QLabel("---") + layout.addWidget(self.label, alignment=Qt.AlignCenter) self.setLayout(layout) self.previous_outdated = False @@ -314,7 +307,7 @@ class VmUpdateInfoWidget(QWidget): def update_outdated(self, vm): outdated = vm.is_outdated() if outdated and not self.previous_outdated: - self.label.setText(" (outdated)") + self.label.setText("outdated") self.previous_outdated = outdated if vm.is_updateable(): @@ -326,9 +319,10 @@ class VmUpdateInfoWidget(QWidget): update_recommended = True else: update_recommended = False + self.label.setText("OK") if update_recommended and not self.previous_update_recommended: - self.label.setText(" (check updates)") - self.previous_update_recommended = update_recommended + self.label.setText("check updates") + self.previous_update_recommended = update_recommended class VmBlockDevicesWidget(QWidget): @@ -438,11 +432,10 @@ class ThreadMonitor(QObject): class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): - row_height = 50 - max_visible_rows = 14 + row_height = 30 + max_visible_rows = 7 update_interval = 1000 # in msec show_inactive_vms = True - columns_states = { 0: [0, 1], 1: [0, 2, 3] } columns_indices = { "Name": 0, "Template": 1, "NetVM": 2, @@ -466,11 +459,24 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): cur_pos = self.pos() self.table.setColumnWidth(0, 200) + self.setSizeIncrement(QtCore.QSize(200, 30)) + self.centralwidget.setSizeIncrement(QtCore.QSize(200, 30)) + self.table.setSizeIncrement(QtCore.QSize(200, 30)) self.fill_table() self.move(cur_pos) - + + self.table.setColumnHidden( self.columns_indices["NetVM"], True) + self.actionNetVM.setChecked(False) + self.table.setColumnHidden( self.columns_indices["CPU Graph"], True) + self.actionCPU_Graph.setChecked(False) + self.table.setColumnHidden( self.columns_indices["MEM Graph"], True) + self.actionMEM_Graph.setChecked(False) + self.table.setColumnHidden( self.columns_indices["Block Device"], True) + self.actionBlock_Devices.setChecked(False) + self.update_table_columns() - + self.set_table_geom_height() + self.counter = 0 self.shutdown_monitor = {} self.last_measure_results = {} @@ -478,26 +484,34 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): QTimer.singleShot (self.update_interval, self.update_table) def set_table_geom_height(self): - tbl_H = self.toolbar.height() + \ - self.table.horizontalHeader().height() + \ + minH = self.table.horizontalHeader().height() + \ + 2*self.table.contentsMargins().top() +\ self.centralwidget.layout().contentsMargins().top() +\ - self.centralwidget.layout().contentsMargins().bottom() + self.centralwidget.layout().contentsMargins().bottom() + #self.table.contentsMargins().bottom() # this is huge, dunno why + #2*self.centralwidget.layout().verticalSpacing() # and this is negative... + #All this sizing is kind of magic, so change it only if you have to + #or if you know what you're doing :) + n = self.table.rowCount(); - """ - if n > 6: - for i in range(0,n-1): - tbl_H += self.table.rowHeight(i) - else: - tbl_H += self.table.verticalHeader().height() - """ if n > self.max_visible_rows: - n = self.max_visible_rows - for i in range (0, n): - tbl_H += self.table.rowHeight(i) + for i in range (0, self.max_visible_rows): + minH += self.table.rowHeight(i) + maxH = minH + for i in range (self.max_visible_rows, n): + maxH += self.table.rowHeight(i) + else: + for i in range (n): + minH += self.table.rowHeight(i) + maxH = minH - self.setMinimumHeight(tbl_H) + self.centralwidget.setMinimumHeight(minH) + maxH += self.menubar.height() + self.statusbar.height() +\ + self.toolbar.height() + self.setMaximumHeight(maxH) + self.adjustSize() def get_vms_list(self): @@ -549,7 +563,6 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): row_no += 1 self.table.setRowCount(row_no) - self.set_table_geom_height() self.vms_list = vms_list self.vms_in_table = vms_in_table self.reload_table = False @@ -595,9 +608,6 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): QTimer.singleShot (self.update_interval, self.update_table) def update_table_columns(self): - #for i in range(0, self.table.columnCount()): - #TODO make elegant column visibility actions - #self.table.setColumnHidden(i, False) width = self.table.horizontalHeader().length() +\ self.table.verticalScrollBar().width() +\ @@ -937,7 +947,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): if dialog.exec_(): model.apply_rules() - + def showhide_collumn(self, col_num, show): self.table.setColumnHidden( col_num, not show) self.update_table_columns() From 456c1e63d7158b2592904789148124579643291c Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Sat, 28 Jan 2012 12:25:35 +0100 Subject: [PATCH 08/31] Added settings dialog. --- Makefile | 1 + mainwindow.ui | 32 +++- resources.qrc | 45 +++--- settingsdlg.ui | 401 ++++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 383 insertions(+), 96 deletions(-) diff --git a/Makefile b/Makefile index 6a93e59..ab0570e 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ res: pyuic4 -o qubesmanager/ui_editfwrulesdlg.py editfwrulesdlg.ui pyuic4 -o qubesmanager/ui_newfwruledlg.py newfwruledlg.ui pyuic4 -o qubesmanager/ui_multiselectwidget.py multiselectwidget.ui + pyuic4 -o qubesmanager/ui_settingsdlg.py settingsdlg.ui update-repo-current: ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/current/dom0/rpm/ diff --git a/mainwindow.ui b/mainwindow.ui index c349a8c..79d8597 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -16,6 +16,9 @@ 0 + + Qt::DefaultContextMenu + Qubes VM Manager @@ -65,6 +68,12 @@ 30 + + Qt::ActionsContextMenu + + + false + 0 @@ -243,6 +252,7 @@ + @@ -327,7 +337,7 @@ - :/root.png:/root.png + :/appsprefs.png:/appsprefs.png Select VM applications @@ -359,9 +369,9 @@ true - - :/showallvms.png - + + :/showallvms.png + :/showallvms.png:/showallvms.png Show/Hide inactive VMs @@ -373,7 +383,7 @@ - :/firewall.png:/firewall.png + :/newfirewall.png:/newfirewall.png Edit VM Firewall rules @@ -495,6 +505,18 @@ Update Info + + + + :/root.png:/root.png + + + Settings + + + VM Settings + + diff --git a/resources.qrc b/resources.qrc index c6e6649..360ff93 100644 --- a/resources.qrc +++ b/resources.qrc @@ -1,22 +1,25 @@ - - -icons/qubes.png -icons/appvm.png -icons/netvm.png -icons/networking.png -icons/dom0.png -icons/storagevm.png -icons/templatevm.png -icons/updateable.png -icons/home.png -icons/root.png -icons/createvm.png -icons/removevm.png -icons/shutdownvm.png -icons/resumevm.png -icons/pausevm.png -icons/showallvms.png -icons/showcpuload.png -icons/firewall.png - + + + icons/on.png + icons/appsprefs.png + icons/newfirewall.png + icons/qubes.png + icons/appvm.png + icons/netvm.png + icons/networking.png + icons/dom0.png + icons/storagevm.png + icons/templatevm.png + icons/updateable.png + icons/home.png + icons/root.png + icons/createvm.png + icons/removevm.png + icons/shutdownvm.png + icons/resumevm.png + icons/pausevm.png + icons/showallvms.png + icons/showcpuload.png + icons/firewall.png + diff --git a/settingsdlg.ui b/settingsdlg.ui index ec81baa..19d0b76 100644 --- a/settingsdlg.ui +++ b/settingsdlg.ui @@ -1,18 +1,22 @@ - Dialog - + SettingsDialog + 0 0 - 672 - 351 + 694 + 483 Settings + + + :/root.png:/root.png + @@ -22,7 +26,7 @@ - 3 + 0 @@ -31,49 +35,111 @@ Basic - - - - - Name & label: - - + + + + + + + Settings + + + + + + Name & label: + + + + + + + myappvm + + + + + + + true + + + + + + + Use this template: + + + + + + + + + + Allow networking + + + true + + + + + + + + + + Info + + + + + + Type: + + + + + + + + 75 + true + + + + AppVM + + + + + + + Installed by RPM: + + + + + + + + 75 + true + + + + No + + + + + + + - - - - myappvm - - - - - - - true - - - - - - - Use this template: - - - - - - - - - - Allow networking - - - true - - - - + Qt::Vertical @@ -92,8 +158,8 @@ Advanced - - + + Disk storage @@ -135,7 +201,7 @@ - Private storage max. size + Private storage max. size: @@ -155,14 +221,14 @@ - + Memory/CPU - - + + false @@ -180,14 +246,14 @@ - + MB - + false @@ -200,10 +266,47 @@ - + + + + + 75 + true + + + + xx + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + MB + + + + + + + Memory: + + + + + + + Max Memory: + + + + - VCPUs + VCPUs: @@ -211,17 +314,160 @@ - - - Qt::Vertical + + + Networking - - - 20 - 124 - + + + + + NetVM: + + + + + + + + + + VM updateable? + + + + + + + + + + Kernel - + + + + + Kernel: + + + + + + + + + + Kernel opts: + + + + + + + + 75 + true + + + + [] + + + + + + + + + + Paths + + + + + + dir: + + + + + + + + 50 + false + + + + dir_path + + + + + + + config: + + + + + + + + 50 + false + + + + config_path + + + + + + + root img: + + + + + + + root_img_path + + + + + + + root volatile img: + + + + + + + volatile_path + + + + + + + private img: + + + + + + + private_path + + + + + @@ -229,12 +475,25 @@ Devices - + + + + + + + + :/appsprefs.png:/appsprefs.png + Applications + + + + + @@ -252,12 +511,14 @@ - + + + buttonBox accepted() - Dialog + SettingsDialog accept() @@ -273,7 +534,7 @@ buttonBox rejected() - Dialog + SettingsDialog reject() From 6481a551def9251472d589f0b94523ae403a0d6d Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 31 Jan 2012 11:17:09 +0100 Subject: [PATCH 09/31] Backup restore dialog. --- icons/appsprefs.png | Bin 0 -> 55771 bytes icons/newfirewall.png | Bin 0 -> 25443 bytes icons/on.png | Bin 0 -> 31290 bytes qubesmanager/restore.py | 129 +++++++++++++++++++++ qubesmanager/settings.py | 149 ++++++++++++++++++++++++ restoredlg.ui | 240 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 518 insertions(+) create mode 100644 icons/appsprefs.png create mode 100644 icons/newfirewall.png create mode 100644 icons/on.png create mode 100644 qubesmanager/restore.py create mode 100644 qubesmanager/settings.py create mode 100644 restoredlg.ui diff --git a/icons/appsprefs.png b/icons/appsprefs.png new file mode 100644 index 0000000000000000000000000000000000000000..71324c7592bbab0c37240eef7c64160d183cfbd1 GIT binary patch literal 55771 zcmWifbyQPt7{~8M=P2nC1Oe&pjaEt!Bt=lVq?BfiMoH;T38h1j9*s02t%P(p3>f3b z@1C=>`^V1C`<`>(?|q)n_xpU*(N-fNq$dObfaKXz%?%ZcIUl ziO?mpH;0w3e+aOrOx&QCHy-8FyT|B#3r2>arM{EPWB7s2U7yebh8jR}qm%3Xi$+3j z_vY$uT>^GZcOPq}k;N*rVfaiNh%uPUj>GkU_Q5M20Gv zxKfACcT#>@S!t9^wtdzA=H=>IT~bw5)!5R~Qc_V-VSBnUB3EWmu@i7HXPcyR{pqWv&`hYer&5IV>!!wm$%4TZ`A>fe+?t(=Ub3 z6c@%lUVX&EWyGk7M789u9?9qp&DRhD+AeL|!y;iz{h5u0g{!I!$4hY##UZ65z3ult zcrgl3%glO=+M!@t2ZXA9_mXM{F>5~}g9m`%#>y}H0?Ytu`_gMp#x^rEGdaJDy=|YX z^;AD*IO+{bO1M7IY4s2uOFy>Cdbf{&t+;@ItP6S3cES#q{#d5mbmUcVW?;l z)T?i3`V(?3WjjP(FMpklRRK!HM{Hlf$uTd>49wi`_q`MgIn66pz`uiM4fu+*SC(II zhu|<;qAG4dF+v$n=pMVG|Bea$JGVQ$oJsn2*6tuP4T62W!(R#PW>_3|9h18(KfCfz zTwdPYJ*2sYR+2JAR3F57vYa07ZMwv4dp*e+6ifI(#zef#Rhru^Xc+2N+9WI}IF6-; zhp?QfH2b${)Z}h$ZfpCv*gEb3v>y{}QL$8s=2Z*e2{fyqKbLTZLtQ|{Wria;ayLBq za;HY)e z+C%&8I&`-KDn@+Z?&yO7TlewYlh4lD`!4ghv#e+gjFs$P=aJxH$pwU1|Kcolkr9!? zk~v)m-Kq756je3%1z<8}UK}goZ632!D(Kr^e4)39?jHMGA5=JYh2E7S|Znhl;95Ur$#xd;DZ=-{Sm`%*z<9mIkQ|p{P zYmxpT#~im8l?;1R??}5U7E_`JF=0@;V?=SkvaTvi=h5#DrK?DXlj1-YUTuS1Eo^W$ zK3z*SI4^L*V%@;{!(45n@$`z)6s2oxU63(|+jc3e?kd4oyR%>Dy(#KhuIMC(G#Fa}${WKO#_|iD>ROI?*wYf+aDE!1N5`I&;wW)JdJ(A3Pzrc4c zE)j8s&U3ZauJLby2M&8rghDe|U3oz;j_ax}^Iw?Outt|9_+-8PD%$iia$LcuSY_ ze!K3Zh??l@q7V^fSC>k{{i3wo>!K!}cIKg*kh$S9eTzKx4T6^zk1VlCaX%U0 zMi`oynzA%)7iS(^Z9fm3?~U-VBItORjLhT2XLJOX-(%9}p>{5xM@L6Xf|G!JhCg0Cfsx*RP40D1%k3b{P>2I8jm6m zi+YG191=sM_KosiQd*F!_6zAPXZL@3{>1-vpvL9=CBGVfxL#xy%=PIkQURP`(g7wz zdR)vEKa2|*6?2?^)nfHC;eKI&cipKdF>YZnvIZa6_?;UU4g$JZHuy5si&AQ;e?tWM zSgOV|bH~?M@gH%?V6)Ie$>a3m!-NOAAm0uv^J+?e`KZw;b4-FQkTB$>LO=s$BAwPp z@*hmm7P6P3IpS|C?TP7Mht-@hbEX|LlSKFXn{Qu#o7i%w!y3g@;kVw*K~@ic+{z@v zi9TVE&9_JV8xrV1uTyGEu0){e^)diZB6+E?j$6vI5eO)`g}zbhJxd^duS(51Q)ieXH?h?YYtTzMV1UF3Py{I!3aFq_{a>xLtISk z=dz?l9n!~4xlsCG_);4r4ni;&$Flv3K_vBrAI__ zu*Os|4L?70>kcO|mpksEyvqz%fCVA#DHtaK3sp>vE9RIwTgK}kG=@O}Lmr%I)k(LH z7Xe9P2|D6Pe75tavC3*S!sv)KG)J9&))mZcIlxfQT#QwkJ$8Zd+n+gNNW$&}QStDi zm=H)Z^XE{J;FJ_Ee)k;4jzV3 zv^d*-Ya*as_L)VxK7}olNs{;9L|K4;JZH9Sq2Yh!P4`4nvQh9yHtX^k0i)jii3g?h4$K&-y>M1)oVe~sz#E&QFViiN}ZxwmPNIpPQld)%Pv^=)sFMk*%3Ja`d| zowWf==XT`03X)wHmZFg6aQP)qZ zT2JV|U=&TKuN9oX8T%P$9xk?-Zhg16Y#|1ad4ZqBFhiN9_J+?xUr3P@e(3~I{r&rw?bXQ0i11TQGOo!JWihe#tDH~l``(X%HKwjOxitK1l4633N?kOB zb76e9;+&}(1O#1cQ{lfq#0H5zaLP9Rxf$Wu##82T{XPqsHxoHl5SNYl`0Y|Mhg?29 zuhdnNw+ec9HP&%jocYf8GcO>Rj@X7sa?&B_fVKBPFsLW+G;^tS)!D^wq2D4d6Ji?J zGp9dvet9`L&1YKrF^q*?V|?kro5_gm$KxFT@YjFV^E;CsNUOgM@ioG~P5MdAO-4Gt z{a9-wVx96+TmxLAanE!oMD(lBaIn*aWp%{g3OTBiCfd14S(wLDKlZKH)v-TGT6=M~ z%Iij{v>ZsEipId4TwF>%Qa&Ce4w@l-%sdk7C5182?Uo5n9wylgUsf2OFrOVpl#t)< z0m*qEx-VC9@vaaYri->l5-&UgRX@94Z#s=#DU zWYpf6$;cPXaw=(^3U%A6XAbf4{q!SbqBM2 z#I{#diWY8SBXMh-kqEWN{Koc?X5Mt=3hBuNWq*U3lMpxqA zIk}M)?&0A5INNZ!Q%=o@2o(JrWL)tw z|D7$Ap5trXLmt!*1G&m!t8yHZGO-dpaa(MsZ7Hp~3Dm#NZTvKle}`$~yMXu8<;J;> zi2y3>tad;MEA|7x6$PyEwZm_(9jSSZTtBOhPpLl-TSiv)yl2^%J_fWNLzvmPF1nvP z$(2G-}4!(n}jeBn~n z+E>peij$$Ye5x(8NCqe8T8NE6Dow`O^ve`plfD$$#7qc2xJI9jnd(`gC0^lSzb?@e zGa5v{=3@z=6jXEMEG-y&fbaU+8v9=y-Oxx(;h~^vI;bLD$g)oiO+6U|0Ae_*nncVR zz%nmTK#nM)1r{kZ*Ig{$%TjW}zOyKHhlIFV5<(h0qPZ((zPPrI)rw!8HuIgK4H|A< zrA1wQ+Ndgv(us5qz?XG8XobS~Z9qNsOPe2b5n_SJP3nbqv66Do_D+Ly)=ld}k-r&re%$|T>}hG1`(OYtsGmJGlI_q}4w z05M!(8UFjBWLLOE@t&ZLmP>&Bw>m|cEU@`kDTU?dWd<_*(%^8!da1(-Ar7f|hY4~L#-NggXpg0i84f2vgIBwH@`KKuBSR83jX`!hq% z!bldjj%gxh)7+zuT8vTbHPy4X==a>41UifR*5b&s$%ruJ$l{q0casp}1y}=Y;kwp+ zR1m8>%QqLHm=Az>U-Yv&-slI>O3?oN^n3dA7YybhPjP~Oh>!YXy`+B?X;}DcR;0J} zboLoSmL}ZZs(7s6TvzCiODT_yr489$av+lq_?kuZpl6A84! zJ}N7U1L?Tb$yvdw(Flj_%29s$y^!r*2)Ir%e5sX7tmvx!if$u4`AP}yP+C=jjTN0i z$st&}{OYARrxd%t79kjK=ovm@)eoLzd|-&;`(*|Kv_)Wv;jsj`IwXKP4r~Abio>DH zvAN><72xV2j$lNjw9Y`v#zw*g3nntk^T44HZ{|HgjF%tkp|5cnHWo4PkldeUVZEk0 z^`EZ5y74KE50Nl_y>fi{YAt_CRco9U!YqUu3;*b5jv!a=)Pf|f{e8~Gh3(H?JO(uX z4$FXU!bosu(4+GY=Xyek#k`Pt#yS82`>c@H#QksssQfD>7I717jFpI648~=_er+#z z@jZipHkr{{t@*2}T1a}M|CKvP7@$AUatg|j zvH3_TzUI)UmI%<|19v^?_=f_}LuE6?tQs<>O)5tMK_OElA+9AE!i?L?GLU{03qYOb z-nIJwOyF4JAOe09kN*HDfmmbQJY9qhSv&v`Aq7w?5|8!;U+mHh?#twf6r=M<@@_x8 zGh~?UWHCG}m>v~vn!Ui?_d3P8jObe1;A6B6BJ};)pR|ax(x218O!9-9{I`$Y?pr@P zU_KEieLf^5mno!;kj7a790a?&JX^Oys$4Y+KhqR@bIqeh2Pixj;4m&d9pD4nuqiE0 zmxn2Lwts9OUbRTMknlzLfjDyH{2J`?O+!WMVa$v*u@!TF!PXNDM=Oaw7%`)N3R zCIiQ7Y~I2bd7np>0ql5KxFZ7yyee9FWcFtP3nJ?}W}RwwAwrJ}h+(OsK(V8bCEE@7 zh-ZHSZ(BJ~`AgjpBxI$Mc6SVdYeMmQ?O7sc@|gL8o~_Kq`-za}2i7gQVRJOBkOdTT zL&t3mS6IQD6THujBscNS{_Sio$R~yd{aq|4&0!#m->`#b5u6>(h0aHzE|Ecc+6yT&k|ASK6Yq}#HZ-Jh18g*>`Y zYb;&Syrw(uMdysoH8{t2hnrwgF;b`@a-T}M^nFd7^R+@`YqH3)$4!MIAAcM|(#F{z zWMhTq{Uv9x__SJxYmAgU^DE{N$$1Td9i9V$-6_<6Df#Gyct+7iip{pizUiiBP`O&? zeYr-v*F*P2mmK^Jz&fBq1a4+~ExO$!!KLm9U+<8{1#E;c6ghGGsDC;OSS|DR(PKvp1*W<{tb&uQKfuwl)N3h*ku4r+&?hZbx4%= zD%G^UBUJ)`ArC)4aU}FpE@b5rjPR%lXBjX(27v)|`>1V|20!j&h>rZ?5IA>m4F`DD z`dk=m-~<@9DG%@!{CW2MIlwx`8Ix0~RW}1U{H4q_7mfd^ z^ptVahg}1cIvXwfX|Qv(2SOGL?T^Zhqsv+fVPhCxZk?ub;-V<-aT6DLE5kPiWF6Fk z@}zpk{y2x(dvu)dZM`AWt^*%MLUBozHt>JNF=NaO6Qj`Q@C;$=Pv{(iZyZa+_k9Nv zL?w1Ao;5alyy%=7+j2f_8i@M&9PTbUnRigX-3k`qU&c-L1H22#YE+7dK^z1It+*xsW`asAS z-|?*q1OkRLz187r{^TxeSf6jWuP{MZ`67~UP7?@{f5&W9*(x>z;ZruAjg{i5US?OO z1rYX~;E?Gg??{{P%66DD0?`0>c1XeeGp%AiXokhzcl`Fb-^9T9Qo!Y3gGFr3hvFsv z+z_0rCY)9U!V?i}97sQV63c)FPKiab`eosMc&4W0PTt>6V3XMkysL|3N z`AVm=6y!GKX6S*ynBb}rQ7p_+1T->nOr(dZX}QEzQ2AkCT2~N$FNF}{Ll_<+=dJU| z*hGKVVH~P_P#sMf76e}REvKV!!Mb}lfD7PmX}W~FhZuuy3dT-OHfhOTgm4d9iJ+rB zPK=bwS!*fXMeR>|$T^L^x|Z*!;Zo}0t$U*ZLVyK@#CwOCh=ZT;l4Js$Sdum%q%poI zteY4{5mec7(Ckswfw>mIvga@vT|svXiIdw+_Qg=h9DcduR=rq~ZH@37Coa%=#`PAo{V1bY)Xt20Cq-1m}K;4t`*{cXy8MvzAO~^Fu zpomPjycn|BI935M2cWS#Ss}e&`5n@K3;&vKEXm17hmU~1qO^l?I?&T$$f`UDRDL0sSGkH^KJJbGP!pIJU83yd z`E=((4$=D0Q~4;p#W=owm{!QU8TAF(jawAl?jh(%5+-Q9PcQr5dNLm638I08MyHac zMR+4Vw84%`{iCv+O1CZ{t_7|j`%$c zEc!&LyH~RZvOG$_%Bs(x$!b~+12`2#=}Z3UQ+zwz!_wjm)*~a`n$QGo>ZM>d{L&;` z7E2_Y=j)>|xOA`z<-FN@<-UqLgkQEmK?-9GA7MM$VeA_pALqpTSON&E(YeP#7IN2n z^79ipR&OFMAT<|Nwq*E`(=>pqDKXF`09)oK#7AVS1z0(vgY9l#brRN%*bneQil0p& z@X^D~v|1}|3u4VoReCQ99nP`7+-&Gm<8ovF#b!>^-!eRVD4K2q4pvDgcbRo0#(2+& z(Yj3LvA){%a`}<0DBojqR2MU&E7}EHw!A*9DaHv|$x8z<4=?uwq(E@)eE#Tft@~-3 ztoAZ}7~*7gG<9xzhq6b7jj7{}ky>$w$h+5!x4553XO6vy9SQOmdj4_4x8Nf`0jvA8 zY|nQv;Hs6@^!0&{@ix5ac~tA2Bs5X`ngJJpl87K8+j>*^OfTAp_*?eO1)pXw8sQ*b z#>uOXM2`wXNk?81F0)?W=6X)*#P(n=Yc3$7q9!I?aZU26S#%jn2&AP-Ow%hB9~K+I zql5>}sg2zlt8f;r7&SXzrUqJ^*6;hMPxXtgItB;XSTz3U`i`11!Ynlk<6`jFH2f2i zUjEbwL?iV^#;zf<5Xs_^k?@W0MbYf|NIDtw%u zag3z6fJ@KN*z}XXgm`&hNH2xVHRcqaliT2uH>zG5NeUvz9y!X{mS!tvEZGtY%fxGQ zaGMUB06GA^)w;~__BoZ^MI2XXu-oU2cGX&X8&XkIH(ru}p8?s#5#5G~DtA-Z<90CH zpm<9M>O2!Zm>|#t^soe+O~mn?a13GxD(znk8uPL>h?749Uh{qNvZZ|%gY5r^d zcXe4qeXPR1cG42D-R=Mn{k+0c-T&YxBRq#5i#dY=GFh*jR9uVfSF2g&7-=x#UC)hc zJ;kCtn4s`c?^V@(TVAIpeny$4?@+k2Ec)b#Ys3&azNc(WxMho`mWQyys*`cIBv-E> zU((@-5|)6BY57`%&<^?eryqnuaU+WSTb3B&=YX##l&9){!m9I}~UWMC{> z{w3TI^QWgNwR9YD&9X#LTC<0?97!B{N=YNuJyZkptcbLbfoDQ)ip@Nz0wBv|-S`tc z1)BBgGHUGn?Ckx?sRA<)dgqb=lI%ukq$3<>DsP7iRHFF93ww>DUK2W;Ag$u#ukK3C z4m5(;G6lxl%1pivWGH3@I&pflDUf?lT))2AW3~$}@V@yHHWoXGV=x9i`TUPN8Eh zR5b?B4M*g&Nb>;`0Dyvaq}SYu!P^RM_+V_|BZ8w;dd^zc=cmc*rdw&6bG$&niy;~v zMohtxlTL8NdOX(^*Ol|SVRg4#)KM{OL$ud; zNUbOq_C2Lh}tM>0D{Y(Ex}V6FHMX@!q%&h__e# zP!cTn8hIwz{Kv}BzqZ6ZVo!YmWzw$WPC-bye3?d}X&s@A61l1ef-?7eniClw{Tu%Y zMkXCGe!(Yk*X>&-0%o26{5Vctje}wwMJa$`Se_*q;Ni#pH9o$)K6-k(On5~7Pl0KL zH0C(Pdp+6koy~h$<9n7nC4H2?-{3UIMl~~>sMQk|QYk_UtZ{GP&7&us7!|j~7gyhJu|bnxjIJ^xN-Z_;h@e;i5tK2=mADH+Mu;6@ z%V#850|LsOfK7UcDo4yO$0vRR>K8Rc%^Q1dB;f5=pI$UQ3W2F30Qn&4P@A!n)Lz^) zUgA}35%Bk}#_ua4!|u6qw`Y@|@%0G+P23~HRAT*Yll7lD-v2%_PqV#SY&ANL_}^w* zjwL<@B7^U9GvY3bXufGd5!Ss?hlZ^^Lu1ru0mu#1!s`bnK=9=kwYy&&o~!7q$?Y#! ziS`F>B)@5^w04I+sZIa*oB@VM*0;ZApM`viXh)w6u>sA+F#E4U5zCoHH6ET+ndMQD z_!@rUup!qs5W$yf=0#uF>T-_)0P?OHH*_eac06PFEe^;-+O@lxq~7LgGAWW%`KG_y zjR2oeC77D%$T+%tvth=V*QD#v^9-&Vqb;RSgN-{IRb5h!8hvrB&p;OcCE1EPezcB2 zzq%MtPLqYQWLl5aD96*eR!ERdHfn z!_34z-rahuweq%NWoK{i>}c8Jbotm_?aKRbQ-O93^k4&zZY3Z~M)Y{6tO_IHSjYLk zoDQo>zdo6ps}7saHA5u=*JS?BAH@}KO0C>q`hS`0O0EbmEch9=Zk{Yh3^2mNO=haVkoDa=8ua>41+ z`7{!Y5AN!_fNnKeVb8PbgfFUM>~+ODM!-D>scZbT^&O*Z56brxP*}KiSYSX9$Gulp zY{a>SjX1_n&jR;J@bhm5x-VnOkO7N+hiy+5Pf{8!%n;1_wyuM-LXEBdG7{i698xlg&thYeC&u4+28==WKJ6 zMU5rR$B#398CR^wu*JWivg~1+qDGqp%IKT0Xm#g&=8ov18yDV*nQ8Jp^>QUP@tOMe zvi{92?x6F}1Y4O9Gs9`pH=c7}>l5Y>&AnE16FLd+2!-|3S^71D@X_C;IFD=n2;srmRfDq+QJ-n>Rb!mVR42+N*l6afN-O}~Fn?}#EhRRwR zdNVf$5A1zlh|b}~)!}3MQLl>pjEhj;^m<9@eL(Cp(zX3|1ZV$nLt*cY%+yHWsUSYe zJMWrKDhwlnKXBi2B4n!G-G?in|1A0S`-EsJkjv91&V>(p1wD4H-?dDGv()mZ)h16Y zGR2)0uei9$aZ?IK&BDyZ2si+?1|?z=s=orpUv#YilwkTA9@FhW%R&~(gSvcNY0pd z8bXQ*g<@eTVG=l?0wM{BBA@BLM1g*<1yPRgS^!0_z3>AkBstHZa+kMIysnt!noSb~F3=`-(h z14%N6t*6xAUw+@4vMc*l8t_f*U!T&hW+SQwFFa1U()SUfv_<_r~ z;SkF!?Xe2)ls{wDHU~qsI^I7cM)qyBwoCd_a5E%r38%C)<8yKB4zE3%;;wy2)<}Pp z1|9+*4i56N89a43Q~5_iuxOWqy!eR6m!lt!S==NEV#@W7H!<@HGmRx1uSg$+s82fc#!SnZB~nuMXov~BMOFE;IFx@Fd0@!4vB+4{}3 zUT3g#a%~RP*k4%qxv*7q&@h@H!6@#^J7|M6W_FNm`?z=<>ost0cdRLj0~#i6z1}IS zf4}|W*);w0`SM?zn$k)g=nXTDeHNlO>SR)&hlIxgC`I{abLT(yeS<8(Dn)nGi)YuS z5qp1ttts$J5TZ+vXn7dD@rArOoXKm$kFznjf*5cfuI_tKAhCNZrm z1(ZT8dW7FZfPp^ienU#{y{oiLF;V%|aHI#`Kn@-^3^|ewiVNhvhKxcN@qVlZ|DT)O zG;{65u@!QDC2KvHwCV%shdodHm1;orO?4k4vJR!a7p{_PR^>>~9#IB=#$wK}6l}nP zJPGfexvCPxv(TO3%1q%|N@vp47pX+9AZpn12m#*!N{B-a$bs9Z3E51$blFqPazMCzJ z+#*826X#|gK{VNs>8pEK^^cNvcMoRld-VJukr4V155Jo#vYL)8r6&(IYx~mU!U$lU z3{p}1>Ds+3(DaB0-MrO60LGEwskWk^`m)Hb7=kC<)&&ND7?F&hld=@RdjsW9Kc+3@ z2mu%WRR$NM`xvyhjD*vy?b;>b%;+1ToM3?#D)|sebBqkV4Rt1dpvprB7oG+4Dk&NWj3kuZ2jen z-|FD=|EFC@#~*^t?C0eFr(0b2?9-D<$p8%cYV!ytmH72`*rE2z{B!4_x8F0R08rI= zV|Oq9H?coZSxnjrY?cpKugi#yn54?FYNvL~4TCl(x@{y#o{p zZBbDmLei5Zux>u;HMTs5PafoWnK_h`@xV-3wOL25|9Jvw=8l%|y4Gh$^&nqgpQAce z7JA0K=@GxuvXaE`+S`9rnX+!u4;D9yH5T80MA>J3I0LNkjie8LtoA$>oL4NIcOj&z zkTKcLHOZ#1~1~vsC2o4tG+G-6t0dJkpwT=;Hcm=85zEAq@Mo`L`?x4SO@^SyrIf z7nsfhv`sf_@bcYpTp$$>!H9$Q>6^IfCAIcDvJbpVD)((lsRTcr$H?U4pT5E54T**D z;^8nZoIeT^p9X@@LO;X0Lj?MQ<&pG`W$T@nOqSxt|151dte_f@L+7|y;u5R${!TOVHTBZw4;F4G)$NQ$;--S1A96Q7>mHDNsrZy1=C5mDi+sxC{Zt7Ah=P*+!p;A$O8dd zPJnSxDP5s`N&X`N0I;%+zp6F>0mYDF4Y%B>VlW8ceQcjnQf4QKU;3*VST&)u3zai} zHe{HQ6XJ0x!%Teca!h6N>lu8HXGCcu7`$0bcuAtTp$od~u)VP3B|m zFU{Auge~k;L({s|v^Eti3GQ=b=%cRqY|wVf+2-B_W0867p68Oj01h>Fc>!H@P&(Zs z^QXF%P_@eHs9JP+v-x&G?18xYRRq^Qb3xla)9=ikn$Uu@d>joLJSI2+p=}`q~dHnRn0scJc3i|$3s|eS1cYlBXjZ|lXMKWNa%p*7j z0I8?n?Z@up2I{+I|I4A?vKAl28GgN8GIQ!Pntorm7!T8qt{rNB6RR!)$5gC~`Blzq zMVq1^L2ZFQxH=ks5vsL#P1afuJu0U*>G^^oi!8Z2qiS;Ubp2^rb2&v$ zeyRpj$C}4F-{e%b4~X|29ZN1^%P5@tCyKf2dDN{MmoZ9@Y*aYzn!O0XR(XKTDuLw|d=+?$dztDMN{|Y?;e4eL6t( z3pf!A7<`=>JeIle<#PPYVJOq&^>>+zor*@Wdx+A02|o&9kqr{Nm5QFNUVXl@`k*^= zJaBw3?~_mFyy1frEHaFe`hFa+F2R5c>9RX)+GKvD`7^6SPow>Zznq-c?j)6)+SgIT z=O1BMQ(^RWZ{PEXvFh;+Dw3TgJYIYFoVizr55ONj`7EoUPJom+IN8y4{T zqtc!jP;jgKw1;>ZbW4bZXyM{8spUmCb!MUP-CNH|&&G4wWJ)~{T0!q6KG@3Gb4kET zgnM03un;?+L>$Cl*0><87jHe>GTq3XlV)u0aaC+@D*5c2UfapW~y%FOt(@sFyW{%&x+j}tB zuC@LV)~x&k7xZRwic|y8nU|q-HWFzuj)Ec0IxWbKFW6Ay2M$9@v)PNjI=*}NaZU;C zH}B6J8q}{*m*18(u+$F5tfbGc}0kRvTp$Rz$ z%-54skQePw)IPwA(HPus5V2oh2K2vGSytoMc!%9+DtJR1a<-^p@B7eCP4ixS9zER3n&KNltLGtbTG+?>GnBIh#C`Rf??lr0q?z< z{v6pm^yythI0%`Jr;|dcDGo%F{Xb5`+B)5PQNTcNj*}b4WWrR%aXo4o!&yZrixqTP zKdG%DNY4&zI?W!V|9+j~H&Is(nqIIj%%WSxQ$och)g3<7f!Be6Jf{2BPCFkHnHAzG zI(Gc)aAr~idK)!+OA_bd!%PwcwRF~>*V`}QttG~4o(Lf0KFuFC&&UA}t1Wf8+|=2rG4J zQ>P!DLwaqDWEzT4(E`;@bG3KYVK+qINCE3~gcX3$ftO6%)`z9w#i*rZMsYYiJA^JN4&oxi|69^At(Dc~<-omVvh_!ifp)@=HIJJ?!W}^$Ki# zNz~?t)%3VTiaRjOj<8mz zN>BQ&b7!3<&%9VIcL()L1|M}3d*9tG2~5NkY7sayk_^<&baD>;HyC^t3;=58)8-V z8nak83fCmEOhi0V8(FnG*FB{d>e1NR3AP~`cj z-jLak3vf@{UC@Ywei;^5vJCw}klnqVlRT39 z@w8|yuR!QrA{Z38I_X=XXg;v(h}+@xLX%Nht55scK(tyB9grp&`M2~XwI*oUZ|P>y zjO7m1&yu%d0*Hx#-01D#afDFz7`!C&gdJm4lZVQ3&GEy#68ZD5VNPR#vY(&wR#1l) zYUtSRtFV1B3gW|N=Wp#gE;K>Ky|B%#%O87lx#ZT*BidnmebCBAe+=!{;E(jS`- zRLo1~R4bWuJEo(+iNKen1bWEB47|Ar1hLoN}?Ve0t|zvfg(@!=wJl+C%+$G z1VX}gXU`<%c`)>+IlnP@iQU9NpM22NUrhWn?Tl zHXXE)VRX3Gl-~V6J-I5ekzeoAq@Pv_r8gz z?>w{L%(H2eykj3dDjCc2%+|c9_~NdydUnWB&J9 z+~QqY8p!Q;aPQio3ynMp? zPRwS>E)Vip!m=;=+Zl@%DCY2X;eXdYr}%R^VpQKk4D^Y=ShHQdYmDT>Il3g<(}a@f z?vnNcP5R@Pp3C;z*Zx+e=J+Ds9w%(xDL zlG8&<=B!UQrcGTQ9M*(m96}HksDU;rf#13wu54N#NAu`Dc+6fvNU;zuw7`2xJ>*zb z=03-G(#b-|-=eCCKc4gD##M9j23!1t>a9Fy4!B0aF8eh>;KQvsO_`fUUA1q?*B$}y zF(v#(TXZYJDuo)&-4NINYATAwqTU;*+vqe~x=ro@A#jwPsVyq#1Q{XA-rfX3EO3-%BFeHU?ZM=7eN;x4HR<7asz^ycPIa| z&D49TiXOmzBpZ7ll%5z9)os1@^sQjIx7IS8^u*w+_fsN5g)0Ob1lBe9)Nqv*tRj>p zIlS&2<81ie$|E{PDz%RWI(*4jwF-)bi{n90Z9oO!m4FL2>LO_mbowU^x7%a_D7_u~ z#+^&czPR+&T4FEAPW#R}LinsC&{X^UKsAFlhpiz{gV8i5asaD^_rc| zFHVD1!7h%yzq%=bYR3(q@=KLzrK$7N+B>*s{QYcr$_Rwp&0-I{kOHVIz{ooB1+={TGJ zkLQEH*S^~v&4=wbJ@8O1Cos%?iB<0WFiy8H5UlkUNL#2zP^^SnU2p^XZ^4hsSX!*= zf6Nhk^*ecdvvUj#u-L(VPsa;HOh)4J4(jmom2X$ys-Q#&0b_b~l3(EgW4gQJ{5zT- z#_dzH)IMJJ3?Nw-S%G2PA*xfUGP; zzN-i2bEk=ki@%<_zqF_BHBA^)(}@lHD{Ib6t)vhGcxB+<@iqQAN#W2)`D2mul-jL& zM)0w8eOHyqE5o>LI~N)kfp^s6IhB7x@SIcXzHw3wk{_( zDwqqo$F1p{4g#i&@#r6HXRb^ey5uE^O5Re zWE=4`=s%9mIx4EJ3*&cYU}%PxW|T%sT5@P41PMVxhHgPg$)Q0IkdRJEX(>U0Ar$Ej zK_rxJ>HhBf{pnh)CHHd9x%=$>Jiq4wRPt+Q^(lc@*o3*y_xs;&y`^WtE=hAV2M*^b zZ}IK;7Vq0<6oD{&4-ViyQvNVC%skcNTP>Gv=y}?(|IE``V*NgDQkuf&%n>>GZ$eN* zP{;sIa+o;;X$1k6sT)0A3k?0ti%+Z>v~nLra+nL-D0OG?TfrlLg0?fsO-&%<0)qOp zcQI6L9bH446}%5|%MEvt0|tO>5;6f)hZ#j+KQU6{OvCF|Z4j}!ko{Y=Dj{-HyX;Q2 z-1pq`Uz$7dWhPp;O#K1-*Qd4q{_j<6p-(^J>SWY-n0bsU6C{sW%Hx01v0%^MPi;FY z690PAP~G}O8;Re4$GMb`{>b-lGw}Mir6V94Ob2t*OIU|TJ>@^Hlwm;*V!q(Xc?XgM{!7MmN@->|l zQ>wE+4a>?1fJ>J1e^E$xApC8<}2A!cT)UV7OANbX1H6=CxdS z#j@Qdd-u%+JE%N}9RW%arZxm0kjcythBY27X|B%_C8MssU#ZSIZ81$dLhCHS;Hh(W zoOr=d_0K5Rl`m+Q=wbGY1?3V12M|C>YoF(6W&M0>N>?}&;1thN;X8N)1=KL)WP$)- z5M&L}k5yh!qsjY;VTYrAT?Yk!`q%QJ)#}^LG${ehN|W-w3xGLkqD5uQUe2z68)}%y z_@&6nXQmW=${xny)O8r5(}9N_FrZ-!ZkBj&m#;82H>q)YGu%o+L|&H^dQX>g#JG`B zQ>{PT1S0=pj=c2YE&Z+(<4&ZTxqYB_Oys8Rbg%mZ(RNvZq8nOw9baJQpduv9&j;i( zm>Yd|TWb384}!X!Hl<3Y0*0gy2WVz)enpc5Scc#G5r1wE{c0!$dzJt3jejNF8NcInGWsbOSnHOefzu43eqP4zG?mK*c zD2V*R{UiDQ`k*d0>3u+eTSA;u^yu=p7&d1rG>|VQ)3?d1&jMK&`7{Wyr6u7e=-2hf z)$cpy76b{hdrg~pk6xW0;^Gem9^Gj@IuF5~LzMuWCxQ_eZ_ZoYD zmsMVDis5taT7*>@Wt43M?!xiTmmf)|>W@(hj=0~8jB5(SKf22803u*r(R`W(Z_JWCJ5;pNjyd&}YDGgW7kgIV__M>%mJOka z;?*8MX%GE|fWHSN4wuj6c6Q_S_7X%pO!ZBERjwsA<7D3Gm3f&&aqko=C34`*+N&qU z^S7PdJD`WXoYWWmL$L3bXNJXblQAk#hY4qGtupbEz(`{3ucm{p4Y|pGY->z(1#N#! znb^)zBcdP5N`5*k`!2#m44kbj)O7`x_;>#d6a7Tr#bEk1ye0{a4`B zkuDs;vVkh2g5%EyOupEljuACxlevlV23+XnpT^D9Ck(o>+Kvt-&nShCzKff3biQ?W z;u8x{qeTNOK9lm}I-#T}8%ehK;)tzq#qAjLtAd#%w190svBc+`%YXEQo;Vxb7vDra zSj0(uQ964<+K+?5-~#;Zd}o1~H{}n}p8!rE?cjjgz8#SN4q!YU0_b@5@8IXU_)g2z zgF1#ny*3Y;=H;qSGp}P2VbCsbk1i5%{BY)SvXmmc>IJ3v`AZ4*8hb8bjGEv*BmgHn zdKQO@@3|j{`80<9#O0hLltbKX9Cc6cp2Dp~M#hl3OY#1ny#!o|>#UTF9`|eTl~$*= zZkg>}ki5kKNMOwG2%n!K_9}d$UW;nc3jJkjePt;cD-qcuzQg=k{>jk2e0cemcImuY(`*{(&H@UyBcfDgc z>84AG^h%Rdh@L2<(eN4T3oCA^iJ1!EglIiHC_$@qWr?M%H05ZFS1oMkoku4*1djJo z5r*@*HVp!Q8=xpG9Ps;CO^lNa@%cg8KIN46ui~>Q?oKW@DGW6W1;}so8|$?X4rf2_ z;rf=@$%u++6+Sc1;cL4$9{C|9zGfcuH7r-t#gmnN3g)=^w$q-lsW$4Id{6ebFuq#8 zjycL7=!BXYvn9Iu5B|I^1Rqq$(Urc5wZ1?%`7n>CT4j4;zO@zprgxPQNTP*nyU_qlSY&? zMe#wHqg~xKXig5Fsek5f&&6WivjewxzJwjReT{M&Pg(jRoWcjs!BM2%1-RX_2%QPV zM>2+GACD`#+rR98n25tVyHJ}BV5Y6>AP0CpJ2J~qO*xDyPD*&s`{Jul5%anHpmF1? z@})2onBhzcJ<^=ZbuhW5$?)#z6*+iA^Pp$ops<{=%k?N)+uw4+8fhn70(&PPy-=Su z!bJhrL;P>}j(qc=y&H!0BGKl$6Sr0#1Tr1*+nLYHmOg;5o|_ZK8WLF5^I64D0|mO3UTL zvGA}o>k$>A|Lbe&8@$x&JgL+s%Kq}og8=6exWnoMM}}Bgj#vXir%+Z!k8^sI?vPTrvCzSE~;W|bV5`&`wXqX+k$u)j&= z^)ugm8P-#A_T3M3$Fh$+-0B{*G>JdjYVt_Tr0cL|o(O1FOlDLc%{gJ7t*uLXl}<%{ z7JPAiw`F^RIJ*HI!YgM`ro*}m8-JW?`sxw){E0mjV#->zr(u4PsA*MDJ?NC|378*` zotQi4hrcb+5X+nBtP3~K3i*1P$Cw7i48qkqu-aYZA1sjE`JlSqwq@{}do`F?@+!roMIc?$wOasqX%v%qKq^9-G-=H>Ow6$DZ+(4G+ z)H7i$@B_d&Rkxm0RtH^gEd*0yZNH->7e~y$sIgm4k!uV9~4zKEb{d&g8_{^lA2z|=R zFUj+FBMdpQ$XB<|{?JPWGTc}Iu?O%6j8;P%ML&h@MssiXSVh>Sl4!(p`D`)xfj?Y( zuy}=BT!j98G;S%O!Q+hk#Bx`j}042r8YBW7nlFJ z9JxSSoeduNUfu^4Bet^BG8gL~GRyvDV9>yjCiWF2MoMtVBm^P$pNpc~z{kyensdB(&ra) zDlPdIYgtsxDu|Tvqek;Gm!oP|J6h~OZ@mfMtgP_Thi{ZHVp3&#OGd*tYCYKDx?e>lV_<@eJIQnOvxt6w5Q%#vu9@6T^l$j2@sjJb7Mp{A*B?rWd zX}Ee_LKZdir)zX+^w+8FS4B01$d)c&ou%FFSFzJpRmz zw5_1IiCSL-w&mK6(+>%p0V0Gowj9Vd^H(cA8;5nk1C>N|q*YWE8)lrj^CAIN`*KNLSA)ik?1H;o%2O z?K^_0su{)8e-d4s%;TQV*uCE4RF(FSgrV9n9lBIVpCVQ*KfF&=t9zP@3qCLX6+1x~*(eR60S0c_038zgWE0K?ytf&e_&@ zUrT(REfAQwejM~|U_*GlnIX3zI$+#>G_0;rr42u5#u5m2LlK6yDDcDZ88Kvn0}!oi zgA()acaMNz{m~hph2?~J(;0<2Oaq9U$*VLpxyur5V;Nt+m1tQY8 za_nbA#s(fHQGu%HDZJB_2gb#Yn=gjhK?wd)^#}z^xwPLApT^xzV7(S)6WdNhhc^p9 zC;~ZwLi;pGEDoRv`{czx2;F}G!Q9^L-QJ9Z;1U%QlF?!u@Zwd^dK<_0_ot+ypGNDv z;zY}bw{Y6+SD}i@8l?ZQnnL*2_N(>WNAENqE#qkF0Rd1qf6#=nel)Jl(%?0>^nx~w zEMYI`_R_Kn^G#wcOX1rSE%{bk7O@U`QdqaOi6e;A-_@6&KB2YYg^l#mp`X7XR%JkM3w?cJ*xLk$Gji0OG`-sGc%svm* zNsG(;w52?~J@ks(bN}^1`h@V2om-)<8H&{pD<9*iW%fb^$^M()#Fe(Wvw{CwBn}h% z_}AxKtxjgH*EL#-?}e$t@uh^Zk!TB)|Xt;y0XuY}EvQgPU*;%P?bo_EP zzuq)lkuGM{p~I+@@sKS_e8fubk@3c-h7Q%aF0{202b$+?0(tkLDMe7Ls61w@ zbs9iVpe6+vXcf_ZfwSX5>I?s7(u)|JD9`8-2iVr_mG_M=t{hP=b zh>2wFoL#T?&83dFte%XUd?w~mY?A;z76dNSsSC$mGy6ZJXH){#qObk586;6Aa$yip9l3Gz-0D~X#G zfJJLUa6XBQWERR#@&5eH+(&w3vWntYOf~m~n)w^o#NyhoBH={Ot@wR|n4Uxy$fL2k zWE@0NbjF1G&tRbgThfQI!uEd<<^>mPoFf%2lx*x%9&jFf)XaP_3&7+|)Xs+g(+Q$p zpSy9~Bm?p?jSxEF&$Tdj#abJWoC~wd_(b+@<=!dq-F-HP{d(v4O7RkgLv}$lcZ-%l z4{o3&yr(No4AoWfvJH~}RNjy@bznBR#IIqdHM@wWB~W6aa1%!fINCxJZDIeEBc0)7 zf2KWu^&Wq9{krnrQ2{*iu6(SZi~j(JVD4nv%U1l?fDLe zadTtn*P2V#m;ZV`cbRY`wzoZgpRc>ag73@S^O@XssXFGO@P~CyxKvMePO7rw3~4Bz z%8Dm9JBp-{?qmQ;23riD{|=iNoF5$>Tt$&W)HRiph?(;WiJ|Gg<<%Q^3q)W>q?$<4 zZiR7_cq>a$S4KaaU4#P8R$N*mIoL6xf>$SRPxkfb%fj_z5r*PEYnMg zh&T+B*^ro;v30G#pNQ|DBh|KeH~mu8!VLnYw*_7VhIiIK|9J;Qfe?02v+Qr1*RvWPGl@+6CRJ~2dQw~+uN53PW&f<@LWXms;PpEpVR$IjEWSSTw)Uh44(;3`-=zbN4<>RF8{vi4JGb;lukB!rlollK4cH z00Ap60UDybPy6On*KQFPeg1Np&oaN57v~Uw)!%gL$nUDwqX|9j6oPgYi-O#{S7}p9 zGu2ZAaz=w9Z-F!)7ZqF{;4Hfw{`M}{x{gR}S0uCVdhLJAGERLafCpTst^6p;1tqdL z)YSfiKqf-~eU#|~0wm1hl~G08iSI&XjEON-k1uCF#P=yGZcA^g*SUK&oQ$l!nEL^X z+|U=14{nisqnWPK<`X4tltM$K2e0_X{UrYP;7JhIcK#oFsJ=DuqnWQV*AD%IH7bymgQ#HWj2cwJYLM z8L%I4%z9+f)}GGcl6kk^4(|aA^HJ&~F;@!7VT%hu8;7eRn2_AvNEG;f<)WlgDfi|t z;Ig-%89a@*{uhzM>q!h04msgnPN-|y=iXkeg>``b3RY?#-h1f$$6VuBINN~Oi7|*hpqv|bA(Qz(gU+p=m178no zB2)j$w=~5>oNU|Dj3_>{h143!%pVCH+XMpcDaL|1|23jJz2Z0k)c9sI*(ZUZ^Zrg? zfUtjPE4;Vyuj$ker&+EqpHOe5jI9sIa3OV$H8UN|R{L`g{~2E{@SYH8<=D=~(+6F1 zo{of+-$|7E``#EqCQ(-wjjJB#L!^0q-4k>bso5);DkVn?4z%;gh?gPz60c`S|fuWfv)_pAUqr)+yeG`aZ_JiuL9;LYN zD_{k`HJ&9aPaG-;6zyv3*;Cd*%#^yf_$uky6c(AE0?J>c-z#<7xSzC;R`d$bi+DK7 zX|-DND04)xKOlarZpV@B7>F;o9q8&CN&vx?`bCSmPpBDv^&Sk|o39H+3@vGz|Op>e8gn3Hd*M^?Q1 z^lM3WlUXP9U)f#X)1sbn_7Xf*0K>0$4dPEcG|R^s%Zi;gWcINzF1EX`iQNbto9ooE zxF->t3i~Tmmqz#ZdY@bn?C$i8wm%2C{p}C;Qy}Ok?_)V1m9Z;Q$qA^C95S3eTq&x? zsyL?ffeOR0u+^Rd7PU5AyMOtP#abYI>a^ce;u>&-rxBwd8*a6L%$n%5>EU&?|H^%B z9dQ4@BT(~VL=pD~=kp_9L1}RczG}}z%ai!KYawKgMvkR{5X)(XK%n{OSj;Pxysh!u zbyc|nTp}9$NfC0XZ=K%9P0tGo1T=${VBtUe@Wm#R;vrbd#_)w+sKS&&ibN{QYap=V zbj>!ni6Yn|UCgXy-e#g)gm6aJwGov~_r=txO@jA~Tk#U{6D9b=7G=j$hp9~7-Tm86 zGRAHdk#!1nRjm@zJ~h{0UBTc0c{(aT9G6Rk5*C`reI{#1c# zqqI*Ya@x~ZXi6flFeu4es>}zeSJ7m`KyDh?HfKZulzIA|Dyt`E69chGWX$81AI4R! zMo&BNC$B1b4a&vPhVSKh4HKq|LgGqSGk8mG&hOgaW^D@xaBnVrN*~Mi@sO?$nPfSuwf<0xs&+^5yx_52Qez1_QP@TA*Qo*6cY+h(*dU>3hqy z2WH~dsYyL>PHvIry3esc>#s82+xDy-_~l+ql=OxCAoa}605=m`F>4A0Tv4_ji|?R- zt+Kgk^Q#pVdOHr}PYzU?Yac=mScXsmK1nrb06S@De%%7<`*vKr9~}rj!PeOVWa@gt zzH;Z?lt-OJ0?F!nnV}BjyTk#9(^5twR#aIdod9r|yYms>+qV1u&p-fhLckY2Cvuxk zY7Vy2s3$JXyy$d3Of;Es>C+OhY@)St2U7XRxHgUIor5MX8W-;ae=meZY69>`8h47f zQ38YMV(GHijX$EOEGl2b&Uh5!gMD!MNqU^esgoBSh(P4|{>P1a{D?=<2xZ|^7e$tk z6DveSFm2F8oLiNvPidy4@=KQ=rD0g9m@qCP30=@^%Ku9{cV#V7(kBJTa|9o|4wT{k z=uzX?f;w7~p-{rGXPJZ7L*8wN4*oD0oG3h&27g8axg0U297)FqOQ#x1!JwZdfk2&t;Lq2P zucAUJw?W%xcv+?flXBzUjZMzIFMQb($nfu?kQ9BYz|L{l)LM|jd zDw62Mrlsd1vI(Fwm@r!@EzBQuh6fezAM515ahEV|wsL0;)T0DHL9u^pBb|CV^ zl8#I%98XRj_0zF)&pY4%i|{amYp~8V0BWRsp<^TuTIAU=mqe|&OYgguAKRU6a+i!d zaYvQLFX)lj4btoZP_@TVA5ed}R%@FY{&{(9!&BM=x<#33h_ zsZr*{ z7xC|aqr1%{Zqx+>GzCi|HncG&6zD=!L=H|8DX~XEDwIx_(4;K!Yii@3;C{CMzg~4S zoTbzHhu?>2mmPEloNunEq%ftBpPeDzCv|x1BI4rWPxbXpAC;wJrj+vP!N-0LQrU+F z5mUV}liv``-zK_A6Sye)_EPHjz0N=?LSAE^i=?+0i$BfyEfSMutTj&Wh?8!5U@4dM`~a`wI7hX(-v0!Y%_ z27#0_)*TCW2u`Dj?%bXGTobdJc-piI3sPg*W2=v1F*URw1lecxj_V~>c7?*w9qCK6 zVP-&^FWV~Cw|bKx1ISbD+8}t)xv^r3e8n9_Fcd$$IFt!;^zH>NogpijDwC!S6%lbE zU#LziV3?dc$U$c>8zaFg*FPXN*hs4-_!Bs9RYH2c9hl6&m9oG?E4d&3EXD#j z0!Khib@ejCr?PJrui7;mfvBEEr1!jMs-rL36XyFh{AgUtkZdJIcWH4c*<3u~7dssI z{00X<%h6R&)>f!Fh~p7PlUOZ#-zQ|VEapcp4x}!cy0gWnyBYq3?EBsKiDi;Hq1jaC z$ZJsnfQ5uNNx?W+@#SD9Yy}lBfT>*W<;?wB(bCca$NP8zv}mX*p$@fqiR(|>Hm2L3 z0!RY~8!Bq*acb|%0?b>Mz-zasu+3hB4ArV-HsCV={mOl94q&V1fJ86AM4SR;N_nmP zSKN2n!4Wkeh|?L*hr~=TO_6yz|I%!myl?Oro8EJ9k9qA$U=ZD+Ta7o8V!uT0dgDbp z*~DCa>Q{NhPnutEH>HOA?LGrz}>p!}GFPoVCblq%Vbs z1GNoMP;+?!2k*E0YS1H;8b4VcXNYW&{{LxJX$-Tkkc^giC~2S&n1P(R*TT45v4fya zein@6C|<@UbKuh~_e7=7J5;r<(o?Iy0Z3a-g*kSjr>~EGuR;DjMHD11b!h=;(4f8# z7JuzxQP1z(CSekQ&~DtqT1eE8wqz~kyKpe{2>FZVDL+<6Bf)=5Ji&Em%{pdgU3^Xd zusTdnazg#JV_5z3pzGtl!`d(U@XdaU$*rQUw-Y^gYFUPrBwF0m$dlS*>wo?xwJ`RX z8UQFaI;LtC1z2Th+fV!MSMz4b23*KYPy<&J22izLn9ejDK#zVWl>&w~9+XZvsY5=> z0%8CbbZJPw2745CODG4&v_VQB1cL)S2106{_HD$bjJw`8y#L4JRBAo({4c-F%QutY z76l$3HUOxQ^_@osC_{oD?|qpF&I@CHxb9k&RYQQj<7xOZg3b?)anr+myB#jG8keNk zH;|tmsL+|Z>E0MDVDLel0{|wcv1C>+QnW_(cFyIVq+c602jyXb? zd>klv)-i4a6Z<3)MbV#tLni6yiTiY#xKP8@TxImD5D$aYyMH|Yk{p4kgA zL5Vt!byDsWr^#pPA}Geq|Im{VsOS6^2no^A;}i{Q3=7dB@%R-y>Cv~eqJEna+>Dt4 zp$s$G?n4jz@)|h#&u-pHdUzva^Y;I(Z*&y&ZG*r26K!Vy{f-jbp{#Q%y6#*8phX0i z{XP)I1Xy(Z91CT6{r6oiF_WYREyz}58w*V`wA~(Ld_7Uv4r;(tI02=1j(A)Jc)6}6 zMVVImLLR)Yx15X!_XY7kr^pLJ0Dj?7B|J=bP9nU+!wVRPCD=bLbN7c}(#i<)JR~Rs zXW0CYTSV1v__QWIT~WTlg?a1@81oDWFEd)BuEHT?%W`tWlS0nkq_ACq@w>a8aGcdVIvY5z6~!wJ z7&P8Ay?yw$qPnnrn6Z@i+IL*1j5roI#4lXK$$JoIn?;7D0FJI4&XF-8Z_i=omDl|u zBxA&uR9@Y7@#48V5jQo^;zFn{3Di)bOn#w2Fbu_Z{Jh;QDqWNh>gw=3!aWoslmgd^}hJlRQS8{il% zfnr{e)xY!1(F+Mk2B@^j&nbVI(*bmVaR?!fLZv(|v?GGl@p)rdK^tkyW-LS`Z(_1Y zs`7IW7MH*ENY`e6d+o;WhRiP_hf37-w{*j*CpT)C^p}A}$EzD~F1k1MpT3Dj#}KF~ zhI;qn=ufL~fGY{3yb+nk8llJ_xp^GW2&Jy=OTo z?^zNY=RCMhFX6*$_HtV2l|g)A!QIeay(Q5J;0Ycrnez)jo`VuKX!7zs&3Axh;rT_< zy>?RQCxkeIsre&-ujb|h?GF`4^VTd5;`>GYZ6?*~PFt-T%2%p2#Dx0HAgRyK8PK^E zjF_z9J|Lluxg+rOV@`v5LN{#t;}IHyNqQ`~Vd%a#WTvtK!SX-~ae;;%aG{gVlP2)V zob7(uyJ>&vwwpvw!zj$jo6U#pw4@aJJ5opy_O8(|z=pl`jC`~_7xx)q8Y-vn-mBC# zIuHp!Vx0s{OvLbgW5~Na4L{t%sn&peywI-8Ka4ac2Jpo$O3&-!xrd@|x9UFj4jo3D zdeP|?&@anLGuPa}{`$^lHwJApvzmSU^jV|`4#_zLLE#CF#`{&8i1&wkU-sJM?27B( z`+_>MTM7V>!bXrVF#21OI{6AZWQV@K-@aZDbF&6c_v8pTbpp|?0iTHv;DYavMmR}g zr|EX~`+4#XMHn#ZVN~?AA}_)xMV-XUaP>X`@XP!WWF<%GG3FnkH8e!YAVdG%g8IHB zejYPs`R2{O)@b|a@UI{l&ljdzcFZ!ExQB(0J;kQ(z(31D&C%DY6huNZ2?S?0JvFEC zId3(?su7RWJ1^A`@Sf@U;Hg(jQgft809)^yKfE^g_z{pGxR?XMfZwrVMsTsy!bZh3 zCol(pdg85H|CC5TVl4t4SopgNNC7r9Qc=`clfwTm#YLI+{_{)C;@tU;3$JhXebv?M z+RZ&Ruw*(%fH>siJspl^Z#MIoNjb`;lBgSkcMHG7@qV;72hYmh#ae;Wi?b$yS{xE?(0>d6a7^b&XWxPLB4_o{?fC>5fo4?qa7$ph( zNCS`e{$EiLEdMUnd80#@6%7(W+R^WrF$ompsZ%s)8vNG*O9~fY*DYT?tVH9uTj?qB zugIz(G<7pPj@{MsVRIh|H7XTHh#p6D81?2@j_9Cknnef#{9^G9LVt;$eac^ScZKv~ zNBjFnX9-&2<4Z_s@Fi?!BtzZfHgLE7GCqf_jab_735;y_?U<`1U^E`e&WW}w!h@l~ ztQ(WQzP_rwBVfdZ0~zsN#i|tIYXpjH4;T7K^CEHoOQc$mTExIOme5AZl;0kV0YEx` zGRfBTILN$UP?oMqwpy-ODK&U7SYUA!*(0Xoj8z-IpHNbW3A4~i#c?hR6EJX~L_G1o zt@ZgRgOX@>3vy?1wl6tpFxN4Y4|^f$%~rWGJ=Hj&TTK5X=!}7{DdF$f-o;$2sw=+v zt%-4b=w%lJx)M=ye>_xPJQL)xfGN`1Gl97z5KM5iYXBR#$)qA+qV70y)`ChNi8|O7G4iZv&hN1Aj zrvempj=^zzLxpp4Z~-*~s92Oa(HR6^r18mRhJbya8-rQk>hF5XZ~u}xsC~f{ARGWg z#QzzUQuf_)z3FN%$4ZsJJc3H&n-2)opeEn^8SKmYr1fd%5I)g&*C6m5D_40-e{8au zLFlZ7(?ul?p|20e=^$ItZ5mJ)Q`*(yW!sr8}fecM`C8+EFRx-#! zc!~2Dy-wCbyMySU#DvCztQYZrV_t5WAmZML&~?PJs%s0FoRzOpY;`*mpyI7GpR|W> z*Mq8Munt;HWd17r6(S*5UaWC4=h{BUB}!fzn#hBx$JNdxh{}hd-Y{Y6YtS+>Gn&M@ zHl4HpKNuOw6lFCkqSUN;RIbTNj?zVC?8~vd4*Ly`E^wh-G?p&<(ZK}Sfq$PI?dq}& zzLry)b58loJ*d|JGgI#UD9^>faShwHovGoutuDA+=03h*H7=`;TE3|`6c-sq*-@I^ z%-r5(zlpweb-cq5-8{>$s4zg}eu_)JLZTW9(Wz=f< zbl%&XUd)m>pyq4#?`vr_SC@U=UslP)ObDd&2yzVuX?W`Of=dPTG4XA7*gx-MHm~1Ay#J&yn2yI={C$6ke`a1@l~cmbiv; z4M-wW=_$#!lN5#xD)a9LtGnGm+JUP&0+4s_2am#`qTEzXDGW1BETMcNGp`r0!8{;t zEE;rk<#aB5hlA${yLik4o!?#E!HbK!&DSS$lTH{+DJ;ED5yFP@kNWK1LS(Pu9!n!k zTQN&t^wz~|_FniK&8`nJ%pX362$ERN(90qpQ>ywn%wVDLa?CydFwc;haXuVV*6u*J zKz}@nK5%xKtTeXwZ!SQ^-0eDN2EsN4GfN>``DjQ$A%z{%M9};{@8PQYa9A!#)>8|2 zh?`O;TSHFZrr#P*EtPp7H&yOzF?3D%K^(RgNkh$<8-BUPYDFzo?%^S8Td4b ze+4;UJ-*W(EFGkO^$m8*^Yrp3rK&l(S5>Z+jC#Mg5~h@F(oQEbnL-K&?UYKs6lr^I_i$sl?iQ&*ptjVG z2sQS|Mx<<=#Y|PZInaC-UO00<4QDL0C@C=qJS)0F*gN+@L-yyLIH_HqRNr*)tJn2v zH`b!V7L~w5$zjm}U=&FoTJQZQ*@H4x%t~#zbWzw1@{^-=EtJB%(W4BmBcOFJk3&tM zqr$s*W9#ls0GTWnUkSm0d!zDUcw@YzTz@q%bYb_wh4I%tZi%RO2-r2W#@5TmwQsd&vx>!lI)FmS@BTUeIAh>SSY~=fy;Y5-OW??s|BI&)BpZ(_~Jwy6aW%V(+a}-il3(Be60VL=X)Gexg~{{vR>cK==D0m zgHzF^eylN>HN3WiT)BV~K7*0X5Ed@$nX|{s{ASYr^ zFFLG~ zY~G<_aMa8bCn>Jw2Bh}+nmBt$#akNMQ0j8AWr2oVUKmOjaxa`}! zpYstWbnuaY+uizXo;(g*y`##BLU~rb#gV3)E$_*`IFFSo_>Mht;_;8bFq6shUPWzp zb=9wOAH@sUZEIF7PMD+lCkm7o>)5J?Y7HZ#ixn)I3~q-4VQ-n$rZ2w)o1*C`n-Mkr<#O}7}P!Du79~~ z{${_%qMwT$;G^@r7ADH{s7DohB2OWRTJv8E;1(wLbNzO$*%L+e<^TlOm*GFDg@9C8 zbXJVPq}cOvxewdXsAs~ENzcNEL149bgU|g$**XF@X;ix25ooi)TS#!p#VH|yGAuc>1;q*%(-Um(EnAP({@VL_p z`3eBo|9}s|#hU4_kfb#CG-dw8J;J$AB;$s)eDO%7Ze<1__hqpP-9qy*0M+-)kP+Xo zr?)dQ;PVEVM_vBh29O@a6-{7HP+>AbKf8?4(D*oL+pDu3++6C4Tu>^DMvc)(muRAn zFX;k5%>8#Kq>;jcZ=LiZ$b_+5M0Z1#G#|`;?Prd@X(ldziRdA~M|DSbV+f|d37p^L zOk`}fyKM8mYcDaCGnB$a9o4`V8;%qv{y;7yWoXwd4bH|vF5uNW-=9?aDo=qCaQn-o zEg0H#vhf+WUg=ko_lrpm3XL;t5Eez^`D5X;4x5{Nc%r)vbIw zByUa=6?1|K1_qpSWx!z;^M@(`DUX`Bg?>D=x#S!B6`bAz z+9%nh_CNoLo4frlU=j~6^sK8QeouY0J!t#cho8gg9_bc_9wGBa-Dte!_JJO0+OMc&7bvW=Lmg^EZy{Jv5(VF%H)Dw^(h zXe(NE2Y>?qQ8KRo^Mjig9{~w^9C?ypM0)BFDbg(T2z*6MKH#`ESKM);bX0eZmoz$)XWsH0jZZRrZzw2u=?Xu~PPBLX>6(2O_W~doI$_(ZlmD>N3T1p9-S9kBh^;2> z)AqT+y<;w!w04ug{SLf%8*7E8#N?W}z}*G8G4gh$*XrmyA&xo~xIv*-os}F}rg2T* zJpo>}HpR}Km;4VAQKV0Def{PvR13?6dxGJ-LHhaguUU0Z-P#rhA)wZ}K%iqM~hDn>_C*+a%PxUNOH?PB42j zUAl`ypol@<>9Q#H>#CKw1OI3$2lngD=p*0o0KQl!HMR|`7R$zO;i3rIy&whn0oWQw zJtD|+gE>oa~fVqKH4vWIBQjE}JBcC{0Opi#SvZz<)RW3`qFO=@j(({+T+g~}W-XJ9^oK2-LZH0g%ZDq0gq_ou{@{1`EldW>g?|0 z-V7XZ_zx62@L?9Lvs1`*1qhR3Tu`R*Gfj<+BRk$#Z{YeQ`ENcM7cH;5bOAJpEmejY zL*K}v{yYo0h^;cV%A?*{QdqTa$Ss_$zb-ZMkz(1-#m zR>%zs6>(bd$GuJ*L7)eq1BkzAtbM=H2K8EBMYD0<#Z8hw|3U2dL& z#S@3+0|zGF|3*G$j1B+P<}0z8Y>vWDGzvn|b+jILnzzp1(lyZWpYX9cjlnNWy32|z zz~SSDU%&I$eg8?*oB+#HpN*iC>scF=GK%F>wd_!M%S6Mft7EKb^EFW6Fua&}!KE&IwBY%G(W2SNBqj261i)9r*0TTU=u;3k3L=0rB(U~RZ{y6ILvw|*g3 zm`_gZ#Nj-O*e~{fXA$S8F zYPB3^5uL0&!%O)Tdv9}PvU4}&&iQ@kojg9RgVQ>sUyt?~HpvP@QQ2(;`fFm%JFn*NB1O<|>1VtBI5i|bA+=W2;e8$K}HNtc~H z*HBs?uO8+YD+P1T%k3>d<*n|x9hlB;wU>{5h^S`Yz3|9F>^=^t7J= z@b#RJct?0`zMULRro{5}fx@)SLHu}?)h3ReqtaXR_p=PR#HEs{1k%6b)XlXhM{bW{ z1cyoWrvvgDtbJ711-gAmFT-ja=YRBg=D%8SXTyf7g&>DTkYdN<9|kpIOK{L~bG5_R zxK;Vz)AX?@^uJCt%NV5R;o`z!q*;jsgfEbs%agq%h7KL>MczErO9wlR@!dWulXOt%ZTpSk_W_>4?=`s_o*x2(^9{wV9(KKv&4GD>@& zTCo5Vme=^4k=<97R4%;h>c__Ivs#Jqv#n$A${CZL8lmB?6yE*Xk7ZfG?Q-INdGDuZ z9u;(i$XD=Hxy}jOIpFWl>jk)&Z%uU+v5rIE00_tP^u_Yq}{A3|KUt zPkVcA{xu$xo+PDH&QS^^w}V#bihpCc_C2EZ&AWo|Fx}*b1##jzGvY{p|iEgGVfJl@=N~m=*#K(gdkp! z2_4>nTNt~RL|or~-bgH$fj+BY<2zpxNgVY7og^_n*o&Vy+hlg2`~1jkNFn+%F>FrD zXq?JWPeQEgHp&k3TvGnY$C$;p2BKHxs!YOQIL@)C-YLG!=(4zNA4Fb4w$%cD0bPOxOEvC<{c=JH~Ce- z&l+h)^hTSxF*v{aw*!@bpE`E63zSDZGpYC3IUy%N%Fdp2xP~w3;TFkVWKf)h;Ikd) z{8BseNCs&ME1!U8uyUERWeN#sHx)O?aHqM!={Y`o`?ptDqehg`U0f|Od5Wa6`X3ePmLM$tE)QH-|;=Mz*M1+FOst~s66#N%S?^D`@h z`gpht>R8pgti<{Tc{m)OiaL9;9$-2qKg}2wz4sFf;9f-ett&i0)D$?c-tXDpAzIz^ zHzCEL$QWqNyY%2dC^NN~DPAwUusd+b;^R#b5qITA+Su;jH5bCGQqIrjjqWCHz;yq0 z(Pb`B+1AJgw4%|{$0lsKkDs_aD0q;0Ey&=r#qg-<#kVEMA1m?&m$i9jWRtr*ngZy+ zFq)+*VNB*5f2^U%Z~)ImV3O~`ggg=8)@a}&iO4uOmG5J{Zv#2L8w;7_C;qKPY@Kx! z3Iyt!izqk~tyhMY)V{gtPZA$)S$p#1qdZ|;oTSHr;G)OrMUJ6Ns~Qy%?b$DN>I1^w zXVD+#GSpRLCZ#{l^DM~{E#W@Hl4q_>6uIs*LGdHM;-;t@=pjb&jWiEr})@4 z6VfHr!xB;pmha&i6=F4A{IWkB`EQW3o&KH^GLD*5%N?#rj2uL~kDN%2l|UxI3}64c z3EEKI!MD-EnzjX~p66PmVofl0VgP16)>5o1S<4wowxMJHWO?#bwvIKxhnn9s|E-^s z-<(_2PUG=v;NfCVv|xyfu2OJ_`hRnV4;FL$kCZO2)c5a2By6&%c58@v#hSW?UhqnI zbqjOokjs*7{#a!59&mMe6_OP-8BMO<_{SJwJB_wfNM_5pQI20tnsE`fDE>)J0%miKIpKV|5-dY8SeyqISrcH~a)8Ic>?AeGMsp*_FZ>CDB=|Ix-BK3W*hvQe|sNp#lFpHik5pmPPk#J)@J#4DTjw zh{72s)&6Gv+97kO2U7C8xJ1{hpy(Aw5uvYI4m=64oOZ1GkTwpq0l&#a@rQ;Gxy6Mz ztJTo2ATtx5SmVi_E=tA;ZGuGonoJP3e*qq$vPDcbZU+ha9aDMo2Z#UTcO_LG&A^Ul zKa{=^0Q5>?x`d8n`Fde>uD4Dvk7{>vt^QjR>T%Ow0qobXh09>WhjD|W_31-;b*IF7(S?A=P|7{6bsrTq3?n&tf`zIxT+~S-va8e@YQ1 zxTvxyL$ptwjtoS5g+z;$oeUqB(2tc%qi1Tdzpq7%+}v@dHlkuo>D~I|1^k)ikvoiU zdYSCfw1~ln2uY`bru*)JhDIwH1UQ)KbNH`tlg5+$1%}#BN`9-?c>5K5=l#yc`xmHGPJzn= z0?NvtL(jX-Ux+0Q4806L_$r)!&o@~PMg0{@DZvRKeMfgH?MokBfrZpP#^~NtY#sn5 zSB-ID^MmLG<3jXRx%#x@m{wnW#=O7;uW2&y|7S{R8|i;Oiz)9)Ki%uu8J_jH$y_3D z-YLWnONEWxH-R>dbdWsJ)L)Pv(r3TvnyRpL%#i8D5_wVPmZf1Lr~&8NXuI?Xr~l`) zAp=EN+8Vre`uAqj{j@voP$Ai4Eal~LFQDkICr%q8EPL9K+5D-nu<6H9lgP`r_*67< zTWC~7hDsazzuq?|v z)^o7CGb(;hirpcax?T)$n=_|~w`2LN*- zMd`p%ZOIZ1b6p8FkQj}!FjJA7JMXd2f6s=2t$4A0eAkKOT*F;tp$2I=2%t(2aWSr) zMzYo1bQzz+3pX}bzVzDR3g%##zd9tqF$Ox)F8dJ5Gw?BZ)Dd^e8RC_+-c_uch4!~) z`VP?YTk1S`XIA^RSaU|5pG(Q-Ke#k{T0TO~9&T_@?q+ve3Z`wH^Zkqo)=7j9f*gfw z+-G%J972gPUPyo@={mC1bJjr>zo0&Qd<$fTuUipXMg$dfFlLVPB-tLYGdj%t&_`P) zMBO{5q|@E)S9~e+PVs)`J^aqY@1Ho_ig3_yB>d*U>{YIflxmz+)+ijz<_pTGTQUFs zr7s6kgN>hhSPer!u$SEGH8aIwtx+P+u75o2N@iSB55L+y=%LtrM2m+vU-}*+h7B@FrsC*P zcYgJm%DyD`9b>(Af`fE|mouwbqQwL+r-$TbYH(~%*X62ChV04Xd{=zg*a5ZI9?tWm zyU1@-;9B@+2J?L=Lm^~wYHp66bCNx$X!J>$VUSiL?Lo#yM(Rd$ zEyw$podR4f#h*qRJq}|1?s!(bx;i=gVlxmx7<)?ElPTd_<#DuIe^w^_pv?*#0B4kN~8vn1Ax4zF!~sNYg9G&xbiz~_vFuaO#Vr5lp*8j+@opM(;OSi$x>ifhYOA;K$F!*G zJ{(E?gCb)ju&n-1;io7F;L38^-L}Ck7b}dRHVhqUN3_FZT4^?`H`{I_7hCC3m5V<3 zi?gGx>5>3*9bGK5-qrgh5U*&nf9AIF@P84dmUHt8h)n_;O6QyKaPR#(DfYb%U{eUo zWd-=H1?Zr*(QYi&r$e=5%bzTO2)=TD^lDLw~HS$LA=Tg zRLSE}`6AUQ~E0grkw}pm-_0rn{ z$3U6&8m0Mwd*RPXwd?5S;c$;%-XA)Fabo^#p$kJ@JsAN{8oc=uG#ciw@EJ=n&-lME z+xld`A6Z*40{8m5>TzL1a!f|%&o~a)EaO`c2V3|sbRjz6xTR1S_ICn@vBY#*^R@|r z%`A1MWpq=h`T+;j0HpG$zssmujP|?yjrH2v9$I@)K5oy7mMNTwd&)*@FCP%euAQ6! z^z0{5#kcM_L+-}&K9GMHZ?+*=e{G;U@}jJkrHdq{)E*fz5E>HOh!JiM>w`iOwjY%x z?(JGNAcD<>&QK3R-4^WJ1@(;{9Eu3NvSKmy*fkLW6t=JM1=0Du1K0|Qu^}VGFB)cg z6%U13FKPHHUYFrTeITC{jxY5Lm?%xsL2Mq_uJ>BdiB-PeU%YYs0`bDYF##Y_1Ip5Y zr%K5^Ucrr?gt0cHE~u;6TYZ2T%Tj1aQTR4B3K_xXaucI=3NY#T;4i)J`C-T3Dqs37 z0F&77O*CAUu8hFKB;c~*{^OK#&kTRrF~>#}vxV_`U;M2#Dim2&3lCayFGz&?G?~DO z)r?byi62ns>9%Qhb3RF$&jU;h{2ThA`^=$7nOQo4qhlawc}eY!S_`mc;MVhRG@6Cv zcWdtzT19<4Tmh5!hBl&uB<5Cx$rtfQ_Twk9x=Kn&L5w;q9ah`9(=ms5t%EUlqx&V{W%5%Fd=}Daa%&nwj;8I3ww$P zDR}M5gF>h4RQ{J;i2MHMs^1SyB*PtC3Q8S0!zQsU95N#z&e(-(%~N({egg1|k$GGh zIpIC5{kRs(=1RNc9b3?3{A4pR_H)m^G`BtliKLm8?Pg~fw)=8sS@k1# zDVq3%SXXYn`yYhg$s}zdI4Wh~qek2c@rs_1HRXeMKB(e1oay@OU2;c9s1Jb`5H>#e znfGC3{5gt3S2~a0RA4R_w}Up{jg&#w_Y&`^i0mh|>9}q5;yLn(|5C!s(ASxHR-1J8 z4ojR5p3RL2w0uzGh_%eVOH55s2M@(1_ZOwu`i~ZR@;?z^yXoX2htv%o1%IcDF@-l$N5j%Kr9$$`)~G`ULK^~7^V||a%I@7U(GQxW*7Gm zXeO-$Lp7=u>}40P?}*nEaj{q^?DT~Yh?V{6LegN|pOOnLP5({e<1EHK;D7_x3Se0> zH~`8Rl0R&78j69t?^xrTAJOz5ZhT~)ZXDJL@%wvW_|Yu^Zg&e}m4j9(b407O#xK8m zk5906DmPRiYnDl&)7!nBj!1&^n920E`4hxdqOmNcI@d?;LQNmnDuKF3oI5xrXmBV&}L4LktHU}E)hI9>}KcPO=qjj zkE&g{s;4J($9^imw#G2OmL23atL5wSlrc#k+BpY}Pj37})(?4bDJ zoF-$_yy|1Ii;dn58odwJ0s2%{p){Br$ku8^gMw{vq(AYRVS*|FjJ(JyG4O*gP>=9#C=aJJIJ`#@lOGL3^_0p{{WE3H_;*XVSuy?9bW5*WHvbm) z?)}ooj*d}A6b8&*2h*6P*HX7A%B7hiSg&r+1FNAr#LSW58n){bP0wnxF!|{l&%`?rW|N<)i~_Ip)#Oxs{?fhqX-~^Gm`gU+&;}rAj@j`BfNS;Pc z0b1oKtt^|7(?CP)N1=gB=WV$SEN#z_jxsKP46)%q?S_t4dV|-GCZ|x}AUy?cA$g2q z#RkxQs;8_YeZfL!vXg$5XQ;2`fKRpTq18U9v1I^56%ed{M31n)?R&CB;t4ifBM@&&?qT*;v!%z0Hi=G8c-{9` z-`?+Cd_8pfEM#cCjiNxX%rLm5`K;?x_~uyr`^*fg-$pfwgY)Wyvc6wx#ESE55X6O)tH2E!;}4hmRNU zuwMSAM7Xv3*P}~ZoPndNon>`8;o|oMQcu}n?i8qWC^ZfXG`c=~30D(s&4r;0VS?&v zMJ=IM?~H%!Qe#m-gMc5ZLgh~Dl3UG4-wL0I_u;*1`=P-x=b@e3gE)3f$Fh)NRDBTq z@tTSs=?`T?xneb1gxqd|crxw+N{C}bQg4F87h!*5$pG=iE-7-@;V!!R*fBK=_pRU- zhvS9nNen+w3l*-!%pFJ*-`|nqzFy~t8zGAe&91oWFkGbAq3F6vg7;jXfW$Gx(N`Gc ztq&1X{-wW3sJPSXtgFZ5d|pXsM^K#Cw@)YWc8jagJ;E?EJWwfca#E^qEzH1W5s&Vn z(8HWW=GDsx2gc!jV#XNB2l>GK_k;lF(5-Av42qgVr#b0QyUJ)#M#vBp4D-1h(Ho0StOV@0(S#+q_`xcnu~Bn=29gZsgk#qZ0C$wFwA z_M^i-_D3zbfh1B+?BlUoIuQLs3}q;R%qh)u$s<5#b|f#HU7tfbJ5BtZMJaOHYzPv<`Wkd6$_vbdmR#gZz!B7LvhXS1Bw;_wGU`y^FpvGnHi#JD^0ICcbage8QV3{I!osjTtLY;K$Smxuqe zi?GwnBg8E&Cx9g74#1r}N$T9O-t!w>ec@k zz<0+42Ln+HcIr3zmA1?^V`9q}?=}V3VK9exl4bp_H8_%;+7-rB5;3$s1OQTRLE6+B zVxF}mNb6Z2CF^{YLl4&clcEMoho{+klnZ9-63KtT zN<~en#~?e~y-TO$Zp3yZlKs{!d?o?o03+z0u}zs@*IKKf83B`mIYraINvN+Zs|xpg za4?*4(;>RnA<>tbcS@RHOqY95olQ${E`_uSDtBzM>p856NUS8~J%6c#O?JDW#)&NV z;Qr3~I^W4>>G!!m5)GBp--3x8s>`eEp4z+XB^iNv-B<|?Jid&Bj7y@{Pp%A57C_#X+S z^>ZHuy1*(#fjNZvs4tPBx1&y195Gpj_yIgfa_)N#kkAj6Jps3r9;` zj6()#m*I~Pj}tfV?$KmQY6*=K(aK{7+d#&Wsc`leWzk) zIy$L$QTLWY`A-6ra7hT&fbIAx8!a1;{Qu1{M*vtA0oyjxe4`yV|nPF%+kS9Yn z$$q~IgwTqeY;m(0#hIFO+rCb-{UhQBC)u)up*On!0h^nPmYX6X%!U;BdENcBaB)0W0 zU3ak)%Im?wJqe}<4<6ADnp-LPNO0+9T%1f6KT&X|xk;U<_L0{n75K``u|ngKc>2(r zkDJv^MxZY5I;iH=ylUHVIUI7hj}@BFY>Ntvz0gTRJapH! zMqi%ZAyMNx6s){9gEI<#sUZs~zH95+9Z?$c1fQHXHDi z0uZ547}|tLkutcLx6j%(Aa-=aN(jB{R2@X6uBBO8psz-#df1)j$f2A{RA0mqg%%zB zI=k&t`S<=C!L^g}^pD1&+alw!W?O73YN0QGA>%xY@;S>)&(4QGyvRQRZ>Ew9r9_YE zE7;GO%T-fG9eC0!ezC;S(&?2HFprV{QYFq9=N5IrIAb|%n<#0~KeoA8b)dA1)=yyk zT=p#m-i#MK&Ae=ml6cwsO~kVm)+hL!m|oetHJpkkcJcco7dMqvsIaCs;u>M|wWa!9 zd*ryP8Kda)-;tDM(wMaburruESB2$&v43;p?W`ae*U#RWW(~u7!iVGnljiG}IBTR| zmV7Z#EztF|jD}~VEL_WdI@Q{~nP}IN>S6j)KVN1j&g5i$i(HOilF<;uQ`V^eQ=<4d z!8DX{M=l_y+&o{)15q?CP&GUqfV?W+;zqmmIw7{5*ip)62y}P3`4mi-my#S4 zgsniZpyW)f6I$3*#j=eSx|BQl+)9w5TTW}{cO!c^8!l@>toKxig$J@t;L`Et((&|2 z4iDET7&GWkp4WJ-qIm19^{hxLiwK}%3u`_JOrUSeM~r*Wi_t_5NXmc1vTRhk1p+Eu zrzuSf3rAw7?7!34vH4X6x(txmO--kZ$riz4;BW_C{HmV(^jQs+@$!Vm$!vbB>Wl$2 zF$h8DJ>n@pdT~ev%+W?|M{>li@XurBS9CCfl9U}?g-t1m=bprIUnzC-@nmITaHt6E zvWGI1QPEB{(xGTkkP6Reyamfm2ic^y+wJweMQ#^o@irLh4r+3BVQD&0Mnv9nX4Rr~ zVfg2i%!TvN$qw8>!@5o4O(>rI+E`_tn?LtL`9i`~KxG<$+)*Nrp?u4Aiv+ZKonAsz z;+C_$9>$wkhyfLS7e@7DYlvIjuQ^CkSBEqz=}F*eZaAAH(iH3@&De15UY@0y5B`Ed@Q67xczG2 z(8q1Z>f#O-OQc-exVhKxT&Xa-Caq4yuZB4FLgUfq3uAqJ30MO$63NxNF(ii`gO_n( zucvuvR+w?Pw|u`sP;c+#ncw?0L8n6H?64SG_gqM?Y6MXFV4?(rQH70IF2pF1oUtLsXks4`rhlq;g~Tpo zy(TR_lJ#|nyE(WTCcD@)rcTSZtdn00|L;$I>t|>RJNbOi?+V6yzIDsf_UC->Lv!)a zmLL~D_Urv~I5#h+p6Ps~wn*_w&tI}@Nl0qDQrfosnzC>}M0xy{WOYxCzQS9prAvh; z2K|+W&{S-CwrdxUb@@C(viH_WCFPJJlSmHQt5BDt`qhcv=&IO$Ee-^<5MeRBsoy#* z*%Kd%DXieain{-S(t`J62dS*-t!Phs!gYwiU$#et!k&ecPwGK&Q?5@8yO(6m_pcAP zB4nWs7Td@_#==o9;PgOC5F@8c<`rvZUy>O@6>+COkU= z7dejczZr3usd30KWHB%Eg!fHxff%lTbLWODM#!?8=-)e98uNXlKoZ%5R#bM7s58P0 zO=QQeuCmwP^lNZFx91?GXs#X8dU}{!WJzDeIk<311o&});@Jmj_25$BoEP!;H{&h@ z`kZgKS%kao7ik~WvgT9@w^hO%xw(3kj_Z=-*9{m6g~1f04Z33n!(v#WF%+@Hkm z=ZIRf1MH6tiC+4BO(wIT<7sPVExyZ&H)ek_^D%c6hoMG>wNDSK1M#EQ7p9(A>9PyZ zA6)zrR;LUqd{a)nMwXj4f+tJU@eP&?CCd|x{4XiFKq?G@^>%OGavGQQb*~7#@Q6X( zuO7QM6`M>eZcYTPBUdlAy*~7fjPB^5Y3gOOLFctX<@Yf{l<`sz za*`283buO~%A}hOK7dZpTZj&lZ3r#XXvd7g(Dl$D|AYuY5Oyd&+CT2xp6E zyUdJbOVIDMgE4lID3ibQaS_zz+*`o-tmi0A4)@~s8$RwpEl_2dKW)Dk`bzGJG)$id z{T?&SiWkZ>goPlx=vu)YnD6eqJmVI@W&6_CnYPBSP;$lpi{^D(`)#x_u@-&jb4*DOl75PypfZ7A6Pf}-UU$NXKVSqEp zLr5%GET8>WcT*QFUZ$NRD?%yXN&&a}m4#7y7|l!PAiEI5(TcdqT#?k6^xL7?F<2wx zIfE_(#Sw2*jNn(&1z!;D=g{K0)bJLst9_3-5pS67C`p#fZL^$EYZ9Z$ukLv8-DS2v zII8^sROt_nnQA-%M@3;vj9fX*Ki-^-!%|C?^aGs8LPN~@LfwxF^ZkzYJ1u<_fR z+niMva(Rt?#HGIyMc%@Sqvity^Klv6(Q4`sBr$b}z2t}F52att)|nkJK|K`!1=vHI zQXn%04?yiE8TI>46r@WM#i-0do&!Fj6lqNykcZ8c!sX|Qu?eT2gJR9vg8~TcwYcnZW-p6+9e~-EdLSObkK( z9fgWso7I+cOuGb=Bx*Ph{N74UJo)l-HC<8?(}=ShhJ}G8;iqP~!*{(rr2xDjp6~JT z_vP=OZhFle#{lXHrTM_KR$56y@J5@I{j2&Age-chNi*|ux`~2$e=p^Gk%tbG6e6kT zyX@1;kCmTJToD%S+qCw|2VMlOrzrR3bR=Q{k$fz?x-BAV?JRMhS=Lk9F8!ww*yzw- zxeKT>y6O zK?LY~cl$};dlRCoTScdYU-zs-<%DU83%b)vN?>+VhDuquSD|*eTS|;RhacYLs7JN< zHUVSWPeLtIG20QLvZ3?Rk!OQqZnQvNjkp`_sDy!r$T0we7R`gfgfz{^N6>&SqaFu} z<r$se?;}!DD&x-aTWt^ygV~t}_mWC~SaI znp`fbyNT4`T(`*xup`~Peo7~p8S3Jf$L#r?;O)J?VVCwfhzj| z0JC3yPrdiLdJ#5aF!T=Ia~IeuCCRF$53?bO!-kbMq0^G;0*Hu^XG2G}pWao>rDF68r4 zhKW^Tjeq}Rg1dP#!$mpMo!J~cTVwZt6engzuWv!Om0KQLR`T=yTn8U-HT5X*#s!`( z_TFzV+0!;soAx05zJ%nW|H?T0G_1J4IZl;d??wBu{(JH8ambDTe1ia(gMaQeY~H4K zN2Awori!b9a}?Zga`bQC#+)j&4~}T-2+C^@IAbSR#E*ewsRJ0u#?*98%vzep4P}Te zWnPyuWL%>0K{++Ti{^=H=?E>=__E&76!U&Oy6jED5rE8e5a6+Xa*w@$J@;9Sa29s3 zR@!7XV=J}zG3q$Vo#M>WLCvM@J;{5!r55ssEMyAg7`j$w`;N80#RTPaL&)sDFWeHg z3uXG-#?hJxe^nDZN0_;F=*@4f{eIC&S>s{G4(@A(FCC5Rvt|Hzd4Dde z@MoJf+jkM)7p=AdF(u@>wL*Ya^6QjoroC>>|2YwyB#z{Y|CSJk)|J%-iKh7aI{VtM6 zhGVgeAN2b==u3L&{FXjNB>rO3tSmDMsi%+L%+;Q*`8W>Z;roR9>yla5Q&vL$8ZhAW z7aDNhJt~%lXq;VFM@$xCyqkD+{=E^{{G|f+G`T6Fof=G$&j1|&Sf}1M7xTswzV?Bm z+uJQ1ozX2B57y^`V$i|73etB>O#M&!(NS^-592jcF@!N|d2y2(_4?+}u1nEO3_RbY zgLt;hkhCC55(QAKfq-53lXJsXs|V9=0nY z@6}7Q`OyBDCmh$Tc=>>FbSgwuHjghl4nb-T8{kOY!-Vff6nG!9E)t%IzkjF4h7A=S zp)fN9#ynyA_GOjJE$1pen5fTPj?4Qw!=X+F0GI}>>n26;?EZ(mDM^dSAij2S7ec~$ zGWsibbEu!1!Tm35FvwNXAW}7$AEV2do}I$W;5u$+w$oH~oClk-sZ6c9`&TXi>{$+) zuShP1vb=g;?Z`5(@RbXe{U2<-0*18AL!fI)A)ANysjywFI}p@m`7pV~gk2y>Qa1ns z1AA|b>oaq>9cCFaLw>cqZ=}-ZW{lTCC{ure5Sy%pfLt6-;Kbeuop_4kK7eCQ|7Baf zzbC+;fq+Bla2-}`L5xwXW+BYW_Rp5C6KK|`Wjz`hz6heouIyz|}Oe_!>C3-@(N%Qn(=JQ`~Zr<*Mt0)bVdl28+{mhV4jWFMCTuQ{3DI7?>}ddYJtng zV@>pc5XXX5ct|&U_z!lk!0rgb5ej-!DtKc^W<(O0upt~aABP2lUPR5pb(KO&6i!<< zC5k_PY~XFKLSHYQi2Ya(IWQ-ey@jnnv=q%ADw{bSs*?XT1-8)f0tW+}bD7@0?HV+Y z7!9QXz0=L;&pX2X;gQ2TMeLbVfLFZ7Y{SJ@%J=T zUL-SiVzCH$@};X5{|dT0Q+eI!)WSyAch_5v^_9(Ik!ef8I;i)f)E1Y4Pz>fX+IO+c=olkVl2V?N;&w zR_=Eur*32ElXKQF7R9by-tSUDs1|YwcQNoFi5qXz4dkAaDD_-i9xja1SEhedy*S~AP+ddGKlS=s-S?5OrDFRr z&Ol@3u|Sn{%}4Hpi^(V+w%SSC+=sd9@O7=7bP-f~0o*l7y6x%+O*wMt7e=iaex1AR z_{J{>o*v|WkL$cBs*M*`CHl4!Ye=5{ae#x3WB%=DG-V zT#tfREy8^V^;9P`T&;0%z^`qIi3d-;g=(!eAFk`aT@0eDOP#&8{)L0h-(B z@7Ne2>{%Ev=flxAdZK;O;w-c>Ic?J$OVG!2m@q5Lg$?j1!4xklCI(Lt?qrh+o?s6h z`4R3ZY~``;S&{y5{RadcF7p$= z7`qK6Ji{GXG?LY?Zzd|xCd}e^%3`4PNXv?@A@HU}Jk8}K#k8!VOy<*rH=Y#(nt@%X zv=lzOPz>x|%jN!m&00xf*k>NW6?+mSduO|D7)YLSlCrRcaze^pUa;3X{%(Q6Wv&|n z=XVzD@dOb=S=Ys{Z=_eRX&Z=ua1JgDZ-T6Vye=|v8%@1tZ$erh|6zO>x~n`<7QY?q z7ICP)TRCXLsz_GL@{A|uRmQ!j^y9*@oj5|x9wzKf7f`e$C8K;PuY69SWcgV)UwVMXeZTZ9{O}mY5vPY5! zrsMpJDwn5z9kQ_S5&~neetB+|sOtmcszMlxGD+UWe0|VtwH%!{`u++P9;k~mMmM?E z_jAhD;=`w36k_8eQT>jZvGJ#JTSA`}3;>E+zmri|t`%UAq9rH8@48A1AL=RO5#N-M zA299zC#}xOSGnoC7h%R(7Q&$a#r736B)eU;Gz_{?_k_@vQF!NiMx65BO-539uR)~9 zxwetKXnX@Q%h`2xf=pw!a7`3|>T!s>4~{!yYpOUK3}Ce`GGSw80(U(Qev4uP(i-Mj zeNztdMlZZHFaJGRPOZs*;&38IjNNU>^5_4PAHVxREc_S6v6wdianfQQrwJZv+?_t!m5n9 zI)Hs@y<+!w`w~Rz0x*~s81j@CEUnoSo_otIoOSHr**J5LpO^zD$VuPQZDs-MuOmlS4*B}W9-lP)=bSnIk98NUHWY~!~B4S&t_j7 zk7V$WM??bDqR&SpEHzKNmvECf1fWfRUo%l)!P)(BMT~%jFPh~VB|8I(YM-%RBPk%p z(g^lA_wh;isgUdae?12j#WE83J)b;#3;bx~Nh~t^53eM-;ODGAz^g;-)cqzgVg!3& zx8e_1iu(eULh|8ZWs>beHZym@3$E5p_Gn2eytlwGBT}h>mNDqr>{U!V* zQkTVRZmDW%)lF*gPc1^493&0z!cF72jyBlx7-ku5OKr4*V~v{3ZJ@J?fMdhr&p1i) zyNQSH>~*IQKr1m1IW)LZZ$3_#sZ|+T5&gcXv_+lyd5|1SU?<5hL<@jL9%QS11q^eF z|Gkh*kqlN2?sn>8-Rk4lNb@!JYD8-vERySiU}hJvk84^NedVDz730Vc-`V>Vn5 zsdvBfGO<$RPZ{1!kHP{tn260)4TcQ08PyM_hmL}G<5tqf>sOEdR-OG8lgRhQp_f=P z9Jia9P9UFUT&ZT)`jTBIyImr_)N<`s8T|i zG{MrEwScLt_7k3~^-ty201ZvX!hgYYPsEGb~IS81JUXlJ>pJNm7J49d62g*nwwask# zkV4Hqe@ycKO1cWasJiF91uP{CQi7x}DJ9*zARsB-sR%5gAV^3qq7uHOv=SoH($cXY z3W$`%QnLHf3J415o!CQhIZ%KzSAndGUF{2)w^7EkmH!_s%tU3@NIilQNwCR~i2o^ONgE+c7Kl)N^o;+sW& zQSf&~B7e}q+K@{Z<6MfkBvdN|o4*D|)H!i@xN}s=e*p*$8V--}QQdCiMcTt>9C|DX z*)KMp<_+Q*_X9Pb13)EKPRitZzU+vxzG9-*aChC8=L1B$1q`{Gnwdtb7R;PIrg6a7 zm4){3DCH0Lp(Fp;MI8}KuO9tx1#|o@(`GJ417NEqp$DRUyUmCplOsB=S;y*HS`32y z0crVJ%-!=j=Fo$mnx8-N11qI&%r)%XSWMqFYZgtlPg+4mcN2tZ`6(c zbG+M-yx;cw!FlH@z)|1@DP`cK6%-f3;i*#jl}Ar<%G-sKrWB)W*2yK^&XW`fML19Y zvNlYM8z8IZhK*#g-ZMptJ|Fc)zD&e9rC*C4AvV|^2gd;kNdPpk;|9Tiv6TJ#M+XKy)0 z95J9&Hme#eSX{g(Ui-KLN>Hjd@(2w-C1YGuNMu&QL-(~kqmO0KeEltI#Ab^Y8|IK~Ke-z_X|p!C32b1_%!B^!Ulz-YtX*oQL>O>>c5pou7)RUenHQ z5Zb%iPWxx}Ua#@@VCTFts&3rFzh^A@uXt-$5YTl{@G>T#ob}%wUbmpZGyzdPxb1xt zD%7r3Dq|ne_{-N=u^M`w`w32cryYOZ;rovl9wXGd8iKa%DzghjO1nqnoQI>F z^Tqn{L%G~yjxhtWuxT8Q;eP6cD5v|)e0vzXdIXhzf3Msc58S8gekA_92KonnCr*6=gmdT$Z~d`sTjhHO{ARER z6bi4uksZ0K*K5qUX{33@RJ564vta0{Kw+#QqjO&vq6VQJ#}y{Hm#w#zFklO@3`3~l zZ}gpn+_^)g71qJy@s*9L$90(mmx^ncdp18MzVNH+XM{dkx{D5w%kwr_Ky%v>S6&-C zeJu*7Pa6IdR@+#bs}d}s26T4BA6lewJ`xZtqj@c4EZF^!Q=B+lIxO(Yg2X#XYr@Mb z@R=*ffV#YRCLaQcmv-h$_*hfu8tY(qyYY|g^l6G}h-?WcBzpeydNPf?Rj|8+D=>@t zJ!@oD=I5(x?(;nWXi(Ydn1DIv<863h-wc#E!U>k^#0cB^g(y^|vb# z1V2HKwYN&Ib1pBBBNyw>sw+W~`1cXvr-$32>gVY(iy4_r7#^x!YkFyFFd(F|J_G>I zsLzAL)!hH;8BuXfcbp&x&kVre5O+Bv_QV~0HDX$r{uuIQ(-Di{w2taE3mCF>WrXbHTvEm*SuTanK< zi5Xy4>;4g&gabO~WsQ*d4N%k+uOQ4btoAGT$!zV@{WbB<@MG!io(7IYXqhlI!l(W5 z@=K+ti;g^Q7&U^us#%&IiC%zyy<;A|F7ZI)`j`Yn^k`W^USo<|=^U|P$tyi_<4f$c zM~ojkDRK6)j({6|OA3*OqdfMf_Lo;>GKtIa)64w~3=G|4_c@Gj;Ivk_0-|KUyse2R zsGL@S@=x~mBDw>HLs2ytp^$Y5_i$3CM{;*QWr(^^p*6O~vAxM~&g#6k*Q7|A z6(h#8M9n6DM;WO9;H^H|kz__5J|VfVh*$6lVpnWguTDMwe(_B<5neNqVIK&2IBjNe z(OEWVoWWxSDdv+%`nA3N3EVx_H|PF96pl@tE*0M@uVI5+H$`47c81}FK32&^c0eyu z^qkqqg5cKNJ$1tP)}uadtT*9`(as4G#2vs*pIp-KA*8(y%^#g9&NNoZr1hi#Z=6my zQhD3@nIW`-3ne(Gr*ODtZ(yx;ude7UgQ{BsN#tcH6q<=xAk94a<;A6>mQ#d{jgvYS zId)slw^{Puoc`F8^OEP($u^l&>8o0cc}T;6X;s#EUvjg1Cz%9Oc4$~k?6U?dT}#$J zuB;^9Ea|jPvwy$Jat1ln2T2Yk-v_?9CjlGgOF=2FAnN;@OSdjBS;spZY0vMCO zrcJ)9q!ifhaa2=N+f3#`URd9KBzT4J%4){c$spIy&u`4jHShcHk*5LcqY5`y{afoj zpYs?*AL;DhhHxr8YE+68J<~OpfAf1Nbf>D{v)`fC!`OKAxao=V{7-=4)orNo$NQT;C_xWms+ zNKqq3UkS3gSLov1;Zogv&j334T!So$+4U#sjVhN$GL#aE5ru6iv$14rP!iNxAv}`x z)@*yQ?-W1F>rQT`X;)m8b*)Dya=Sj6TkRxO+WywBhdB>_tUHbGb@BXxEI^OY92?N% za}I1}kOI$-Kuchgm5%Aisk)ytlZl@FG(sQwK@mIp$|;8~$NV1Nwo=ikL@3%BJKNxK z3DEQ9I{W)KJjSG;F)14=zk0oK)w91<8yi(%X1Iy~Tx9_Gy=%pk+<0N(SIU`vJDRZP z2cnhi_W~arPh35Ac-ODHadCSOXcAB$6N}e{8$LKrQlRxCFWxpZmw*>ZJoTumujIpS?}V@u@$VY8R8x0xlE` zyc3!V6ohJMO@4oCR{j`Lf$_a8A0%yp)jhK!;%$u`JLe_5y@tH;ZrKMtO!n68-jDLZ zY+on#XhPVw-k~Fe)!5F-@dlODyq|z=-;49eAW(2nVQE_YSHB@)XhiSxC6MjX2@wGn z>}Z!x+{AC{%SM6{T`^8V6NW4sEW0!(vg1Lv!~G2O^lJO_Zk=Yc6?37@KSmU>#+8){ zBJQ2jQ?Z~OuMnhKsh#?8Rl#87nO1QItZ>J9g8O^6lqoyc9Jd(8PS+U(lai9!B>}=5 zU4)gvIOybS6T)`^xbu8l%^T6N_SyKVBQn6pe=UrpS^4KIXjXYG+~{Fv*uo_B;)`!S zQ(3|1A&sgOinfue?rPwUjeg^?$ntk!UhdcO7s=N8R%C)*nX@;x> z^EtQM<*oYdJ{;KFMsiy}gKUFqBEx5M-dm3*C;w`lHjsexHhQI8Z+7i%6tnq>aU04E zq#fe%E>UYi|0v_RytQ5bj*_FFiJhKIl-sT^(L+X-WX`f`?4dw`^D%W0!}?IEM_Axu z%pmF9brvv^SD}a%Iv0}N=#MH2n$4BA?+%mr=onPw*2IUsF?sG{P_y_1q`BHN zg7T9oUNO*d^sSqIL_l~X5R7CgDJ90c)K35Qh|;r``%=VTpZtH+%*?# z<-=52#+-Tfv;)b)Z)G4A>F;m4fYCW8=gdfO4xdcxRl|9`?P@bAZbSB5PquzfRbDa; z-;Z5c9j8H0CMR##Gwyy&S4Cht84{rz6&jChYYVQVdB;nx<{!7nmut<(YST zpE+8&k^nlZcLA$uYc#1KASwMBYrIb*a0CLOhvlZ#lTGcCIVSXE)&K0!PE}<@Npa~~ z7Pf)?zq3-L(x>$20ZGd#;P+&CccYSgv;3-|&I_0K?h!s&iS7}m0+O)8BQrg!gd93* z3wqiq(|dYSW2}@U66qn^FQVjOU38DmL_nQmlMX+DR=D*B2uJT?Q#OBn2luI-&6ax%VU1w^@a(Z650Z~ob7of<;1Z;GTvWm*O z-07O$-;^<(%)`NP5*;*);rZ(&L(ajg;Ei(&7K%YU0%dtGr48a9?!ODcO0Hn*mzt~! zo125TNAgr~K>KF!ly?>vH+N)a`t014+bYTOm61D{$cO0w2+Qifh2@#QR0F^}}iFbE~ZkLnlc!y&<*n&_C98jisR0R(WH?~C#BXQ6 z+O9WE5N#CzExW^~Z~?i_{KoP?o^UlM4LDcXiopAxB81*M6rI?U=;qS`Mg z(l9BYW3Hd$VI$$%dYJ{=|u*As6jVT3!o->FwFJy9EBna6PDkEAAyk8 zLnl2e!0oESFciG~^KZRBQ@X-?kg=e}tj@Mj1;6!EVx6PBeyTtXV2Qmbb*)24(N7N< zPEhS_$QH#D*oDuo@$T#&3J~Ka1O>|8-2z9XC507a{N1LeC9hU;KntgpeBcFeC?yPy znWuzbm9BjZk>Mr@MF^80`EptxOmV4UxS0^oYX8Bi7ytK$_&K@YFup5Sz;ZP|v05o&3!;8P zG%aaoegQfQn*#G|@FT{uSS6ht?n{=0$Md!Lyn6+CLJ>AloE#aR{!2`oA2X(wb#i(- z97Dw*s)f}MfR3-&fDhRp5S-zK4OE1!H^NvAe!_cZrvzQlyVGhL*cKo3P=c@6QTIH! zSPxsu^@{0(l$ix&pe<@!pf)r5vN$cq-k^pF%;Q!vcnP+&9|+MMrt<>DjFuP+x*urWwh4l!%UTwWo0=<_qntMzi5y8p*I?ngCwMr_*Ca<=#GTuFI{dCT8 zBEW{x4bowJY7$^~a;A zqQ|MA#(omxz-(wia%k_Y=gy}!@&Q5>nfK#ZMErevi(H|p!XrVlB@m^gOb&<`RQk_{ zcbF~%yI%~trh8e!s2*yOgZGMo_HOOi;A=MS_JqANgkbv^N=6R`HdU!JZC_w6Y$+|G z1WZlGrpBU~e8t#%5{NJfkA-4Gbq;5X%qePw9#}YkQp_e}AF=;j+{jTAcFCl0HA|dT zE+&4z(DE!lN&G#Ttq>sJha3?4HX+$WKtrmyt z<2yOlEEqe&e-mGeSeA4yVqp&Zcg9lA6JPPss3I6sjm% z?Zt?(RzSeiU0&JN*JD%H?G~@p4Ssfoxw}ghe`v-B zXL^!$^`wDCpv@Zs)Z{rgIHr7K&y$ox1(yV&;dvK*Op1;#_!;d)x1Z+ZlJ=4vKSwc~ zeXz8h7hR8~yzx5hdK#`T)K@N_huO>zmjB69| z0-}XwFk&;jYy)H1s+llF+shBJbZ!F@}Px z{fri%hwMIK+Leq@1OQLdlCj+hJEmyc-J2@)#T}1=*G!?HL?B#Q^v&_mJH`%P>iBFj zT{AHazfjpFsvVoch3$BXhWan0-SG(ShuDe7c zb3)D`alG4SOs;Ja!Z#umBwx4TO8@m{bEe#-Qp+xMj{dLW=HF&`__js-Tiwf@-59xj z`uX|@0;)8qtX`xB>EpoGlJ6{EK z`CHSEBc2<@>I6w+6(en;9?fFM9(o)lJgA|e!%&xoYSqQ_xyOE4^Z2y1f=t=6ZH{LG zAwRMTorwn7d-=(s=A->QY+jxgp!?eL~5jA-*QIp8bgcf6A5K2A7T z>C4*awY5=XX6ax=Bv=Q1`4#&sSL>dKGMy#w<2yXOkJA&>_2;R2qJjjQZ{H0gCLIfB zB(YK}OO9I+elDX&bHMgW-iEJHjT0?~*I{j5u;rN0K8hE4?A^e`L!~xh4R^sihziZ_ zYtK5`Wp8BjpZAjI!;gr5rf2U0kO*)6GtCC+>F4eBKYw@}CM>b4qzQ+$^x1{zpS%-x zXz$?_DJV`9@1NU=s~Z*#qz~K}*f~v@8R6?G^`&PIBu|3Bh|$M9L7kI}aW|$p16^$R_LsyBa5>)3t|{-|XI6OX~_u0f~Y@ zAvn0ZN;P1tacVvzar?Y0)AT76&+jgaV&5z2ip$uyUJ(bkUw5ktcU$EOU#oHL+?}AN zew3Q6m5@s>A>$r#5D$=EPpIKUaG_4&#`nRC9H{r#A(|xJN%S|F0B)*Yr+%T>S52h~ z5$lv^bXZE zvk_sHl8OoB=jD1&5uHesBqg35d&%XD1eJw+N{;7Iaam!(h||F%N_jhSE(tg%9Cv)O zff<)$s@dNZpSzon&L^pBn=j}JRVXHf`gyY`(mRqB&ZNZb4xY;ir`ws@*ac1Z@xB#N zyb%5xtHYv_+;J?wVP-O*5@H=Giqt(^t#pz$Ol z^%F&pv=fnD?PA2|KL>W(+BbH>|FS!_?2qvzv%j2tQZ*_>TKSM1c=F8Uh42+0FeoaY z-B@)_u`SFrRwRA~!iX)k?sHoz&6ICTHkFrh3A`;AdCL53j-Gw{*HPFfM6GlZ=eaH` z=^>=aJL$HM`|53@5;a5nj)!atLzuPyh&~7*{9^}=xIY2aKT5S_R+7kH%>jhrVFtJV zY1V|k+{-!=bl~~^F!9+a2q(eOM9?Aki8PS&75B@PV?wy?qDvgQ=VIUDrMYV@coZ8h zNEw{*igY`gy}DEEyKPU`?db7UDNL~Myt1?L{h=k3qL6dVb`Zg>sV8b@_p=I5zdCS= z&zb(EKW$IDvk~B zeHBJ1pl-GW&fA~Z9*A~dnQ!vWy?T?Ze(lr}DU5xr;qm~FA(eJdR5CKF&BU#;?##OV zk6cEMJTrr(hjRW`YO&P5pIXk)?l-Q^`%x)O;?24=0lEHr2*CH& zT>E|SX-@DveXe^1CVO&^J6Q48Y(m*7@oQeQSCfSgwy&B3-wxLD-t#!lE(~1DEC`9Z z(g<#Du$;r^*F`k=31Yk_qC--(an4W=ZrM*?0MzX%Cb+mN_|rgz z>*K+o!FTGT076dg4?Jzc!~+#Be=K=U^?sK65Lq1t@%<8E=~`x}JTczKwe(dd!a23bw4@)buyBRHCp6?&#yBN5BQe?=oFB;L26fFvxeqr(s5SQ-09Cd;>QZ{0z zce_1YZU-Z{Z)xYhzAK}(wys~eZN9O*RWH=;yGvXtW3bUqX+BgliPiJfNlWyLufW$6 zfaUBChT_@$v`yk+5Oznci#y!1Yog~=LX|gS+QR$V6<4AdEYHv9mTnpbne@WZ9v(5| zO5F&|&&CT?;rtXG4#$=ZjAYCD@Ajy&2mbY@ z1)MPiL^I=_cz}0pgtwzGjO<&o0^?cZOVKqC@%+%fhL>87R^FjJC#vycmd;yerdd|+ zF^?82);)EkQ9bH^sgz5tP#t)XINHBi>M2`}Yv|_QV*$w_h=T>W-YZn&Nms!W-h2_n zGfc46MZd{g)NfYb(%%H&#o$O|)kybWrsm}}itekDQ(n`YRHwUrx~DfhKNfj!@hehp zsjuzP6d;s3x+~V(YlcHhR8wh9;kl#GL80R3e+T*Z4TMF zPP!a=x8)~)R+!+|7P*Ws?qbZ753^Ur@dq2SBj~BvxElNl{M_uw?JgNOYd7giFyz(v zLIc3n*k-5Icb92?0INB+$iXp=inR)$F?ShQl8&skEzv#Pfu{EKOzU)+hImK$qQch9NgNZce%QG zo7=zoKy7bQUyEzgd4*M+o@Hsl2ltGFb@f~Gf--W*(>8n}Hd@Vt>zE4GV;CM%f(K`R zG-qWO3;(d*aR6eT#EN)AVp6x<$N8t}n0R#0TXuzOKk;!2BXTF@k{NQl%#MD$tB z!gT9i#y&*^Gvt`^aObcN7-EH&M{--P^>^0O6wTNTijJQ)9UeNDCYAE63`E?q6eDKt z6E6Of>2?T-qjSy-zGpToyFfFIHydU9iH1OwGQmwzq-yE$x0+;L;*(l4i+ZRJHbC;U z^=dIBKu?@7+Q`nD4~iV#T~EWt5Efx#YCS#5$vcFR zdEYnWl4~mwRL9uwCdn<-JB#(iGPSr&_%C3Xv+DfC31wcXkAClnuG3;VBRno0+~*9g zD-4axZM>&UV5n{VG#sr50fOw<8Pdj;{`&6URQlJseJe}C`6Ju$LTkx-DrRY~qkS*? zey}lDzOD0bcIXxmD^Wc7IALq}92pxIS0zrgFkV#5OF)r{J+d zwE$ZfKe=%^yRclvgD1Ti2h!0NR{t3twKlw2L%yaM{d4zM zIGtKeSd24-0GBKQ`xPcaEAKUrZh(cIwHgK%%?zCGVpVu;58Hx&ADq5O5Xuj8%@)VJ zNw|1d*RM@+p)NnZmibrs>s><~`HHnSovO3g`j0!WFL<-!ubdDCBrgT05+Wz$3B@U@ z*U&_qi(TK5aL$P%h<=r#_%y4vDLTjK{NJJM=PeHQUR_piZxRW>Y4^ADDi8#mJdj(g zr91s(VNI9JlpOCHos4<6_^iO@S5c%e!v z+CZv}ij?nRwDRF>bx3`t{=R3B@_c9M(`r5TPmxl2FLI%VQiJ&eSavy^INfB^I^l@a zHfvTp*@OFFzP;wj-V}Tj8Yg!{fB4_d)Ls?;4n#M>N}VG_NYUyVBhN8its-k-7I&!k z^A=&aapnE!?VLchlT&X4_$z|FrOP|4Nk0y?9xZYRF+$MlCnKWR6-2l0bis$pn~Btt z6e}A!S2v=vOh1J4)BEXx7s_8(x2MeU=31^a?(zj*$crc{Mn&*&;Lj5(?raZ_6|hKl zL>^Gf#rGW(w!|1B-s-aLTHv>wQY6jqO{7bq&w&?m5{rqiG$^{BJYc{`2Uy{}#kqd-Y>|(%|e~xMR5VxWy6L&N*l zjmU(%ZWu9?!ek}es!)$?V1xAohUaBy)j`^S>upHOGUEHDud4j6h{NLa1+06sp^?Nu zuHuIB8WN+Mn=}yLdb2ZlS}upp<11xVUJ#V`Fest<%Rt`T#ZF{d{`bd1`UFQb zs|v_x_^m$_CV#TN&cRo)xt&En+OHKZreC$jHRYIvGd#jotYYu&KgBTXy{R(bun#L=*DC@LLAS`sjQ7Xe5%KxYRhbw+{G(hO?hm zwp>G!=GH^ziQXKPDq5@If=LH7valwKo#~OMJ@M`fYk%^!*|s=;4W=O9?*B=vGZ8Yj zrwM`jY(8sWdER=LN6JVkVpHGuwpf+NtHK^VjjtxHXGLXe$c0w3k)ji#ulGh1w&8-~ zu5YvIVL|70zc1R`$VIV-cq8PVB;>OvJjY^;0Ihxb<^>1hhR0m^g*&U=+cOuH;>7V# zS?Z$uLOFj181fkT+Pp`{7e}No{2p!|<-G#c<=e8WtE!K4&F*#@@Gbf?jo@swNsPao zDi3k}?MzM1x!M$6mbEugS@c2(t}xjZt-NF#1M6NV$a6ve?PpeI=Dfxx_CORud1vDVl*|h(@5^(t~7anRQB@O6Q>9FAe_p>iqo%? zPA@u#K3UhnzPeFtOdSpNJ>-70KcD;g#H#TAI5axpk*5#@`SYNs;an5Zq4+qlOUGfb zJ^mM#CVNNmN8UzHX4a<4ljq5%vL_#Q0(NgyyBgXG70%O7RWOE+d@A?WQSG6X_e7Qu zR#mt9-q%r<-0RN1horX)%H&+ACAbbYLM9j(ECOHA_TccrrP5Cn)-Op`hiI z3*}dR!Ln*8J9}RQZw^AywkNwoVm$lC4FsDPBa;bU;oW%QF1^g{*tB zY|}CC9HLXq=Luyc^v%fm#D`@Aqw<1XS?^To z-^PW|uzIbzAwGxVds8-7mgV*5Y*uAcU@-q8DBTspVNE2BM@S0Zj~CuiguwrF<$6t< z>RY{uSfxOsTI@ zZoZk`)Tf;C+rNGN#eARqPI$&o?pi;KLS6Lj)y2p6fYiy<&W$VAPU|t(&mWqvSG~PT zrbBElp@VmOObP-;8(L4ESps)8CrBg&XrzVBybbvM_{x48Z+k)HVaClz{EfoeXDt&N zN*b7}vu6`ynHD=CI#lC}8?%EYiCVk4TAd)KrSConARxnf@+0^r4+Xtp4Lbi6RW-Yz-M<>#hZH7Dm<1zs8B9<7ESF zajS`>k=?BaE5zrh`CLx|P;+KkE>vtw%XLfqKjPc5T1(y~m|8j6lse?}Vh1tCG9gz8 zuMUaJM&1p?C_7`|Zt_7$4_|^)Q66@Yms z>UDgjcolqQJ?ooiD}%k&GHG&utg}Q>G~nV717z}h zeBzMw>rtr_erE=Ek^Jxqg_2-QK#C;a{h`Ei-b775o;Cdm{Ton`X0}3p^+}xVJ z=5QFCAx3ztk1NKNQBP3oifrgkSx6m~NBD16V1Ay?PqY`8H)X)S^Ud#A%M(3(zU-ipLVpF z#OM93{pH$B7Wwyeg_=aYh*@R{qEc#C@Sd7;4STKHO{MI~{Oso^ac?CU#&2D|6Ke%*7<`xsdWK9T)@WX|mNTZYW;r8GMozHXK z2TU>9|J~>X@DQQo}m(QR5+Iq*C|5rV?VZ8NY9%J6A>BKMCN0zXCaSEk6 zA6GMVSqqFBjd!ZCU66?o5Hew~?5IOF7j`k|eobVc9O9cpc@g|%0dhu|=VpI0SPp8Q z*nTFizzXEAq&Ts4O(7J@LShAt82m4$ysd8WT#!$#oq4xb230UUu^4b(r9jxHmj)qS zXgNRV&%4T&`*3?rFiDM$3?d=Nv3ba-c+a^949ikv`i|B!0L^^4GUps)vw24GL-e8r zTTPy-)}4L1#LbF$OBedm@Vn*@#aDm*TP&HTtLrT69yQb(ACvX-n9`=#ko+yJ@mq@d z&bsfr!v{k;*fE)vGoAT}T9X?fjjg8~9IbDA%KGk0!#eOl!*DyHVE38iJe6ak!S|72 z3b8R}#6eT`S>Zby4hmb-?*r*eM%nGgI`Wh|+$%#LEz=2+-`6IOymq(^1k;u|QylUN zHzlP&3jO}ooYdMU#ov}>4;`)LA?PT{%HU3W+%b)O=J8ri=1K8<44(zV=te<@HAMhELLm3?(B}6Fv1Eed_(3$j zW)l-?4R7Wy!LJk43tAp9%5#$!BvclLwU!f@!mjO-Q|k)h^26*L%0ZX@Q~$JH_ik;v zdwhShUxt;_?nw=*06}FJ!t*d(Y5tAb9J5+*+m_#sQ$#jI*n!}9{T<}LOi|Hgw-*E@ zMY@y}ADO7=!RT8Iv;xcU8^hxMy7pY(D)2;+$M{nHmn`#vY z-S?q`RUuh;Z#XvqU^1RMy=V=&9?x9YZ@S-iId z%PQBpqo;>f`}?N0pM|wC?i;r@5qY8Zy*UYdBNgkDNtNd%Yf*bqZzCJBrFBaemF>U# zU{&1VBf8BRlOfXz%2L42;9dzvG|CBW<3Mml-}7#giP1%r^FBaj++cQIJz*$Jn4VQ7 zIcbldMDSeO8t&#Ie}=!iRttWq=$Jju63t*LFVPT07uH|2%{ocIy3k$0Jib_MA=WpD zBoc)Y9F8GZ2M!MC*KG*10Lq6TdYp_1XJb`h`tZe$<{t7nF%9ue`B3*hPeGZ_XI8Gd)x}Y&4iJUu!`5ty@t}LdJ$HjG zla^4p@R*8nk^z(v);W0Paf^hFn9My)O+~yn){H7vUsO0LY$~KYGq&u+A@t|Af3si8 zj@qRM(}N9NWpRj8C{k%_^1nv5V5)htf4aCEqp8p#I+vI1VNI0{IUxY9hq~$Fp(Eg^{=j_Ln$X@N)3lX!qk>;ow_cbX>RsQxN&W zb-d!WVE3Lur1^vBL3sZM1~ng;F@$#^PJ1tyyzqHS7UTp9Oir*jRx80Q9Lze&LXt-NI%PCjuY#rclKl=x9y3!Lr2m8N7TG~$VXGpsMm8mGI0H6de)#oyL zY`Yo!y9GiHbqQ;+&E>as1d5;dT=55H-?6`+9p*Rf)9gSu1PS`>tle&Ec>eL#myue< zftCe>Wt`nUJLO{HD;Ht@AkLRRcZBe1>#dn{Ijx7^srmB5i}JY5DZ%J3te3s!;@N*8 z9&gCK9-B2jJ!rJ$w($zosFo|!PadQF6Mlvg;4-esR6qMsJM!=6_v(LUwz!k($jxE}jIzp`v(ToVpjjTv)Tb%&`FJPuUky377%M>3yriY{g|=n(6NaB{vM) z*!Kaq$vBAIeYCnUkB&t)TsEI%6b8{@_8)x>{4n5Q{G1iqRKS70*W=3tddqy&N)uW* zmh+RnHY^dHQGDVsyPF`VD}cL@AFmb9WaOm1$$qYNDK?jLr#d}fGcMWpJ5N0|))0K2 zT-<^$YcXeByy!BkDrpZBFCKqhqrg-(lY4ClUs)cneh|jkqoABR#H>ci_-{+J9vP*% zzn|3JA|e_{>q)xuKSqZ)jfWfUxkMPnVua=u5)ySquy_{sBEynL zm(JJuNsc>=@r3!wX*fyG_oDnQan~uGfimj9d|XbPJ(|&HBH*dgL?j|{2$wCxWJHM^ zsGnfj4y#GHTphd@mm7FqQ=9P%YY+gXsu|oY-ELeEo^7nW`PP8b@$AE~9|j1V^iw*l zEcz8XZte@%vihz((Ns)esSTFuv99rnb_O9s;5T8@74^3F#T(pnv*+?B0$_&07BVcS zlb5aZxZk-}O|Sz(>|9PqTF!Bba@x885w3sXCSLFV5cwX#$TY#|EOz9IeOf2)wS3^oQZ=z!gC8koN#rn$ zy)A#bJxgWKTO@++~Ukt8RT)A`7p|G(+)r(8D1q&%wD?Zkjop{qmID#@cmfy4SK6i)kT z2wVQrJ^m;!v!dQv79x;Lpg^mwz#UHaB0dfSa$=tU-ci~)(M`??znk25C{ExS%*J2>@Wuj zw-7#i`}NnN5944s{|Q&mQ|Teg+8c-g3?&oJVg3&xbZTQYH zL;`1X+B7#&24d2{k{j~(TvNdNg~{#mf@wdojdF=TMZ>sy@h3>mc zgA(!*eTO?pl2`LsehS&$7w&gB#gfoCM1BX`kB#>{D0z8Me?@EE28cq0TE7U2-tdQH zSMuz*&vr-9^hzjKVh8%M-`k++l<5EPHg*xX@?v{L*lm^?*ohL1E;@=D>5$<25k_9Sc}{ma(B2%*04l#^8N^mel(-&{{m-MOe+XY z@Sc7Js^*%>U);+M6?YWrXQrI%QSF58;V*zKY-}U5$)9)Tm*+EAK;UW-)3$l?N zPkagotFc?gMT0~i2!YcMVs^2Ge$OwCn^WLj7b|e3TwuSWj=k}Z$UNN)%a~P>lyn5f z$qLqUb_2p9gbgIeCL-7fCJWY1oEoWCO+_Y*0zTTN*AVon$udg!Tc__J9tOlc1Xn8N*@>ie*ds5q6pA$b_mTom0Wf>f zEKS#fl|W@mCcu(iZ<9u%Za`ypYN%qp+`?a+2IX5ZeBOf6^UVuVuX-!i&)AC2660Q# zDf6X#f$ZhScjz{8V8az|>7D6W%LBM9;qrGM&v71>Wz~5|FW~$o6CGC_gn-kY=^~i< z%L`m(?G?YUv*evbvU$GAb31fr+mS*S*E*;6I; z5P}>XB7V;R(q924kk{1b3jw#MmA;ft3_?6v0s>!`AgX1gvGqbMm4v{uL~%?GFP|>! zTJrn{S6Fuu&-mg_4qcsrIFj9gpyiM{>?Jy}iGJ7z8jZ8zDbOK!NOx^ri`P{@pRrh> zoY4aBcy5pVl_%~+-CBZ?)-Ppjn>>kgoeOefu-`=3xmnu^I7v1P9gKx(HnjeE*DAWo zc>1^CK$0S(fpi=fx_mt3hb-Djk4v!l6uGqL1AFi|{9RYTLQ{96OG+J7(e6GP)#}go7q+acDDv z@`7@|w?2ozkleTSXQAc?t57hb0*w{`cUDigJ-k+45#Iu3BEU)502q!}1byo)B$4YB zcP^KXnFd6(fbY2R$GY@nmqz47ymU9^9xPV{S$Y5M6tEWZ+xZLtJ2J zKSJ?nK#mkh=xM+MA8jRmzXTd$Ab&@u#Q1N&;rMfs z{MANIKcEAn6=T4cpKIhQxb{?kURO4GUGGret(+5rGC#4>r^^f1{elUZwrAA~`oMFO z18OXwU_c76Ch3!-AYc>%SN%@Rn9(|cbMI#m0{IBPB}_|0y_-Y^G`^apQ^=(9Mw3KJ z)RQ`Rg2Rw9okDx)THFX=xjEo45T^!(kC`G4?u?y69QYm;?JvYA!>37G60e`xGNqxA)2_|@s$rALC^>MI4MVy>pF)t; zdYNu$Qz#G)>a#(m0MA%}q$aLQDtG}!R{KZmVS^VZ!%V(#L}d2uj}mI&C%Myi3cVS) zu2M25QpzM9P#9P|oZjiFSmbKqyhPz6x0nI^FBxd3!G*~Q$j~4GXKma)!=w15jO`fo zqix4$_rG9mn+b#m#Rwa#{#0ctuXaS3Nur5g@sYozp!hUs z8Lsybpa(%cf`PYO!UW1wg*XEvq(#eKAbzT!z@bJXMv4z(RoX}7o#T1wu|48~M>)Eh zXAq?@YR9R-qIUq<#_i{2J~bmdXv%^h#mSr;p}l_#sRCa|KE)HgssI1qucfvd)8l6; zz!cuxbo`qBJQ+^iwM5v#7s&j9h z*U%e6{zOfvDMQ2iNZI8RVT@_tXBnxJmSZLPJK{)g(9{M2%EQ1jRgwm+Ci=)5{bk-H z(TZnee9@7EkBLv_a~f9|ZI?JUviKR1cNqMVImQdQ_LMC@NN2}4H9-!%=RsTjFRnyj zq#%Z+d%}*>f*mPxNA4J`>yU&JW*Bj#WCnz4e>Rx7a|?g#u%9q&~Vj8?nD(oVnm zu>uitFq$e05)1%t$n|ZJP>>rB*6_f-a?13Kx{_3?cK-LrXqaB<@*1ICp^XQ|Jon)J zXWFyb;lzVgg=DB5$(!&s!LD}4E4rRy2zUVYH6bAXR0rL1&GCiG^4HFXrCRFPOtzPE zxSbFP7lm>o!V2De$4y*(HEM+z)fktdIWP~Iv8A{mP5TIZFy^JiL1ARQ~>(#)I2^J zc;Nv+>TMtwC~@;?SUIFX3;A};Al1p`-ZQ3|e^%(|VTY+goT%(~R;_}mOh0f87>Qu| zQpGcN5=l&4R-?SX;^Lpw;}=%inopdPRs|CV=Sx2bfSNZR5~I^`koz&2g9?Zx=Oozq znB4=%dJEWNl-Fj)!~>DmdAf+2$o5Ob>Zlx^xFV7lFVsbmA_?=;)(F?@3@i(=K$?5e z;zqTamUI1&VkQ8P0_fKA<@2HP--I7`N+4M(N*CkSn@?-PKKcec+@3s|(%BUiki9AO zjy`*0$cyK6H^NzC%EykiQlbh73k37`{zs8n0JUnse*?v zV2-QU${<9*iLIXE*2jDvpjI+`Dx{Xbeh%t-Ts9_2|JlcdM#^WEEdBmS2L_+d?b2D@ z#jpmi6sit|OA?7MRpST_^7Wxmus!yMIq7DDCJBsYk>c=|l;VdD;ZC%o4+s3rRIM&q z$s8YW0OvwS4ebBW zk@PXb4N`knkZ2VOVsB90+?Yh|}N_%RE95}~cVaThG+pH3J9=|^!e_~L9e2b;|q|KrN?RNhm zj-%89=)A2CggpV5SC>@JV#w149fLpOw#U;iz|;*v=a9c|&XAEgF~`_SKL#oKJkZw9 zjFP)Pyu&e;Fvg!-(DV;v%5%5NSFTQ`yG;V_SZ_zG@z}^Q_4Xd4Q@G#*D25#qgScRK z25SzRm`h^9pD@rb$n-W>5TM=jTz*gb1S_zQJoyj$hIqgKq4`U+d=uF)Gy0P$Q?H*p z{IX@MN9x7E$-X=VaMgZXTuLrH4koFrKbAI4`x!O)`7$(UAIY@Tlff#*m{wom#RHV7 zPAl_;Yf~)AU1GJrVE&M~?fR(vKukj%`J5cgPK7%+U+Hbvk4Q9S#XwbSBJFb32s8wq zcbrxDEo<^A^FJ>=b+@ySP_x>XX-O`Yh=m)=u&fSJE(6cpYbiW#oLNV}cjw;{ne9$` zFVwsPEcY{bjoz(6@+g=+LUshbw59`b-jLx14q5g((rWiTyxGUqQDsTO^VCG*geZEP zz71}lE^F4j*orjz(?w}ZS#U*NqAm}feVqR_$ru3 z8;KZQ&N+h)C@9&ik%7h@#A@esh8><*{aziWhP(O`ntq#J{R&X5NG*>Ix<@x@%8k~# zXLKIZ4Q~QIGg%HHxAd<2;C{;OTY`6bAIUvf13RpsWo%<(I~`%M z;dQyLIC%h$?cWff>yZp2Kdxy>+_qdE54=Aav_rK~lBmOt!cq#VcRsTX?TjqiB9h7( z5VS0Jbi*2I^{zM2eqw}S>x6pt%L(TE_e*r-6?YD6fq?_jeusUQ=#k$QfEjJ=xtVAu`Ss%$c1!$I_bbj*l+1i|BR#%S zo|DLx6s+PtqX46eEX62mRguAnuIv;fD;b1DRJHplh9%paC>n}FVsS2m$Su?me$x@* z`PkcueT#m4Cniz4*fsi2CpM(HGrcu+)L;d}!m#fjbybtQE-!oy2zX?_k?*~D|3Y0z zGKXC82=@A-9-vr~Wt=@NF=za^78jN#=3%Gwyj86w{!;YtacRuD79&iufE&8USw9s29 zDiliC-0IJ|YavMyHfvZ8T@s1gsGR-?E6A1d0*@?2%>2ACX(QUFeL3`|>}&`a79kfbfSQXm%ujT3S4`(}(=hU6=&|dsoc&_>MH5yr*jA}LjP__EKX~_Emrrlp zEkGQm&%oKVZlGPrazsyJrLneUtn+OXZ@Y;nOR1#ZHfx|gl z-j(q_nonk~sdh#uVJawr|U0 zp0FJ{5HM~nfrl9|xFIF*B*o|Pz&;48 zn+iHl`l_>R*h#p!POOv^h7`%J{F~L|r=!z4qp;1>!_c;7ijG&fbeeeZg^T6Wh%EWR zv-ehfdTAIax3mM3{z$pm${3}A4*dZMSlK#XlotGJfPBOPy;FG?C_iz7 zK4l9Jq*?wT_`IjX@4L~?ICa)Dr#0B$sz^SQj|ZAI0Zt5x#bUw9d6wscYyNqcrOkfj z3slpxx8Tb0Z!9-wBKpG*@E^WB+m)cO>=_uT4{b1TEa~Uwyajq80W*)XF8(;qAH9Pi zM_+;1O?*&EulI+jWamWmU@7SV>SHr$UoXiWH+s^KQI+Kxr+(`xJvVAYtG@oh9b4Kz49?>Gy~KCTm?Coo4&)p0O&(lBa;FyK|W|( zt9yNX4Xq1;^TeX8Cc0k`ADdwNjl*ZsfzM*k+F}fJgP18hFVOOIPlKe&`Wi)&-W<1@d> zZg*WjyEn3I=4T3LWDS4wJd<9FG}7Tkq{nDgS((Y?WKZo#4^{;aIfun|?g~D@*S4sy zf-&Y0fhPhGxCWw%36+TV>Ep%}wxhkdd30>H|AtExZXr|`ts_6Zbq_Q4u{HB}Snh{T z@Ik;WAJ zl6l zKsOH`+!Lem!;Q>CpH&uqAu0N*kr!lH*lLcimWXXXC$7F;yL}GR$VU|Uf?HECFwYtu zNWNU@@dqg{*~8@SP6o1=Qrtu!@CWqz4{`E)=uAm1cgAviErfnxsI}--SI9Bfw~TCV zbBz;zN*;9%5;4Y=a>0qE_^npL$>G|%MvNsCuJZwUM-zEcD;vfXV|79BKI8=DiCc{Z znA1eGwMp~jW5wqeh5@Q{s+@zVTHK8bk^QDLBD{L19uCXDK0)E9cN;1+ z)b-F+>?9f!KGM83H>3cy(QdX7C9gMlUet(<-6RLqVPMu$4&Og{w>!X_AdLea4Pz^9 zeYq5LAD*En-~Gmy9}g7IyAGdzp*to5#aoqx>MZijhe_!wr7t1llB6f?mOAo{{+;D# ziZwT9KDfrI;B~Fq^_dLB38y};>PGi~ko}BBb3mXc}^CGxco7dFTqRGoEQCQ~=06&9?hB{r7NB zB~7cW!MU}Q$v@ot-!HuXcVSQHlYgw)f71IO3j3d^o&ez055Tu)kH=qmG@#A&18`?x zRPze~#d*tqNvLn%4|dSfx3Zu4$e|CCI@!Qf+uKxbwCztlxAvB*jxaq5-) zZ|D3&A^AsE|9N{k?%(HOZfiMt|3hj26NLc)yrcyuuRj=v?=l_7^Q0iR$wAe6Nj$cT zf8XnBKHi9X&}rR`mG1Wx+nH^<%Kz#W4lKM@xewUe|0kRN)**!Fw-0K&oKXMy+<#&J zU;ie4c>e$JEbQ%nr$s=VdRP;xH2}!pw95+AdFxU5XMS>!hz3NR005d$ zK@Gx#v9GXxtrzT|Mp$-2@suuvrPdPbA1pw{rK>Gam_i*9V@z}Msr7$_DlGNa8 zISivac&X#>pR&)h?ceVALK;9p3#j$<;MtG`LAdizPW=t3Q%~F0&iSXV{=)#^g+=)D z!H?sQaQ6O(R{bZcHvl+=14||cU9dMkf52Awbz7SsurmbB2FD1SIZ2QS` z_}bkcgS<0NlYc_1{>!_r*KhhZ1L*UG<8k|1T^QfmTF40~su&FGzU9HfuASys1?}R; z=yLQcCqAL(&upjP=C`F5vN`L+ezaF>Gr%kgYV@&wUdVR)H1`l9e~p(uNar7y{=)nJ z!3sRF$HO=kgIN71ss{k5ziT%=SY0om2mo$51mE3u3XX1-&cK8+K}h@XHTFRbUZ&_w zb~9U7XYh)OYC7~Wah%iwYGno#%7Rn>vX5AXsg@x;-`BRhCr-{kCjIYxA3y!+%RKf! zXw`pt*Y*5euVz4g+k6~G{_RLS)ZS`N5tboAUMNI6v>FYd3LfH;V`K&#<#jkHX=gIL zuBtP|o7~`>Y3_ zYixIedEI{3uNlC%2WR2T=@alJG6R~Ml#rm)PZ>sqFNwEyhO!f9d>0+# zWjH3mT-T3Q%Q(@~0P@=UH@Nqe_hE9a%z#;JyPiw`jIO>xz0`cSc9ED_-BWXp%>2t2 z{$v2~!~$G)^fRE94XnvOPPzY~Q~&v`hv2uJ%m6I_d}nKHe9G>)PUZv@X#tYdp{56H zWUKZv8PJ~_-Gtr5K3<381DIL>rSFou?!MGE)fLR?*5FY35Sb(w7OjODFqujJ56xx2 zRNKEUPCdA@8-F*1{E@3qTfjUgGygJX<)4-PSabN}`1-3WcxscJ@H42Ae^d$!0QgD@ zqT`Q$2Y*<5G`8gP1FRuIq9Oo@3iVFIu@%^sTE@4uqpU!0&Oo(@2T5yU*2f&rD5|jI z(1-Xes!~*KGr%kdo_SdKCqnGhE83UuhGCSn%S-=sLH(cm8E!k^aa;^<&c9Hr|3qN| z0G%12a|ZJ5^qsNYF+1WiUME190g1Rx(NMZ{M33^7s`*ash@zXaoA|Pq;m}RqnFUg* zoro9Ne8#yCk;!aqzEy2A-~=Z9FGY&x{98T$b#dx>XvYUfQU8_i^R~Zq{t55@X)oZw zdluS;{_LcGDAj*{`yu&#T~d%2E#QiEMlmg*MHCHBVKVt#_ z5B)Le|MoIGxX&E?0tH*&i}NqE>OUWXko>-`Vz70Juucou_Lv=*7SN*c1!z@0+RqjD zAV6tv*N&D8PaNiDIF5(u2(;2{IDtTh@pVpB9&};Jt=K?*ty*TlT`-m%U`l^CX0L@) z&ucrXEtf5I{^f0dq5k)Zs=ufG7vH>BFa34SztF4yL}3E}Uxh;G_|xCP?6KWgpF0Gb z6fHpg6;%r`^xj@|zY=M0cTQ{%FNH!#O`iw-6kg)+nNVlh<$rFu!*biovBb$Em=;hC zGvKR8&%T@2FQ}VS&oAIagu-d{{-a>~4`d3{e&1Sw2lt(WbKxxgb%j4etNsJ^2LQDX zb^xH(3oHSG({{qP-`*aV%3>kOxX=Q0ccy9qPSRfeCn#w{Y0FFth>mj`j3`oR zm=Aa9uUq(|0RSHQ;N}cenZF1K9zPnFb6)`E1tcwnLy3y)KH&=pSk3p+E;3du@7?S) z9Ft%&x?Ld;JqS=)@yv}bePEYNo*J|nZ~_L-yA*setV+kr?)#vDgaiQrKsHrqCcY;C z$jTLeMcn_bxC^&J;+Twj z^e5*}S~+O>R4FKDmU4{N^^5Q2G#pO807koSd*Fw+uc4fZ?h4kmcQSS~|`Xc*d z1psSE&{x&7}-X_q|fctl# zzYPE=qc3(LK?wkMc>#${Mq}hH2jZ5_cC=G^u&f@;Q-tNO$VU-tGCjb#40N-=f|T}7 z=g{JwvE5-qyTN^_?|Cpmz?8(9+t@9=2*+X@G)EzE)CVX{I`T$FACvy)vUB@Z)?A8v zmiE?mWD5%aPl*%Q5Y>KU*pHU_UW0dc{x^<6fyuq5{mM=LdT5!S4@PbCEZ%8G`D5fG>1(OG?tQ8xk)IeVWs7iL(DS zLBO^Tf|BR_j+p`FEg+h{X%=$NU0?oqRjVDKuQ@OUX~e!RIH4}IlH{0gPcJ}=ULP&ohcLsRb#IP}Sj7N7%w<7^KA9fvq`2POG zfvL-9!g%+wlIz*tlizke#?$Zw(CmT7;wA`W28zBfa{P&cALRHu|NaHI;G|b^ zA4+Y1D*z07=}#0!08snoCn8V;1RIY+*Nq3@+U|CYqv^tY9f7!|iZG7|kc42W0?iSi z%&?rsA*fP~?Az$)Shx1IngrLud8MCuAT1#CPnfgvT>6ka?>csUr|T)F_pU(d7n2#6 zIE4Ll_f;*Yp4+$*5wP=+Z>}WnpRe;Lru&lD|Kl}yW%tK8(h_s))P8Hu|Dcxse2D7Z z0hfL`X#u+IKotxUCvS^wzPk-B=CcKp!XZeLg(4PHfJ=#_sRfkd52#YR$as-D+%1oI z@dwO8_WA3Pxz)W7iDQpPbbW960O@PKirn10YwFZhvx#R=tPA35QN z3Vo~^z?z@Dg6}-I1Pf5g@NP5y2Bq{DqZC^5q6OF`2j#Vc+Yi8xx0ryhNrzxkgavsi zaU%Jc;;rT<0f1M308n{5o3{D)mHYw+vi@!&znKK*xdL|~_wE7aF-qTYfwsm zqQL|Jc3Obd7f_@HkSTE8zPM!U2&_k1K$DUoSnLmAmrt}Qra(4$X>WIqJmk{3=6r2M)5HOZ?A@xhkj6uz{quY)^;xK1Nps{owQWtDlQ2l-0Q(2%_>9x7!PnZW& zeFeK8=3|vH|9%nndw(UK-0MmF9L0?8#qlSz{syJ=CkitFsQq)ABVadAAb)JuCfM*N zJL9Sr;Si(*p_1xB^vEb8KpRO6t3LorJGXu7u}WTibugxyrN&La0Nb&h)i(-)&a;jZUNkb|7#G22MQD{o0f={p&|z<`9%p=Kiyhz5g8h>(tt*=XPy!`cdj9 zpX$r_HC>qQOKQJN_mv?(iuqsr9!|daJ)ZDmWqzI4UpL_==g_;*Q`iAO?W0}20K3_O zRuG6kd?3!9IvP96G~r~tU<&Xbk@h2*0`w2p1cXztR`x9%uA|!T`?DLGL3=t7n1g{6 zcK3Acrpxp*19R`pLHd&2OV6p7w0G+RJvUPON&TlE8t5$w`DG1L^7G}rGA8ps`zii% z)Qdc{LX7Ri{6i+}S2Aq%?tqUzsxt+3AdrAF2*fuUiLTlE;Nr2JSg%R(2jueu^1sLq zBwCTr66B7-TxrjOvTt?Q^zh>Iio=JHNnizmc^Ei;il=KgU49maYLPU=<*&*hSXH2J{M{^($ zY47u#*a58nGYcRv@Z*T5>sbLH{g+wDJ*`=bpk7Wrr#|o;|F{VGMNE&sh~7xaPj-JQ zQz-JWdH`!Keg~)gX%e^N;QsHvr;`t$90Z9ggbrPGgX4=lumr@W2_P+)#Lwcp9>Kd2plmQfs3 zvmJhSj1oGeCy)RDNcr3oi!-FC+5EnTD5&1|-$|O6@ebNLh=74W$ukHse8R@fy zrWTJt=C3R5+bR2r6HEZeK0Odh7S=U1Nx)k~6Uy`9u|O86NPjz5(K8vv+%_F@WH=L?jW0vmQ=#O%HC+p!&Hrl2$h z;%a!1|5^-2Q-^pBq)#B@JvvY=JGjzzEh9=N_}IMGHF4@WhWZ5aOQ#>%`~7S3$v%0H zza`V}y;XQ>?`L?aWtI7z)czr_`satV-W{m0-(I2uI#WQ^0umc`qw}_Xa6?Bc+IfC} zB}X9tnCYcUwkz&sX%1M5i+X*b-Oi~+JC{Dz)T!sxE*e#AezBw%F~1t_lOBIr*PlI_ z#l3j%TQB02H+#6pU#I%(oPJIrzahi?L_+}pq8JT2p}9GfiOHsSE@6(;!b1wBNjpThhk`6c5!0f3iy z|Bz#TVCVpV+*g+tvzsrV`UI??z=7iMX$%Cm#&0@>Ilw_cBnlNx9wJ0&8etybZ)3VN zb-11q)UUL!aUb+!BUFBp_=%**@QaxF7kwu4OJ|=d`BOsrFX<~`emChq?CgG(20H+# z_u%9YkTVCZAYiKr5D1*M4Sv(nRB#NE7C<0Sw9WuYs^$@P+OE}HK{8naH%X+yb`xqYXW>C{m z008wqbp8OR$bcOH@M*>yj6mo0d*H$`ZI~)SKxPZdS_1rsX@@icJ`)XvDm3}ogbvz3 zjT~w<4dJ=9Eomnz_e#d6jUF3~8|C(kq8^<2jbeL$KzREX^x@TG=i(fXl3!(hHP$~Y z?0%Jo3IM40Vg&%}O2fM8!&*}S0t9A%z5oK!F~~CpBVx5F`D)M%!ep!Yll1at)qH_W z%?pl(CWPnIPL1tL=?`I2QGS9b<|z|=WO{E(S;*tfRe1PoFXAE;l6O?**MWdl?YHyz z4{7EHh9&^WeWyu7ZsvfhDR2sii0z(8RbWqNN)$?t!58E!m!E^a{a^4vPc&sg@emd&d6 z4|(PnPhm)r8wgl60V@Dl_wyh?+jTQ=#E#=}ShJWxD5C@Utig!T2Ixg55TF-ZO0&Fg zKrBL{0>O}W>Os%=auy$7v?E?{q>{={dHpE1_vHc}Qu41(Va>x!aP?_#;USoen}J^S zx9+=z`-Z&TZ+RL+j_jO*YAG=%pMWC>oUtW#I&giQM9#saq6x%Bu0TF%h=|IIQRbkO zY*cWRRNB0cAfwj^cw5V-bzdF|vs16=sVLrk>EM%d__>JBn2U|lr?B;Nisa9${p@qa z02W{R0e*4gLcEItBRi@7R-0ckzEiS~-Q5l;8j1j*-j|(I(9Imsfq-lqJ!~qb9k&@y zTB{XPrDITP17s4!1l*VoLUypV%PcNpsBY|x2?jX<1OyItK7v8#)Ki~Cq`>S^#k(IB zb=ir-N#cttK3osaD${vo{eB_&7xd!QAN&u$c)J&?QLOe`8Q;qEZaMzL!s%zFp$Y)% zy*Wh&bZZRgK1(!#>7D5K?RNP72HmD#ASrVNRBZr?Zy?cPh6}mGiwK6#DoI3{Vjsa^ zb?QOfH203i3hF-J#44@aNotq?*t3j-02jTv0^?^Dd!MKFvd?FhGJF4j_$!K;Sf~2C zF~6PKKdhLaXgC0X+%FddtowOQfcDSW96NnuDo$({CIM*zGJTjk2#b6J6fSHxBL&19 zGDm>>1r)P@{^#r1nP5=!VjE>kdx?C)t}kMKh4JaWdRFiP9{du5@nvFBm1&x#Zl z|7AWdzWgJ+f`aOv7>zYPs#0I=Sl9}sW?0JR-I_Y31N?&n+Js=8-e? z6sC``jczLf0x}Bit>WswkuwUmzLfYD<|noP)fKq=jJNSurt(vKUuAw31guPNt>x#H z`Rztx$kI>;0P8*Kas-?}z|IUPG6%%D(LZdD19l#VL)()~8xZDz>KiCA3wX7mHgl6q zL_`3`I-boT-ParNA39q71xJDfOq z1a?fu3Q>ZD$R3n=19^>r_b3V@sb~c3{|Hz86{n%}7Z^E3BghptL%hqr@{O-G+Y3{daqtUz^Vy&1p$8kkEUabBi6$S-OU)weFM?rN<(sm;v&<44p2FA z0)tqRtIr$|xOd#(j2+=0xneMo03hvasXa*Tz;1S8(Xoi)yc1jgm^cmv1OzB>rZ>bz z1XvlgBv*eA&wG%#q%yu_dby=Uh;LCJ=3YA=SKYh_@1el3D&so=fYmQxW&8&7YiKwD zfO@}PrV-nj1Wo{;Zv0xm-v;~d%FKc`W)>ubZy**eI0>bHfEYVJw}6P4jDVoIgYlV2 zkL8k8e4l4{LjXjspe-1J%T!H3o|E5KhCzZ4--Lu$7(bF%CWzr9W)wq6d44jPER?TL zTY4a$!3Ranw=#Zy2A@#(?_Gjxe)T?{LNUX7VSXo--!8+i!Tf@T8vt1E*(p!JuCR!X z`E@{$zYduNKi?Ez-D(u}Yb79v7UvA)BL=F#m0AH2FM`>`Lx~ak6gU0! z9sCP=#@DI+c0gcdd^@S%V19*$BLG?YtlyPr0<=wQN5}tdiv0-)THtH<4UxEHx}A}suf6oK^_p|%uFCHLE7R)egK8J^UNG6T*#_PKshJR7IIP(V|JR7 zn-LS6GKKXK@;|BgCJo8-imlJ%`68olWg3egD`k8h5JiD`Ri?IMcSzck;3Qx7Og1M?S;9u9%NkQOxvKm0u_MtNJ%9=gk9Y8mItZ zxsjq9RZV{=`%=@Fc0CGezAE`8!>i1{j4}QEKK$<=OYpB-7U6xAGNzr>*D<~w5a?q2 z4ST;#Y5+hnIfV)B9E4UUp&jG9X#yocK-<9+F?E;G*lfcN>@c+9({CDacXf?qvQ@Hdo5{ss{E9T*|7-vseB6)spInZ)Ov-i)6m-ae!zY*dKfEobcPF}SEyJ&$f zp-9L0b|Byf2y~yY`Clc!%x|am+ev;q0MKa$I%)6ZMnBK_So0iZenpmWB#X#sW)JvWIDG^Bo2sR00$k)5MW z2Lw)F-~S;oPXB%zti_#?fZthUju3YK(OQl1oG2uvnKxdQTyGr0CnGv!R%uFPNsktM$b81 z8)z_oU8n&7)giAypzeF!+(Lj}^8!?|GXd27AUB}F_@ST%092Q}0D=5;0tCC`PUq`> z$BEpevKP(3%W?I()_vXo4XPqF0H8MHl$BQwDCoX-`qcG7CAVmR^?y5t*L~;oY1fAa z)rcAZP+N+)9P`T1UPFCe(t(|P0e(LXhwJEm%kEf%YD5hHs1HRv9Cu@AH!1H``b#o= z?7J~{gXsqeH2|RgiksHHS`h9~!L=6BKT;%t8RRaJjZQq~}Q3C*m237kxHkf>f hQUd@Q)SzKZ{|}yoR%m~?8 z$38gY=llEX{r>BDy`KAi?&o^0>%Q(pQ)4}P8g3c@0O;@P>plPg(B&@(fKgt4?4Omo z1HcQ=U0rSSkeTf^>P9}jK*{FEdvacNYoQs!CcU+>KGku_HS}MjpfO!7iaKycuo86_ zElyZzI&BH^j4R!~m_r|Ut2dD5T-~9_pGM#C#-ksSQ4yN|DA{AC#nEEVq0Oq&*1};v z3|(~f_wi>$z!-YGa04l5BJM**S zC!5JT`8)6z4BB2(wld|vsAvtrB8JpO<4t;HYxn=L+(G@JEI0N`Ma?{OBbp|RFK8bJ zTaRZ@O=L4nWKvnoyO@cN2U&EcJX5R4Q8w}2=B1t(y0I~(Nj0J{K{_tFQns_2ypvcm zyQ3AEGFSN_tHooU`ee8L0S((ILD2E!ht!c$(*BsL-$bw|SmAUpX^*-PHO$;k~m(Qw1r$qpXj1)v#HIh5^RO zf*AS4K*PzzD_uIT*O#mIJh19r?*R?KCN_F&_;KjR`DCoOi&h=a%!x1CdfBpLDU`rA z(#c>j0ul-Rf|^b8d?_ybb!O+AS>6lwcP#!P+73NB%kF(Wdd4Ucl;wK?AbOW<71%DrTZVOE1UJJ|?-o*e-j@j{Ojz2O}S>b~upW6Qf{EelnEOVjnP z2)W^mVV=xFZzl%n5DFO0<+>=Truqo|jj4HwV#^oKHWsw4_tZ&!E8-22``C;29Swau423PCFVZfK@k6mOhFKD_<(>RS3o@<7*Xg*t_)e16D%jYn>xL_$n zdb30?L-{M!WYM2CjoSWX%8@mCd>Zs$_?gDYM-Nb|dPu~k^n-3mu$_%H1mRQrO8Qo> zy6a5*rE8eTxNq4ZhH!qLsRpcYgn!(rH^H3UsHvWzni69r=as#1*WdUo{hA5BxNK?& zqSd0L8FBzx{+x~^jx?Q8d^onQeyZ^}piZ?BETu96zj(9!6Yh=xV6LQF&ZD~bir3;NT<{WiTtRiD4*lItaUkK}6?q3jM6lr$iflVcmI z#TsSoAEjD`c>&|Bw}W)GXqq}YUbz6Lsb3-HEik^WrHtR1KQ+2<`lAr!^#Df4H3aS% z1a+_D^z8HwZfZ_=&FaOH)FTCELP|l#9gn(qvX8sKd*fjz`_V~H>Rda&Z0I!9DO}Nf zBatg;Ccql=L?Z;-;pcGliiM!p1R*^ou|G>@ggrmo|Np?y0xMSt?;bX(mVornKo%>M3JR z(Awo?K%<|Xe4HxfxMT|Fhyn0>FO`P)$@pEGy=tyo8o zo@^L@z2$UkSG9;Z=Fk|KAA#ESZs2-2!Xvq|YmU|WCLdAs^DnrSn~yef;!h?8WG=Nu zq!sO-a&^M5-RODex_jOYfkRM7>Ox_L(x<%`smK1eVz7yHz^Ltl>~Zp<)(y#ww#07C z9B(qaCJnH$ol(pYrRXH^i{q4Pp-H3g`<%%PYiE(IlI}mQ z6W=P#&(FX&-;vl8ty#wmQfncPMOEz{e^eF-F2G4>YMD zq_=H)i7ITY2pdHRu&B#hg3{+bNNIlMr=%ns4NF z`(uG^%Yte%;*N4P{p5}nDv73dYtx+s@xlvw z#*k(`_hKmC8pkymjI0}}+YlvURhHLu^qOKWCUj~GJVxy{B)@EGm7;qAw?9T0F3T<>PGmy${>** zb7eVW=NK}EB|6hE$!Rq z&qB0HGq>&NQvK`;N&HcwN?PEppsK)F&VOJj*NW)0M~eaU(nwRgH~&Zh@r&V?p|7 zPZ+c{kJ)lm_R}Ls_Xn8l{O;2u)%j_y&cgT&M z%N1Zg_^o52t!tscea)ufNcdvmqVDJp<0|O-!&GG^`)Rp91$ukMy1(O zgIGqvY|rO=xf|=f<-?sPA2g-fLMHXQmYJaOvViUfLe7Q`9r>(H0~awNFPinJ=f`Tt z@lp-etQI*G+l0%g{lj3D|Fr(sQBmdc#cnlje5`qZ>T1s8AqUxohuK9o=W1>?ba(t2 zk9kpQ=r(%ah<|$3k*BIXeAPhI64R|vs$#zv=eG0bDaMVFGd2gKEA3^YwP)Y?7wyL8_Su`P+DmZFdd6Pek+xaW5>lr0T7|DVb~8 z9joBcX5@9@oxZK+5z9Bd{x{M>{I{s=%+)<-Lk(a(n@t%t;Q384d#HV>uh_^#_|Mt) z@Y1>o5vsU1pGy;LA$0@hp9_LeZif0~E(Wc#v^q6G!}u~6m6s{vxq*eC$TEHFSI1xvr|9)TcKE3yNPpzB`9pU77HKo=zT9P zF)SaV9bbFh%^9t`C`@>9dGx~=$zo5Y+xXq$p3)A~B=$4?ymxJ>)99ygqs#95gF@#a zEE82!;gP{@TFl8QV=C&`q{nx?_kCoWiSaesz0i712sFfjmPF6dD&QFWlgB(SPho}i z=dH1Tvy2^f=d22~80XY6$cQYbgj)DFf@tX0@EtF; zA@9!L8{#&-s?weKpK_L|ghmO1z&e-j?>Hwf;*Qw&YKYom%ez&N<+JG$4|yc@1<`gn z6%Cnk!f9H+__?Z(%(#&?kk`-lVolQH?4arJEOo+c;EcwDI^rcT0F43kJzqPY(FS*& z3Xl|^Oq+d!6^P(0u5R$cuc?I{Q0c#OK5`2=eJ6ZSn#sZW5iZRJz%y}vtxidse;Bna z=cP_^2-qxqXVLLir&i#cJbwALCM;9DmCoQ(dSIY=OaOS`Dfm9r*?3Tkjo-S|?JKbk zGoT8SB5Xgo{hz=pOW9BRR^&t*&W|w?(y%lC_z?7qScFBTZlVAz#KCoO#wN3u*u(n2 z8|H=`-sqW0#D?#lOQVs6)K~d-5@XM9V!?-yHfe4F5JE4EiZGhav$(VF#F*!^vk6kE z)tKT9A8>H@Yk>LA?4K@w4tYlTwKLq(qW4H|CuPyl1bzO(1Svq+7~Shaere=-5jYM| zhqAXoc;Q*gW7oBPTp#to8fWNa+~3B>bHaH0r*2*+yxUxOo@(v?p9mpeHAH>}#c6G?600?rC`0zPO#cnXe+8x$gU-zU3t(CP_`nOfVCDQ{Cc!(}sIQahuKpnS9{2)a?2E>-<)FEuLRTKH%m zjaHAOH9r^oVuxUTxq-SGFxRcX|HLmLc%a4LkJ>wJ=W3@C&n4t%DuWLK9LUoAADHqr zlQX`E(g6N${guERljhbx#5J?(5Ek6b%ZL+&r} zMYBo5roxgS4;ez_Yyd7bn3VUGGH6;7m2ll{UFC_rUg|HEy}YMi2vu)Y3su>)ou!=$ z?K?4>GW$^m?Co^={1SICk-Cb3nOTG?n-J=7{MXbD+z@k+x)uPohReXKB_SGj7Kdz| za9oc~JmCiXh6pH1aPjnmLBcU34ph-%s@44_zY8}JhJj)n0=4#7(kgECNK}jRe9kb$ zjb^X+`7zvP;-VtcJc8}?B0oLv~l^rgC`B&xK zJ#F)4(qkZ00D;Fw|7l|}OSlLnl0OQz7YY%I*+Ly40MlR4^QG87zf&8bf|ckmZ_fuW z6gxTg@CmjeE)Lb@b1%EjZ;x*F;r8DP<2deTVBxl)n0@ZhI47y6xyV7O@3ZfQs}c?} z9xN#t{=~BeU}FDCs;ma$3i~x_c28()@9#P^foiHhWH~+}gK%v;| zk5x7#4uJb-$I9fO^R6VJZaIBoY!BMYTq1Eq&I6#IwD>z*GgFp6VqM3F3)hh8*`m_= zaOn|%NrDjAKrUGnB;;};sh<>M+KFqmWZ(-17r|h&;YOdXIx{sRvh`;r&svu08%GTg zpL3%0>17&~J&Nh9Cx3{Q8V3S-vxkUNv=K-;3-M@;vxO_7X<+U5uy&}F%=8}yzaOJf zuGC=2+0D2(flsHC%Y&2+OZnY=Iy6&)YPFY zIoct)rK>U$454SJjAXA(5rurPZ%my%p;mYFkk@*<0{NzOY}RT9?mOR`mk_a zL|j;~bI7<+Xpj~5iJnm!Mkfw1E%lg%a4k6nq*{+4p}S@itbCn}vauM_l%ww@OzSOv zd@I$U&~_GQ&!=k*#(uL?8ZJ71J?HhN#pPwD_@JZn!hk9KOVJ|pFT#I+o>Gk*bZ`im zN_JSi^B!3kA<4j<^{9gC=@SgsKO}Xi!m7=Z=;1e%O{*-^j^sL>hIgF4*sjOQYo!3D zO9FpXcs0-CO*+fnb8XaQM$ao~oxh}=6E*KFxKaaC`RhXu4^Klxz^_?QNBp|^U0dAa zTb^nhD*kxD7~i3|Ip$-L|C=BmerAPExT>LN%_I#LsDoas*y98lj@3XYp_HREoG-1e z8{m&i+lwxid?)I9m9zx%-zS`(e{z!@a=}emA+M|%e9J|1D}dQlCz(nlDoFIL%R=o`X5k%<1OuPQL*XdA$r1FIOx@ zL#xjG%y3#I-2`__;M07dmcVB#-B|(eC0~ogh)1;*URMp|(n-D5S zxO&SYVZxpo=`#E*Oa%h6pi2O3JHN}Aln#x>aZ_}tsA9u-N<3kb-z$s zf`@9h9_5&c0!EN~oaiF$F9jMRnj%!+bc|Nxe8QH^~6`p>{`>>&R z6)c#7U1kaSjl6TA7BR7~H>vq9>E^veZApVPX2FrSukLA?kw)>Orim}<>gb@hXADin zcmxfg$pCM|p?FY8 z7x{iad5hNr$ou{n?HjUXW!9nrj92e=q}e&w-*@g60^}N_!tyb+zNDRRK2be)bv(UJ zN}M0u>~fN@7QuKnfwdm^c%XQ%TNPMTneuLyh~l=52qZ;4eBxTLIO>@Xad4d^*m}JE z?QkX^1@hQUIDp`)sB;LRsDh1o{+pfiW$EjI5?DsJeNc@gh~xZi?ueW%p~Qp)i#)+2 zClCdl)_|sA0q72qkI%m;+w-|J9yJLt{f{*|e5D(_sqESr8i1?ezx~(Z+7b80~^A&>*OoxPDzJ%cs*dv+Q zg|i(pEyku^U6{fKz?!AEtk1}AU$ag(R?)FOVgy<42TgenBo+Y z))>B?>m&L4>D_KaJvBf;3Y9dtax9*_+d!tUJN3OgNPbMgON-6U=greY20NcTJX0i6 zd`vVfe7p~mXaMI##K{_w-JYXK4^1fAu{ngr#}Q>`v$HcxRT&YK#mRX~+l<&opj6UF zG*Gj{&;9XU25_r9f523%@;OMT0wi!FOTh`waah8O8m5qrZVOy+$0yAC#kP|)r&tOm zfm&o9BR-Y{zAx+V2{=Tpf#9=yP89^wg}GVF)LzLGtx3}3&!Ri_FMe+M6!t9a(SpE; zckIr@QgNS!|1bg+A{40+_YB{@0k}p*&k4LTn^&oHW(2-cXnroA@#%MJ6wQ7iYH-$zXa*0q8YG*sE=_fX)o)^+W^$GjNC{DH=TDQgL@ttkSCNNu z0*z&n0u(9YZx?>YcSlYtHqjQsP-gSuR7|tMr8MD|UU%NEZ;EA4m|jDj?B9l73M1~b ze<33c`5w5>$tKq<5#02+w3s97K)9{e_O(XmN!=!s7VtK1Nu(0B2B5}B#~~O+;`;?8 zZOW-#G$RzqI<6_|?sRI~my>SwjxFC*SwN59mqU-?+wVsjyTP}H6nH2+`Cn0ClLNWK%>nB5X1NJz#+Jj`wJMsp0B;!jeE_=+$kt#;P}*v2SCQA24mZZV^zwU#|wb zGk^xE>VDB&_lLG6rVU^ z)d^rhNzY=m8l1BU{}7Gf+m13B$34a5*E~Ny8c3;^K2}whaQKK`Wl4@jKj4F|nP_s6 z{vM;N>s3|(rZ>Gdlzm_bsOLZr7Mx+1cCowGw$&rXv=YJhXy}DgOR5=cA3S~&hJ$q6 zz+RUw#UPv4X{+6PUMJ#s#soxRpOz_md-O+C!ut9ANBe4540_l-)0xC60nm<<$#I(% z5k<>anH)RK67G^f(B;AEIg;;YX}I`K?@9gcL@S&nOx%fVd&#^jG^2XLz~L_#Pb2vq zW_x0+a|4)a#cL%`bXlsWxja zWg%=||4MM{7X(NQt_3*n_=(_4MF~%)Z*&I>px&1~QlA*G~TbT6hr2xRLrt5qbo zNKy`#rb%jhJ?lI-(IioOm*M3VSU_z>WNoLqsq+ZkCPyuD^hCXY8F*Efca^#(i0T7t zLFZ6Z*KSP95^Zp#;@O7&O~QKszdTVC(f>VKp4Y`~k9AIluM7=cv0_7cgrL{ZT^;@}xKj2`o0QaN7> zdHloheqp9c*$4S`xmh`&6GZv&ARrXfZoc?#qzGOROY|@ybMjl)w>BWPt#%~Wf0 zoq^xHAY(Kx!p~g%qD_s(-~s^C4u-9k6#UA{DlGkXgsx>VngUp*+_$<(iywcl)RDF1 z`tHUeOZewulW^p}E!wY8-`QX)p*TnU%qv~GhZhQ02SQJM?#wEcZ|V@$17%M*nP)51 zusFatMEa-_pa3pXw-bivL*M=)$O=PQCE@bESdC2az|LP3RzDZGtel(jO{xpnnaGcy zeRtAW!}zGx?b%DnZy_y{HchyYwGscu&6eglB1Wz}@7_MmCWIzx`IUZwUx&^fg71pu z&p2m-E0A@I-Mm^Q)rMa*sRVnQpcQH;)RG!wB%kB-$*rp_h^vsNU$ACxuk^kyHjZj{ z4}ctiPziXuh!#AWrmwyC>rtos#bC7~9h4IKr)gy#SGMV)a4&)Z04Ao8Af+uS50JGM z=ak{gD+gKCE=PGj@z;4D!$;^2ot`@t8+Ye-sR!I#snePy#PU9`Q)lfl+1m0MeiBL( zS^c*qITtX1{n|kA(GB?NG=T)1s_&7|bhoO70cUUW2R49ET})Ki|K|H4>sJ`mvTSnn zGvb-ku6Cd1Cl+vYGcJ`=5llLuN5?ljYYIyGx}$0RCRX(ufb~QkmMcej)FIdSmMUT` zt)Vs-d?xUbBwWHw&In%iW9m14@1O^I^HFU6>a{?Ld-x~IDW}g^Ez-YQfBc5aEV#6Q zN$^zS77&J)#zv(qReSE5H)#k%7v402nkrC@R!Rj#E24e4iNi{uedAD0i+~q$;G72- zF?-<@c>aN>Q|bxRz6jK%W33T57u%0yc@+h=;@Dtq?Hi0nEQ_=Hc_9xQ;Gvypb?HzBoa-nLaTk<)?$sAEVn*uiP;Mct80P3bO9X{_jW0 z!NYkgw!kytn|OFpYmvUSG!LLYgY2StTE*tiJc<2tKT>|h$`^=oY~N4%hEYJ40Kg)PEu=Yf1PGlw^X?(2BjxX^*lT(3 zIdv;R$EV+m^%gUS|Kv1-5*Mw6Zp8q``B7=O#ckn(uR@k6Ck~e#Mg>5+ojjxVT{xl* zCsuV*5~-y}_>_T)J;kgIJ9n1e4gZ5m1+GaVzT&comUbJNN-m(4HQHOjTICYO5!HtG zEBCx#?{76l-tNg-(^$F_CU3fnsfpao-NA&JZZ z%q=GQ%~=guCmlCq#0ibY@#iZt-zLi8!w!PFL+sf;P%i8QR2Q6yFhw(}l3Df+Oi8RaleHcgc0RtK*Iz7;jK{qKU#xt2 z?IrfB>NiV2GhM-2$?R9G=6xTmHTWF|tO;yN1fYMsWj2N_CoW4#ZSR z08AGC&;|r(q1&w6=WD<;AYg*@q8Kgl*W!H}GI7;mJKKEjxoq7?UnEXgAZ0B$#aPr2~CHG=Y^ z{4zM?UyOUGBRao)S-&mI^{fsV(bo*-gz5*O9ly++i4zW*(38+sj5g&tP+%Yd{Gn;Q zZ3Eg%sRxH#-Zm~@m8FROmVc%v9k6Zxhr&4vIe8>ygDJ>z z=AfYSRV)7z2Zsxt)9Dv>vTnR>X}`-4&qVY*3Aj~d_BWK9*7$&A+z2F z#Y!B(pyHBnHfEr-F0k|CncPYarx-SJQ07dJ73s^mU@96`CH$_iQETX;XJ#Rbyvogp zOQ~Ieu<98%P$Q0RFJ|PBqMv)kv-X0(+bUF@EWKk@o?esmy@CYUIulxG&TMyQ?u1sV z*dh-2=QmZzhwBo>m>rVEoZm!U0rb^uu4i$;S%06M##!|c8?i>5{l>~6Df~tiPJD3_#BzVMSiIS^ScJ&&c z3TwI3k!v=T(Na4HZ~$p%ek@4K1xd~SB>(oCZ{&p|T`Ay&Q)Ur0GZ@loyA={T)9)h= zj4%~7h0uU4+b+rpfy}d`sL`JZX`EswXCL@V)MA>raGc@9o4Wk;|^H>yBzxsa3fZh^d7 z-FC&-baf!qRkC8D*?HK~=6|e{-Ld$t;&uewRI+6NNo_-ce13A_;`!~SG1aBuojRdC!q!v9YNMm}5l#vSe3&Vv#HvtJ`j zzWW=?1RbCruliI88Oqf%YiAOIH~}iC%V${7An6o4@ zZ8sx2D^jBOC$s_+F3$@q)wD@wZ|2=aW!j-cz8$8=E$&qrRzt^RrKNUzJNc$5x>1h?oj0I`vxgyoe;ohRm`%)65%{swGm_hE*f;RV`jHf_W`&EfmGJ z|H_=ltWvaX&uDCzJ4@e-=_uVMjRuB>ht2VaCG`?NRi(C1S+wM*P!*800znP@)6n3` z^Up>{pSG^6RimGR8lQr9;e|$WL+P}6Rn&q?1t4IYut4#tKKP~K#Zi)$Ez_s+v%C@X z?UUc%otmC}AQcnSo2;;N`?OKEoPEbj!g>R@rO7Fq0?Vr3k5VerD>#b>xV6?AMUuV&pz4a71u?(gWFUzz;Zu358=yA8b_QqX~LPmFS&&$sX=H@EOg zp#9}znciJd1_W5XXCU|rX4DqTLaq8PruRV%0Q?Y>e|B0`wi$o#Al047tR)~yx)!S? z1j`&~=DcETZBBbOI(;c$ccxy2e4tU}y`qd#7))6XXMQ9{ zg;jgyfS)Y=U#o9L2-}UwNTpqg=fTPDOVmN1d6Uv6J^s~&BAv@+PfuZT#IgPRZaYd0 zJW?)4bvYlf$4|=+zFx@KHeUkm@pQ%MuGEgVOlOKtthJd|TLguAE%B)scrh84l~!=H zLXqVOQ4J6H7QV>X;`iNfYO=P)aT$u`+ z5H=}I^;POW4;whH2)KJ^I%55kvy1%m7rCl1ai?IlCo0vy75Bs%#0o$Ko%?`FFXB-k zW_?AePr%<70RcS!?G69%DVyTqJw)>yrs|;d$3)j6f726}poPY(|Fs_11*?6=_IXiX z*A*tRF)e!LV~_k;pwm0&FHS+I2=B+GNFjFOT+dKu!CTF@4`O#x1t|j>p}&F$DqQiM z@kjV+LPB@**~RAJLR@~kquNqJJX8U&_Ko@=+&jpQm_s>9wBB(XybXB(VAsfpsfk`~ zdM@+@QCge!~J=_P%QZgwVP z+QUDG3Ctb;MJhZb=vT{J_`QjGN98O8vXDJ3;DV3K(5<0^^5+cZ0UjXf4A~NbQQfz4 z9Ym`3`>8Hb36gWqfUVq)rZK5#6<3p*w@IaUQ)NfP&wx!uggh)nB2hiCQ1j?TcI5fe z&H~^clIiK_OqC<*d4*}5boHH~w7cT7{fE8H2#lxpsh4lD59bk;%&6-JW4hkzw+q7E`uixz$Poi@T`->4$1EIV`>$5RZ&^w zm43|(|ID4cTFM8E>kI>2VY0buJN5M19b8e(&O3}uCwsNMZ6XAcXZ6d^RUAa=(~2u2 z0ZgwlU2iief++KffNrTYdPi*VfOw%_lxe>;ur#9rNcS5QX@0(?j$O7x79kdIs}&Ew ziSqoKMk%8oi#8T8o=0)txX@XpoI~_bjsvJz@?;egk9ohF$E3hoSN>41*1JK8I|sEPYd zP)f}l;22(`FWR(f z??v2sfEIs+-+6N(&nI;4*cN|U!Cm$s{K-#z91MET>p_d&hm)E$&EO{t_sV=VPI}O@ z$D9@O7Z>KvH8)J^k^d;O{NW;yKDeN$S{hTT!HMSo#Lt^S5a=BEQ93dB^}mZN2EC!8 z#Kmdk5TV2i`yIUTWY63eYiAK*^z*4E!j?%d&9^6hUpZ>1#>C7ys-gZj#`Tf$AhPx0 zZHCtft@Xaw&zd2jajM-ky)}%&WZ@QuG)5YBLThdx^YP^3V^~lj8&wI!(`>T41MRWOTdC+9 zS3R0-+rIlv-4wLr8Iu0S7NE}9ydxy%}y*kMg(6+C(V?Ud(CAo{2ACO1wk9zSk3NyZq*erSRZuC z7q#%@T-e+qPT>_}1WPU-mwqFldLKsouaN@0cFwJiEx988)=BXUc(|E75>Fu8S)j$t z$O72A#79J=cXeuok?@Zdk}Di53Bbi%C+{A*EH&27OOc;Nb3UUuNDCeGB)@Eqv3tK$ zulYdLZq{eSKpv(W#H*&)S?XnKkG<=_F9v@_>wFI>>YawdmbIC5c4tn?-+T7Bfi-z| zh2m&pof;PlvEc_8Z^y5+H3eMuhOYdtqN*R!KHqi}Z2)l-Ikw1S!x5i0F-eP}5wSZo z&aZXeqB!og@W$6CFW%KG8~v~dfj^(iYA?RY1R6;qe_5x{;=6*okpI1!m2v$nW#hlS z&F^DbIJoMaF;ax1_WHjv((=P}oF~~OBxGxzn%XnsCa>tBzUKUtCs|&iaUqcX84jQl z9e8h6Vv^uU3s`DilQgAjN7E8jn#lGK7>~)2nT1!EegK*EeC3DSt&cnZ&Nv*!k}4X* z;p$l@e)Yel^c9xBY*5v&j`l`rh@x;Ge%)jm+3P<#WWJ%lyo;BXIN0=>mTZ)|)O3r6 z;8~vFv_ScmTw8@vFTNiG6@dYQL1^TkAe4 zq)R+I(kPCjao1fiRq<+$l?f0;`>#{aMysr8F0!b|Y}#hp6WKC>}DEO2ZMq zqQwOHQ9OJb!&LZ{OSUDJL&dZn37Oe)BAqoqCb(V|-(D0@KS(_!e?AfI)B5~7^}(x3 zdq`d6gDGX@fWNoJ`}Zi~(n~Z==rFg{wUJWTNj>k{7D~ek9AjWd$?XP8=#iS7kqyX) z?>{7~IC9B8nr>+wxSEZ3or=LdICZqUU^RprE&b5cNP(J)VS6C{0TkSl1*Ci9VHbY1 zRT;=K)@T+$ABJWB>w9CdzJq|g2mYAGpG?kF855`>=dz>vG<6X9iX?STt=uF&_C-cw z+#JPB=8;$Z$wC{=;z$bJXFktbj77mHbZ~)&iBrX;MWN5CoG{&B49JX8guK3xzxaa z_rTJ)34uwl^aAV&L=lpfj%v<&jD)>`%K(ye`kX7chc}G znIv_{rg29Uq|E;daY}LZ7K3L;tHT@9KOde*0K9iUcMH^FW~fYd&n1qICf2;aiBPZF zgSNqon;7sF;PklQo{(whkc#0-1sh;l$>y56o4|MJi_2Cb_j{5sU;T~*)=ycPu-oll zc%~Gnjk$u2cZTBR1L*{?$H^KZ4cBFb=+)57{%(HwxD=LOt8f@H&j-M0iiQ%N}d>w=fS^clh`aHAeRepog8kW9b$zNvJpr`hP?T|Y+k#fyU~9Y84Aml@T|=j;GTQgDJ&4*5{$v>NOGnYkRi z%Ai+&t+qV*@7RPhVUl=Up*o;&Z*Ain1-L30Ropv+9@ zOzHaIOfy{iPBno%lI-ZZX7py(f?wN%n8l8mnd2%`_5Qnf zl~l}Xgg5FFqLc#=CDbNrO3oB+GN-x2`4bE8P@8@rlr8U$8gzI!;#%UF=tM|}vaxftBTY-p`w)vWDtRy}B1o0Ako~vio-ClF0*s&bTmcGvQ1JzyG1Ohf*xdUiYaEkw zR@j=Gy*1ItrM{=Msyj-CZ65M`!@JrHa+p$K>Bl*-)FnVa=)3fVxgAvpkWH#7_)1#b zb2%N|NsgReML7Qrmz`6R&&n1`8e;k$c{j_S#`qeri1Coh*t-WfUy3|fAJGUI=~|8O?afh)Opcy=L+lde`MUa87gZ0F)1{g?f2vye|SEVD>D z7I>pXXw*lb;1J~A*1b%*LVx)5_-O7;F-CHhp)$coPtjHr)1|^BJ9HQJ&>r>tqp8z1 z#A%-Zbx6<}&Rpnqr_wL+^`_aCk}NoMnJ_hJ#!BpV006d9sDb)KlUDg0B(pH1{9;#KWO zr=ykCnt8THG2l;zEnG+3FD#kL>V9bX=(H9<4c5U zJqHIh_D09?o2d7Z{+HU-W{3X~D4p8cnB8HIed_4AbGU*R=&I%VYm8zY_8=k`1oHLZ zLg2;Ny;3HTE$~|YbMB-kT7A1ziyY?eD+NkkT8pWbE;nDHC`;9@X(vmWr^=(E1xY0fwi!?YeML7L+KV0*4gjhf^>- zakw1%%nnz<4}M&p|4XFNCMmeHm@aUjeyIRShR*TCU)eES{BRL4wi#NGY84+^WD?`z zbJYR;r)1Ri8H7^|{!VnH1d3eNIqSpBRUj?7#3Kv1xX~)c%vR2maV#@l?r=@_*puL) zE-$1w)(u@mOmNgORl)YCgPiDzw34MA z$-ImW9WYL2j0Fa%v<AfgKnrKhCe%@) zq^`fi8+MUgmEcC8ZWUlo9p!V60g&T>dw{A1p!B*x#fz_+p?EsZDFqeYFI|Xb?|uL} zSL}B%6L#oL&ZuWRX6d6~^Fjw_S47CAv;IrN283%XglV-+&$dB=cz+L^F4eV3v7j<4 zD6j+c8!OO)x<#3<_#hLEpfugy z{*z9(yU$-`11!Eoq(fKu*f$I*(;F0=zZ8+$uvLi-TR%?6&*?ueRE5F(DR-?8frqT< zY)Wxq&{Ii(J$xGs=&l3bQP_=?8tF&V1K$;3@u$ykP|OGkQK%Aw`-{$if=??>qpl;w zS2WMWU>!_coXYL_lHzx1Qy$?}9#nICmREV3AumRa{0oAe-r+cYQs9zlbYmiJT2KP# z4q{Os1f+>iE(e-`MjHqgS$_QOjt7Y4Vbv26@Ml^L2C83DRLgg719>s^z)S2p2$=s? zJ4ay_KOD_ae~*@Z8!P{?<_%b5c`xNX1Pb)L4jU1Y(~<%6UwWRVYR8Upzr0{%9rA!%m@z>|eZ@B1SF@wLoYvC#X~S6Fuw5fJ|?<3c?FF_c3rf8bAwycWyD znl?l`V)7MK(~q(TNdfweb`WeA4d9qZ4McmMRLCzXDuJ2aQ*68jr}>ogVcD(gG(Hlq zWtE}|-W4!yQY9a*36ckz`GGpWc>q9&#kFkI*t-1b;Vp*)NqMg+DYM&a+~9m0F*9Wb z=Rr3*wB}A(_NEe2c)$RiYUo#x3^nIl2K#TC7GZi0BHn+Ce+|j+Fxp4A4#hbqB0^7m;;MJV650K?#f%1W+8IO)u?0GG3iBD!_+GQ5K;z z38A7F(m;WYp^)WGDi%PUJ_pD>dkh9b1kmt*(IEI)xyXJ5ajKwyX&M3~gUy*45pSdX zOGl`H6I1~`H*iw={^{*I;9>xQp}@w1BmikJnhFpKCqDsD&>e|)D&M=*@2hdQZPUjsvPVEqD93LQjP?oBidSQDxF{eFnt}t1Z-3wrpcn0i!v{M z;Jo%wchQj^ICq`klM)4_XU`8^T~)iM-1!uDpwQMUh={Obhk8>wyZFnTSgKG01q-zl zR?;)vK87>#u}-uE(jSdJkCqD8)^MQU^btLv(5{0mJ*oM(_n&PXBY@KC{-^eTO}&Xf z)ZZKb{hp06wy~!$L)PqDb|xt*MajMvMI?J6%#7WqEK!z(k+NjZUYH?S+GZ>vlo45z zv5#fUJ>TE`_}!0t{(zbH`^-7lxz2UHUe9yVgmI4NMs2*l&|UH***-BAMpfAV=@cjf z0>EHHz(RL?H*cNF_B8#I;nGZC;r)u>U&gXFBqqz%yu+V?3yB z+uFpRZ&9-M!>12%ed9Z=IEXVuH9*spM|q-UF=C^9u6^0y{xoKC@eW`^=5_|m70ok? z-z#+WZJihb)F?MdIcKG;`2e+*RRlb-CH0zcm)e2h_O>_~^w;@Bh8mx(ZPhTw8B{%V z^d#Er8>9DImfBwEP+8}%pAQ6C|607U*K!LKcokh}2&hR5;IGVGrz(+HJguE@_}=N3?$ku}K7g#V;KSBB5s^rmmJthw3gDnb%b`^HeRc z0ZYWfVofnRG#@Fws3Hi%VN4j}B#|}wGv>}o`z!JHZwbK-KlPs^D8ul@F3+`@SK~(i zMPHoU-PtWCy6Nt{@!jYkl|l_hRJ}&Mt57-uu`euaV*mW+bb<=*7aQ(Qb1Qrwe%8{` zq=;U;j>C|EX9C@bV?Hc>NAcR~noaC9B-&TuDtVYm$c`Jj)|IxsqB)wLuh4ZR{=yk& z9zgG)x^1k8lb{@X+Fx8c8^0?1s*n4b!;HjTWbz+`6o9)wI3Ha*w8gmi*)NqWQ~*(9 zYKW@VRsUOXUYO!_v2U78XS^cx?KM)TB9+`JZ(`oZpmY?i9e*aD4hy-stm;^5Y@e9H zbxx6xFhP9tt2bu*zWw(iH#?f)LE%ep$n)@1@vvt38)IHXuVfX^-=xgGKjO@wfr^?PJ9Lu2X zPVW0!2KsJ%KyKihc!fuV4CHBUK|V%$WqO=MdJ%ieve8QYsOdck((nSSXy&&zknv6y zLwdl)4dXyd6DSU`A64OIx` zscW;3*|t&<&_-5$<1XJhX-THkNhMbnu!CB9q=%J%CQb;Mw`_~s>k8b~jSrwf%07j?Xzi`(*zX^*f2hLPAQC`tj zG#2Tu(LgFt+kfnE>#=0>S=8lc>p0 zFxJ^uUmETGd(4~dw@pG`JQ}o@jIq(lFvM`Cwapijl)7*`9NK^Q{(5qQBU}<>EwQd! zN~BG9)khrK4aTe{@xIYjD60wF3kz(FBCdjU8TB&wXl&@e;1lg@tR~*tqv3$~w6Of1 z%wH1Wq)#vmI-?YN!e8ZhT=e)|(rNg4@c50c9wDFS@c0+81tNgqYSM$TQV!1S-1%7$ z)JfrxuWxu5o0<#U06~-aM;!`Dp#O3^MUPB8PD^mc*g*-?Suh{WnbSL$noRg(UO*^= z93tcb1^eU>B51YJ?!S8T-48rUkL`5T0;F;0K{Vrd;FTxXX#fIEc^^A!$|c%5qr->d zyrT^lTyk%mlYu*<5BE=^8{AJ^I0{-l_l0>oO_av0hf&307KtF|O<*nspvr&?Zg_fZ zbe{`$2Lr0xm^%za{nZXkRrXRTILziG4Z)+pvYh_;zaA;OqGSO@D_@ zIO+dXzF6M6=U`Ie|639KTjy~XhSab|H<&z!YsIE3fz#khJrK1p<;#fQX#Oun1+qSNo~I^}q{>FMSZ|yT{u~ zHrUVuHIg z_MW8(JcHG3o%b=&73%qv)B{_l?wa%AC1TU_CB6i~#w(KRVGr-@NJJFRtamk$n zzSQh&0sYGFmnPZnEu2K$j`Hlj^#6CquQ!MM zS>e@K>?G;r;kN!~tv=JwU)R#*!0UTV-RN8%$mxXuIlV z$*Oz=XkzeHw!c#l|H(oT=j&tj5fQi%1ZEGI{~%es2O54#<d@)k5?CzsP zczW)1X$9c+uCRBh4p;IDs)~+9MZ1bCk92$DF|k zq9YrIP=_)9_rVuChz$J<2Bf!Gsf*QDtOjR>Iw1q~-wgE*ib%ciNBQvuCC4SinyH&Ak7my&#xvUS~<*qvyE(m+}d5+ z-3w@y-=LDYoMokCys75J4crzeMt3i?L!_%6Tume8S;O zR?a{`9vuq?bnb8h5X?{R@FR8g>RtIN5M%v#4+u9ndJMWKEvb7PHym59h{KT#P6G#r z)+7XJTQscA1#gYTb)yNN|9uAx3Qu8p5-&F3DBX_+b2J>i>B=5y(~oImIc=^9FFc+& zALK`>>?Nr&N98esAd6wFkvs6RjcI!`Yy|81+Vj{KlU2jE@9|7AfMm#ej@HYo2W(Y% zaUG1BvC$CimM1LmD(-YQzoWB z0Lcb__r-6b&v(lK>9OxKGrbKa{rbKw-?ff_zTJ7zr<|d-{`5FJH_|U@`VIr)c3&LI?m<2uj>*7q6N!J=su&%|A_8^p z0$Tc%-0_OFE8&19Bd6{yv!6yKiGg@TEpV8e#de@Z0x)&z{9fE?iKOlmwmVBc$%G42 zUvF;@#`kd|M=a!Y@1(^Kcms{Rej~HSjWsf?IU3=bz=h0kG3nJH^_`9l$ea68_TFJ5 z-SQ~Ui6hetcntHv!TxSm1P6{lbB)%nM|^9eOkyrUgXrY_YztEQz#2)@NG&jbG3Sr{g+GE#&xX??b&`Sc>=4@$dwXYZa| z`+{SRq=&rJ;qAQt4T@FGn9fpC3FB-YHVpeql3AyXdCT-URj)~B&$RzD&+;V}QE03r zG!_hu-y^Y15wD{=0?noF4uyeQ|6-2Mdk&B^AmGjnv^UHwn5B5BQtN|gSrn2V--;Q3sH#G130D7#J_bDxDzh{S+ev~&cU1g)en-28M zp|}Nt9kC-Y%Gx>YJuuC7vnF#lXr9OoG+q46Jstvl$}i%tyq=zCcP&GX-vc^c51v@w zgoeu^noMI|BGuOSmU7~<{C|u$H|k@o!}G!1xI;9x;&Uh`rTGXvc*%JrxYA+@>P^I} zSU5CAm;^tM&ni8;`pD?j&xv{H^zg5zwESCF3xVw?Q|c>gPWm%)=0|*~0rgBcSCjzG zZoLOJ!pw<+0!73a?)=*?WJJEKi?T z`tPL+fa3&z_1%66b$>-$c+1w`d~|9Z1Y=_tI<_SSv}kc6hN_pvs=r2Ylsu!zxi4*R z`=4J42|JhEZTAk*x$~lvn77C7w-LG@V7Qedx|5%DXcN4Z0JHy0oEc~CKa_L@fH^j$ zq@m}Re?L8P3)f%rvScfHl1dX`CGP-9ApXD8veFLC37vI9lvDOVzwtSa&KKQ9ceu^& z?uu36yYSW98dmKrtv0LV-E9fOH;oVqgn1k^8i`vY3l)ljGSn}0jQdKY-b^jg)9rWdMC%p9 zS=EE3+Eej)SYC6F%ZD?P%n@w*H%BRVzkZW%hwMC{aaL9!clO^zA745pYo?Kw*n zM~nHBzZ`$eYCnUt46Iu}``F_uK0EP*ma9IZhoqf5NLb!z9L~IDWQGGO*P~ZN!ywsW zW;w>hA_H0BsJC8uST450Hm2HoU-Wk0#zC|Iw%@GA_#I1McKj=&Z=$GE*iDg|Aonr; zUtFEr_9D^@h|5ZN+UEZYf%HNdF3NvDG)AfsrRNYV1mt2n6vgMk=d(Eq8x^m_O@4c$ z>g46sA#JM+fbzbZBr5s{t3{O(&FMk=s(pu_l~%3H;cT*ux^J$|Ypcg$Ay@RA|MA>m z_MRH4`~K${)qlo@E2mB+qPK#_X>GcVUz*D*qaf$Iz2BLFm1X^+#`AL4e(& z^T39=GvvuitRL_9rngWn?2WFz6?Lbg%FiE!;_2WT-VRx*t!&!WYia5S%D>6COVO0P*!Ht`prK}->&zO6HKGz4IrA^ zRH&%lU$p?@DRNx9^GJh{!gjy(KuVSrKVc>dnr- zw?Vxxu${g;&;>E`7dVqGy6SjIbeLSV5F|v9TwHob-Xf!!txN6euTXE7*?}}bYs`iy z>nOptP-N(8EKtG-TZdr$<-Q^4PkcLNePDzx)0B*{YB1?I@kq2j69hdjg!z#$x;4|y z22slRRSsZI!{-F&a*}WcX?f$2_zD>uQT|IC7)ga#7i@pldjLW^w>DtVm1zcSUR%2+^?x(R zPWcESy>K|L?G(X9`LAoU2-ZW48@G&Y$<62oOc5t2N4hIkk(OBf`64yuehI2JFC-!A z>DPmRc1RvzU0HSyXa?IQPrkx^jo{gw(jcOYw?$K=YzGP4=!I+FACD$+Z5lPcI_QO)upR#kM&Agv#Pa_OF`mPp#Y*d= zpKwy?NApoElkN^r_=on&ImX@J=_-04!l-aF_@AvE>0Fu+0EO3ylH(7m+JR~78Jr@H zyh;PTqegB%Vn;w*o2&HyDZqJ=bJdDN<~blLNP5f-32pbfp%`D}rh0i-dkm*t*SF=p zu-4F=veU;0Bwq5EJ<9N5vOn7)D%I|c$(#Uu*@Fb)ViamJqXR3)5Xk18Qj}1WIbsw; z%5&7!if+|AJ&y5M2Z<@jOP+uN!7N&ah^07v*^~u_(K#l#^x>H-9<#si#bN9w7p76^ zE_~HzIjUO~C&$38AJk)p+OY$i(Z2JuuTZA^LoUoyL8@Pxa#xV6wCsd{rw@9hk0_bj zzR+sic6xG>?ZFQh-#X1Jguh_iS$Ock8EM<`i^vTcXdofy?=3C(NX}I>(&uv{o9RlS zKc6po^ZKJ~3kkH%|D48`jv#wW`QC_!x_69Nldc5{~%>BN>}4l!SyZ zY=()ZnK8%ar&IQZ{mR`-7IBrrZVGFcqOq=`6%0?eq~3pzg{+H=4<9bytSPa>xv5GP z_w0dqB$J`yYH)gY>{Jw}AABqj3G~CbltHnSVg4VkPC{%?`J$a(GdftYn$wY|S}?{J zq~?gdJ5KJQD_ZuVWR3#ci9ArPxtzH}&emHq+8eIDTf06?7C2@^bR5xo0Z-yR@dLlo zoTAWSo~aHlu>q%L;&YLLHRoGQ1WikMKA>mv6iHx6G`H+Epc({P(Zw4(h7Dqn4dRUN)9Vq0P_Lyw{^ zCj7PFR7;s7cFjTyE4!I~>93-csU2x~@+0%Q$?_vsnfNVO>GHQ!VXqPx)zaklI>`7M zBdzU7UAE|E|D>IQ%8zkx{#1=1sQnlz z`Zt%@(Ol3ZzehDd)&-rmp&UA~LB+B=MEyuD|M-^CNP3}lyrLRGW^`*|`6d()d8#O5PaxJDc4valwwzp7d>ypxM_tUwo;wS<4rVH(iJ1z@;n>@ zw+nuM|1ROcvyC?u7PSBYS+P&2O5)LAjy3ahgVtWjFwk$FS2`z!|KC-ec7S6=FtW{E zK6d1Hx$Z-!_BCki;y3!A-bPaRF;z6XlW8Sb{SrcnTjFtvlg-`}Kw$2r2ipDI* z#zb?*PwTr`P8(X)I#v?ok>OB-;St`Zk3P{gDB)0#<+t&u|DCo)BPw=u;vQt|us^4j z>}tF3Rj>ho`z40{_xFs|9}Ay(z2g}-i(LMV#c^{UHx2x7MZNO?sJgB^>*)egxjcSc z-A4%|>qG^BRD>5Ez+K{c?#I@m(zx|}{W5=yn_7QlFys%S|AoI~F?k}5a>ObDz9WR? z(jah76N#AxO{T*804)=VrG*Q-scMYf(A30p4RQlU3hQMnoW9KSyLA3WaHAsa_v&w> zXNag_K4TgL_Y8$Q8*}Rfv~JI;0UABi<%0HzDP9se%VRpb+hjPen)}s`&g|?Qti4RA z^u_N35A#@tG$M3+ zmoxY948~_iVTJ$BkvGE7-kue~S?hZeE-Ng#I zQ1HFm<-423OWvP8zoo%bAQ18@9@+}t`;==)B_AztnsSSo2}S1M5whruxwhW zq=58rFPsCxC5H#j`Sg&2U{Zr+mW4fc}tNZ^S-o=0aLoh)0LUoz-dnBN^4{ylP2^F0^3uUE5 zvqL~A5(P*vn2){wMRsQCFvn!7uvzNZ8ZCBUzOe_PRzvD_z4>=AhkC8x z%mlxQNrCwO36UfVDE`V>VJJxH@O5ti{2jDae7d7wU$xR5%5}5ItwH#VG%wN?)h2%H z-^1nB^`o=__kc1K&GJn{d=Wh4;!$2jM_^UaI|RQY5bvxucHWY?{ozV1=k3c+55$Ju zb7Ff?DqlM#@@h!$3_kw;<<@GY-4qgqg8)wjy-MLaPe7yGR16Lo_f0!jKQx+DRrXoQ zk7Up8HI3&##0Nv(y4ADx^2Ghy9G!dV@4zpJ?g2S3$X}7zGc#6M_;`OwO7y9nAa$TF z#^nD;aLPR@)#3%{TU>A-dyiSq-{kMybg<$f6(P^fTbd2xqlbRM08o}g&Z3K-FXo#} z#!mCz_SIQ<>-$y66H1m|cBhGQL>mr(>&6zrzz&Z%uc=<>AkU3W=01K(#_XxuoHKbS5cAhN<_kaw;^G&gRLY(y_ZCa$PERf>N3}fO>}caS zS3p6pAfOoB0FPD}iN$H5^MEs9=?S&5S7DXb{zu@6B>X<-GXp3FlQDfu90Izr86FHB9%-v&! z-h|zL}9xpog?jW^wS;)aCz17gU+ued5sqkc6UqxdXSizV%kkKFr*XXZx^Q z_F7&fLuM<~Aa=E-(LihTP6rJ7qK%9L^4HDJ?>Tmp=e7(^9=1k-qb_?W>nISh168ks z*D%kIN?BEO+8_EJj`QI|7G7^;{trYKQ0um|F!j3!Ai({x7I=WP*Npqf4R~@dUYts- zncUxYpvibD$B*XuKe4AJwQ{0woiF0*%3*U6%~bU-bDA!>k#)){)9U6s2*-~nci#Dw zlmA=Ex?to0&<+@kPisV%JqGVNICj(~`BZh3zY8+lt_>1kUxO<^(SpKGGVz)BT)apc zOm+0r)%#yKD2+GuBa%J&sGA`JrkE^cIc|{@L)=xLoot*vmKaow!4edMp{4 zGHMv(K-IN*!ATCviV8F)>Ye{6L_Y=5ntv{@Q1NS)=u~GJGsu60s%X}93fRvd^6lt) z2ade60a>o-W~OXHGR2LuaYEuAQ4e=Reh(E!jm#n_UXlb4k}}A_5AAAwiO~6&EjHKx zdeI)uX*=|mSx6QiaPKEyRKwG9zpBxkX3B8wo}E!C@!3B`{~{Fm_gQmz@Ldy~GE~~G z6$`a@HFMcHeRpLCufe!13jd(~h5at1o{@3vzK6k~OW1o|aTQA`NNeHdtU{MJM>`vc zEEfCMOsd!UM{L6^6e<&4xK5iXjyUKvhkhfx_ZE^KT7?N%fm zm%95%-F#OriX?Sq_Bc}h`%1ZtM*=&?)!f@$H}QS$C(;0LT2Wy3w)+Vv*drUWGgD~B zbBXZB%7CmmD)m*ht(ogLSz}k!pPU(^9vcu9?zz9hXwx;>*%tlEdwT$G0QK?pVv zXSNDOk0VbyKGzU$S&80ydb7~2!Af^NOc4xroapQfonM92X?N;LeBZh)&S7NTRTk?E z^RDA5q?{a?p6DHAAKj^$CU$y4l?qr4eue{_7`-?u(kEmghdhI>o0q63FfB3_bk(16 z-i_5Xj+BKTBbxk34`NULNS%{>dIDVZ?j)B86;9-Z1CCr{rCO6_Y;X9UbEy0_Rih4 z&TL&!fMVZi|M(Z5k01ga+ zniEtr4&5|>S;Mup&0t;Ngosph0Z9(bnl)BShUOM|afWb=wC{ zXrM;*-z%<=Dd5tvi}{=$RqXD`lzer#9u9|$cT@8yd<?c2<{s_ewt8sL9`?(2&!hw-bd)<* z+_cbbA_ss^poEI9>+EUfS~n!FgPpudq}^4F?~$C!8Ilkoglou4GN2XdiV~4+tOBRt zx@UR9wHY4o!F;8}F>Y!+>n<5I34ZOZD1YQ~+;Dh%Iw>nQ2y*Om@iL-gP6>IDJ}~F{ zE@*UbV>7RNIy?rVa+qqveI*EM*DOQ3rS=a9#{e>1L0Q@WHYW zqZX2}0kWX9t%JWE1Kf*{7j%(UcDF}vaHmi9$G8+-C#iE-eD#*1U8ltb#Vo&vjMBPO zzyKQ}FgnUlUiCQT1A?d@KXiyn9Xi|zF_>X(w4D~n@xxa7ha<$HL3nbP*MQHhB8~vr zlYd?UY;Yw`FsU9fB%`mB^VO#&)vdCPGusy6I_9xL+U+TSM%tVCYItk9UYa&h@FPjQ z2cm+6UNBZEbGN4qhKl+wh*-vj;I9AL0;6}}N{ zI#Gm6v!{N=zL-u2!TL78HgAmtc@?FzlRAwrE`Le^S zJ=0=k&?+MWAK8S)l)Jx@gEcjm*%ccVU;%O6P}XF0XJjg8 z@xkfvRM&z6+Pu~`6SIVA<85k=3%YJeqMq8%0;_A2Z@7XIp9&bOk@8|jjw8?e_#@?m zMlmeKk#TSa2zY=krvHTyPxJ6$ zt>yN|E83#+VjtUf_0m_T-^S44t&ybzv=iOkogc;7_! zd;kZ5&nsP_C>YY?S~mqw6MT$jE8sH4e9ly+X4lzZ2=@ROXXFcE;i3#rPTbr5>r`bM zRQ_i82~Lr+ndn&ZlbHUgvcD!&Mnzlt0cG>~wBtFG%JmcW_vL4-LJrOb#T-bA3(T~r z&Wu01i$s^e-|v1%e=Tv*Qu~xfaU0&uxCQ;A>4mLo#1#V6t*$=#Vr0R1tfbQ~gCS(g zOeD(00|86PmCEzXwM{Q7YA%k})hrUaFFjsC;M*(`xR_ngHy;CYIgS^nx!#c#szBj|PKn7%ck_bpMEStq z%k+!~tJQ#-pmp14xuvt>LnZI>slN^f5HRA?)BGd# z+a^d!F7=}e5RL5%D35sJC-}Q(eO7FuuZa*&BF@>|D)E7pqg1+tRX5pa#}li}LY8y) z%_4WLYit{H`lNMv@lj!!k*Ev(d0YL*`^|o1V}h>ECDWyU)4H*OWIUOhKtMW6hj4iuMge`tsZfd95MyG_*2%MxOV`~TXw`7Q zm`#Uk{KcgChWeO=%k#@DS7BTp$;MtRpgX{Qh@#+zq$E5-E-Lh~z)3A5XPtTx{9X=_ z+bNDwS?Ia-?TQl~N?`Y6L*~d{2gsQ#xQG9BI|)7<%5=On1s?EcZ+ze4772agyVzI| zzhyWDSZFEo-d8}@<@((?`;dwtMoP3iNKM==vw09=iCygwA7cIJDCN|@RVg$fCs$ z;J0!Aj2hf~S^wQA+q;k)b&kiqqiZ1(Ym zE43YV`%ed$WBZSc3T!Zu493L4sk{TDSMk#w?Zvj?U$|)CeNHpyBK`%%91O2Qp(mf8 zmU7NucU4BPq{+|Z5>7E|bQ(bV7gr7aW;9cy*&)eihyVUdxHOuF-?-@tk5V zU`IPx1@!#p<5s9A>t2h?$7L);w3OH%JEQ-0PL-cM*-rwX^U!N*YJHo5{7OkP0x`>* z!}`9kT&ckbizb~d`&yQ3?T6TuL<~Po{=M|$OluFAZD}^kkU&ZJ`=0o4L*&w;Yo!2}J{#AYtKPu~zo|nF; z!$*-65&cnIdQ|0ZeHHi&4%Z54-ka7<3y|XDo|A;tv_+`1AH+Ni<~9e>c$jg95=c+ydWMb5L2uN04Q zc@;DuXr1$&dxiSGAC5XIXb2OU=@zr)fwuZJ!K?J|Tn&})A0?t((XA&W9U;ds>arQ~ z9?23~y>a+sh7lXTugb`tPZk+05wVhZ+>;TmcAodRAO*^Q5+QB+0?PI1s{LNdzqjuU zzC2yKY_)y8Bhq$QlXT$$xa$NkbvK*8(@jB z4(}NqMywg~(F{x+;*&7}XUNuat&~dO*rVjQJcOq&0dIWNg?|3Vap!8uQRI`V?riD- zGR#n&evb#EixK%4o-?6q{^WQ@DIp}~xcFOeyNNEiHa%cCr^N4IfHC?^#U$sR4DjGD zKi}VTQq*6Vkk{0iJ-u*|rqgE1l@WqEZ)1^{q)HC}s`Fvs5x(fdQ*EC;Ny);IhAjO3 zFTc*iJ$7fMnRkWQv>L3Yjz_bh1(PmCfP{z34)`_#O?f`zRGk?%kT}}Vf86^92BTI2 z(LUQtF}_pwt^wQrOSi#+eMMa->ORC)df(`Jy}A6+iTf9+`GJ=dnBx`F;@2&Ue?RuX z^=U;SQ`JAlZWkBD{gO?|T_Jzufi>FR_OfPwH^|#I{z{Qip42?il&8eSmwXoLh^wU* znSyqdU0+W-H?q~3dot!amlbFSIfETT!hE{v{Mgsowzp^e?>2vSl;Y$A(J_{wi>sXI zhp_*aCid@}bhqtAd{kc!{NX5$W4n=@HEs!nV>^=C4&C4$X`Cu8!#u}w{RY}@u59p- z{Jz$w)Ejw$jTr_G$7AikRKCCdz4SeKt-0o@nbt;qMEif2Tg*~$c$IA3 z(1{;wC+cF_UE*G^EoxYGo#5qV|Kbvg1Y76uhTw*<-6xy6ZSG&WKRx(0lj6x> zLHPd8($a-4)nWfY_8)Dj|1u8Qm03CmrqxzMq;d@_BnCIbq{eSV&Qfi`hRs=HXyZpy~hb)iSdL^F-6p!lM#?_#*da9O^(;@X==j*Qme=0xM&sKDVt}1 zL_bV>C`doLwbSgi!)75ZMW2;xx___vAD`i)XhG)$_6km(7+0G1G$p3;2)6d2Eu3ec z(()}-8XP!smvV}RN7Wtu>hxODTl8e)cM39sRO#V?@Euj>GIBi5YKUw%ogE_yFe5ej z@dZlpO~69u^~%aknno|-{ncBfli*pQcoN5rM-rCH_4)%t26@;Mv^SfoKkEOLT6Kac z6h17E62(3P-Rj-7(%KX-O0EQp$@7p5Dp6w#R4&}psgncvwTnbP9QG$jN-f}criEGH zOr4X43Fmv?@A`a(Pj`-k%PInHv_*)a{6Dupwl4bpeTA#9YHtQ-`6x3iyDF@tI&JI6 zTji6YPuDol>8hzg+51{nK1)&!0{+TOhfG7D> z1aQ=K`K}~IoyE7a^L?2Q2X>!BiY^cZa8Gtp0GY1>TJ7M-C7yLbl9T)Ip0}4)>d&aR1StFYvBi?D%(VKQ zSElPm{vp&BRXtEE|2M|%egW&ecJRc}eHU)BTSA$F8L391QjVuulcxQ!bIN-xsKvyB z>z|Otxi$?jof zp1M0`1_#n_5+9A{{k~vW2Ph|iTAl-c_pD=$$M67E$mQewv^=!^RQJro)5{Hl!#1{D zTheV!M7>M_%>6r)ifFz8!V^ujMs7a7``M;%^lR{zO80%}knnX!`4_g9$n)$tZDoQqizWIQ&G^{w7Re4-h&7oe2O zaLpW#u4fmv(L3)&@efcV9$+hVyzc|{^JpQ>>t2Ed>vgrea)s}>VDcvyvAscJ?uMn< zV~&V1J?e#5xrNf~jOu4EfO{?)S)dF32Iv_HFvk& literal 0 HcmV?d00001 diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py new file mode 100644 index 0000000..247f470 --- /dev/null +++ b/qubesmanager/restore.py @@ -0,0 +1,129 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# Copyright (C) 2012 Marek Marczykowski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# + +import sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from qubes.qubes import QubesVmCollection +from qubes.qubes import QubesException +from qubes.qubes import QubesDaemonPidfile +from qubes.qubes import QubesHost + +import qubesmanager.resources_rc + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time +import threading +from operator import itemgetter + +from ui_restoredlg import * +from multiselectwidget import * + + + +class RestoreVMsWindow(Ui_Restore, QWizard): + + def __init__(self, parent=None): + super(RestoreVMsWindow, self).__init__(parent) + + self.setupUi(self) + + self.selectVMsWidget = MultiSelectWidget(self) + self.verticalLayout.insertWidget(1, self.selectVMsWidget) + + self.selectVMsWidget.available_list.addItem("netVM1") + self.selectVMsWidget.available_list.addItem("appVM1") + self.selectVMsWidget.available_list.addItem("appVM2") + self.selectVMsWidget.available_list.addItem("templateVM1") + + + def reject(self): + self.done(0) + + def save_and_apply(self): + pass + + @pyqtSlot(name='on_selectPathButton_clicked') + def selectPathButton_clicked(self): + self.path = self.pathLineEdit.text() + newPath = QFileDialog.getExistingDirectory(self, 'Select backup directory.') + if newPath: + self.pathLineEdit.setText(newPath) + self.path = newPath + +# Bases on the original code by: +# Copyright (c) 2002-2007 Pascal Varet + +def handle_exception( exc_type, exc_value, exc_traceback ): + import sys + import os.path + import traceback + + filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() + filename = os.path.basename( filename ) + error = "%s: %s" % ( exc_type.__name__, exc_value ) + + QMessageBox.critical(None, "Houston, we have a problem...", + "Whoops. A critical error has occured. This is most likely a bug " + "in Qubes Restore VMs application.

" + "%s" % error + + "at line %d of file %s.

" + % ( line, filename )) + + + + +def main(): + + global qubes_host + qubes_host = QubesHost() + + global app + app = QApplication(sys.argv) + app.setOrganizationName("The Qubes Project") + app.setOrganizationDomain("http://qubes-os.org") + app.setApplicationName("Qubes Restore VMs") + + sys.excepthook = handle_exception + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + global restore_window + restore_window = RestoreVMsWindow() + + restore_window.show() + + app.exec_() + app.exit() + + + +if __name__ == "__main__": + main() diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py new file mode 100644 index 0000000..a213af7 --- /dev/null +++ b/qubesmanager/settings.py @@ -0,0 +1,149 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# Copyright (C) 2012 Marek Marczykowski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# + +import sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from qubes.qubes import QubesVmCollection +from qubes.qubes import QubesException +from qubes.qubes import qubes_appmenu_create_cmd +from qubes.qubes import qubes_appmenu_remove_cmd +from qubes.qubes import QubesDaemonPidfile +from qubes.qubes import QubesHost +from qubes.qubes import qrexec_client_path + +import qubesmanager.resources_rc + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time +import threading +from operator import itemgetter + +from ui_settingsdlg import * +from multiselectwidget import * + + + +class VMSettingsWindow(Ui_SettingsDialog, QDialog): + + def __init__(self, vm, parent=None): + super(VMSettingsWindow, self).__init__(parent) + + self.setupUi(self) + + self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply) + self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject) + + self.app_list = MultiSelectWidget(self) + self.dev_list = MultiSelectWidget(self) + + self.apps_layout.addWidget(self.app_list) + self.devices_layout.addWidget(self.dev_list) + + self.vm = vm + if self.vm.template_vm: + self.source_vm = self.vm.template_vm + else: + self.source_vm = self.vm + + + #self.fill_apps_list() + #self.fill_devices_list() + + def reject(self): + self.done(0) + + def save_and_apply(self): + pass + +# Bases on the original code by: +# Copyright (c) 2002-2007 Pascal Varet + +def handle_exception( exc_type, exc_value, exc_traceback ): + import sys + import os.path + import traceback + + filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() + filename = os.path.basename( filename ) + error = "%s: %s" % ( exc_type.__name__, exc_value ) + + QMessageBox.critical(None, "Houston, we have a problem...", + "Whoops. A critical error has occured. This is most likely a bug " + "in Qubes VM Settings application.

" + "%s" % error + + "at line %d of file %s.

" + % ( line, filename )) + + + + +def main(): + + global qubes_host + qubes_host = QubesHost() + + global app + app = QApplication(sys.argv) + app.setOrganizationName("The Qubes Project") + app.setOrganizationDomain("http://qubes-os.org") + app.setApplicationName("Qubes VM Settings") + + sys.excepthook = handle_exception + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + vm = None + + if len(sys.argv) > 1: + vm = qvm_collection.get_vm_by_name(sys.argv[1]) + if vm is None or vm.qid not in qvm_collection: + QMessageBox.critical(None, "Qubes VM Settings Error", + "A VM with the name '{0}' does not exist in the system.".format(sys.argv[1])) + sys.exit(1) + else: + vms_list = [vm.name for vm in qvm_collection.values() if (vm.is_appvm() or vm.is_template())] + vmname = QInputDialog.getItem(None, "Select VM", "Select VM:", vms_list, editable = False) + if not vmname[1]: + sys.exit(1) + vm = qvm_collection.get_vm_by_name(vmname[0]) + + global settings_window + settings_window = VMSettingsWindow(vm) + + settings_window.show() + + app.exec_() + app.exit() + + + +if __name__ == "__main__": + main() diff --git a/restoredlg.ui b/restoredlg.ui new file mode 100644 index 0000000..a37807d --- /dev/null +++ b/restoredlg.ui @@ -0,0 +1,240 @@ + + + Restore + + + + 0 + 0 + 700 + 399 + + + + Qubes Restore VMs + + + + + + + + + + Device + + + + + + + + 0 + 0 + + + + + dev1 + + + + + longdeviceblablabla + + + + + dev2 + + + + + dev3 + + + + + + + + Backup directory: + + + + + + + + + + ... + + + + + + + + 12 + 75 + false + true + false + + + + Select backup source location: + + + + + + + + + + + + 12 + 75 + false + true + false + + + + Select VMs to restore: + + + + + + + + + + + 12 + 75 + false + true + false + + + + Restore options: + + + + + + + + + Do not restore VMs that have missing templates or netvms. + + + skip broken + + + + + + + Do not restore VMs that are already present on the host. + + + skip conflicting + + + + + + + Ignore dom0 username mismatch while restoring homedir. + + + ignore username mismatch + + + + + + + Ignore missing templates or netvms, restore VMs anyway. + + + ignore missing + + + + + + + Force to run, even with root privileges. + + + force root + + + + + + + + + + + + + + 12 + 75 + false + true + false + + + + You're about to perform the following actions: + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info<br />A lot of info</p></body></html> + + + + + + + + 12 + 75 + false + true + false + + + + To accept press Finish. + + + + + + + + + From e38f4df7fee94379c0581bffbb47d1963fa0fdec Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 31 Jan 2012 14:29:13 +0100 Subject: [PATCH 10/31] Backup dialog. Plus an extra page in the backup restore dialog, so that it resembles the backup dialog. --- Makefile | 2 + backupdlg.ui | 218 ++++++++++++++++++++++++++++++++++++++++ mainwindow.ui | 25 +++-- qubesmanager/backup.py | 136 +++++++++++++++++++++++++ qubesmanager/main.py | 29 +++++- qubesmanager/restore.py | 4 + restoredlg.ui | 53 +++++++++- 7 files changed, 450 insertions(+), 17 deletions(-) create mode 100644 backupdlg.ui create mode 100644 qubesmanager/backup.py diff --git a/Makefile b/Makefile index ab0570e..df8c4b2 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,8 @@ res: pyuic4 -o qubesmanager/ui_newfwruledlg.py newfwruledlg.ui pyuic4 -o qubesmanager/ui_multiselectwidget.py multiselectwidget.ui pyuic4 -o qubesmanager/ui_settingsdlg.py settingsdlg.ui + pyuic4 -o qubesmanager/ui_restoredlg.py restoredlg.ui + pyuic4 -o qubesmanager/ui_backupdlg.py backupdlg.ui update-repo-current: ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/current/dom0/rpm/ diff --git a/backupdlg.ui b/backupdlg.ui new file mode 100644 index 0000000..33de66b --- /dev/null +++ b/backupdlg.ui @@ -0,0 +1,218 @@ + + + Backup + + + + 0 + 0 + 700 + 399 + + + + Qubes Backup VMs + + + + + + QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage + + + + + + + + 12 + 75 + false + true + false + + + + Select VMs to backup: + + + + + + + + + + + + 12 + 75 + false + true + false + + + + Select backup destination directory: + + + + + + + Device + + + + + + + + 0 + 0 + + + + + dev1 + + + + + longdeviceblablabla + + + + + dev2 + + + + + dev3 + + + + + + + + Backup directory: + + + + + + + + + + ... + + + + + + + + + + + + 12 + 75 + false + true + false + + + + You're about to perform the following actions: + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info<br />A lot of info</p></body></html> + + + + + + + + 12 + 75 + false + true + false + + + + To continue press Next. + + + + + + + + + + + + 12 + 75 + false + true + false + + + + Backup in progress... + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info<br />A lot of info</p></body></html> + + + + + + + 24 + + + Qt::AlignCenter + + + false + + + + + + + + + diff --git a/mainwindow.ui b/mainwindow.ui index 79d8597..59e2da6 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -128,19 +128,6 @@ Name - - - 50 - false - - - - - 0 - 0 - 0 - - @@ -199,6 +186,8 @@ Options + + @@ -517,6 +506,16 @@ VM Settings
+ + + Restore VMs from backup + + + + + Backup VMs + + diff --git a/qubesmanager/backup.py b/qubesmanager/backup.py new file mode 100644 index 0000000..8d223f1 --- /dev/null +++ b/qubesmanager/backup.py @@ -0,0 +1,136 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# Copyright (C) 2012 Marek Marczykowski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# + +import sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from qubes.qubes import QubesVmCollection +from qubes.qubes import QubesException +from qubes.qubes import QubesDaemonPidfile +from qubes.qubes import QubesHost + +import qubesmanager.resources_rc + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time +import threading +from operator import itemgetter + +from ui_backupdlg import * +from multiselectwidget import * + + + +class BackupVMsWindow(Ui_Backup, QWizard): + + def __init__(self, parent=None): + super(BackupVMsWindow, self).__init__(parent) + + self.setupUi(self) + + self.selectVMsWidget = MultiSelectWidget(self) + self.verticalLayout.insertWidget(1, self.selectVMsWidget) + + self.selectVMsWidget.available_list.addItem("netVM1") + self.selectVMsWidget.available_list.addItem("appVM1") + self.selectVMsWidget.available_list.addItem("appVM2") + self.selectVMsWidget.available_list.addItem("templateVM1") + + self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed) + + + + def reject(self): + self.done(0) + + def save_and_apply(self): + pass + + @pyqtSlot(name='on_selectPathButton_clicked') + def selectPathButton_clicked(self): + self.path = self.pathLineEdit.text() + newPath = QFileDialog.getExistingDirectory(self, 'Select backup directory.') + if newPath: + self.pathLineEdit.setText(newPath) + self.path = newPath + + def current_page_changed(self, id): + self.button(self.CancelButton).setDisabled(id==3) + + +# Bases on the original code by: +# Copyright (c) 2002-2007 Pascal Varet + +def handle_exception( exc_type, exc_value, exc_traceback ): + import sys + import os.path + import traceback + + filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() + filename = os.path.basename( filename ) + error = "%s: %s" % ( exc_type.__name__, exc_value ) + + QMessageBox.critical(None, "Houston, we have a problem...", + "Whoops. A critical error has occured. This is most likely a bug " + "in Qubes Restore VMs application.

" + "%s" % error + + "at line %d of file %s.

" + % ( line, filename )) + + + + +def main(): + + global qubes_host + qubes_host = QubesHost() + + global app + app = QApplication(sys.argv) + app.setOrganizationName("The Qubes Project") + app.setOrganizationDomain("http://qubes-os.org") + app.setApplicationName("Qubes Restore VMs") + + sys.excepthook = handle_exception + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + global backup_window + backup_window = BackupVMsWindow() + + backup_window.show() + + app.exec_() + app.exit() + + + +if __name__ == "__main__": + main() diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 76abb11..144a09a 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -39,6 +39,9 @@ import qubesmanager.resources_rc import ui_newappvmdlg from ui_mainwindow import * from appmenu_select import AppmenuSelectWindow +from settings import VMSettingsWindow +from restore import RestoreVMsWindow +from backup import BackupVMsWindow from firewall import EditFwRulesDlg, QubesFirewallRulesModel @@ -601,7 +604,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): for vm_row in self.vms_in_table: vm_row.update(self.counter) - self.table_selection_changed() + #self.table_selection_changed() if not out_of_schedule: self.counter += 1 @@ -615,13 +618,13 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.centralwidget.layout().contentsMargins().right() self.table.setFixedWidth( width ) - self.setFixedWidth( width) def table_selection_changed (self): + vm = self.get_selected_vm() # Update available actions: - + self.action_settings.setEnabled(True) self.action_removevm.setEnabled(not vm.installed_by_rpm and not vm.last_power_state) self.action_resumevm.setEnabled(not vm.last_power_state) self.action_pausevm.setEnabled(vm.last_power_state and vm.qid != 0) @@ -878,6 +881,13 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.shutdown_monitor[vm.qid] = VmShutdownMonitor (vm) QTimer.singleShot (vm_shutdown_timeout, self.shutdown_monitor[vm.qid].check_if_vm_has_shutdown) + @pyqtSlot(name='on_action_settings_triggered') + def action_settings_triggered(self): + vm = self.get_selected_vm() + settings_window = VMSettingsWindow(vm) + settings_window.exec_() + + @pyqtSlot(name='on_action_appmenus_triggered') def action_appmenus_triggered(self): vm = self.get_selected_vm() @@ -931,6 +941,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.show_inactive_vms = self.action_showallvms.isChecked() self.mark_table_for_update() self.update_table(out_of_schedule = True) + self.set_table_geom_height() @pyqtSlot(name='on_action_editfwrules_triggered') def action_editfwrules_triggered(self): @@ -948,6 +959,18 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): model.apply_rules() + @pyqtSlot(name='on_action_restore_triggered') + def action_restore_triggered(self): + restore_window = RestoreVMsWindow() + restore_window.exec_() + + @pyqtSlot(name='on_action_backup_triggered') + def action_backup_triggered(self): + backup_window = BackupVMsWindow() + backup_window.exec_() + + + def showhide_collumn(self, col_num, show): self.table.setColumnHidden( col_num, not show) self.update_table_columns() diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py index 247f470..9371f85 100644 --- a/qubesmanager/restore.py +++ b/qubesmanager/restore.py @@ -60,6 +60,7 @@ class RestoreVMsWindow(Ui_Restore, QWizard): self.selectVMsWidget.available_list.addItem("appVM2") self.selectVMsWidget.available_list.addItem("templateVM1") + self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed) def reject(self): self.done(0) @@ -67,6 +68,9 @@ class RestoreVMsWindow(Ui_Restore, QWizard): def save_and_apply(self): pass + def current_page_changed(self, id): + self.button(self.CancelButton).setDisabled(id==3) + @pyqtSlot(name='on_selectPathButton_clicked') def selectPathButton_clicked(self): self.path = self.pathLineEdit.text() diff --git a/restoredlg.ui b/restoredlg.ui index a37807d..88360e1 100644 --- a/restoredlg.ui +++ b/restoredlg.ui @@ -16,6 +16,9 @@ + + QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage + @@ -228,7 +231,55 @@ p, li { white-space: pre-wrap; } - To accept press Finish. + To continue press Next. + + + + + + + + + + + + 12 + 75 + false + true + false + + + + Restore in progress... + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info<br />A lot of info</p></body></html> + + + + + + + 24 + + + Qt::AlignCenter + + + false From 49495d6444e4e30f7dcaaeb87c4e2a82336e8823 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 31 Jan 2012 17:29:00 +0100 Subject: [PATCH 11/31] Global settings dialog --- Makefile | 1 + globalsettingsdlg.ui | 126 ++++++++++++++++++++++++++++++++ mainwindow.ui | 6 ++ qubesmanager/global_settings.py | 107 +++++++++++++++++++++++++++ qubesmanager/main.py | 6 ++ 5 files changed, 246 insertions(+) create mode 100644 globalsettingsdlg.ui create mode 100644 qubesmanager/global_settings.py diff --git a/Makefile b/Makefile index df8c4b2..28f3a5a 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ res: pyuic4 -o qubesmanager/ui_settingsdlg.py settingsdlg.ui pyuic4 -o qubesmanager/ui_restoredlg.py restoredlg.ui pyuic4 -o qubesmanager/ui_backupdlg.py backupdlg.ui + pyuic4 -o qubesmanager/ui_globalsettingsdlg.py globalsettingsdlg.ui update-repo-current: ln -f $(RPMS_DIR)/x86_64/qubes-manager-*$(VERSION)*.rpm ../yum/current-release/current/dom0/rpm/ diff --git a/globalsettingsdlg.ui b/globalsettingsdlg.ui new file mode 100644 index 0000000..850361c --- /dev/null +++ b/globalsettingsdlg.ui @@ -0,0 +1,126 @@ + + + GlobalSettings + + + + 0 + 0 + 568 + 342 + + + + Qubes Global Settings + + + + + + + 0 + 0 + + + + UpdateVM: + + + + + + + + + + ClockVM: + + + + + + + + + + Default netVM: + + + + + + + + + + + 0 + 0 + + + + Default template: + + + + + + + + + + Default kernel: + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + GlobalSettings + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + GlobalSettings + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/mainwindow.ui b/mainwindow.ui index 59e2da6..b5e8fdf 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -186,6 +186,7 @@ Options + @@ -516,6 +517,11 @@ Backup VMs + + + Global settings + + diff --git a/qubesmanager/global_settings.py b/qubesmanager/global_settings.py new file mode 100644 index 0000000..373ddff --- /dev/null +++ b/qubesmanager/global_settings.py @@ -0,0 +1,107 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# Copyright (C) 2012 Marek Marczykowski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# + +import sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from qubes.qubes import QubesVmCollection +from qubes.qubes import QubesException +from qubes.qubes import QubesDaemonPidfile +from qubes.qubes import QubesHost + +import qubesmanager.resources_rc + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time +import threading +from operator import itemgetter + +from ui_globalsettingsdlg import * + +class GlobalSettingsWindow(Ui_GlobalSettings, QDialog): + + def __init__(self, parent=None): + super(GlobalSettingsWindow, self).__init__(parent) + + self.setupUi(self) + + def reject(self): + self.done(0) + + def save_and_apply(self): + pass + +# Bases on the original code by: +# Copyright (c) 2002-2007 Pascal Varet + +def handle_exception( exc_type, exc_value, exc_traceback ): + import sys + import os.path + import traceback + + filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() + filename = os.path.basename( filename ) + error = "%s: %s" % ( exc_type.__name__, exc_value ) + + QMessageBox.critical(None, "Houston, we have a problem...", + "Whoops. A critical error has occured. This is most likely a bug " + "in Qubes Global Settings application.

" + "%s" % error + + "at line %d of file %s.

" + % ( line, filename )) + + +def main(): + + global qubes_host + qubes_host = QubesHost() + + global app + app = QApplication(sys.argv) + app.setOrganizationName("The Qubes Project") + app.setOrganizationDomain("http://qubes-os.org") + app.setApplicationName("Qubes Global Settings") + + sys.excepthook = handle_exception + + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + global global_window + global_window = GlobalSetingsWindow() + + global_window.show() + + app.exec_() + app.exit() + + + +if __name__ == "__main__": + main() diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 144a09a..183fb24 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -42,6 +42,7 @@ from appmenu_select import AppmenuSelectWindow from settings import VMSettingsWindow from restore import RestoreVMsWindow from backup import BackupVMsWindow +from global_settings import GlobalSettingsWindow from firewall import EditFwRulesDlg, QubesFirewallRulesModel @@ -958,6 +959,11 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): if dialog.exec_(): model.apply_rules() + @pyqtSlot(name='on_action_global_settings_triggered') + def action_global_settings_triggered(self): + global_settings_window = GlobalSettingsWindow() + global_settings_window.exec_() + @pyqtSlot(name='on_action_restore_triggered') def action_restore_triggered(self): From e525bc7583ac9de84015923910ba5da83a4dc2d7 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Sun, 5 Feb 2012 18:41:41 +0100 Subject: [PATCH 12/31] After meeting changes to the whole gui. --- backupdlg.ui | 168 ++++---- globalsettingsdlg.ui | 197 ++++++--- icons/add.png | Bin 0 -> 35372 bytes icons/edit.png | Bin 0 -> 10625 bytes icons/flag-blue.png | Bin 0 -> 15295 bytes icons/flag-green.png | Bin 0 -> 16619 bytes icons/flag-red.png | Bin 0 -> 14244 bytes icons/flag-yellow.png | Bin 0 -> 14940 bytes icons/pencil.png | Bin 0 -> 30106 bytes icons/remove.png | Bin 0 -> 12744 bytes mainwindow.ui | 60 ++- multiselectwidget.ui | 14 + qubesmanager/main.py | 202 ++++++--- qubesmanager/multiselectwidget.py | 19 +- qubesmanager/restore.py | 2 +- qubesmanager/settings.py | 4 +- resources.qrc | 8 + restoredlg.ui | 315 +++++++------- settingsdlg.ui | 680 ++++++++++++++++++++---------- 19 files changed, 1045 insertions(+), 624 deletions(-) create mode 100644 icons/add.png create mode 100644 icons/edit.png create mode 100644 icons/flag-blue.png create mode 100644 icons/flag-green.png create mode 100644 icons/flag-red.png create mode 100644 icons/flag-yellow.png create mode 100644 icons/pencil.png create mode 100644 icons/remove.png diff --git a/backupdlg.ui b/backupdlg.ui index 33de66b..04cb139 100644 --- a/backupdlg.ui +++ b/backupdlg.ui @@ -25,10 +25,10 @@ - 12 - 75 + 9 + 50 false - true + false false @@ -40,75 +40,68 @@ - - - - - - 12 - 75 - false - true - false - - - - Select backup destination directory: - - - - - - - Device - - - - - - - - 0 - 0 - - - - - dev1 - - - - - longdeviceblablabla - - - - - dev2 - - - - - dev3 - - - - - - - - Backup directory: - - - - - - - - - - ... + + + + + Backup destination directory + + + + + Device: + + + + + + + + 0 + 0 + + + + + dev1 + + + + + longdeviceblablabla + + + + + dev2 + + + + + dev3 + + + + + + + + Backup directory: + + + + + + + + + + ... + + + + @@ -119,10 +112,10 @@ - 12 - 75 + 9 + 50 false - true + false false @@ -150,10 +143,10 @@ p, li { white-space: pre-wrap; } - 12 - 75 + 9 + 50 false - true + false false @@ -170,10 +163,10 @@ p, li { white-space: pre-wrap; } - 12 - 75 + 9 + 50 false - true + false false @@ -182,21 +175,6 @@ p, li { white-space: pre-wrap; } - - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info<br />A lot of info</p></body></html> - - - diff --git a/globalsettingsdlg.ui b/globalsettingsdlg.ui index 850361c..0795e1f 100644 --- a/globalsettingsdlg.ui +++ b/globalsettingsdlg.ui @@ -6,8 +6,8 @@ 0 0 - 568 - 342 + 678 + 288 @@ -15,68 +15,157 @@ - - - - 0 - 0 - - - - UpdateVM: + + + System defaults + + + + + + 0 + 0 + + + + UpdateVM: + + + + + + + + + + ClockVM: + + + + + + + + + + Default netVM: + + + + + + + + + + + 0 + 0 + + + + Default template: + + + + + + + - - - - - - ClockVM: + + + Default memory settings + + + + 11 + 26 + 134 + 16 + + + + Minimal VM's memory: + + + + + + 11 + 57 + 139 + 16 + + + + dom0 memory margin: + + + + + + 156 + 26 + 121 + 25 + + + + MB + + + 999999999 + + + 10 + + + + + + 156 + 57 + 121 + 25 + + + + MB + + + 999999999 + + + 50 + + - - - - - - - Default netVM: + + + + Kernel + + + + + Default kernel: + + + + + + + - - - - - - - - 0 - 0 - - - - Default template: - - - - - - - - - - Default kernel: - - - - - - - + Qt::Horizontal diff --git a/icons/add.png b/icons/add.png new file mode 100644 index 0000000000000000000000000000000000000000..a15dd10aa874b482dc6293775044085e656a6026 GIT binary patch literal 35372 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 literal 0 HcmV?d00001 diff --git a/icons/edit.png b/icons/edit.png new file mode 100644 index 0000000000000000000000000000000000000000..b17a7ce4bd569f2eb257f9620b6a7e259a1050ab GIT binary patch literal 10625 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 G5&s7zi257= literal 0 HcmV?d00001 diff --git a/icons/flag-blue.png b/icons/flag-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..6f6b4ab14ce527df9a9aa749bb4e785811861f04 GIT binary patch literal 15295 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 literal 0 HcmV?d00001 diff --git a/icons/flag-green.png b/icons/flag-green.png new file mode 100644 index 0000000000000000000000000000000000000000..9b0b06d60b9d6bf7a83bb84cfd23bc55b9d10f2c GIT binary patch literal 16619 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 rH~Zn3Yb-uP9tacj|JOLoGFj%<{-j7 literal 0 HcmV?d00001 diff --git a/icons/flag-red.png b/icons/flag-red.png new file mode 100644 index 0000000000000000000000000000000000000000..3a9b9ac106f4a4ee019496f50e495816c3317e93 GIT binary patch literal 14244 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 literal 0 HcmV?d00001 diff --git a/icons/flag-yellow.png b/icons/flag-yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..ab7835afe0103bcfbf7cf32f7b58b39a30a021f0 GIT binary patch literal 14940 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 Mj)``qCNBE_1N^)_7ytkO literal 0 HcmV?d00001 diff --git a/icons/pencil.png b/icons/pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..1ecb10cd7d8ba87b41e649da92789c12885634e8 GIT binary patch literal 30106 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 Zw~$_Ehf~a5=q=goGQ+g3ligalT(n}&m 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%0A8McxV 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

FB9c(5&5|hBK)l;7iueLNvhRi$gX=2 zzcr$1Tme@#-Vp&2dS4 zNl{}Pz09l&B}((C^XS2=N19}wOAQX+@YhZp{#HKl&3n;AHBLmfMe})S{MvGh=-qF^ zm*+|yeLm-mo_vHPA-LYOv^e<0-JU=fUbG|_Q7&n%KyY_%W@3u7Fazk40)J#x6Qpyb zqVje98fpIpf6v9IW3Z;$THBKpTyXqsh{2f#y)lFVK!Y}_fdSDd<;4c7NCT#MQAgTz zHp#i_(xWF917@jSiM>`{+5a~)7uq-=Vux7S*Nel&1NZ&wu73<+hfVa_oXmWA>8)rQqewGuUvfeqJ`wYnf09rTh*+*>Z9@o=pwuIbg)>W#V$@2lZA{3rS+ezxB` ztjl~+Q>hIE(*iA+r*tzS;`=PE4H$M}W$L&NLiV2O`M2LGnYr_UJ8|E$jDvfN`Y$s( zss>NGro3TSR}P$}9>Y`~DMcr$R=WBV{E+kIsCjAOaT5C|;rln9QEna?AXI~nPv?n- zO@T72=GlyJ_SvH(;BTs{qb{rBo8-(j(|Z?n%lz$Zi#xqsk@>$;c(=#Rk}b7B))OMr z82Fn^O*c!YFO>@}#1|7?Ox>1S;zr6jWDV9JDRCe)4%F<)I%6m9(G_6zm_b)=mbSXkA$5BBR2h7WB`OgT3Gu%!yk(bm^~@~U2HrfY#FWY)y8_3WuK zTk}2WIf4tF7m!ST?E@>at{Z-#5e`0&P=!ta0W`WZFE0~Oy$r=3z@|LF+*hkO^kb3C zTHhNDQ+a!F43VZ}pXuZSidv*2wyLCZSx>R zEl}@CMLq;04yAC<;)jLKyTK6+9&WJd7yDj5y~uHh2Ki(K#YuRj97Q9FJC#!!Ah??Q z1_t(%mnB7tg-rQ_q>`9S0(pBzODA5WHpU;`Jliszcu}K)fl0{%bq*KjJvh7r?X>Xv zklo3^7Hjz+zp4{)ste_`FzI2(HK{p?{M6%u+QS=lRs*<17PO~koA>_Y3xwoZFqQwvs2v>vO5 zVy2C*+4yq7S}*Bf+C)30TEkzX7%0n^G3W+H>N zR72xWG3W3yFe163?8-v&BFCSDAK5w+mqQP4y`eskp&Cfz#_Ceu9o)Cz0>vK=0GFh| z*tnsSHv_(gxd3MBB2e(_yMSA~j|1htq}bDOeRjWgnxXflsk}9ZXv;{%Uzt|6SsU<- zRh@xlwH__cMxof@JNmEd2bMNx;xH=Km5pygC|Z{rh&rGli5;P|MGieaRxQSm`fK(Tm%ZSjnh_bURRb#vtDZvYHQyJ`)qkq7k z#qm}SWXW2D?132hU*9#H3eHJhrRO~6nt5^#iJUltI;suZ98>)J{36TGvd;+WPi7{~ zd>tF7vrsCIbeo5#Ud78fF4}|@!5}mb6(X07Tb#dH6gtgTQ9hCtVh*h=K99q{Ge6&m j{x=H$|5*j*sUXnp@Q9OFM#S?!G=Mw0MmiN*_QC%L+x(^d literal 0 HcmV?d00001 diff --git a/mainwindow.ui b/mainwindow.ui index b5e8fdf..bd5bfb7 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -22,6 +22,13 @@ Qubes VM Manager + + + :/qubes.png:/qubes.png + + + + true @@ -42,6 +49,12 @@ + + 0 + + + 0 + @@ -87,10 +100,10 @@ QAbstractItemView::SelectRows - true + false - Qt::DashLine + Qt::NoPen true @@ -128,16 +141,33 @@ Name + + VM name + + + + + Upd + + + Update info + Template + + VM's template + NetVM + + VM's netVM + @@ -148,6 +178,9 @@ CPU Graph + + CPU usage graph + @@ -158,6 +191,9 @@ MEM Graph + + Memory usage graph + @@ -198,6 +234,7 @@ Columns visibility + @@ -212,17 +249,6 @@ - - - - 0 - 0 - - - - false - - toolBar @@ -522,6 +548,14 @@ Global settings + + + true + + + Upd + + diff --git a/multiselectwidget.ui b/multiselectwidget.ui index db9cb70..f5e0ab6 100644 --- a/multiselectwidget.ui +++ b/multiselectwidget.ui @@ -86,6 +86,13 @@ + + + + >> + + + @@ -100,6 +107,13 @@ + + + + << + + + diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 183fb24..ca76aa5 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -156,7 +156,7 @@ class VmIconWidget (QWidget): label_icon = QLabel() icon = QIcon (icon_path) - icon_sz = QSize (VmManagerWindow.row_height * 0.8, VmManagerWindow.row_height * 0.3) + icon_sz = QSize (VmManagerWindow.row_height * 0.8, VmManagerWindow.row_height * 0.8) icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled if not enabled else QIcon.Normal) label_icon.setPixmap (icon_pixmap) label_icon.setFixedSize (icon_sz) @@ -187,7 +187,7 @@ class VmNetvmWidget (QWidget): class VmUsageBarWidget (QWidget): - def __init__(self, min, max, format, label, update_func, vm, load, parent = None): + def __init__(self, min, max, format, update_func, vm, load, parent = None): super (VmUsageBarWidget, self).__init__(parent) self.min = min @@ -198,10 +198,20 @@ class VmUsageBarWidget (QWidget): self.widget.setMinimum(min) self.widget.setMaximum(max) self.widget.setFormat(format); - self.label = QLabel(label) + + self.widget.setStyleSheet( + "QProgressBar:horizontal{ \ + border: 1px solid lightblue;\ + border-radius: 4px;\ + background: white;\ + text-align: center;\ + }\ + QProgressBar::chunk:horizontal {\ + background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5, stop: 0 hsv(210, 170, 207), stop: 1 white);\ + }" + ) layout = QHBoxLayout() - layout.addWidget(self.label) layout.addWidget(self.widget) self.setLayout(layout) @@ -298,21 +308,26 @@ class MemChartWidget (QWidget): class VmUpdateInfoWidget(QWidget): - def __init__(self, vm, parent = None): + def __init__(self, vm, show_text=True, parent = None): super (VmUpdateInfoWidget, self).__init__(parent) layout = QHBoxLayout () - self.label = QLabel("---") - layout.addWidget(self.label, alignment=Qt.AlignCenter) + self.show_text = show_text + if self.show_text: + self.label=QLabel("") + layout.addWidget(self.label, alignment=Qt.AlignCenter) + else: + self.icon = QLabel("") + layout.addWidget(self.icon, alignment=Qt.AlignHCenter) self.setLayout(layout) self.previous_outdated = False - self.previous_update_recommended = False + self.previous_update_recommended = None def update_outdated(self, vm): outdated = vm.is_outdated() if outdated and not self.previous_outdated: - self.label.setText("outdated") - + self.update_status_widget("outdated") + self.previous_outdated = outdated if vm.is_updateable(): update_recommended = self.previous_update_recommended @@ -323,11 +338,37 @@ class VmUpdateInfoWidget(QWidget): update_recommended = True else: update_recommended = False - self.label.setText("OK") + if not self.show_text and self.previous_update_recommended != False: + self.update_status_widget("ok") + if update_recommended and not self.previous_update_recommended: - self.label.setText("check updates") + self.update_status_widget("update") self.previous_update_recommended = update_recommended + def update_status_widget(self, state): + + if state == "ok": + label_text = "" + icon_path = ":/flag-green.png" + tooltip_text = "VM up to date" + elif state == "update": + label_text = "Check updates" + icon_path = ":/flag-yellow.png" + tooltip_text = "Update recommended" + elif state == "outdated": + label_text = "VM outdated" + icon_path = ":/flag-red.png" + tooltip_text = "VM outdated" + + if self.show_text: + self.label.setText(label_text) + else: + self.layout().removeWidget(self.icon) + self.icon.deleteLater() + self.icon = VmIconWidget(icon_path, True) + self.icon.setToolTip(tooltip_text) + self.layout().addWidget(self.icon, alignment=Qt.AlignCenter) + class VmBlockDevicesWidget(QWidget): def __init__(self, vm, parent=None): @@ -353,31 +394,34 @@ class VmRowInTable(object): self.info_widget = VmInfoWidget(vm) table.setCellWidget(row_no, 0, self.info_widget) + self.upd_widget = VmUpdateInfoWidget(vm, False) + table.setCellWidget(row_no, 1, self.upd_widget) + self.template_widget = VmTemplateWidget(vm) - table.setCellWidget(row_no, 1, self.template_widget) + table.setCellWidget(row_no, 2, self.template_widget) self.netvm_widget = VmNetvmWidget(vm) - table.setCellWidget(row_no, 2, self.netvm_widget) + table.setCellWidget(row_no, 3, self.netvm_widget) - self.cpu_usage_widget = VmUsageBarWidget(0, 100, "", "CPU", + self.cpu_usage_widget = VmUsageBarWidget(0, 100, "", lambda vm, val: val if vm.last_power_state else 0, vm, 0) - table.setCellWidget(row_no, 3, self.cpu_usage_widget) + table.setCellWidget(row_no, 4, self.cpu_usage_widget) self.load_widget = LoadChartWidget(vm) - table.setCellWidget(row_no, 4, self.load_widget) + table.setCellWidget(row_no, 5, self.load_widget) - self.mem_usage_widget = VmUsageBarWidget(0, qubes_host.memory_total/1024, "%v MB", "MEM", + self.mem_usage_widget = VmUsageBarWidget(0, qubes_host.memory_total/1024, "%v MB", lambda vm, val: vm.get_mem()/1024 if vm.last_power_state else 0, vm, 0) - table.setCellWidget(row_no, 5, self.mem_usage_widget) + table.setCellWidget(row_no, 6, self.mem_usage_widget) self.mem_widget = MemChartWidget(vm) - table.setCellWidget(row_no, 6, self.mem_widget) + table.setCellWidget(row_no, 7, self.mem_widget) - self.updateinfo_widget = VmUpdateInfoWidget(vm) - table.setCellWidget(row_no, 7, self.updateinfo_widget) + self.updateinfo_widget = VmUpdateInfoWidget(vm, True) + table.setCellWidget(row_no, 8, self.updateinfo_widget) self.blockdevices_widget = VmBlockDevicesWidget(vm) - table.setCellWidget(row_no, 8, self.blockdevices_widget) + table.setCellWidget(row_no, 9, self.blockdevices_widget) def update(self, counter, cpu_load = None): @@ -388,6 +432,7 @@ class VmRowInTable(object): self.load_widget.update_load(self.vm, cpu_load) self.mem_widget.update_load(self.vm) self.updateinfo_widget.update_outdated(self.vm) + self.upd_widget.update_outdated(self.vm) class NewAppVmDlg (QDialog, ui_newappvmdlg.Ui_NewAppVMDlg): def __init__(self, parent = None): @@ -437,18 +482,20 @@ class ThreadMonitor(QObject): class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): row_height = 30 + column_width = 200 max_visible_rows = 7 update_interval = 1000 # in msec show_inactive_vms = True columns_indices = { "Name": 0, - "Template": 1, - "NetVM": 2, - "CPU": 3, - "CPU Graph": 4, - "MEM": 5, - "MEM Graph": 6, - "Update Info": 7, - "Block Device": 8 } + "Upd": 1, + "Template": 2, + "NetVM": 3, + "CPU": 4, + "CPU Graph": 5, + "MEM": 6, + "MEM Graph": 7, + "Update Info": 8, + "Block Device": 9 } @@ -462,7 +509,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.connect(self.table, SIGNAL("itemSelectionChanged()"), self.table_selection_changed) cur_pos = self.pos() - self.table.setColumnWidth(0, 200) + self.table.setColumnWidth(0, self.column_width) self.setSizeIncrement(QtCore.QSize(200, 30)) self.centralwidget.setSizeIncrement(QtCore.QSize(200, 30)) self.table.setSizeIncrement(QtCore.QSize(200, 30)) @@ -471,15 +518,21 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.table.setColumnHidden( self.columns_indices["NetVM"], True) self.actionNetVM.setChecked(False) + self.table.setColumnHidden( self.columns_indices["Update Info"], True) + self.actionUpdate_Info.setChecked(False) self.table.setColumnHidden( self.columns_indices["CPU Graph"], True) self.actionCPU_Graph.setChecked(False) self.table.setColumnHidden( self.columns_indices["MEM Graph"], True) self.actionMEM_Graph.setChecked(False) self.table.setColumnHidden( self.columns_indices["Block Device"], True) self.actionBlock_Devices.setChecked(False) + self.table.setColumnWidth(self.columns_indices["Upd"], 50) + + #self.table.setFrameShape(QFrame.NoFrame) + self.table.setContentsMargins(0,0,0,0) + self.centralwidget.layout().setContentsMargins(0,0,0,0) + self.layout().setContentsMargins(0,0,0,0) - self.update_table_columns() - self.set_table_geom_height() self.counter = 0 self.shutdown_monitor = {} @@ -487,37 +540,43 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.last_measure_time = time.time() QTimer.singleShot (self.update_interval, self.update_table) + def show(self): + super(VmManagerWindow, self).show() + self.set_table_geom_height() + self.update_table_columns() + def set_table_geom_height(self): - minH = self.table.horizontalHeader().height() + \ - 2*self.table.contentsMargins().top() +\ - self.centralwidget.layout().contentsMargins().top() +\ - self.centralwidget.layout().contentsMargins().bottom() - #self.table.contentsMargins().bottom() # this is huge, dunno why - #2*self.centralwidget.layout().verticalSpacing() # and this is negative... + minH = self.table.horizontalHeader().height() +\ + 2*self.table.frameWidth() #All this sizing is kind of magic, so change it only if you have to #or if you know what you're doing :) n = self.table.rowCount(); - if n > self.max_visible_rows: - for i in range (0, self.max_visible_rows): - minH += self.table.rowHeight(i) - maxH = minH - for i in range (self.max_visible_rows, n): - maxH += self.table.rowHeight(i) + maxH = minH + if n >= self.max_visible_rows: + minH += self.max_visible_rows*self.row_height + maxH += n*self.row_height else: - for i in range (n): - minH += self.table.rowHeight(i) + minH += n*self.row_height maxH = minH - + self.centralwidget.setMinimumHeight(minH) - maxH += self.menubar.height() + self.statusbar.height() +\ - self.toolbar.height() + self.centralwidget.setMaximumHeight(maxH) + + mainwindow_to_add = self.menubar.height() +\ + self.toolbar.height() + \ + self.menubar.contentsMargins().top() + self.menubar.contentsMargins().bottom() +\ + self.toolbar.contentsMargins().top() + self.toolbar.contentsMargins().bottom() + + maxH += mainwindow_to_add + minH += mainwindow_to_add + self.setMaximumHeight(maxH) - self.adjustSize() - + self.setMinimumHeight(minH) + def get_vms_list(self): self.qvm_collection.lock_db_for_reading() self.qvm_collection.load() @@ -613,12 +672,14 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): def update_table_columns(self): - width = self.table.horizontalHeader().length() +\ - self.table.verticalScrollBar().width() +\ - self.centralwidget.layout().contentsMargins().left() +\ - self.centralwidget.layout().contentsMargins().right() + table_width = self.table.horizontalHeader().length() +\ + self.table.verticalScrollBar().width() + \ + 2*self.table.frameWidth() + 1 + + self.table.setFixedWidth( table_width ) + self.centralwidget.setFixedWidth(table_width) + self.setFixedWidth(table_width) - self.table.setFixedWidth( width ) def table_selection_changed (self): @@ -738,6 +799,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): @pyqtSlot(name='on_action_removevm_triggered') def action_removevm_triggered(self): + vm = self.get_selected_vm() assert not vm.is_running() assert not vm.installed_by_rpm @@ -885,7 +947,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): @pyqtSlot(name='on_action_settings_triggered') def action_settings_triggered(self): vm = self.get_selected_vm() - settings_window = VMSettingsWindow(vm) + settings_window = VMSettingsWindow(vm, 1) settings_window.exec_() @@ -980,30 +1042,33 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): def showhide_collumn(self, col_num, show): self.table.setColumnHidden( col_num, not show) self.update_table_columns() - + + def on_actionUpd_toggled(self, checked): + self.showhide_collumn( self.columns_indices['Upd'], checked) + def on_actionTemplate_toggled(self, checked): - self.showhide_collumn( 1, checked) + self.showhide_collumn( self.columns_indices['Template'], checked) def on_actionNetVM_toggled(self, checked): - self.showhide_collumn( 2, checked) + self.showhide_collumn( self.columns_indices['NetVM'], checked) def on_actionCPU_toggled(self, checked): - self.showhide_collumn( 3, checked) + self.showhide_collumn( self.columns_indices['CPU'], checked) def on_actionCPU_Graph_toggled(self, checked): - self.showhide_collumn( 4, checked) + self.showhide_collumn( self.columns_indices['CPU Graph'], checked) def on_actionMEM_toggled(self, checked): - self.showhide_collumn( 5, checked) + self.showhide_collumn( self.columns_indices['MEM'], checked) def on_actionMEM_Graph_toggled(self, checked): - self.showhide_collumn( 6, checked) + self.showhide_collumn( self.columns_indices['MEM Graph'], checked) def on_actionUpdate_Info_toggled(self, checked): - self.showhide_collumn( 7, checked) + self.showhide_collumn( self.columns_indices['Update Info'], checked) def on_actionBlock_Devices_toggled(self, checked): - self.showhide_collumn( 8, checked) + self.showhide_collumn( self.columns_indices['Block Device'], checked) class QubesTrayIcon(QSystemTrayIcon): @@ -1139,4 +1204,3 @@ def main(): app.exec_() trayIcon = None - diff --git a/qubesmanager/multiselectwidget.py b/qubesmanager/multiselectwidget.py index f0c0c23..46c7cf2 100644 --- a/qubesmanager/multiselectwidget.py +++ b/qubesmanager/multiselectwidget.py @@ -9,7 +9,9 @@ class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): super(MultiSelectWidget, self).__init__() self.setupUi(self); self.add_selected_button.clicked.connect(self.add_selected) + self.add_all_button.clicked.connect(self.add_all) self.remove_selected_button.clicked.connect(self.remove_selected) + self.remove_all_button.clicked.connect(self.remove_all) self.available_list.setSelectionMode(QAbstractItemView.ExtendedSelection) self.selected_list.setSelectionMode(QAbstractItemView.ExtendedSelection) @@ -20,15 +22,25 @@ class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): row = src.indexFromItem(s).row() item = src.takeItem(row) dst.addItem(item) + dst.sortItems() def add_selected(self): self.switch_selected(self.available_list, self.selected_list) - self.selected_list.sortItems() - def remove_selected(self): self.switch_selected(self.selected_list, self.available_list) - self.available_list.sortItems() + + def move_all(self, src, dst): + while src.count() > 0: + item = src.takeItem(0) + dst.addItem(item) + dst.sortItems() + + def add_all(self): + self.move_all(self.available_list, self.selected_list) + + def remove_all(self): + self.move_all(self.selected_list, self.available_list) def clear(self): self.available_list.clear() @@ -36,7 +48,6 @@ class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): - if __name__ == "__main__": app = QtGui.QApplication(sys.argv) ui = MultiSelectWidget() diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py index 9371f85..d243a25 100644 --- a/qubesmanager/restore.py +++ b/qubesmanager/restore.py @@ -53,7 +53,7 @@ class RestoreVMsWindow(Ui_Restore, QWizard): self.setupUi(self) self.selectVMsWidget = MultiSelectWidget(self) - self.verticalLayout.insertWidget(1, self.selectVMsWidget) + self.selectVMsLayout.insertWidget(1, self.selectVMsWidget) self.selectVMsWidget.available_list.addItem("netVM1") self.selectVMsWidget.available_list.addItem("appVM1") diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index a213af7..b7ce7e1 100644 --- a/qubesmanager/settings.py +++ b/qubesmanager/settings.py @@ -50,10 +50,12 @@ from multiselectwidget import * class VMSettingsWindow(Ui_SettingsDialog, QDialog): - def __init__(self, vm, parent=None): + def __init__(self, vm, init_page=0, parent=None): super(VMSettingsWindow, self).__init__(parent) self.setupUi(self) + if init_page in range(self.tabWidget.count()): + self.tabWidget.setCurrentIndex(init_page) self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply) self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject) diff --git a/resources.qrc b/resources.qrc index 360ff93..483f372 100644 --- a/resources.qrc +++ b/resources.qrc @@ -1,5 +1,13 @@ + icons/pencil.png + icons/edit.png + icons/add.png + icons/flag-blue.png + icons/flag-green.png + icons/flag-red.png + icons/flag-yellow.png + icons/remove.png icons/on.png icons/appsprefs.png icons/newfirewall.png diff --git a/restoredlg.ui b/restoredlg.ui index 88360e1..8ff2cee 100644 --- a/restoredlg.ui +++ b/restoredlg.ui @@ -20,170 +20,168 @@ QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage - - - - - Device - - - - - - - - 0 - 0 - - - - - dev1 - - - - - longdeviceblablabla - - - - - dev2 - - - - - dev3 - - - - - - - - Backup directory: - - - - - - - - - - ... - - - - - + + + - 12 - 75 - false - true - false + 50 + false - - Select backup source location: + + Backup source location + + + + + + 50 + false + + + + Device + + + + + + + + 0 + 0 + + + + + dev1 + + + + + longdeviceblablabla + + + + + dev2 + + + + + dev3 + + + + + + + + Backup directory: + + + + + + + + + + ... + + + + + + + + Qt::Vertical + + + + 20 + 215 + + + + - + - - - - 12 - 75 - false - true - false - - - - Select VMs to restore: + + + VMs to restore + - - - - + - 12 - 75 - false - true - false + 50 + false - - Restore options: + + Restore options + + + + + + 50 + false + + + + Do not restore VMs that have missing templates or netvms. + + + skip broken + + + + + + + Ignore missing templates or netvms, restore VMs anyway. + + + ignore missing + + + + + + + Do not restore VMs that are already present on the host. + + + skip conflicting + + + + + + + Ignore dom0 username mismatch while restoring homedir. + + + ignore username mismatch + + + + - - - - - - Do not restore VMs that have missing templates or netvms. - - - skip broken - - - - - - - Do not restore VMs that are already present on the host. - - - skip conflicting - - - - - - - Ignore dom0 username mismatch while restoring homedir. - - - ignore username mismatch - - - - - - - Ignore missing templates or netvms, restore VMs anyway. - - - ignore missing - - - - - - - Force to run, even with root privileges. - - - force root - - - - - @@ -192,10 +190,10 @@ - 12 - 75 + 9 + 50 false - true + false false @@ -223,10 +221,10 @@ p, li { white-space: pre-wrap; } - 12 - 75 + 9 + 50 false - true + false false @@ -243,10 +241,10 @@ p, li { white-space: pre-wrap; } - 12 - 75 + 9 + 50 false - true + false false @@ -255,21 +253,6 @@ p, li { white-space: pre-wrap; } - - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info<br />A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A lot of info A lot of info A lot of info A lot of info A lot of info A lot of info<br />A lot of info</p></body></html> - - - diff --git a/settingsdlg.ui b/settingsdlg.ui index 19d0b76..59d0c1b 100644 --- a/settingsdlg.ui +++ b/settingsdlg.ui @@ -26,7 +26,7 @@ - 0 + 2 @@ -35,131 +35,123 @@ Basic - + - - - - - Settings - - - - - - Name & label: - - - - - - - myappvm - - - - - - - true - - - - - - - Use this template: - - - - - - - - - - Allow networking - - - true - - - - - - - - - - Info - - - - - - Type: - - - - - - - - 75 - true - - - - AppVM - - - - - - - Installed by RPM: - - - - - - - - 75 - true - - - - No - - - - - - - + + + Settings + + + + + + Name & label: + + + + + + + myappvm + + + + + + + true + + + + + + + Template: + + + + + + + + + + NetVM: + + + + + + + + + + true + + + Include in backups by default + + + true + + + + + - - - Qt::Vertical + + + Info - - - 20 - 157 - - - + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Type: + + + + + + + + 75 + true + + + + AppVM + + + + + + + Installed by RPM: + + + + + + + + 75 + true + + + + No + + + + + - - - - - Advanced - - - + Disk storage @@ -168,7 +160,7 @@ - false + true Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -181,16 +173,6 @@ - - - - false - - - Allow to grow - - - @@ -205,64 +187,39 @@ - - - - false - - - Include in backups - - - true - - - - + + + + Qt::Vertical + + + + 20 + 73 + + + + + + + + + Advanced + + + Memory/CPU - - - - - false - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 10000 - - - 100 - - - 400 - - - - - + + + - MB - - - - - - - false - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 1 + Initial memory: @@ -289,57 +246,70 @@ - - + + - Memory: + Max memory: + + + + + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 10000 + + + 100 + + + 400 + + + + + + + MB - - - Max Memory: - - - - - VCPUs: + VCPUs no.: + + + + + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + + + + + Include in memory balancing - - - - Networking - - - - - - NetVM: - - - - - - - - - - VM updateable? - - - - - - - + Kernel @@ -378,7 +348,7 @@ - + Paths @@ -469,6 +439,168 @@ + + + + Qt::Vertical + + + + 20 + 88 + + + + + + + + + Firewall rules + + + + + + Allow network access except... + + + + + + + Deny network access except... + + + + + + + QLayout::SetMaximumSize + + + + + + + false + + + false + + + false + + + true + + + true + + + 40 + + + false + + + + + + + + + Allow ICMP traffic + + + true + + + + + + + Allow DNS queries + + + true + + + + + + + + + + + + + + + + + :/add.png:/add.png + + + + 24 + 24 + + + + + + + + + + + + :/pencil.png:/pencil.png + + + + 24 + 24 + + + + + + + + + + + + :/remove.png:/remove.png + + + + 24 + 24 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + @@ -495,6 +627,112 @@ + + + Services + + + + + + + + + + + + + :/add.png:/add.png + + + + 24 + 24 + + + + + + + + + ntpd + + + Checked + + + + + cupsd + + + Checked + + + + + meminfo + + + Checked + + + + + + + + Checked services will be turned on. + + + + + + + Unchecked services will be turned off. + + + + + + + Unlisted services will follow default VM's settings. + + + + + + + + + + + :/remove.png:/remove.png + + + + 24 + 24 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + From 5e6530d0c932e77626a9630ee9518148357ecada Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Mon, 6 Feb 2012 20:22:11 +0100 Subject: [PATCH 13/31] Manager table columns sortable. --- qubesmanager/main.py | 221 +++++++++++++++++++++++++------------------ 1 file changed, 130 insertions(+), 91 deletions(-) diff --git a/qubesmanager/main.py b/qubesmanager/main.py index ca76aa5..adaecc4 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -104,6 +104,18 @@ class VmStatusIcon(QLabel): class VmInfoWidget (QWidget): + class VmInfoItem (QTableWidgetItem): + def __init__(self, value): + super(VmInfoWidget.VmInfoItem, self).__init__() + self.value = value + + def set_value(self, value): + self.value = value + + def __lt__(self, other): + return self.value < other.value + + def __init__(self, vm, parent = None): super (VmInfoWidget, self).__init__(parent) @@ -117,6 +129,8 @@ class VmInfoWidget (QWidget): layout.addWidget(self.label_name, alignment=Qt.AlignLeft) self.setLayout(layout) + + self.tableItem = self.VmInfoItem(vm.name) def update_vm_state (self, vm): self.vm_icon.update() @@ -124,30 +138,30 @@ class VmInfoWidget (QWidget): -class VmTemplateWidget (QWidget): - def __init__(self, vm, parent=None): - super(VmTemplateWidget, self).__init__(parent) +class VmTemplateItem (QTableWidgetItem): + def __init__(self, vm): + super(VmTemplateItem, self).__init__() - layout = QVBoxLayout() if vm.template_vm is not None: - self.label_tmpl = QLabel ("" + (vm.template_vm.name) + "") + self.setText(vm.template_vm.name) else: + font = QFont() + font.setStyle(QFont.StyleItalic) + self.setFont(font) + self.setTextColor(QColor("gray")) + if vm.is_appvm(): # and vm.template_vm is None - self.label_tmpl = QLabel ("StandaloneVM") + self.setText("StandaloneVM") elif vm.is_template(): - self.label_tmpl = QLabel ("TemplateVM") + self.setText("TemplateVM") elif vm.qid == 0: - self.label_tmpl = QLabel ("AdminVM") + self.setText("AdminVM") elif vm.is_netvm(): - self.label_tmpl = QLabel ("NetVM") + self.setText("NetVM") else: - self.label_tmpl = QLabel ("---") - - - layout.addWidget(self.label_tmpl, alignment=Qt.AlignHCenter) - - self.setLayout(layout) + self.setText("---") + self.setTextAlignment(Qt.AlignHCenter) class VmIconWidget (QWidget): @@ -166,33 +180,42 @@ class VmIconWidget (QWidget): self.setLayout(layout) -class VmNetvmWidget (QWidget): - def __init__(self, vm, parent=None): - super(VmNetvmWidget, self).__init__(parent) +class VmNetvmItem (QTableWidgetItem): + def __init__(self, vm): + super(VmNetvmItem, self).__init__() - layout = QHBoxLayout() - self.icon = VmIconWidget(":/networking.png", vm.is_networked()) - if vm.is_netvm(): - self.label_nvm = QLabel ("self") + self.setText("self") elif vm.netvm_vm is not None: - self.label_nvm = QLabel ("" + (vm.netvm_vm.name) + "") + self.setText(vm.netvm_vm.name) else: - self.label_nvm = QLabel ("None") + self.setText("---") + + self.setTextAlignment(Qt.AlignHCenter) - layout.addWidget(self.icon, alignment=Qt.AlignLeft) - layout.addWidget(self.label_nvm, alignment=Qt.AlignHCenter) - self.setLayout(layout) - class VmUsageBarWidget (QWidget): + + class VmUsageBarItem (QTableWidgetItem): + def __init__(self, value): + super(VmUsageBarWidget.VmUsageBarItem, self).__init__() + self.value = value + + def set_value(self, value): + self.value = value + + def __lt__(self, other): + return self.value < other.value + def __init__(self, min, max, format, update_func, vm, load, parent = None): super (VmUsageBarWidget, self).__init__(parent) + self.min = min self.max = max self.update_func = update_func + self.value = min self.widget = QProgressBar() self.widget.setMinimum(min) @@ -215,30 +238,52 @@ class VmUsageBarWidget (QWidget): layout.addWidget(self.widget) self.setLayout(layout) + self.tableItem = self.VmUsageBarItem(min) self.update_load(vm, load) + + def update_load(self, vm, load): - self.widget.setValue(self.update_func(vm, load)) + self.value = self.update_func(vm, load) + self.widget.setValue(self.value) + self.tableItem.set_value(self.value) +class ChartWidget (QWidget): + + class ChartItem (QTableWidgetItem): + def __init__(self, value): + super(ChartWidget.ChartItem, self).__init__() + self.value = value -class LoadChartWidget (QWidget): + def set_value(self, value): + self.value = value + + def __lt__(self, other): + return self.value < other.value - def __init__(self, vm, cpu_load = 0, parent = None): - super (LoadChartWidget, self).__init__(parent) - self.load = cpu_load if vm.last_power_state else 0 + def __init__(self, vm, update_func, hue, load = 0, parent = None): + super (ChartWidget, self).__init__(parent) + self.update_func = update_func + self.hue = hue + self.load = load assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) self.load_history = [self.load] + self.tableItem = ChartWidget.ChartItem(self.load) - def update_load (self, vm, cpu_load): - self.load = cpu_load if vm.last_power_state else 0 - assert self.load >= 0, "load = {0}".format(self.load) - # assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) - if self.load > 100: - # FIXME: This is an ugly workaround :/ - self.load = 100 + def update_load (self, vm, load): + self.load = self.update_func(vm, load) + assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) + + #This was in LoadChartWidget - double # means the line was a comment. + #assert self.load >= 0, "load = {0}".format(self.load) + # # assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) + # if self.load > 100: + # # FIXME: This is an ugly workaround :/ + # self.load = 100 self.load_history.append (self.load) + self.tableItem.set_value(self.load) self.repaint() def paintEvent (self, Event = None): @@ -257,57 +302,36 @@ class LoadChartWidget (QWidget): for i in range (0, N-1): val = self.load_history[N- i - 1] - hue = 200 sat = 70 + val*(255-70)/100 - color = QColor.fromHsv (hue, sat, 255) + color = QColor.fromHsv (self.hue, sat, 255) pen = QPen (color) pen.setWidth(dx-1) p.setPen(pen) if val > 0: p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100) -class MemChartWidget (QWidget): - - def __init__(self, vm, parent = None): - super (MemChartWidget, self).__init__(parent) - self.load = vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0 - assert self.load >= 0 and self.load <= 100, "mem = {0}".format(self.load) - self.load_history = [self.load] - - def update_load (self, vm): - self.load = vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0 - assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) - self.load_history.append (self.load) - self.repaint() - - def paintEvent (self, Event = None): - p = QPainter (self) - dx = 4 - - W = self.width() - H = self.height() - 5 - N = len(self.load_history) - if N > W/dx: - tail = N - W/dx - N = W/dx - self.load_history = self.load_history[tail:] - - assert len(self.load_history) == N - - for i in range (0, N-1): - val = self.load_history[N- i - 1] - hue = 120 - sat = 70 + val*(255-70)/100 - color = QColor.fromHsv (hue, sat, 255) - pen = QPen (color) - pen.setWidth(dx-1) - p.setPen(pen) - if val > 0: - p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100) class VmUpdateInfoWidget(QWidget): + class VmUpdateInfoItem (QTableWidgetItem): + def __init__(self, value): + super(VmUpdateInfoWidget.VmUpdateInfoItem, self).__init__() + self.value = value + + def set_value(self, value): + self.value = value + + def __lt__(self, other): + if self.value == "outdated": + return other.value == "outdated" + elif self.value == "update": + return other.value == "outdated" or other.value == "update" + elif self.value == "ok": + return other.value == "outdated" or other.value == "update" or other.value == "ok" + else: + return True + def __init__(self, vm, show_text=True, parent = None): super (VmUpdateInfoWidget, self).__init__(parent) layout = QHBoxLayout () @@ -322,6 +346,8 @@ class VmUpdateInfoWidget(QWidget): self.previous_outdated = False self.previous_update_recommended = None + self.value = None + self.tableItem = VmUpdateInfoWidget.VmUpdateInfoItem(self.value) def update_outdated(self, vm): outdated = vm.is_outdated() @@ -346,7 +372,8 @@ class VmUpdateInfoWidget(QWidget): self.previous_update_recommended = update_recommended def update_status_widget(self, state): - + self.value = state + self.tableItem.set_value(state) if state == "ok": label_text = "" icon_path = ":/flag-green.png" @@ -393,32 +420,41 @@ class VmRowInTable(object): self.info_widget = VmInfoWidget(vm) table.setCellWidget(row_no, 0, self.info_widget) + table.setItem(row_no, 0, self.info_widget.tableItem) self.upd_widget = VmUpdateInfoWidget(vm, False) table.setCellWidget(row_no, 1, self.upd_widget) + table.setItem(row_no, 1, self.upd_widget.tableItem) - self.template_widget = VmTemplateWidget(vm) - table.setCellWidget(row_no, 2, self.template_widget) - - self.netvm_widget = VmNetvmWidget(vm) - table.setCellWidget(row_no, 3, self.netvm_widget) + self.template_widget = VmTemplateItem(vm) + table.setItem(row_no, 2, self.template_widget) + + self.netvm_widget = VmNetvmItem(vm) + table.setItem(row_no, 3, self.netvm_widget) self.cpu_usage_widget = VmUsageBarWidget(0, 100, "", lambda vm, val: val if vm.last_power_state else 0, vm, 0) table.setCellWidget(row_no, 4, self.cpu_usage_widget) + table.setItem(row_no, 4, self.cpu_usage_widget.tableItem) - self.load_widget = LoadChartWidget(vm) + #self.load_widget = LoadChartWidget(vm) + self.load_widget = ChartWidget(vm, lambda vm, val: val if vm.last_power_state else 0, 200, 0 ) table.setCellWidget(row_no, 5, self.load_widget) + table.setItem(row_no, 5, self.load_widget.tableItem) self.mem_usage_widget = VmUsageBarWidget(0, qubes_host.memory_total/1024, "%v MB", lambda vm, val: vm.get_mem()/1024 if vm.last_power_state else 0, vm, 0) table.setCellWidget(row_no, 6, self.mem_usage_widget) + table.setItem(row_no, 6, self.mem_usage_widget.tableItem) - self.mem_widget = MemChartWidget(vm) + + self.mem_widget = ChartWidget(vm, lambda vm, val: vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0, 120, 0) table.setCellWidget(row_no, 7, self.mem_widget) + table.setItem(row_no, 7, self.mem_widget.tableItem) self.updateinfo_widget = VmUpdateInfoWidget(vm, True) table.setCellWidget(row_no, 8, self.updateinfo_widget) + table.setItem(row_no, 8, self.updateinfo_widget.tableItem) self.blockdevices_widget = VmBlockDevicesWidget(vm) table.setCellWidget(row_no, 9, self.blockdevices_widget) @@ -430,7 +466,7 @@ class VmRowInTable(object): self.cpu_usage_widget.update_load(self.vm, cpu_load) self.mem_usage_widget.update_load(self.vm, None) self.load_widget.update_load(self.vm, cpu_load) - self.mem_widget.update_load(self.vm) + self.mem_widget.update_load(self.vm, None) self.updateinfo_widget.update_outdated(self.vm) self.upd_widget.update_outdated(self.vm) @@ -528,7 +564,8 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.actionBlock_Devices.setChecked(False) self.table.setColumnWidth(self.columns_indices["Upd"], 50) - #self.table.setFrameShape(QFrame.NoFrame) + self.table.sortItems(self.columns_indices["MEM"], Qt.DescendingOrder) + self.table.setContentsMargins(0,0,0,0) self.centralwidget.layout().setContentsMargins(0,0,0,0) self.layout().setContentsMargins(0,0,0,0) @@ -609,7 +646,8 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): return vms_to_display def fill_table(self): - #self.table.clear() + self.table.setSortingEnabled(False) + self.table.clearContents() vms_list = self.get_vms_list() self.table.setRowCount(len(vms_list)) @@ -629,6 +667,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.vms_list = vms_list self.vms_in_table = vms_in_table self.reload_table = False + self.table.setSortingEnabled(True) def mark_table_for_update(self): From ce30f9a83f0706a30d7bec19462314121cb45730 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Mon, 6 Feb 2012 22:32:36 +0100 Subject: [PATCH 14/31] Cpu_load >100 workaround in the generic chart widget. --- qubesmanager/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qubesmanager/main.py b/qubesmanager/main.py index adaecc4..b32ede9 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -266,6 +266,8 @@ class ChartWidget (QWidget): super (ChartWidget, self).__init__(parent) self.update_func = update_func self.hue = hue + if hue < 0 or hue > 255: + self.hue = 255 self.load = load assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) self.load_history = [self.load] @@ -273,14 +275,12 @@ class ChartWidget (QWidget): def update_load (self, vm, load): self.load = self.update_func(vm, load) - assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) - #This was in LoadChartWidget - double # means the line was a comment. - #assert self.load >= 0, "load = {0}".format(self.load) - # # assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) - # if self.load > 100: - # # FIXME: This is an ugly workaround :/ - # self.load = 100 + assert self.load >= 0, "load = {0}".format(self.load) + # assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load) + if self.load > 100: + # FIXME: This is an ugly workaround for cpu_load:/ + self.load = 100 self.load_history.append (self.load) self.tableItem.set_value(self.load) From 59081c132b7353d48f8bb7665ec6d1aefa5ae122 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 7 Feb 2012 12:48:53 +0100 Subject: [PATCH 15/31] Fixed get_selected_vm mismatch after sorting. --- qubesmanager/main.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/qubesmanager/main.py b/qubesmanager/main.py index b32ede9..8ec29d6 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -651,7 +651,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): vms_list = self.get_vms_list() self.table.setRowCount(len(vms_list)) - vms_in_table = [] + vms_in_table = {} row_no = 0 for vm in vms_list: @@ -660,7 +660,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): if vm.internal: continue vm_row = VmRowInTable (vm, row_no, self.table) - vms_in_table.append (vm_row) + vms_in_table[vm.name] = vm_row row_no += 1 self.table.setRowCount(row_no) @@ -692,7 +692,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): qubes_host.measure_cpu_usage(self.last_measure_results, self.last_measure_time) - for vm_row in self.vms_in_table: + for vm_row in self.vms_in_table.values(): cur_cpu_load = None if vm_row.vm.get_xid() in self.last_measure_results: cur_cpu_load = self.last_measure_results[vm_row.vm.xid]['cpu_usage'] @@ -700,7 +700,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): cur_cpu_load = 0 vm_row.update(self.counter, cpu_load = cur_cpu_load) else: - for vm_row in self.vms_in_table: + for vm_row in self.vms_in_table.values(): vm_row.update(self.counter) #self.table_selection_changed() @@ -831,9 +831,11 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): def get_selected_vm(self): + #vm selection relies on the VmInfo widget's value used for sorting by VM name row_index = self.table.currentRow() - assert self.vms_in_table[row_index] is not None - vm = self.vms_in_table[row_index].vm + vm_name = self.table.item(row_index, self.columns_indices["Name"]).value + assert self.vms_in_table[vm_name] is not None + vm = self.vms_in_table[vm_name].vm return vm @pyqtSlot(name='on_action_removevm_triggered') From 7113d747934139e81cb244ceda9f0333e74d77cc Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Wed, 8 Feb 2012 18:21:15 +0100 Subject: [PATCH 16/31] Block devices attached and detached to/from VMs via combobox in Qubes Manager --- qubesmanager/main.py | 199 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 170 insertions(+), 29 deletions(-) diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 8ec29d6..8a2f79e 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -398,21 +398,133 @@ class VmUpdateInfoWidget(QWidget): class VmBlockDevicesWidget(QWidget): - def __init__(self, vm, parent=None): + def __init__(self, vm, block_manager, parent=None): super(VmBlockDevicesWidget, self).__init__(parent) - combo = QComboBox() - combo.addItem("USB dummy1") - combo.addItem("USB dummy2") - combo.addItem("USB dummy3") + self.vm = vm + self.block_manager = block_manager + self.free_devs = self.block_manager.free_devs + self.att_devs = self.block_manager.attached_devs + + self.combo = QComboBox() layout = QVBoxLayout() - layout.addWidget(combo) + layout.addWidget(self.combo) self.setLayout(layout) + + self.connect(self.combo, SIGNAL("activated(int)"), self.combo_activated) + + def update(self, vm): + self.combo.clear() + self.combo.addItem("None") + for a in self.att_devs: + if self.att_devs[a]['attached_to']['vm'] == vm.name: + att = self.att_devs[a]['dev'] + ": " + unicode(self.att_devs[a]['size']) + " " + self.att_devs[a]['desc'] + self.combo.addItem(att, QVariant(a)) + self.combo.setCurrentIndex(1) + break + if self.combo.count() == 1: + for d in self.free_devs: + str = self.free_devs[d]['dev'] + ": " + unicode(self.free_devs[d]['size']) + " " + self.free_devs[d]['desc'] + self.combo.addItem(str, QVariant(d)) + self.prev_idx = self.combo.currentIndex(); + + def combo_activated(self, idx): + if idx == self.prev_idx: #nothing has changed + return + #there was a change + if self.combo.currentText() != "None": #device attached: + self.prev_idx = idx + dev_name = str(self.combo.itemData(idx).toString()) + self.block_manager.attach_device(self.vm, dev_name) + + else: + dev_name = str(self.combo.itemData(self.prev_idx).toString()) + self.prev_idx = idx + self.block_manager.detach_device(self.vm, dev_name) + + + +class QubesBlockDevicesManager(): + def __init__(self, qvm_collection): + self.qvm_collection = qvm_collection + self.attached_devs = {} + self.free_devs = {} + + self.current_blk = {} + self.current_attached = {} + self.devs_changed = False + + def update(self): + blk = qubesutils.block_list() + for b in blk: + att = qubesutils.block_check_attached(None, blk[b]['device'], backend_xid = blk[b]['xid']) + if b in self.current_blk: + if blk[b] == self.current_blk[b]: + if self.current_attached[b] != att: #devices the same, sth with attaching changed + self.current_attached[b] = att + self.devs_changed = True + else: #device changed ?! + self.current_blk[b] = blk[b] + self.current_attached[b] = att + self.devs_changed = True + else: #new device + self.current_blk[b] = blk[b] + self.current_attached[b] = att + self.devs_changed = True + + for b in self.current_blk: + if b not in blk: + del self.current_blk[b] + del current_attached[b] + self.devs_changed = True + + if self.devs_changed == True: + self.devs_changed = False + self.__update_blk_entries__() + return True + else: + return False + + + def __update_blk_entries__(self): + self.free_devs.clear() + self.attached_devs.clear() + + for b in self.current_attached: + if self.current_attached[b]: + self.attached_devs[b] = self.__make_entry__(b, self.current_blk[b], self.current_attached[b]) + else: + self.free_devs[b] = self.__make_entry__(b, self.current_blk[b], None) + + def __make_entry__(self, k, dev, att): + size_str = qubesutils.bytes_to_kmg(dev['size']) + entry = { 'dev': dev['device'], + 'backend_name': dev['vm'], + 'desc': dev['desc'], + 'size': size_str, + 'attached_to': att, } + return entry + + def attach_device(self, vm, dev): + backend_vm_name = self.free_devs[dev]['backend_name'] + dev_id = self.free_devs[dev]['dev'] + backend_vm = self.qvm_collection.get_vm_by_name(backend_vm_name) + trayIcon.showMessage ("Qubes Manager", "{0} - attaching {1}".format(vm.name, dev), msecs=3000) + qubesutils.block_attach(vm, backend_vm, dev_id) + self.devs_changed = True + + def detach_device(self, vm, dev_name): + dev_id = self.attached_devs[dev_name]['attached_to']['devid'] + vm_xid = self.attached_devs[dev_name]['attached_to']['xid'] + trayIcon.showMessage ("Qubes Manager", "{0} - detaching {1}".format(vm.name, dev_name), msecs=3000) + qubesutils.block_detach(None, dev_id, vm_xid) + self.devs_changed = True + class VmRowInTable(object): - def __init__(self, vm, row_no, table): + def __init__(self, vm, row_no, table, block_manager): self.vm = vm self.row_no = row_no @@ -456,12 +568,13 @@ class VmRowInTable(object): table.setCellWidget(row_no, 8, self.updateinfo_widget) table.setItem(row_no, 8, self.updateinfo_widget.tableItem) - self.blockdevices_widget = VmBlockDevicesWidget(vm) + self.blockdevices_widget = VmBlockDevicesWidget(vm, block_manager) table.setCellWidget(row_no, 9, self.blockdevices_widget) - def update(self, counter, cpu_load = None): + def update(self, counter, update_devs = False, cpu_load = None): self.info_widget.update_vm_state(self.vm) + self.blockdevices_widget.setEnabled(self.vm.is_running()) if cpu_load is not None: self.cpu_usage_widget.update_load(self.vm, cpu_load) self.mem_usage_widget.update_load(self.vm, None) @@ -469,6 +582,8 @@ class VmRowInTable(object): self.mem_widget.update_load(self.vm, None) self.updateinfo_widget.update_outdated(self.vm) self.upd_widget.update_outdated(self.vm) + if self.blockdevices_widget.isEnabled() and update_devs: + self.blockdevices_widget.update(self.vm) class NewAppVmDlg (QDialog, ui_newappvmdlg.Ui_NewAppVMDlg): def __init__(self, parent = None): @@ -541,6 +656,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.toolbar = self.toolBar self.qvm_collection = QubesVmCollection() + self.blkManager = QubesBlockDevicesManager(self.qvm_collection) self.connect(self.table, SIGNAL("itemSelectionChanged()"), self.table_selection_changed) @@ -563,6 +679,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.table.setColumnHidden( self.columns_indices["Block Device"], True) self.actionBlock_Devices.setChecked(False) self.table.setColumnWidth(self.columns_indices["Upd"], 50) + self.table.setColumnWidth(self.columns_indices["Block Device"], 250) self.table.sortItems(self.columns_indices["MEM"], Qt.DescendingOrder) @@ -659,7 +776,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): continue if vm.internal: continue - vm_row = VmRowInTable (vm, row_no, self.table) + vm_row = VmRowInTable (vm, row_no, self.table, self.blkManager) vms_in_table[vm.name] = vm_row row_no += 1 @@ -675,7 +792,6 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): # When calling update_table() directly, always use out_of_schedule=True! def update_table(self, out_of_schedule=False): - if manager_window.isVisible(): some_vms_have_changed_power_state = False for vm in self.vms_list: @@ -684,8 +800,10 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): vm.last_power_state = state some_vms_have_changed_power_state = True + update_devs = self.update_block_devices() if self.reload_table or ((not self.show_inactive_vms) and some_vms_have_changed_power_state): self.fill_table() + update_devs=True if self.counter % 3 == 0 or out_of_schedule: (self.last_measure_time, self.last_measure_results) = \ @@ -698,10 +816,10 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): cur_cpu_load = self.last_measure_results[vm_row.vm.xid]['cpu_usage'] else: cur_cpu_load = 0 - vm_row.update(self.counter, cpu_load = cur_cpu_load) + vm_row.update(self.counter, update_devs=update_devs, cpu_load = cur_cpu_load) else: for vm_row in self.vms_in_table.values(): - vm_row.update(self.counter) + vm_row.update(self.counter, update_devs=update_devs) #self.table_selection_changed() @@ -719,20 +837,40 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.centralwidget.setFixedWidth(table_width) self.setFixedWidth(table_width) + def update_block_devices(self): + if self.table.isColumnHidden( self.columns_indices['Block Device']): + return False + + if self.blkManager.update(): + return True + else: + return False + def table_selection_changed (self): vm = self.get_selected_vm() - # Update available actions: - self.action_settings.setEnabled(True) - self.action_removevm.setEnabled(not vm.installed_by_rpm and not vm.last_power_state) - self.action_resumevm.setEnabled(not vm.last_power_state) - self.action_pausevm.setEnabled(vm.last_power_state and vm.qid != 0) - self.action_shutdownvm.setEnabled(not vm.is_netvm() and vm.last_power_state and vm.qid != 0) - self.action_appmenus.setEnabled(not vm.is_netvm()) - self.action_editfwrules.setEnabled(vm.is_networked() and not (vm.is_netvm() and not vm.is_proxyvm())) - self.action_updatevm.setEnabled(vm.is_updateable() or vm.qid == 0) + if vm != None: + # Update available actions: + self.action_settings.setEnabled(True) + self.action_removevm.setEnabled(not vm.installed_by_rpm and not vm.last_power_state) + self.action_resumevm.setEnabled(not vm.last_power_state) + self.action_pausevm.setEnabled(vm.last_power_state and vm.qid != 0) + self.action_shutdownvm.setEnabled(not vm.is_netvm() and vm.last_power_state and vm.qid != 0) + self.action_appmenus.setEnabled(not vm.is_netvm()) + self.action_editfwrules.setEnabled(vm.is_networked() and not (vm.is_netvm() and not vm.is_proxyvm())) + self.action_updatevm.setEnabled(vm.is_updateable() or vm.qid == 0) + else: + self.action_settings.setEnabled(False) + self.action_removevm.setEnabled(False) + self.action_resumevm.setEnabled(False) + self.action_pausevm.setEnabled(False) + self.action_shutdownvm.setEnabled(False) + self.action_appmenus.setEnabled(False) + self.action_editfwrules.setEnabled(False) + self.action_updatevm.setEnabled(False) + def closeEvent (self, event): @@ -744,8 +882,6 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): def action_createvm_triggered(self): dialog = NewAppVmDlg() - print "Create VM triggered!\n" - # Theoretically we should be locking for writing here and unlock # only after the VM creation finished. But the code would be more messy... # Instead we lock for writing in the actual worker thread @@ -833,10 +969,13 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): def get_selected_vm(self): #vm selection relies on the VmInfo widget's value used for sorting by VM name row_index = self.table.currentRow() - vm_name = self.table.item(row_index, self.columns_indices["Name"]).value - assert self.vms_in_table[vm_name] is not None - vm = self.vms_in_table[vm_name].vm - return vm + if row_index != None: + vm_name = self.table.item(row_index, self.columns_indices["Name"]).value + assert self.vms_in_table[vm_name] is not None + vm = self.vms_in_table[vm_name].vm + return vm + else: + return None @pyqtSlot(name='on_action_removevm_triggered') def action_removevm_triggered(self): @@ -1109,7 +1248,9 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.showhide_collumn( self.columns_indices['Update Info'], checked) def on_actionBlock_Devices_toggled(self, checked): - self.showhide_collumn( self.columns_indices['Block Device'], checked) + self.showhide_collumn( self.columns_indices['Block Device'], checked) + + class QubesTrayIcon(QSystemTrayIcon): From 43d479375a08ddb69d460a0369c85d6a5fdc065b Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Wed, 8 Feb 2012 19:02:35 +0100 Subject: [PATCH 17/31] Conext menu and shifted View menu in the main window. --- mainwindow.ui | 33 ++++++++++++++++++--------------- qubesmanager/main.py | 9 +++++++++ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index bd5bfb7..66a1b36 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -90,6 +90,9 @@ 0 + + Qt::ScrollBarAlwaysOn + true @@ -230,21 +233,15 @@ View - - - Columns visibility - - - - - - - - - - - - + + + + + + + + + @@ -253,6 +250,9 @@ toolBar + + Qt::BottomToolBarArea|Qt::TopToolBarArea + false @@ -552,6 +552,9 @@ true + + true + Upd diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 8a2f79e..e503c71 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -683,6 +683,15 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.table.sortItems(self.columns_indices["MEM"], Qt.DescendingOrder) + self.table.addAction(self.action_settings) + self.table.addAction(self.action_removevm) + self.table.addAction(self.action_resumevm) + self.table.addAction(self.action_pausevm) + self.table.addAction(self.action_shutdownvm) + self.table.addAction(self.action_appmenus) + self.table.addAction(self.action_editfwrules) + self.table.addAction(self.action_updatevm) + self.table.setContentsMargins(0,0,0,0) self.centralwidget.layout().setContentsMargins(0,0,0,0) self.layout().setContentsMargins(0,0,0,0) From 6aac5d814820c9504e053f1dfa3be82605380066 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Thu, 9 Feb 2012 08:33:28 +0100 Subject: [PATCH 18/31] tiny fix to QubesBlockDeviceManager --- qubesmanager/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qubesmanager/main.py b/qubesmanager/main.py index e503c71..a5d67a1 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -473,10 +473,10 @@ class QubesBlockDevicesManager(): self.current_attached[b] = att self.devs_changed = True - for b in self.current_blk: + for b in self.current_blk: #remove devices that are not there anymore if b not in blk: del self.current_blk[b] - del current_attached[b] + del self.current_attached[b] self.devs_changed = True if self.devs_changed == True: From 8286a0b929e7a48abc508676e250b0f0f2ad4294 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Thu, 9 Feb 2012 19:47:21 +0100 Subject: [PATCH 19/31] Appselect window moved to settings tab. --- qubesmanager/appmenu_select.py | 92 ++----------------------------- qubesmanager/global_settings.py | 2 +- qubesmanager/main.py | 8 +-- qubesmanager/multiselectwidget.py | 7 --- qubesmanager/settings.py | 59 +++++++++++++++----- 5 files changed, 55 insertions(+), 113 deletions(-) diff --git a/qubesmanager/appmenu_select.py b/qubesmanager/appmenu_select.py index 9c56d47..5544ce9 100755 --- a/qubesmanager/appmenu_select.py +++ b/qubesmanager/appmenu_select.py @@ -69,38 +69,19 @@ class ThreadMonitor(QObject): self.event_finished.set() -class AppmenuSelectWindow(QDialog): - row_height = 20 - - def __init__(self, vm, parent=None): - super(AppmenuSelectWindow, self).__init__(parent) - - self.gridLayout = QGridLayout(self) - - self.buttonBox = QDialogButtonBox(self) - self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) - self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply) - self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject) - - self.app_list = MultiSelectWidget(self) +class AppmenuSelectManager: + def __init__(self, vm, apps_multiselect, parent=None): + self.app_list = apps_multiselect # this is a multiselect wiget - self.gridLayout.addWidget(self.app_list, 0, 0, 1, 1) - self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) - self.vm = vm if self.vm.template_vm: self.source_vm = self.vm.template_vm else: self.source_vm = self.vm - self.setWindowTitle("Qubes Appmenus for %s" % vm.name) - self.resize(600,600) self.fill_apps_list() - def reject(self): - self.done(0) - def fill_apps_list(self): template_dir = self.source_vm.appmenus_templates_dir @@ -146,73 +127,8 @@ class AppmenuSelectWindow(QDialog): whitelisted.close() - def save_and_apply(self): + def save_appmenu_select_changes(self): self.save_list_of_selected() subprocess.check_call([qubes_appmenu_remove_cmd, self.vm.name]) subprocess.check_call([qubes_appmenu_create_cmd, self.source_vm.appmenus_templates_dir, self.vm.name]) - self.done(0) - -# Bases on the original code by: -# Copyright (c) 2002-2007 Pascal Varet - -def handle_exception( exc_type, exc_value, exc_traceback ): - import sys - import os.path - import traceback - - filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop() - filename = os.path.basename( filename ) - error = "%s: %s" % ( exc_type.__name__, exc_value ) - - QMessageBox.critical(None, "Houston, we have a problem...", - "Whoops. A critical error has occured. This is most likely a bug " - "in Qubes Appmenu Select application.

" - "%s" % error + - "at line %d of file %s.

" - % ( line, filename )) - - #sys.exit(1) - -def main(): - - - global qubes_host - qubes_host = QubesHost() - - global app - app = QApplication(sys.argv) - app.setOrganizationName("The Qubes Project") - app.setOrganizationDomain("http://qubes-os.org") - app.setApplicationName("Qubes Appmenu Select") - app.setWindowIcon(QIcon(":/qubes.png")) - - sys.excepthook = handle_exception - - qvm_collection = QubesVmCollection() - qvm_collection.lock_db_for_reading() - qvm_collection.load() - qvm_collection.unlock_db() - - vm = None - - if len(sys.argv) > 1: - vm = qvm_collection.get_vm_by_name(sys.argv[1]) - if vm is None or vm.qid not in qvm_collection: - QMessageBox.critical(None, "Qubes Appmenu Select Error", - "A VM with the name '{0}' does not exist in the system.".format(sys.argv[1])) - sys.exit(1) - else: - vms_list = [vm.name for vm in qvm_collection.values() if (vm.is_appvm() or vm.is_template())] - vmname = QInputDialog.getItem(None, "Select VM", "Select VM:", vms_list, editable = False) - if not vmname[1]: - sys.exit(1) - vm = qvm_collection.get_vm_by_name(vmname[0]) - - global manager_window - select_window = AppmenuSelectWindow(vm) - - select_window.show() - - app.exec_() - app.exit() diff --git a/qubesmanager/global_settings.py b/qubesmanager/global_settings.py index 373ddff..340210b 100644 --- a/qubesmanager/global_settings.py +++ b/qubesmanager/global_settings.py @@ -94,7 +94,7 @@ def main(): qvm_collection.unlock_db() global global_window - global_window = GlobalSetingsWindow() + global_window = GlobalSettingsWindow() global_window.show() diff --git a/qubesmanager/main.py b/qubesmanager/main.py index a5d67a1..442348b 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -38,7 +38,6 @@ from qubes import qubesutils import qubesmanager.resources_rc import ui_newappvmdlg from ui_mainwindow import * -from appmenu_select import AppmenuSelectWindow from settings import VMSettingsWindow from restore import RestoreVMsWindow from backup import BackupVMsWindow @@ -1136,15 +1135,16 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): @pyqtSlot(name='on_action_settings_triggered') def action_settings_triggered(self): vm = self.get_selected_vm() - settings_window = VMSettingsWindow(vm, 1) + settings_window = VMSettingsWindow(vm, app, "basic") settings_window.exec_() @pyqtSlot(name='on_action_appmenus_triggered') def action_appmenus_triggered(self): vm = self.get_selected_vm() - select_window = AppmenuSelectWindow(vm) - select_window.exec_() + settings_window = VMSettingsWindow(vm, app, "applications") + settings_window.exec_() + @pyqtSlot(name='on_action_updatevm_triggered') def action_updatevm_triggered(self): diff --git a/qubesmanager/multiselectwidget.py b/qubesmanager/multiselectwidget.py index 46c7cf2..fbedf72 100644 --- a/qubesmanager/multiselectwidget.py +++ b/qubesmanager/multiselectwidget.py @@ -46,10 +46,3 @@ class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): self.available_list.clear() self.selected_list.clear() - - -if __name__ == "__main__": - app = QtGui.QApplication(sys.argv) - ui = MultiSelectWidget() - ui.show() - sys.exit(app.exec_()) diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index b7ce7e1..be52243 100644 --- a/qubesmanager/settings.py +++ b/qubesmanager/settings.py @@ -45,17 +45,25 @@ from operator import itemgetter from ui_settingsdlg import * from multiselectwidget import * - +from appmenu_select import * class VMSettingsWindow(Ui_SettingsDialog, QDialog): + tabs_indices = {"basic": 0, + "advanced": 1, + "firewall": 2, + "devices": 3, + "applications": 4, + "services": 5,} - def __init__(self, vm, init_page=0, parent=None): + def __init__(self, vm, app, init_page="basic", parent=None): super(VMSettingsWindow, self).__init__(parent) self.setupUi(self) - if init_page in range(self.tabWidget.count()): - self.tabWidget.setCurrentIndex(init_page) + if init_page in self.tabs_indices: + idx = self.tabs_indices[init_page] + assert (idx in range(self.tabWidget.count())) + self.tabWidget.setCurrentIndex(idx) self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply) self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject) @@ -66,22 +74,49 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): self.apps_layout.addWidget(self.app_list) self.devices_layout.addWidget(self.dev_list) + self.app = app self.vm = vm if self.vm.template_vm: self.source_vm = self.vm.template_vm else: self.source_vm = self.vm - - - #self.fill_apps_list() - #self.fill_devices_list() + + self.AppListManager = AppmenuSelectManager(self.vm, self.app_list) def reject(self): self.done(0) - def save_and_apply(self): + #needed not to close the dialog before applying changes + def accept(self): pass + def save_and_apply(self): + thread_monitor = ThreadMonitor() + thread = threading.Thread (target=self.__save_changes__, args=(thread_monitor,)) + thread.daemon = True + thread.start() + + progress = QProgressDialog ("Applying settings to {0}...".format(self.vm.name), "", 0, 0) + progress.setCancelButton(None) + progress.setModal(True) + progress.show() + + while not thread_monitor.is_finished(): + self.app.processEvents() + time.sleep (0.1) + + progress.hide() + + if not thread_monitor.success: + QMessageBox.warning (None, "Error while changing settings for {0}!", "ERROR: {1}".format(self.vm.namethread_monitor.error_msg)) + + self.done(0) + + def __save_changes__(self, thread_monitor): + self.AppListManager.save_appmenu_select_changes() + thread_monitor.set_finished() + + # Bases on the original code by: # Copyright (c) 2002-2007 Pascal Varet @@ -102,8 +137,6 @@ def handle_exception( exc_type, exc_value, exc_traceback ): % ( line, filename )) - - def main(): global qubes_host @@ -137,8 +170,9 @@ def main(): sys.exit(1) vm = qvm_collection.get_vm_by_name(vmname[0]) + global settings_window - settings_window = VMSettingsWindow(vm) + settings_window = VMSettingsWindow(vm, app, "basic") settings_window.show() @@ -146,6 +180,5 @@ def main(): app.exit() - if __name__ == "__main__": main() From 7b5f383f1336d93efa39b1519dd547ac5e2003a6 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Fri, 10 Feb 2012 00:30:45 +0100 Subject: [PATCH 20/31] Firewall rules window moved to settings tab. --- qubesmanager/firewall.py | 99 +--------------------------- qubesmanager/main.py | 15 +---- qubesmanager/settings.py | 139 +++++++++++++++++++++++++++++++++++---- 3 files changed, 130 insertions(+), 123 deletions(-) diff --git a/qubesmanager/firewall.py b/qubesmanager/firewall.py index aef3493..30f4f54 100644 --- a/qubesmanager/firewall.py +++ b/qubesmanager/firewall.py @@ -31,106 +31,8 @@ from qubes.qubes import QubesVmCollection from qubes.qubes import QubesException from qubes.qubes import dry_run -import ui_editfwrulesdlg import ui_newfwruledlg -class EditFwRulesDlg (QDialog, ui_editfwrulesdlg.Ui_EditFwRulesDlg): - def __init__(self, parent = None): - super (EditFwRulesDlg, self).__init__(parent) - self.setupUi(self) - self.newRuleButton.clicked.connect(self.new_rule_button_pressed) - self.editRuleButton.clicked.connect(self.edit_rule_button_pressed) - self.deleteRuleButton.clicked.connect(self.delete_rule_button_pressed) - self.policyAllowRadioButton.toggled.connect(self.policy_radio_toggled) - self.dnsCheckBox.toggled.connect(self.dns_checkbox_toggled) - self.icmpCheckBox.toggled.connect(self.icmp_checkbox_toggled) - - def set_model(self, model): - self.__model = model - self.rulesTreeView.setModel(model) - self.rulesTreeView.header().setResizeMode(QHeaderView.ResizeToContents) - self.rulesTreeView.header().setResizeMode(0, QHeaderView.Stretch) - self.set_allow(model.allow) - self.dnsCheckBox.setChecked(model.allowDns) - self.icmpCheckBox.setChecked(model.allowIcmp) - self.setWindowTitle(model.get_vm_name() + " firewall") - - def set_allow(self, allow): - self.policyAllowRadioButton.setChecked(allow) - self.policyDenyRadioButton.setChecked(not allow) - - def policy_radio_toggled(self, on): - self.__model.allow = self.policyAllowRadioButton.isChecked() - - def dns_checkbox_toggled(self, on): - self.__model.allowDns = on - - def icmp_checkbox_toggled(self, on): - self.__model.allowIcmp = on - - def new_rule_button_pressed(self): - dialog = NewFwRuleDlg() - self.run_rule_dialog(dialog) - - def edit_rule_button_pressed(self): - dialog = NewFwRuleDlg() - dialog.set_ok_enabled(True) - selected = self.rulesTreeView.selectedIndexes() - if len(selected) > 0: - row = self.rulesTreeView.selectedIndexes().pop().row() - address = self.__model.get_column_string(0, row).replace(' ', '') - dialog.addressComboBox.setItemText(0, address) - dialog.addressComboBox.setCurrentIndex(0) - service = self.__model.get_column_string(1, row) - dialog.serviceComboBox.setItemText(0, service) - dialog.serviceComboBox.setCurrentIndex(0) - self.run_rule_dialog(dialog, row) - - def run_rule_dialog(self, dialog, row = None): - if dialog.exec_(): - address = str(dialog.addressComboBox.currentText()) - service = str(dialog.serviceComboBox.currentText()) - port = None - port2 = None - - unmask = address.split("/", 1) - if len(unmask) == 2: - address = unmask[0] - netmask = int(unmask[1]) - else: - netmask = 32 - - if address == "*": - address = "0.0.0.0" - netmask = 0 - - if service == "*": - service = "0" - try: - range = service.split("-", 1) - if len(range) == 2: - port = int(range[0]) - port2 = int(range[1]) - else: - port = int(service) - except (TypeError, ValueError) as ex: - port = self.__model.get_service_port(service) - - if port is not None: - if port2 is not None and port2 <= port: - QMessageBox.warning(None, "Invalid service ports range", "Port {0} is lower than port {1}.".format(port2, port)) - else: - item = QubesFirewallRuleItem(address, netmask, port, port2) - if row is not None: - self.__model.setChild(row, item) - else: - self.__model.appendChild(item) - else: - QMessageBox.warning(None, "Invalid service name", "Service '{0} is unknown.".format(service)) - - def delete_rule_button_pressed(self): - for i in set([index.row() for index in self.rulesTreeView.selectedIndexes()]): - self.__model.removeChild(i) class QIPAddressValidator(QValidator): def __init__(self, parent = None): @@ -397,3 +299,4 @@ class QubesFirewallRulesModel(QAbstractItemModel): def __len__(self): return len(self.children) + diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 442348b..2df906c 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -43,8 +43,6 @@ from restore import RestoreVMsWindow from backup import BackupVMsWindow from global_settings import GlobalSettingsWindow -from firewall import EditFwRulesDlg, QubesFirewallRulesModel - from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent import subprocess @@ -1198,17 +1196,8 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): @pyqtSlot(name='on_action_editfwrules_triggered') def action_editfwrules_triggered(self): vm = self.get_selected_vm() - dialog = EditFwRulesDlg() - model = QubesFirewallRulesModel() - model.set_vm(vm) - dialog.set_model(model) - - if vm.netvm_vm is not None and not vm.netvm_vm.is_proxyvm(): - QMessageBox.warning (None, "VM configuration problem!", "The '{0}' AppVM is not network connected to a FirewallVM!

".format(vm.name) +\ - "You may edit the '{0}' VM firewall rules, but these will not take any effect until you connect it to a working Firewall VM.".format(vm.name)) - - if dialog.exec_(): - model.apply_rules() + settings_window = VMSettingsWindow(vm, app, "firewall") + settings_window.exec_() @pyqtSlot(name='on_action_global_settings_triggered') def action_global_settings_triggered(self): diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index be52243..5c3e36d 100644 --- a/qubesmanager/settings.py +++ b/qubesmanager/settings.py @@ -46,6 +46,7 @@ from operator import itemgetter from ui_settingsdlg import * from multiselectwidget import * from appmenu_select import * +from firewall import * class VMSettingsWindow(Ui_SettingsDialog, QDialog): @@ -59,6 +60,13 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): def __init__(self, vm, app, init_page="basic", parent=None): super(VMSettingsWindow, self).__init__(parent) + self.app = app + self.vm = vm + if self.vm.template_vm: + self.source_vm = self.vm.template_vm + else: + self.source_vm = self.vm + self.setupUi(self) if init_page in self.tabs_indices: idx = self.tabs_indices[init_page] @@ -68,19 +76,29 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): self.connect(self.buttonBox, SIGNAL("accepted()"), self.save_and_apply) self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject) - self.app_list = MultiSelectWidget(self) - self.dev_list = MultiSelectWidget(self) - - self.apps_layout.addWidget(self.app_list) - self.devices_layout.addWidget(self.dev_list) + self.tabWidget.currentChanged.connect(self.current_tab_changed) - self.app = app - self.vm = vm - if self.vm.template_vm: - self.source_vm = self.vm.template_vm - else: - self.source_vm = self.vm - + ###### firewall tab + + model = QubesFirewallRulesModel() + model.set_vm(vm) + self.set_fw_model(model) + + + self.newRuleButton.clicked.connect(self.new_rule_button_pressed) + self.editRuleButton.clicked.connect(self.edit_rule_button_pressed) + self.deleteRuleButton.clicked.connect(self.delete_rule_button_pressed) + self.policyAllowRadioButton.toggled.connect(self.policy_radio_toggled) + self.dnsCheckBox.toggled.connect(self.dns_checkbox_toggled) + self.icmpCheckBox.toggled.connect(self.icmp_checkbox_toggled) + + ####### devices tab + self.dev_list = MultiSelectWidget(self) + self.devices_layout.addWidget(self.dev_list) + + ####### apps tab + self.app_list = MultiSelectWidget(self) + self.apps_layout.addWidget(self.app_list) self.AppListManager = AppmenuSelectManager(self.vm, self.app_list) def reject(self): @@ -113,9 +131,106 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): self.done(0) def __save_changes__(self, thread_monitor): + self.fw_model.apply_rules() self.AppListManager.save_appmenu_select_changes() thread_monitor.set_finished() + def current_tab_changed(self, idx): + if idx == self.tabs_indices["firewall"]: + if self.vm.netvm_vm is not None and not self.vm.netvm_vm.is_proxyvm(): + QMessageBox.warning (None, "VM configuration problem!", "The '{0}' AppVM is not network connected to a FirewallVM!

".format(self.vm.name) +\ + "You may edit the '{0}' VM firewall rules, but these will not take any effect until you connect it to a working Firewall VM.".format(self.vm.name)) + + + + ######### firewall tab related + + def set_fw_model(self, model): + self.fw_model = model + self.rulesTreeView.setModel(model) + self.rulesTreeView.header().setResizeMode(QHeaderView.ResizeToContents) + self.rulesTreeView.header().setResizeMode(0, QHeaderView.Stretch) + self.set_allow(model.allow) + self.dnsCheckBox.setChecked(model.allowDns) + self.icmpCheckBox.setChecked(model.allowIcmp) + + def set_allow(self, allow): + self.policyAllowRadioButton.setChecked(allow) + self.policyDenyRadioButton.setChecked(not allow) + + def policy_radio_toggled(self, on): + self.fw_model.allow = self.policyAllowRadioButton.isChecked() + + def dns_checkbox_toggled(self, on): + self.fw_model.allowDns = on + + def icmp_checkbox_toggled(self, on): + self.fw_model.allowIcmp = on + + def new_rule_button_pressed(self): + dialog = NewFwRuleDlg() + self.run_rule_dialog(dialog) + + def edit_rule_button_pressed(self): + dialog = NewFwRuleDlg() + dialog.set_ok_enabled(True) + selected = self.rulesTreeView.selectedIndexes() + if len(selected) > 0: + row = self.rulesTreeView.selectedIndexes().pop().row() + address = self.fw_model.get_column_string(0, row).replace(' ', '') + dialog.addressComboBox.setItemText(0, address) + dialog.addressComboBox.setCurrentIndex(0) + service = self.fw_model.get_column_string(1, row) + dialog.serviceComboBox.setItemText(0, service) + dialog.serviceComboBox.setCurrentIndex(0) + self.run_rule_dialog(dialog, row) + + def delete_rule_button_pressed(self): + for i in set([index.row() for index in self.rulesTreeView.selectedIndexes()]): + self.fw_model.removeChild(i) + + def run_rule_dialog(self, dialog, row = None): + if dialog.exec_(): + address = str(dialog.addressComboBox.currentText()) + service = str(dialog.serviceComboBox.currentText()) + port = None + port2 = None + + unmask = address.split("/", 1) + if len(unmask) == 2: + address = unmask[0] + netmask = int(unmask[1]) + else: + netmask = 32 + + if address == "*": + address = "0.0.0.0" + netmask = 0 + + if service == "*": + service = "0" + try: + range = service.split("-", 1) + if len(range) == 2: + port = int(range[0]) + port2 = int(range[1]) + else: + port = int(service) + except (TypeError, ValueError) as ex: + port = self.fw_model.get_service_port(service) + + if port is not None: + if port2 is not None and port2 <= port: + QMessageBox.warning(None, "Invalid service ports range", "Port {0} is lower than port {1}.".format(port2, port)) + else: + item = QubesFirewallRuleItem(address, netmask, port, port2) + if row is not None: + self.fw_model.setChild(row, item) + else: + self.fw_model.appendChild(item) + else: + QMessageBox.warning(None, "Invalid service name", "Service '{0} is unknown.".format(service)) + # Bases on the original code by: # Copyright (c) 2002-2007 Pascal Varet From f14f7c3bbae550a2327344447d607bc32fd3ef07 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Mon, 13 Feb 2012 13:50:30 +0100 Subject: [PATCH 21/31] Settings basic tab vmname, label, template and netvm loaded according to the vm data, vmname & label editable. --- qubesmanager/main.py | 20 +++---- qubesmanager/settings.py | 120 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 122 insertions(+), 18 deletions(-) diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 2df906c..d44dc6f 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -102,15 +102,15 @@ class VmStatusIcon(QLabel): class VmInfoWidget (QWidget): class VmInfoItem (QTableWidgetItem): - def __init__(self, value): + def __init__(self, name, qid): super(VmInfoWidget.VmInfoItem, self).__init__() - self.value = value + self.value = (name, qid) def set_value(self, value): self.value = value def __lt__(self, other): - return self.value < other.value + return self.value[0] < other.value[0] #compare vm.name def __init__(self, vm, parent = None): @@ -127,7 +127,7 @@ class VmInfoWidget (QWidget): self.setLayout(layout) - self.tableItem = self.VmInfoItem(vm.name) + self.tableItem = self.VmInfoItem(vm.name, vm.qid) def update_vm_state (self, vm): self.vm_icon.update() @@ -783,7 +783,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): if vm.internal: continue vm_row = VmRowInTable (vm, row_no, self.table, self.blkManager) - vms_in_table[vm.name] = vm_row + vms_in_table[vm.qid] = vm_row row_no += 1 self.table.setRowCount(row_no) @@ -976,9 +976,9 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): #vm selection relies on the VmInfo widget's value used for sorting by VM name row_index = self.table.currentRow() if row_index != None: - vm_name = self.table.item(row_index, self.columns_indices["Name"]).value - assert self.vms_in_table[vm_name] is not None - vm = self.vms_in_table[vm_name].vm + (vm_name, qid) = self.table.item(row_index, self.columns_indices["Name"]).value + assert self.vms_in_table[qid] is not None + vm = self.vms_in_table[qid].vm return vm else: return None @@ -1133,14 +1133,14 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): @pyqtSlot(name='on_action_settings_triggered') def action_settings_triggered(self): vm = self.get_selected_vm() - settings_window = VMSettingsWindow(vm, app, "basic") + settings_window = VMSettingsWindow(vm, app, self.qvm_collection, "basic") settings_window.exec_() @pyqtSlot(name='on_action_appmenus_triggered') def action_appmenus_triggered(self): vm = self.get_selected_vm() - settings_window = VMSettingsWindow(vm, app, "applications") + settings_window = VMSettingsWindow(vm, app, self.qvm_collection, "applications") settings_window.exec_() diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index 5c3e36d..ef9a1a7 100644 --- a/qubesmanager/settings.py +++ b/qubesmanager/settings.py @@ -27,6 +27,7 @@ from PyQt4.QtCore import * from PyQt4.QtGui import * from qubes.qubes import QubesVmCollection +from qubes.qubes import QubesVmLabels from qubes.qubes import QubesException from qubes.qubes import qubes_appmenu_create_cmd from qubes.qubes import qubes_appmenu_remove_cmd @@ -57,10 +58,11 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): "applications": 4, "services": 5,} - def __init__(self, vm, app, init_page="basic", parent=None): + def __init__(self, vm, app, qvm_collection, init_page="basic", parent=None): super(VMSettingsWindow, self).__init__(parent) self.app = app + self.qvm_collection = qvm_collection self.vm = vm if self.vm.template_vm: self.source_vm = self.vm.template_vm @@ -78,6 +80,9 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): self.tabWidget.currentChanged.connect(self.current_tab_changed) + ###### basic tab + self.__init_basic_tab__() + ###### firewall tab model = QubesFirewallRulesModel() @@ -97,9 +102,12 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): self.devices_layout.addWidget(self.dev_list) ####### apps tab - self.app_list = MultiSelectWidget(self) - self.apps_layout.addWidget(self.app_list) - self.AppListManager = AppmenuSelectManager(self.vm, self.app_list) + if not vm.is_netvm(): + self.app_list = MultiSelectWidget(self) + self.apps_layout.addWidget(self.app_list) + self.AppListManager = AppmenuSelectManager(self.vm, self.app_list) + else: + self.tabWidget.setTabEnabled(self.tabs_indices["applications"], False) def reject(self): self.done(0) @@ -126,13 +134,18 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): progress.hide() if not thread_monitor.success: - QMessageBox.warning (None, "Error while changing settings for {0}!", "ERROR: {1}".format(self.vm.namethread_monitor.error_msg)) + QMessageBox.warning (None, "Error while changing settings for {0}!", "ERROR: {1}".format(self.vm.name, thread_monitor.error_msg)) self.done(0) def __save_changes__(self, thread_monitor): - self.fw_model.apply_rules() - self.AppListManager.save_appmenu_select_changes() + ret = self.__apply_basic_tab__() + if len(ret) > 0 : + thread_monitor.set_error_msg('\n'.join(ret)) + thread_monitor.set_finished() + return + #self.fw_model.apply_rules() + #self.AppListManager.save_appmenu_select_changes() thread_monitor.set_finished() def current_tab_changed(self, idx): @@ -143,6 +156,97 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): + ######### basic tab + + def __init_basic_tab__(self): + self.vmname.setText(self.vm.name) + + #self.qvm_collection.lock_db_for_reading() + #self.qvm_collection.load() + #self.qvm_collection.unlock_db() + + self.label_list = QubesVmLabels.values() + self.label_list.sort(key=lambda l: l.index) + self.label_idx = 0 + for (i, label) in enumerate(self.label_list): + if label == self.vm.label: + self.label_idx = i + self.vmlabel.insertItem(i, label.name) + self.vmlabel.setItemIcon (i, QIcon(label.icon_path)) + self.vmlabel.setCurrentIndex(self.label_idx) + + if not self.vm.is_template() and self.vm.template_vm is not None: + template_vm_list = [vm for vm in self.qvm_collection.values() if not vm.internal and vm.is_template()] + self.template_idx = 0 + for (i, vm) in enumerate(template_vm_list): + text = vm.name + if vm is self.qvm_collection.get_default_template_vm(): + text += " (default)" + if vm.qid == self.vm.template_vm.qid: + self.template_idx = i + text += " (current)" + self.template_name.insertItem(i, text) + self.template_name.setCurrentIndex(self.template_idx) + else: + self.template_name.setEnabled(False) + + if not self.vm.is_netvm(): + netvm_list = [vm for vm in self.qvm_collection.values() if not vm.internal and vm.is_netvm()] + self.netvm_idx = 0 + for (i, vm) in enumerate(netvm_list): + text = vm.name + if vm is self.qvm_collection.get_default_netvm_vm(): + text += " (default)" + if vm.qid == self.vm.netvm_vm.qid: + self.netvm_idx = i + text += " (current)" + self.netVM.insertItem(i, text) + self.netVM.setCurrentIndex(self.netvm_idx) + else: + self.netVM.setEnabled(False) + + #self.vmname.selectAll() + #self.vmname.setFocus() + + def __apply_basic_tab__(self): + msg = [] + + if self.vm.is_running(): + msg.append("Can't change settings of a running VM.") + msg.append("telemele") + return msg + + # vmname changed + vmname = str(self.vmname.text()) + if self.vm.name != vmname: + if self.qvm_collection.get_vm_by_name(vmname) is not None: + msg.append("A VM named {0} already exists in the system!".format(vmname)) + else: + oldname = self.vm.name + try: + self.qvm_collection.lock_db_for_writing() + self.vm.pre_rename(vmname) + self.vm.set_name(vmname) + self.vm.post_rename(oldname) + self.qvm_collection.save() + except Exception as ex: + msg.append(str(ex)) + finally: + self.qvm_collection.unlock_db() + + #vm label changed + if self.vmlabel.currentIndex() != self.label_idx: + label = self.label_list[self.vmlabel.currentIndex()] + self.qvm_collection.lock_db_for_writing() + self.vm.label = label + self.qvm_collection.save() + self.qvm_collection.unlock_db() + + return msg + + # template_vm = template_vm_list[dialog.template_name.currentIndex()] + # allow_networking = dialog.allow_networking.isChecked() + ######### firewall tab related def set_fw_model(self, model): @@ -287,7 +391,7 @@ def main(): global settings_window - settings_window = VMSettingsWindow(vm, app, "basic") + settings_window = VMSettingsWindow(vm, app, qvm_collection, "basic") settings_window.show() From 2687d9f5fd52299bfcf28245c936ada2c0469a31 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Mon, 13 Feb 2012 17:47:12 +0100 Subject: [PATCH 22/31] Red firewall icon --- icons/redfirewall.png | Bin 0 -> 18054 bytes mainwindow.ui | 2 +- qubesmanager/main.py | 2 +- resources.qrc | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 icons/redfirewall.png diff --git a/icons/redfirewall.png b/icons/redfirewall.png new file mode 100644 index 0000000000000000000000000000000000000000..337dec889d778cefff679f9542e8f736270f4f74 GIT binary patch literal 18054 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 literal 0 HcmV?d00001 diff --git a/mainwindow.ui b/mainwindow.ui index 66a1b36..1cc5b8d 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -399,7 +399,7 @@ - :/newfirewall.png:/newfirewall.png + :/redfirewall.png:/redfirewall.png Edit VM Firewall rules diff --git a/qubesmanager/main.py b/qubesmanager/main.py index d44dc6f..6b06996 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -1196,7 +1196,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): @pyqtSlot(name='on_action_editfwrules_triggered') def action_editfwrules_triggered(self): vm = self.get_selected_vm() - settings_window = VMSettingsWindow(vm, app, "firewall") + settings_window = VMSettingsWindow(vm, app, self.qvm_collection, "firewall") settings_window.exec_() @pyqtSlot(name='on_action_global_settings_triggered') diff --git a/resources.qrc b/resources.qrc index 483f372..6d47a74 100644 --- a/resources.qrc +++ b/resources.qrc @@ -1,6 +1,7 @@ icons/pencil.png + icons/redfirewall.png icons/edit.png icons/add.png icons/flag-blue.png From 0ca4f10731871291b844c7bc8aecd59e1c277f62 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Mon, 13 Feb 2012 17:55:40 +0100 Subject: [PATCH 23/31] Removed Update Info column from Qubes Manager (Upd. left). --- mainwindow.ui | 17 ----------------- qubesmanager/main.py | 15 ++------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index 1cc5b8d..b216176 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -198,11 +198,6 @@ Memory usage graph - - - Update Info - - Block Devices @@ -240,7 +235,6 @@ - @@ -510,17 +504,6 @@ Block Devices - - - true - - - true - - - Update Info - - diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 6b06996..fbf24ac 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -561,12 +561,8 @@ class VmRowInTable(object): table.setCellWidget(row_no, 7, self.mem_widget) table.setItem(row_no, 7, self.mem_widget.tableItem) - self.updateinfo_widget = VmUpdateInfoWidget(vm, True) - table.setCellWidget(row_no, 8, self.updateinfo_widget) - table.setItem(row_no, 8, self.updateinfo_widget.tableItem) - self.blockdevices_widget = VmBlockDevicesWidget(vm, block_manager) - table.setCellWidget(row_no, 9, self.blockdevices_widget) + table.setCellWidget(row_no, 8, self.blockdevices_widget) def update(self, counter, update_devs = False, cpu_load = None): @@ -577,7 +573,6 @@ class VmRowInTable(object): self.mem_usage_widget.update_load(self.vm, None) self.load_widget.update_load(self.vm, cpu_load) self.mem_widget.update_load(self.vm, None) - self.updateinfo_widget.update_outdated(self.vm) self.upd_widget.update_outdated(self.vm) if self.blockdevices_widget.isEnabled() and update_devs: self.blockdevices_widget.update(self.vm) @@ -642,8 +637,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): "CPU Graph": 5, "MEM": 6, "MEM Graph": 7, - "Update Info": 8, - "Block Device": 9 } + "Block Device": 8 } @@ -667,8 +661,6 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.table.setColumnHidden( self.columns_indices["NetVM"], True) self.actionNetVM.setChecked(False) - self.table.setColumnHidden( self.columns_indices["Update Info"], True) - self.actionUpdate_Info.setChecked(False) self.table.setColumnHidden( self.columns_indices["CPU Graph"], True) self.actionCPU_Graph.setChecked(False) self.table.setColumnHidden( self.columns_indices["MEM Graph"], True) @@ -1242,9 +1234,6 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): def on_actionMEM_Graph_toggled(self, checked): self.showhide_collumn( self.columns_indices['MEM Graph'], checked) - def on_actionUpdate_Info_toggled(self, checked): - self.showhide_collumn( self.columns_indices['Update Info'], checked) - def on_actionBlock_Devices_toggled(self, checked): self.showhide_collumn( self.columns_indices['Block Device'], checked) From 4933f9f3d4500c9adc5cac8a166ef02dc568efc7 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Mon, 13 Feb 2012 18:19:54 +0100 Subject: [PATCH 24/31] cpu/mem usage bar widgets in colors corresponding with cpu/mem graphs, with labels. --- qubesmanager/main.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/qubesmanager/main.py b/qubesmanager/main.py index fbf24ac..981157e 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -205,7 +205,7 @@ class VmUsageBarWidget (QWidget): def __lt__(self, other): return self.value < other.value - def __init__(self, min, max, format, update_func, vm, load, parent = None): + def __init__(self, min, max, format, update_func, vm, load, hue=210, parent = None): super (VmUsageBarWidget, self).__init__(parent) @@ -220,14 +220,16 @@ class VmUsageBarWidget (QWidget): self.widget.setFormat(format); self.widget.setStyleSheet( - "QProgressBar:horizontal{ \ - border: 1px solid lightblue;\ - border-radius: 4px;\ + "QProgressBar:horizontal{" +\ + "border: 1px solid hsv({0}, 100, 250);".format(hue) +\ + "border-radius: 4px;\ background: white;\ text-align: center;\ }\ QProgressBar::chunk:horizontal {\ - background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5, stop: 0 hsv(210, 170, 207), stop: 1 white);\ + background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5, " +\ + "stop: 0 hsv({0}, 170, 207),".format(hue) + + " stop: 1 white); \ }" ) @@ -521,6 +523,9 @@ class QubesBlockDevicesManager(): class VmRowInTable(object): + cpu_graph_hue = 210 + mem_graph_hue = 120 + def __init__(self, vm, row_no, table, block_manager): self.vm = vm self.row_no = row_no @@ -541,23 +546,22 @@ class VmRowInTable(object): self.netvm_widget = VmNetvmItem(vm) table.setItem(row_no, 3, self.netvm_widget) - self.cpu_usage_widget = VmUsageBarWidget(0, 100, "", - lambda vm, val: val if vm.last_power_state else 0, vm, 0) + self.cpu_usage_widget = VmUsageBarWidget(0, 100, "%v %", + lambda vm, val: val if vm.last_power_state else 0, vm, 0, self.cpu_graph_hue) table.setCellWidget(row_no, 4, self.cpu_usage_widget) table.setItem(row_no, 4, self.cpu_usage_widget.tableItem) - #self.load_widget = LoadChartWidget(vm) - self.load_widget = ChartWidget(vm, lambda vm, val: val if vm.last_power_state else 0, 200, 0 ) + self.load_widget = ChartWidget(vm, lambda vm, val: val if vm.last_power_state else 0, self.cpu_graph_hue, 0 ) table.setCellWidget(row_no, 5, self.load_widget) table.setItem(row_no, 5, self.load_widget.tableItem) self.mem_usage_widget = VmUsageBarWidget(0, qubes_host.memory_total/1024, "%v MB", - lambda vm, val: vm.get_mem()/1024 if vm.last_power_state else 0, vm, 0) + lambda vm, val: vm.get_mem()/1024 if vm.last_power_state else 0, vm, 0, self.mem_graph_hue) table.setCellWidget(row_no, 6, self.mem_usage_widget) table.setItem(row_no, 6, self.mem_usage_widget.tableItem) - self.mem_widget = ChartWidget(vm, lambda vm, val: vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0, 120, 0) + self.mem_widget = ChartWidget(vm, lambda vm, val: vm.get_mem()*100/qubes_host.memory_total if vm.last_power_state else 0, self.mem_graph_hue, 0) table.setCellWidget(row_no, 7, self.mem_widget) table.setItem(row_no, 7, self.mem_widget.tableItem) From 30bae277a16647b3b9d4858f2ee64bf7409060cb Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Tue, 14 Feb 2012 19:00:50 +0100 Subject: [PATCH 25/31] Block devices attached/detached to/from VMs via context menu. Column Block Devices removed from Qubes manager. --- mainwindow.ui | 22 +-- qubesmanager/main.py | 342 ++++++++++++++++++++++--------------------- resources.qrc | 1 + 3 files changed, 182 insertions(+), 183 deletions(-) diff --git a/mainwindow.ui b/mainwindow.ui index b216176..0de6d08 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -82,7 +82,7 @@ - Qt::ActionsContextMenu + Qt::CustomContextMenu false @@ -198,11 +198,6 @@ Memory usage graph - - - Block Devices - - @@ -235,7 +230,6 @@ - @@ -257,11 +251,12 @@ false - + + @@ -493,17 +488,6 @@ NetVM - - - true - - - true - - - Block Devices - - diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 981157e..bfbc1e4 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -120,17 +120,24 @@ class VmInfoWidget (QWidget): self.label_name = QLabel (vm.name) self.vm_icon = VmStatusIcon(vm) + self.blk_icon = VmIconWidget(":/mount.png") layout.addWidget(self.vm_icon) layout.addSpacing (10) layout.addWidget(self.label_name, alignment=Qt.AlignLeft) + layout.addSpacing (10) + layout.addWidget(self.blk_icon, alignment=Qt.AlignRight) self.setLayout(layout) + self.blk_icon.setVisible(False) + self.tableItem = self.VmInfoItem(vm.name, vm.qid) - def update_vm_state (self, vm): + def update_vm_state (self, vm, blk_visible): self.vm_icon.update() + if blk_visible != None: + self.blk_icon.setVisible(blk_visible) @@ -167,13 +174,14 @@ class VmIconWidget (QWidget): label_icon = QLabel() icon = QIcon (icon_path) - icon_sz = QSize (VmManagerWindow.row_height * 0.8, VmManagerWindow.row_height * 0.8) + icon_sz = QSize (VmManagerWindow.row_height * 0.7, VmManagerWindow.row_height * 0.7) icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled if not enabled else QIcon.Normal) label_icon.setPixmap (icon_pixmap) label_icon.setFixedSize (icon_sz) layout = QVBoxLayout() layout.addWidget(label_icon) + layout.setContentsMargins(0,0,0,0) self.setLayout(layout) @@ -396,131 +404,6 @@ class VmUpdateInfoWidget(QWidget): self.layout().addWidget(self.icon, alignment=Qt.AlignCenter) -class VmBlockDevicesWidget(QWidget): - def __init__(self, vm, block_manager, parent=None): - super(VmBlockDevicesWidget, self).__init__(parent) - - self.vm = vm - self.block_manager = block_manager - self.free_devs = self.block_manager.free_devs - self.att_devs = self.block_manager.attached_devs - - self.combo = QComboBox() - - layout = QVBoxLayout() - layout.addWidget(self.combo) - self.setLayout(layout) - - self.connect(self.combo, SIGNAL("activated(int)"), self.combo_activated) - - def update(self, vm): - self.combo.clear() - self.combo.addItem("None") - for a in self.att_devs: - if self.att_devs[a]['attached_to']['vm'] == vm.name: - att = self.att_devs[a]['dev'] + ": " + unicode(self.att_devs[a]['size']) + " " + self.att_devs[a]['desc'] - self.combo.addItem(att, QVariant(a)) - self.combo.setCurrentIndex(1) - break - if self.combo.count() == 1: - for d in self.free_devs: - str = self.free_devs[d]['dev'] + ": " + unicode(self.free_devs[d]['size']) + " " + self.free_devs[d]['desc'] - self.combo.addItem(str, QVariant(d)) - self.prev_idx = self.combo.currentIndex(); - - def combo_activated(self, idx): - if idx == self.prev_idx: #nothing has changed - return - #there was a change - if self.combo.currentText() != "None": #device attached: - self.prev_idx = idx - dev_name = str(self.combo.itemData(idx).toString()) - self.block_manager.attach_device(self.vm, dev_name) - - else: - dev_name = str(self.combo.itemData(self.prev_idx).toString()) - self.prev_idx = idx - self.block_manager.detach_device(self.vm, dev_name) - - - -class QubesBlockDevicesManager(): - def __init__(self, qvm_collection): - self.qvm_collection = qvm_collection - self.attached_devs = {} - self.free_devs = {} - - self.current_blk = {} - self.current_attached = {} - self.devs_changed = False - - def update(self): - blk = qubesutils.block_list() - for b in blk: - att = qubesutils.block_check_attached(None, blk[b]['device'], backend_xid = blk[b]['xid']) - if b in self.current_blk: - if blk[b] == self.current_blk[b]: - if self.current_attached[b] != att: #devices the same, sth with attaching changed - self.current_attached[b] = att - self.devs_changed = True - else: #device changed ?! - self.current_blk[b] = blk[b] - self.current_attached[b] = att - self.devs_changed = True - else: #new device - self.current_blk[b] = blk[b] - self.current_attached[b] = att - self.devs_changed = True - - for b in self.current_blk: #remove devices that are not there anymore - if b not in blk: - del self.current_blk[b] - del self.current_attached[b] - self.devs_changed = True - - if self.devs_changed == True: - self.devs_changed = False - self.__update_blk_entries__() - return True - else: - return False - - - def __update_blk_entries__(self): - self.free_devs.clear() - self.attached_devs.clear() - - for b in self.current_attached: - if self.current_attached[b]: - self.attached_devs[b] = self.__make_entry__(b, self.current_blk[b], self.current_attached[b]) - else: - self.free_devs[b] = self.__make_entry__(b, self.current_blk[b], None) - - def __make_entry__(self, k, dev, att): - size_str = qubesutils.bytes_to_kmg(dev['size']) - entry = { 'dev': dev['device'], - 'backend_name': dev['vm'], - 'desc': dev['desc'], - 'size': size_str, - 'attached_to': att, } - return entry - - def attach_device(self, vm, dev): - backend_vm_name = self.free_devs[dev]['backend_name'] - dev_id = self.free_devs[dev]['dev'] - backend_vm = self.qvm_collection.get_vm_by_name(backend_vm_name) - trayIcon.showMessage ("Qubes Manager", "{0} - attaching {1}".format(vm.name, dev), msecs=3000) - qubesutils.block_attach(vm, backend_vm, dev_id) - self.devs_changed = True - - def detach_device(self, vm, dev_name): - dev_id = self.attached_devs[dev_name]['attached_to']['devid'] - vm_xid = self.attached_devs[dev_name]['attached_to']['xid'] - trayIcon.showMessage ("Qubes Manager", "{0} - detaching {1}".format(vm.name, dev_name), msecs=3000) - qubesutils.block_detach(None, dev_id, vm_xid) - self.devs_changed = True - - class VmRowInTable(object): cpu_graph_hue = 210 @@ -565,21 +448,15 @@ class VmRowInTable(object): table.setCellWidget(row_no, 7, self.mem_widget) table.setItem(row_no, 7, self.mem_widget.tableItem) - self.blockdevices_widget = VmBlockDevicesWidget(vm, block_manager) - table.setCellWidget(row_no, 8, self.blockdevices_widget) - - def update(self, counter, update_devs = False, cpu_load = None): - self.info_widget.update_vm_state(self.vm) - self.blockdevices_widget.setEnabled(self.vm.is_running()) + def update(self, counter, blk_visible = None, cpu_load = None): + self.info_widget.update_vm_state(self.vm, blk_visible) if cpu_load is not None: self.cpu_usage_widget.update_load(self.vm, cpu_load) self.mem_usage_widget.update_load(self.vm, None) self.load_widget.update_load(self.vm, cpu_load) self.mem_widget.update_load(self.vm, None) self.upd_widget.update_outdated(self.vm) - if self.blockdevices_widget.isEnabled() and update_devs: - self.blockdevices_widget.update(self.vm) class NewAppVmDlg (QDialog, ui_newappvmdlg.Ui_NewAppVMDlg): def __init__(self, parent = None): @@ -640,8 +517,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): "CPU": 4, "CPU Graph": 5, "MEM": 6, - "MEM Graph": 7, - "Block Device": 8 } + "MEM Graph": 7,} @@ -651,7 +527,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.toolbar = self.toolBar self.qvm_collection = QubesVmCollection() - self.blkManager = QubesBlockDevicesManager(self.qvm_collection) + self.blk_manager = QubesBlockDevicesManager(self.qvm_collection) self.connect(self.table, SIGNAL("itemSelectionChanged()"), self.table_selection_changed) @@ -669,27 +545,30 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.actionCPU_Graph.setChecked(False) self.table.setColumnHidden( self.columns_indices["MEM Graph"], True) self.actionMEM_Graph.setChecked(False) - self.table.setColumnHidden( self.columns_indices["Block Device"], True) - self.actionBlock_Devices.setChecked(False) self.table.setColumnWidth(self.columns_indices["Upd"], 50) - self.table.setColumnWidth(self.columns_indices["Block Device"], 250) self.table.sortItems(self.columns_indices["MEM"], Qt.DescendingOrder) - self.table.addAction(self.action_settings) - self.table.addAction(self.action_removevm) - self.table.addAction(self.action_resumevm) - self.table.addAction(self.action_pausevm) - self.table.addAction(self.action_shutdownvm) - self.table.addAction(self.action_appmenus) - self.table.addAction(self.action_editfwrules) - self.table.addAction(self.action_updatevm) + self.context_menu = QMenu(self) + self.context_menu.addAction(self.action_settings) + self.context_menu.addAction(self.action_removevm) + self.context_menu.addAction(self.action_resumevm) + self.context_menu.addAction(self.action_pausevm) + self.context_menu.addAction(self.action_shutdownvm) + self.context_menu.addAction(self.action_appmenus) + self.context_menu.addAction(self.action_editfwrules) + self.context_menu.addAction(self.action_updatevm) + + self.blk_menu = QMenu("Block devices") + self.context_menu.addMenu(self.blk_menu) + + self.connect(self.table, SIGNAL("customContextMenuRequested(const QPoint&)"), self.open_context_menu) + self.connect(self.blk_menu, SIGNAL("triggered(QAction *)"), self.attach_dettach_device_triggered) self.table.setContentsMargins(0,0,0,0) self.centralwidget.layout().setContentsMargins(0,0,0,0) self.layout().setContentsMargins(0,0,0,0) - self.counter = 0 self.shutdown_monitor = {} self.last_measure_results = {} @@ -778,7 +657,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): continue if vm.internal: continue - vm_row = VmRowInTable (vm, row_no, self.table, self.blkManager) + vm_row = VmRowInTable (vm, row_no, self.table, self.blk_manager) vms_in_table[vm.qid] = vm_row row_no += 1 @@ -794,6 +673,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): # When calling update_table() directly, always use out_of_schedule=True! def update_table(self, out_of_schedule=False): + update_devs = self.update_block_devices() or out_of_schedule if manager_window.isVisible(): some_vms_have_changed_power_state = False for vm in self.vms_list: @@ -802,11 +682,17 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): vm.last_power_state = state some_vms_have_changed_power_state = True - update_devs = self.update_block_devices() if self.reload_table or ((not self.show_inactive_vms) and some_vms_have_changed_power_state): self.fill_table() update_devs=True + blk_visible = None + rows_with_blk = None + if update_devs == True: + rows_with_blk = [] + for d in self.blk_manager.attached_devs: + rows_with_blk.append( self.blk_manager.attached_devs[d]['attached_to']['vm']) + if self.counter % 3 == 0 or out_of_schedule: (self.last_measure_time, self.last_measure_results) = \ qubes_host.measure_cpu_usage(self.last_measure_results, @@ -818,10 +704,23 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): cur_cpu_load = self.last_measure_results[vm_row.vm.xid]['cpu_usage'] else: cur_cpu_load = 0 - vm_row.update(self.counter, update_devs=update_devs, cpu_load = cur_cpu_load) + + if rows_with_blk != None: + if vm_row.vm.name in rows_with_blk: + blk_visible = True + else: + blk_visible = False + + vm_row.update(self.counter, blk_visible=blk_visible, cpu_load = cur_cpu_load) else: for vm_row in self.vms_in_table.values(): - vm_row.update(self.counter, update_devs=update_devs) + if rows_with_blk != None: + if vm_row.vm.name in rows_with_blk: + blk_visible = True + else: + blk_visible = False + + vm_row.update(self.counter, blk_visible=blk_visible) #self.table_selection_changed() @@ -840,14 +739,11 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.setFixedWidth(table_width) def update_block_devices(self): - if self.table.isColumnHidden( self.columns_indices['Block Device']): - return False - - if self.blkManager.update(): - return True - else: - return False - + res, msg = self.blk_manager.update() + if msg != None and len(msg) > 0: + str = "\n".join(msg) + trayIcon.showMessage ("Qubes Manager", str, msecs=5000) + return res def table_selection_changed (self): @@ -880,6 +776,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): self.hide() event.ignore() + @pyqtSlot(name='on_action_createvm_triggered') def action_createvm_triggered(self): dialog = NewAppVmDlg() @@ -1238,8 +1135,125 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): def on_actionMEM_Graph_toggled(self, checked): self.showhide_collumn( self.columns_indices['MEM Graph'], checked) - def on_actionBlock_Devices_toggled(self, checked): - self.showhide_collumn( self.columns_indices['Block Device'], checked) + + @pyqtSlot('const QPoint&') + def open_context_menu(self, point): + vm = self.get_selected_vm() + if not vm.is_running(): + self.blk_menu.setEnabled(False) + else: + self.blk_menu.clear() + self.blk_menu.setEnabled(True) + if len(self.blk_manager.attached_devs) > 0 : + for d in self.blk_manager.attached_devs: + if self.blk_manager.attached_devs[d]['attached_to']['vm'] == vm.name: + str = "Detach " + d + " " + unicode(self.blk_manager.attached_devs[d]['size']) + " " + self.blk_manager.attached_devs[d]['desc'] + action = self.blk_menu.addAction(QIcon(":/remove.png"), str) + action.setData(QVariant(d)) + + if self.blk_menu.isEmpty() and len(self.blk_manager.free_devs) > 0: + for d in self.blk_manager.free_devs: + str = "Attach " + d + " " + unicode(self.blk_manager.free_devs[d]['size']) + " " + self.blk_manager.free_devs[d]['desc'] + action = self.blk_menu.addAction(QIcon(":/add.png"), str) + action.setData(QVariant(d)) + + if self.blk_menu.isEmpty(): + self.blk_menu.setEnabled(False) + + self.context_menu.exec_(self.table.mapToGlobal(point)) + + @pyqtSlot('QAction *') + def attach_dettach_device_triggered(self, action): + dev = str(action.data().toString()) + vm = self.get_selected_vm() + if dev in self.blk_manager.attached_devs: + self.blk_manager.detach_device(vm, dev) + else: + self.blk_manager.attach_device(vm, dev) + + +class QubesBlockDevicesManager(): + def __init__(self, qvm_collection): + self.qvm_collection = qvm_collection + self.attached_devs = {} + self.free_devs = {} + + self.current_blk = {} + self.current_attached = {} + self.devs_changed = False + + def update(self): + blk = qubesutils.block_list() + msg = [] + for b in blk: + att = qubesutils.block_check_attached(None, blk[b]['device'], backend_xid = blk[b]['xid']) + if b in self.current_blk: + if blk[b] == self.current_blk[b]: + if self.current_attached[b] != att: #devices the same, sth with attaching changed + self.current_attached[b] = att + self.devs_changed = True + else: #device changed ?! + self.current_blk[b] = blk[b] + self.current_attached[b] = att + self.devs_changed = True + else: #new device + self.current_blk[b] = blk[b] + self.current_attached[b] = att + self.devs_changed = True + msg.append("Attached new device: {0}".format(blk[b]['device'])) + + to_delete = [] + for b in self.current_blk: #remove devices that are not there anymore + if b not in blk: + to_delete.append(b) + self.devs_changed = True + msg.append("Detached device: {0}".format(self.current_blk[b]['device'])) + + for d in to_delete: + del self.current_blk[d] + del self.current_attached[d] + + if self.devs_changed == True: + self.devs_changed = False + self.__update_blk_entries__() + return True, msg + else: + return False, None + + + def __update_blk_entries__(self): + self.free_devs.clear() + self.attached_devs.clear() + + for b in self.current_attached: + if self.current_attached[b]: + self.attached_devs[b] = self.__make_entry__(b, self.current_blk[b], self.current_attached[b]) + else: + self.free_devs[b] = self.__make_entry__(b, self.current_blk[b], None) + + def __make_entry__(self, k, dev, att): + size_str = qubesutils.bytes_to_kmg(dev['size']) + entry = { 'dev': dev['device'], + 'backend_name': dev['vm'], + 'desc': dev['desc'], + 'size': size_str, + 'attached_to': att, } + return entry + + def attach_device(self, vm, dev): + backend_vm_name = self.free_devs[dev]['backend_name'] + dev_id = self.free_devs[dev]['dev'] + backend_vm = self.qvm_collection.get_vm_by_name(backend_vm_name) + trayIcon.showMessage ("Qubes Manager", "{0} - attaching {1}".format(vm.name, dev), msecs=3000) + qubesutils.block_attach(vm, backend_vm, dev_id) + self.devs_changed = True + + def detach_device(self, vm, dev_name): + dev_id = self.attached_devs[dev_name]['attached_to']['devid'] + vm_xid = self.attached_devs[dev_name]['attached_to']['xid'] + trayIcon.showMessage ("Qubes Manager", "{0} - detaching {1}".format(vm.name, dev_name), msecs=3000) + qubesutils.block_detach(None, dev_id, vm_xid) + self.devs_changed = True diff --git a/resources.qrc b/resources.qrc index 6d47a74..935b43a 100644 --- a/resources.qrc +++ b/resources.qrc @@ -1,5 +1,6 @@ + icons/mount.png icons/pencil.png icons/redfirewall.png icons/edit.png From df02ed2aa44c7345438a7645bcf84513696b5e3f Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Mon, 20 Feb 2012 07:56:38 +0100 Subject: [PATCH 26/31] Backup dialog. --- backupdlg.ui | 16 +- icons/mount.png | Bin 0 -> 16083 bytes icons/redfirewall2.png | Bin 0 -> 18054 bytes qubesmanager/appmenu_select.py | 19 +-- qubesmanager/backup.py | 271 +++++++++++++++++++++++++++--- qubesmanager/main.py | 25 +-- qubesmanager/multiselectwidget.py | 8 +- qubesmanager/settings.py | 2 +- qubesmanager/thread_monitor.py | 44 +++++ restoredlg.ui | 8 +- 10 files changed, 315 insertions(+), 78 deletions(-) create mode 100644 icons/mount.png create mode 100644 icons/redfirewall2.png create mode 100644 qubesmanager/thread_monitor.py diff --git a/backupdlg.ui b/backupdlg.ui index 04cb139..522ce78 100644 --- a/backupdlg.ui +++ b/backupdlg.ui @@ -19,7 +19,7 @@ QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage - + @@ -39,7 +39,7 @@ - + @@ -55,7 +55,7 @@ - + 0 @@ -92,10 +92,10 @@ - + - + ... @@ -106,7 +106,7 @@ - + @@ -157,7 +157,7 @@ p, li { white-space: pre-wrap; } - + @@ -176,7 +176,7 @@ p, li { white-space: pre-wrap; } - + 24 diff --git a/icons/mount.png b/icons/mount.png new file mode 100644 index 0000000000000000000000000000000000000000..8c841582386a3ff62179f2fefcede2c7c5aa0955 GIT binary patch literal 16083 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 Z{|BD;QKr$>jbQ))002ovPDHLkV1n5RMR5QC literal 0 HcmV?d00001 diff --git a/icons/redfirewall2.png b/icons/redfirewall2.png new file mode 100644 index 0000000000000000000000000000000000000000..e611d8967f2700a0b1421b4a7d8176cbad1acfeb GIT binary patch literal 18054 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 literal 0 HcmV?d00001 diff --git a/qubesmanager/appmenu_select.py b/qubesmanager/appmenu_select.py index 5544ce9..40ece49 100755 --- a/qubesmanager/appmenu_select.py +++ b/qubesmanager/appmenu_select.py @@ -39,9 +39,10 @@ from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, Pro import subprocess import time -import threading + from operator import itemgetter +from thread_monitor import * from multiselectwidget import * whitelisted_filename = 'whitelisted-appmenus.list' @@ -51,22 +52,6 @@ class AppListWidgetItem(QListWidgetItem): super(AppListWidgetItem, self).__init__(name, parent) self.filename = filename -class ThreadMonitor(QObject): - def __init__(self): - self.success = True - self.error_msg = None - self.event_finished = threading.Event() - - def set_error_msg(self, error_msg): - self.success = False - self.error_msg = error_msg - self.set_finished() - - def is_finished(self): - return self.event_finished.is_set() - - def set_finished(self): - self.event_finished.set() class AppmenuSelectManager: diff --git a/qubesmanager/backup.py b/qubesmanager/backup.py index 8d223f1..5150cc2 100644 --- a/qubesmanager/backup.py +++ b/qubesmanager/backup.py @@ -30,6 +30,7 @@ from qubes.qubes import QubesVmCollection from qubes.qubes import QubesException from qubes.qubes import QubesDaemonPidfile from qubes.qubes import QubesHost +from qubes import qubesutils import qubesmanager.resources_rc @@ -37,9 +38,12 @@ from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, Pro import subprocess import time -import threading +from thread_monitor import * from operator import itemgetter +from datetime import datetime +from string import replace + from ui_backupdlg import * from multiselectwidget import * @@ -47,40 +51,255 @@ from multiselectwidget import * class BackupVMsWindow(Ui_Backup, QWizard): - def __init__(self, parent=None): + __pyqtSignals__ = ("backup_progress(int)",) + + excluded = [] + to_backup = [] + + def __init__(self, app, qvm_collection, blk_manager, parent=None): super(BackupVMsWindow, self).__init__(parent) + self.app = app + self.qvm_collection = qvm_collection + self.blk_manager = blk_manager + + self.backup_dir = None + self.func_output = [] + + for vm in self.qvm_collection.values(): + if vm.qid == 0: + self.vm = vm + break; + + assert self.vm != None + self.setupUi(self) - self.selectVMsWidget = MultiSelectWidget(self) - self.verticalLayout.insertWidget(1, self.selectVMsWidget) + self.dir_line_edit.setReadOnly(True) + + self.select_vms_widget = MultiSelectWidget(self) + self.verticalLayout.insertWidget(1, self.select_vms_widget) - self.selectVMsWidget.available_list.addItem("netVM1") - self.selectVMsWidget.available_list.addItem("appVM1") - self.selectVMsWidget.available_list.addItem("appVM2") - self.selectVMsWidget.available_list.addItem("templateVM1") - self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed) + self.connect(self.dev_combobox, SIGNAL("activated(int)"), self.dev_combobox_activated) + self.connect(self, SIGNAL("backup_progress(int)"), self.progress_bar.setValue) + self.select_vms_page.isComplete = self.has_selected_vms + self.select_dir_page.isComplete = self.has_selected_dir + #FIXME + #this causes to run isComplete() twice, I don't know why + self.select_vms_page.connect(self.select_vms_widget, SIGNAL("selected_changed()"), SIGNAL("completeChanged()")) - - def reject(self): - self.done(0) + self.__fill_vms_list__() + self.__fill_devs_list__() - def save_and_apply(self): - pass - - @pyqtSlot(name='on_selectPathButton_clicked') - def selectPathButton_clicked(self): - self.path = self.pathLineEdit.text() - newPath = QFileDialog.getExistingDirectory(self, 'Select backup directory.') - if newPath: - self.pathLineEdit.setText(newPath) - self.path = newPath - - def current_page_changed(self, id): - self.button(self.CancelButton).setDisabled(id==3) + def __fill_vms_list__(self): + for vm in self.qvm_collection.values(): + if vm.is_running() and vm.qid != 0: + self.excluded.append(vm.name) + continue + if vm.is_appvm() and vm.internal: + self.excluded.append(vm.name) + continue + + if vm.is_template() and vm.installed_by_rpm: + self.excluded.append(vm.name) + continue + + self.to_backup.append(vm.name) + self.select_vms_widget.available_list.addItem(vm.name) + + def __fill_devs_list__(self): + self.dev_combobox.clear() + self.dev_combobox.addItem("None") + for a in self.blk_manager.attached_devs: + if self.blk_manager.attached_devs[a]['attached_to']['vm'] == self.vm.name : + att = a + " " + unicode(self.blk_manager.attached_devs[a]['size']) + " " + self.blk_manager.attached_devs[a]['desc'] + self.dev_combobox.addItem(att, QVariant(a)) + for a in self.blk_manager.free_devs: + att = a + " " + unicode(self.blk_manager.free_devs[a]['size']) + " " + self.blk_manager.free_devs[a]['desc'] + self.dev_combobox.addItem(att, QVariant(a)) + self.dev_combobox.setCurrentIndex(0) #current selected is null "" + self.prev_dev_idx = 0 + self.dir_line_edit.clear() + self.dir_line_edit.setEnabled(False) + self.select_path_button.setEnabled(False) + + def __check_if_mounted__(self, dev_path): + mounts_file = open("/proc/mounts") + for m in list(mounts_file): + if m.startswith(dev_path): + print m + return m.split(" ")[1] + return None + + def __mount_device__(self, dev_path): + try: + mount_dir_name = "backup" + replace(str(datetime.now()),' ', '-').split(".")[0] + pmount_cmd = ["pmount", dev_path, mount_dir_name] + res = subprocess.check_call(pmount_cmd) + print "pmount device res: ", res + except Exception as ex: + QMessageBox.warning (None, "Error mounting selected device!", "ERROR: {0}".format(ex)) + return None + if res == 0: + self.dev_mount_path = "/media/"+mount_dir_name + return self.dev_mount_path + + def __umount_device__(self, dev_mount_path): + try: + pumount_cmd = ["pumount", dev_mount_path] + res = subprocess.check_call(pumount_cmd) + print "pumount device res: ", res + except Exception as ex: + QMessageBox.warning (None, "Could not unmount backup device!", "ERROR: {0}".format(ex)) + + + + def __enable_dir_line_edit__(self, boolean): + self.dir_line_edit.setEnabled(boolean) + self.select_path_button.setEnabled(boolean) + + + def dev_combobox_activated(self, idx): + print self.dev_combobox.currentText() + if idx == self.prev_dev_idx: #nothing has changed + return + #there was a change + self.prev_dev_idx = idx + + self.dir_line_edit.setText("") + self.backup_dir = None + self.dev_mount_path = None + self.__enable_dir_line_edit__(False) + + if self.dev_combobox.currentText() != "None": #An existing device chosen + dev_name = str(self.dev_combobox.itemData(idx).toString()) + + if dev_name in self.blk_manager.free_devs: + if dev_name.startswith(self.vm.name): # originally attached to dom0 + dev_path = "/dev/"+dev_name.split(":")[1] + print "device from dom0 - no need to attach" + + else: # originally attached to another domain, eg. usbvm + print "device from " + dev_name.split(":")[0] + #attach it to dom0, then treat it as an attached device + self.blk_manager.attach_device(self.vm, dev_name) + + if dev_name in self.blk_manager.attached_devs: #is attached to dom0 + print "device attached as " + self.blk_manager.attached_devs[dev_name]['attached_to']['frontend'] + assert self.blk_manager.attached_devs[dev_name]['attached_to']['vm'] == self.vm.name + + dev_path = "/dev/" + self.blk_manager.attached_devs[dev_name]['attached_to']['frontend'] + + #check if device mounted + self.dev_mount_path = self.__check_if_mounted__(dev_path) + if self.dev_mount_path != None: + self.__enable_dir_line_edit__(True) + else: + self.dev_mount_path = self.__mount_device__(dev_path) + if self.dev_mount_path != None: + self.__enable_dir_line_edit__(True) + + self.select_dir_page.emit(SIGNAL("completeChanged()")) + + + + @pyqtSlot(name='on_select_path_button_clicked') + def select_path_button_clicked(self): + self.backup_dir = self.dir_line_edit.text() + file_dialog = QFileDialog() + file_dialog.setReadOnly(True) + new_path = file_dialog.getExistingDirectory(self, "Select backup directory.", self.dev_mount_path) + if new_path: + self.dir_line_edit.setText(new_path) + self.backup_dir = new_path + self.select_dir_page.emit(SIGNAL("completeChanged()")) + + def validateCurrentPage(self): + if self.currentPage() is self.select_vms_page: + for i in range(self.select_vms_widget.available_list.count()): + vmname = self.select_vms_widget.available_list.item(i).text() + self.excluded.append(vmname) + return True + + def gather_output(self, s): + self.func_output.append(s) + + def update_progress_bar(self, value): + print "progress bar value: ", value + self.emit(SIGNAL("backup_progress(int)"), value) + + + def __do_backup__(self, thread_monitor): + print "doiing backup" + msg = [] + try: + qubesutils.backup_do(str(self.backup_dir), self.files_to_backup, self.update_progress_bar) + #simulate_long_lasting_proces(10, self.update_progress_bar) + except Exception as ex: + print "got exception from backup" + msg.append(str(ex)) + + if len(msg) > 0 : + thread_monitor.set_error_msg('\n'.join(msg)) + + thread_monitor.set_finished() + + + def current_page_changed(self, id): + if self.currentPage() is self.confirm_page: + del self.func_output[:] + self.files_to_backup = qubesutils.backup_prepare(str(self.backup_dir), exclude_list = self.excluded, print_callback = self.gather_output) + for i in self.excluded: + print i + self.textEdit.setReadOnly(True) + self.textEdit.setFontFamily("Monospace") + self.textEdit.setText("\n".join(self.func_output)) + for i in self.func_output: + print i + + for s in self.files_to_backup: + print s + + elif self.currentPage() is self.commit_page: + self.button(self.CancelButton).setDisabled(True) + self.button(self.FinishButton).setDisabled(True) + print "butons disabled" + self.thread_monitor = ThreadMonitor() + thread = threading.Thread (target= self.__do_backup__ , args=(self.thread_monitor,)) + thread.daemon = True + print "will start thread" + thread.start() + + while not self.thread_monitor.is_finished(): + self.app.processEvents() + time.sleep (0.1) + + if not self.thread_monitor.success: + QMessageBox.warning (None, "Backup error!", "ERROR: {1}".format(self.vm.name, self.thread_monitor.error_msg)) + + self.__umount_device__(self.dev_mount_path) + self.button(self.FinishButton).setEnabled(True) + + + def has_selected_vms(self): + print "isComplete called" + return self.select_vms_widget.selected_list.count() > 0 + + def has_selected_dir(self): + return self.backup_dir != None + + +def simulate_long_lasting_proces(period, progress_callback): + for i in range(period): + progress_callback((i*100)/period) + time.sleep(1) + + progress_callback(100) + return 0 + # Bases on the original code by: # Copyright (c) 2002-2007 Pascal Varet @@ -113,7 +332,7 @@ def main(): app = QApplication(sys.argv) app.setOrganizationName("The Qubes Project") app.setOrganizationDomain("http://qubes-os.org") - app.setApplicationName("Qubes Restore VMs") + app.setApplicationName("Qubes Backup VMs") sys.excepthook = handle_exception diff --git a/qubesmanager/main.py b/qubesmanager/main.py index bfbc1e4..9e666d1 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -42,12 +42,12 @@ from settings import VMSettingsWindow from restore import RestoreVMsWindow from backup import BackupVMsWindow from global_settings import GlobalSettingsWindow +from thread_monitor import * from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent import subprocess import time -import threading from datetime import datetime,timedelta updates_stat_file = 'last_update.stat' @@ -486,23 +486,6 @@ class VmShutdownMonitor(QObject): else: QTimer.singleShot (vm_shutdown_timeout, self.check_if_vm_has_shutdown) -class ThreadMonitor(QObject): - def __init__(self): - self.success = True - self.error_msg = None - self.event_finished = threading.Event() - - def set_error_msg(self, error_msg): - self.success = False - self.error_msg = error_msg - self.set_finished() - - def is_finished(self): - return self.event_finished.is_set() - - def set_finished(self): - self.event_finished.set() - class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): row_height = 30 @@ -1105,7 +1088,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): @pyqtSlot(name='on_action_backup_triggered') def action_backup_triggered(self): - backup_window = BackupVMsWindow() + backup_window = BackupVMsWindow(app, self.qvm_collection, self.blk_manager) backup_window.exec_() @@ -1151,8 +1134,10 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): action = self.blk_menu.addAction(QIcon(":/remove.png"), str) action.setData(QVariant(d)) - if self.blk_menu.isEmpty() and len(self.blk_manager.free_devs) > 0: + if len(self.blk_manager.free_devs) > 0: for d in self.blk_manager.free_devs: + if d.startswith(vm.name): + continue str = "Attach " + d + " " + unicode(self.blk_manager.free_devs[d]['size']) + " " + self.blk_manager.free_devs[d]['desc'] action = self.blk_menu.addAction(QIcon(":/add.png"), str) action.setData(QVariant(d)) diff --git a/qubesmanager/multiselectwidget.py b/qubesmanager/multiselectwidget.py index fbedf72..a9de974 100644 --- a/qubesmanager/multiselectwidget.py +++ b/qubesmanager/multiselectwidget.py @@ -5,6 +5,8 @@ from ui_multiselectwidget import * class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): + __pyqtSignals__ = ("selected_changed()",) + def __init__(self, parent=None): super(MultiSelectWidget, self).__init__() self.setupUi(self); @@ -23,18 +25,20 @@ class MultiSelectWidget(Ui_MultiSelectWidget, QWidget): item = src.takeItem(row) dst.addItem(item) dst.sortItems() + self.emit(SIGNAL("selected_changed()")) def add_selected(self): self.switch_selected(self.available_list, self.selected_list) def remove_selected(self): - self.switch_selected(self.selected_list, self.available_list) - + self.switch_selected(self.selected_list, self.available_list) + def move_all(self, src, dst): while src.count() > 0: item = src.takeItem(0) dst.addItem(item) dst.sortItems() + self.emit(SIGNAL("selected_changed()")) def add_all(self): self.move_all(self.available_list, self.selected_list) diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index ef9a1a7..9ca2475 100644 --- a/qubesmanager/settings.py +++ b/qubesmanager/settings.py @@ -145,7 +145,7 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): thread_monitor.set_finished() return #self.fw_model.apply_rules() - #self.AppListManager.save_appmenu_select_changes() + self.AppListManager.save_appmenu_select_changes() thread_monitor.set_finished() def current_tab_changed(self, idx): diff --git a/qubesmanager/thread_monitor.py b/qubesmanager/thread_monitor.py new file mode 100644 index 0000000..4d5e915 --- /dev/null +++ b/qubesmanager/thread_monitor.py @@ -0,0 +1,44 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2011 Marek Marczykowski +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# + + +from PyQt4.QtCore import * + +import threading + +class ThreadMonitor(QObject): + def __init__(self): + self.success = True + self.error_msg = None + self.event_finished = threading.Event() + + def set_error_msg(self, error_msg): + self.success = False + self.error_msg = error_msg + self.set_finished() + + def is_finished(self): + return self.event_finished.is_set() + + def set_finished(self): + self.event_finished.set() + diff --git a/restoredlg.ui b/restoredlg.ui index 8ff2cee..9c9d244 100644 --- a/restoredlg.ui +++ b/restoredlg.ui @@ -19,7 +19,7 @@ QWizard::NoBackButtonOnLastPage|QWizard::NoBackButtonOnStartPage - + @@ -111,7 +111,7 @@ - + @@ -184,7 +184,7 @@ - + @@ -235,7 +235,7 @@ p, li { white-space: pre-wrap; } - + From 6d0a247997dba38040b04ec20b32965ea20868d4 Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Wed, 22 Feb 2012 06:09:25 +0100 Subject: [PATCH 27/31] Restore dialog --- qubesmanager/backup.py | 132 ++---------------------- qubesmanager/backup_utils.py | 159 +++++++++++++++++++++++++++++ qubesmanager/main.py | 2 +- qubesmanager/restore.py | 191 ++++++++++++++++++++++++++++++----- restoredlg.ui | 143 +++++++++----------------- 5 files changed, 384 insertions(+), 243 deletions(-) create mode 100644 qubesmanager/backup_utils.py diff --git a/qubesmanager/backup.py b/qubesmanager/backup.py index 5150cc2..ef3623d 100644 --- a/qubesmanager/backup.py +++ b/qubesmanager/backup.py @@ -47,6 +47,7 @@ from string import replace from ui_backupdlg import * from multiselectwidget import * +from backup_utils import * class BackupVMsWindow(Ui_Backup, QWizard): @@ -63,6 +64,7 @@ class BackupVMsWindow(Ui_Backup, QWizard): self.qvm_collection = qvm_collection self.blk_manager = blk_manager + self.dev_mount_path = None self.backup_dir = None self.func_output = [] @@ -91,7 +93,7 @@ class BackupVMsWindow(Ui_Backup, QWizard): self.select_vms_page.connect(self.select_vms_widget, SIGNAL("selected_changed()"), SIGNAL("completeChanged()")) self.__fill_vms_list__() - self.__fill_devs_list__() + fill_devs_list(self) def __fill_vms_list__(self): for vm in self.qvm_collection.values(): @@ -110,112 +112,14 @@ class BackupVMsWindow(Ui_Backup, QWizard): self.to_backup.append(vm.name) self.select_vms_widget.available_list.addItem(vm.name) - def __fill_devs_list__(self): - self.dev_combobox.clear() - self.dev_combobox.addItem("None") - for a in self.blk_manager.attached_devs: - if self.blk_manager.attached_devs[a]['attached_to']['vm'] == self.vm.name : - att = a + " " + unicode(self.blk_manager.attached_devs[a]['size']) + " " + self.blk_manager.attached_devs[a]['desc'] - self.dev_combobox.addItem(att, QVariant(a)) - for a in self.blk_manager.free_devs: - att = a + " " + unicode(self.blk_manager.free_devs[a]['size']) + " " + self.blk_manager.free_devs[a]['desc'] - self.dev_combobox.addItem(att, QVariant(a)) - self.dev_combobox.setCurrentIndex(0) #current selected is null "" - self.prev_dev_idx = 0 - self.dir_line_edit.clear() - self.dir_line_edit.setEnabled(False) - self.select_path_button.setEnabled(False) - - def __check_if_mounted__(self, dev_path): - mounts_file = open("/proc/mounts") - for m in list(mounts_file): - if m.startswith(dev_path): - print m - return m.split(" ")[1] - return None - - def __mount_device__(self, dev_path): - try: - mount_dir_name = "backup" + replace(str(datetime.now()),' ', '-').split(".")[0] - pmount_cmd = ["pmount", dev_path, mount_dir_name] - res = subprocess.check_call(pmount_cmd) - print "pmount device res: ", res - except Exception as ex: - QMessageBox.warning (None, "Error mounting selected device!", "ERROR: {0}".format(ex)) - return None - if res == 0: - self.dev_mount_path = "/media/"+mount_dir_name - return self.dev_mount_path - - def __umount_device__(self, dev_mount_path): - try: - pumount_cmd = ["pumount", dev_mount_path] - res = subprocess.check_call(pumount_cmd) - print "pumount device res: ", res - except Exception as ex: - QMessageBox.warning (None, "Could not unmount backup device!", "ERROR: {0}".format(ex)) - - - - def __enable_dir_line_edit__(self, boolean): - self.dir_line_edit.setEnabled(boolean) - self.select_path_button.setEnabled(boolean) - - + def dev_combobox_activated(self, idx): - print self.dev_combobox.currentText() - if idx == self.prev_dev_idx: #nothing has changed - return - #there was a change - self.prev_dev_idx = idx - - self.dir_line_edit.setText("") - self.backup_dir = None - self.dev_mount_path = None - self.__enable_dir_line_edit__(False) - - if self.dev_combobox.currentText() != "None": #An existing device chosen - dev_name = str(self.dev_combobox.itemData(idx).toString()) - - if dev_name in self.blk_manager.free_devs: - if dev_name.startswith(self.vm.name): # originally attached to dom0 - dev_path = "/dev/"+dev_name.split(":")[1] - print "device from dom0 - no need to attach" - - else: # originally attached to another domain, eg. usbvm - print "device from " + dev_name.split(":")[0] - #attach it to dom0, then treat it as an attached device - self.blk_manager.attach_device(self.vm, dev_name) - - if dev_name in self.blk_manager.attached_devs: #is attached to dom0 - print "device attached as " + self.blk_manager.attached_devs[dev_name]['attached_to']['frontend'] - assert self.blk_manager.attached_devs[dev_name]['attached_to']['vm'] == self.vm.name - - dev_path = "/dev/" + self.blk_manager.attached_devs[dev_name]['attached_to']['frontend'] - - #check if device mounted - self.dev_mount_path = self.__check_if_mounted__(dev_path) - if self.dev_mount_path != None: - self.__enable_dir_line_edit__(True) - else: - self.dev_mount_path = self.__mount_device__(dev_path) - if self.dev_mount_path != None: - self.__enable_dir_line_edit__(True) - - self.select_dir_page.emit(SIGNAL("completeChanged()")) - + dev_combobox_activated(self, idx) @pyqtSlot(name='on_select_path_button_clicked') def select_path_button_clicked(self): - self.backup_dir = self.dir_line_edit.text() - file_dialog = QFileDialog() - file_dialog.setReadOnly(True) - new_path = file_dialog.getExistingDirectory(self, "Select backup directory.", self.dev_mount_path) - if new_path: - self.dir_line_edit.setText(new_path) - self.backup_dir = new_path - self.select_dir_page.emit(SIGNAL("completeChanged()")) + select_path_button_clicked(self) def validateCurrentPage(self): if self.currentPage() is self.select_vms_page: @@ -228,18 +132,15 @@ class BackupVMsWindow(Ui_Backup, QWizard): self.func_output.append(s) def update_progress_bar(self, value): - print "progress bar value: ", value self.emit(SIGNAL("backup_progress(int)"), value) def __do_backup__(self, thread_monitor): - print "doiing backup" msg = [] try: qubesutils.backup_do(str(self.backup_dir), self.files_to_backup, self.update_progress_bar) #simulate_long_lasting_proces(10, self.update_progress_bar) except Exception as ex: - print "got exception from backup" msg.append(str(ex)) if len(msg) > 0 : @@ -252,25 +153,17 @@ class BackupVMsWindow(Ui_Backup, QWizard): if self.currentPage() is self.confirm_page: del self.func_output[:] self.files_to_backup = qubesutils.backup_prepare(str(self.backup_dir), exclude_list = self.excluded, print_callback = self.gather_output) - for i in self.excluded: - print i + self.textEdit.setReadOnly(True) self.textEdit.setFontFamily("Monospace") self.textEdit.setText("\n".join(self.func_output)) - for i in self.func_output: - print i - - for s in self.files_to_backup: - print s elif self.currentPage() is self.commit_page: self.button(self.CancelButton).setDisabled(True) self.button(self.FinishButton).setDisabled(True) - print "butons disabled" self.thread_monitor = ThreadMonitor() thread = threading.Thread (target= self.__do_backup__ , args=(self.thread_monitor,)) thread.daemon = True - print "will start thread" thread.start() while not self.thread_monitor.is_finished(): @@ -280,25 +173,16 @@ class BackupVMsWindow(Ui_Backup, QWizard): if not self.thread_monitor.success: QMessageBox.warning (None, "Backup error!", "ERROR: {1}".format(self.vm.name, self.thread_monitor.error_msg)) - self.__umount_device__(self.dev_mount_path) + umount_device(self.dev_mount_path) self.button(self.FinishButton).setEnabled(True) - def has_selected_vms(self): - print "isComplete called" return self.select_vms_widget.selected_list.count() > 0 def has_selected_dir(self): return self.backup_dir != None -def simulate_long_lasting_proces(period, progress_callback): - for i in range(period): - progress_callback((i*100)/period) - time.sleep(1) - - progress_callback(100) - return 0 # Bases on the original code by: diff --git a/qubesmanager/backup_utils.py b/qubesmanager/backup_utils.py new file mode 100644 index 0000000..0978b03 --- /dev/null +++ b/qubesmanager/backup_utils.py @@ -0,0 +1,159 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# + +import sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time + +from thread_monitor import * + +from datetime import datetime +from string import replace + + +def check_if_mounted(dev_path): + mounts_file = open("/proc/mounts") + for m in list(mounts_file): + if m.startswith(dev_path): + return m.split(" ")[1] + return None + + +def mount_device(dev_path): + try: + mount_dir_name = "backup" + replace(str(datetime.now()),' ', '-').split(".")[0] + pmount_cmd = ["pmount", dev_path, mount_dir_name] + res = subprocess.check_call(pmount_cmd) + except Exception as ex: + QMessageBox.warning (None, "Error mounting selected device!", "ERROR: {0}".format(ex)) + return None + if res == 0: + dev_mount_path = "/media/"+mount_dir_name + return dev_mount_path + return None + +def umount_device(dev_mount_path): + try: + pumount_cmd = ["pumount", dev_mount_path] + res = subprocess.check_call(pumount_cmd) + if res == 0: + dev_mount_path = None + except Exception as ex: + QMessageBox.warning (None, "Could not unmount backup device!", "ERROR: {0}".format(ex)) + return dev_mount_path + + +def fill_devs_list(dialog): + dialog.dev_combobox.clear() + dialog.dev_combobox.addItem("None") + for a in dialog.blk_manager.attached_devs: + if dialog.blk_manager.attached_devs[a]['attached_to']['vm'] == dialog.vm.name : + att = a + " " + unicode(dialog.blk_manager.attached_devs[a]['size']) + " " + dialog.blk_manager.attached_devs[a]['desc'] + dialog.dev_combobox.addItem(att, QVariant(a)) + for a in dialog.blk_manager.free_devs: + att = a + " " + unicode(dialog.blk_manager.free_devs[a]['size']) + " " + dialog.blk_manager.free_devs[a]['desc'] + dialog.dev_combobox.addItem(att, QVariant(a)) + dialog.dev_combobox.setCurrentIndex(0) #current selected is null "" + dialog.prev_dev_idx = 0 + dialog.dir_line_edit.clear() + dialog.dir_line_edit.setEnabled(False) + dialog.select_path_button.setEnabled(False) + + +def enable_dir_line_edit(dialog, boolean): + dialog.dir_line_edit.setEnabled(boolean) + dialog.select_path_button.setEnabled(boolean) + + +def dev_combobox_activated(dialog, idx): + if idx == dialog.prev_dev_idx: #nothing has changed + return + #there was a change + + dialog.dir_line_edit.setText("") + dialog.backup_dir = None + + if dialog.dev_mount_path != None: + dialog.dev_mount_path = umount_device(dialog.dev_mount_path) + if dialog_dev_mount_path != None: + dialog.dev_combobox.setCurrentIndex(dialog.prev_dev_idx) + return + + enable_dir_line_edit(dialog, False) + + if dialog.dev_combobox.currentText() != "None": #An existing device chosen + dev_name = str(dialog.dev_combobox.itemData(idx).toString()) + + if dev_name in dialog.blk_manager.free_devs: + if dev_name.startswith(dialog.vm.name): # originally attached to dom0 + dev_path = "/dev/"+dev_name.split(":")[1] + + else: # originally attached to another domain, eg. usbvm + #attach it to dom0, then treat it as an attached device + dialog.blk_manager.attach_device(dialog.vm, dev_name) + + if dev_name in dialog.blk_manager.attached_devs: #is attached to dom0 + assert dialog.blk_manager.attached_devs[dev_name]['attached_to']['vm'] == dialog.vm.name + dev_path = "/dev/" + dialog.blk_manager.attached_devs[dev_name]['attached_to']['frontend'] + + #check if device mounted + dialog.dev_mount_path = check_if_mounted(dev_path) + if dialog.dev_mount_path != None: + enable_dir_line_edit(dialog, True) + else: + dialog.dev_mount_path = mount_device(dev_path) + if dialog.dev_mount_path != None: + enable_dir_line_edit(dialog, True) + else: + dialog.dev_combobox.setCurrentIndex(0) #if couldn't mount - set current device to "None" + + + dialog.prev_dev_idx = idx + dialog.select_dir_page.emit(SIGNAL("completeChanged()")) + + +def select_path_button_clicked(dialog): + dialog.backup_dir = dialog.dir_line_edit.text() + file_dialog = QFileDialog() + file_dialog.setReadOnly(True) + new_path = file_dialog.getExistingDirectory(dialog, "Select backup directory.", dialog.dev_mount_path) + if new_path: + dialog.dir_line_edit.setText(new_path) + dialog.backup_dir = new_path + dialog.select_dir_page.emit(SIGNAL("completeChanged()")) + + + +def simulate_long_lasting_proces(period, progress_callback): + for i in range(period): + progress_callback((i*100)/period) + time.sleep(1) + + progress_callback(100) + return 0 + diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 9e666d1..5f578cc 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -1083,7 +1083,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): @pyqtSlot(name='on_action_restore_triggered') def action_restore_triggered(self): - restore_window = RestoreVMsWindow() + restore_window = RestoreVMsWindow(app, self.qvm_collection, self.blk_manager) restore_window.exec_() @pyqtSlot(name='on_action_backup_triggered') diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py index d243a25..a2d509e 100644 --- a/qubesmanager/restore.py +++ b/qubesmanager/restore.py @@ -30,54 +30,197 @@ from qubes.qubes import QubesVmCollection from qubes.qubes import QubesException from qubes.qubes import QubesDaemonPidfile from qubes.qubes import QubesHost - +from qubes.qubes import qubes_base_dir import qubesmanager.resources_rc from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent import subprocess import time -import threading from operator import itemgetter +from thread_monitor import * + +from qubes import qubesutils from ui_restoredlg import * from multiselectwidget import * +from backup_utils import * + class RestoreVMsWindow(Ui_Restore, QWizard): - def __init__(self, parent=None): + __pyqtSignals__ = ("restore_progress(int)",) + + def __init__(self, app, qvm_collection, blk_manager, parent=None): super(RestoreVMsWindow, self).__init__(parent) + self.app = app + self.qvm_collection = qvm_collection + self.blk_manager = blk_manager + + self.dev_mount_path = None + self.backup_dir = None + self.restore_options = None + self.backup_vms_list = None + self.func_output = [] + + self.excluded = {} + + for vm in self.qvm_collection.values(): + if vm.qid == 0: + self.vm = vm + break; + + assert self.vm != None + self.setupUi(self) - self.selectVMsWidget = MultiSelectWidget(self) - self.selectVMsLayout.insertWidget(1, self.selectVMsWidget) + self.select_vms_widget = MultiSelectWidget(self) + self.select_vms_layout.insertWidget(1, self.select_vms_widget) - self.selectVMsWidget.available_list.addItem("netVM1") - self.selectVMsWidget.available_list.addItem("appVM1") - self.selectVMsWidget.available_list.addItem("appVM2") - self.selectVMsWidget.available_list.addItem("templateVM1") - self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed) - - def reject(self): - self.done(0) + self.connect(self.dev_combobox, SIGNAL("activated(int)"), self.dev_combobox_activated) + self.connect(self, SIGNAL("restore_progress(QString)"), self.commit_text_edit.append) + + self.select_dir_page.isComplete = self.has_selected_dir + self.select_vms_page.isComplete = self.has_selected_vms + #FIXME + #this causes to run isComplete() twice, I don't know why + self.select_vms_page.connect(self.select_vms_widget, SIGNAL("selected_changed()"), SIGNAL("completeChanged()")) + + fill_devs_list(self) + self.__init_restore_options__() + + + def dev_combobox_activated(self, idx): + dev_combobox_activated(self, idx) + + + @pyqtSlot(name='on_select_path_button_clicked') + def select_path_button_clicked(self): + select_path_button_clicked(self) + + def on_ignore_missing_toggled(self, checked): + self.restore_options['use-default-template'] = checked + self.restore_options['use-default-netvm'] = checked + + def on_ignore_uname_mismatch_toggled(self, checked): + self.restore_options['ignore-username-mismatch'] = checked + + def on_skip_dom0_toggled(self, checked): + self.restore_options['dom0-home'] = checked + + + def __fill_vms_list__(self): + if self.backup_vms_list != None: + return + + self.select_vms_widget.selected_list.clear() + self.select_vms_widget.available_list.clear() + + self.vms_to_restore = qubesutils.backup_restore_prepare(str(self.backup_dir), self.restore_options, self.qvm_collection) + for vmname in self.vms_to_restore: + self.select_vms_widget.available_list.addItem(vmname) + + def __init_restore_options__(self): + if not self.restore_options: + self.restore_options = {} + qubesutils.backup_restore_set_defaults(self.restore_options) + + if 'use-default-template' in self.restore_options and 'use-default-netvm' in self.restore_options: + val = self.restore_options['use-default-template'] and self.restore_options['use-default-netvm'] + self.ignore_missing.setChecked(val) + else: + self.ignore_missing.setChecked(False) + + if 'ignore-username-mismatch' in self.restore_options: + self.ignore_uname_mismatch.setChecked(self.restore_options['ignore-username-mismatch']) + + if 'dom0-home' in self.restore_options: + self.skip_dom0.setChecked(self.restore_options['dom0-home']) - def save_and_apply(self): - pass - def current_page_changed(self, id): - self.button(self.CancelButton).setDisabled(id==3) - @pyqtSlot(name='on_selectPathButton_clicked') - def selectPathButton_clicked(self): - self.path = self.pathLineEdit.text() - newPath = QFileDialog.getExistingDirectory(self, 'Select backup directory.') - if newPath: - self.pathLineEdit.setText(newPath) - self.path = newPath + def gather_output(self, s): + self.func_output.append(s) + + def restore_error_output(self, s): + self.emit(SIGNAL("restore_progress(QString)"), '{0}'.format(s)) + + + def restore_output(self, s): + self.emit(SIGNAL("restore_progress(QString)"),'{0}'.format(s)) + + + def __do_restore__(self, thread_monitor): + err_msg = [] + self.qvm_collection.lock_db_for_writing() + try: + qubesutils.backup_restore_do(str(self.backup_dir), self.vms_to_restore, self.qvm_collection, self.restore_output, self.restore_error_output) + except Exception as ex: + err_msg.append(str(ex)) + + self.qvm_collection.unlock_db() + if len(err_msg) > 0 : + thread_monitor.set_error_msg('\n'.join(err_msg)) + self.emit(SIGNAL("restore_progress(QString)"),'{0}'.format("Finished with errors!")) + else: + self.emit(SIGNAL("restore_progress(QString)"),'{0}'.format("Finished successfully!")) + + thread_monitor.set_finished() + + + def current_page_changed(self, id): + + if self.currentPage() is self.select_vms_page: + self.__fill_vms_list__() + + elif self.currentPage() is self.confirm_page: + for v in self.excluded: + self.vms_to_restore[v] = self.excluded[v] + self.excluded = {} + for i in range(self.select_vms_widget.available_list.count()): + vmname = self.select_vms_widget.available_list.item(i).text() + self.excluded[str(vmname)] = self.vms_to_restore[str(vmname)] + del self.vms_to_restore[str(vmname)] + + del self.func_output[:] + qubesutils.backup_restore_print_summary(self.vms_to_restore, print_callback = self.gather_output) + self.confirm_text_edit.setReadOnly(True) + self.confirm_text_edit.setFontFamily("Monospace") + self.confirm_text_edit.setText("\n".join(self.func_output)) + + + elif self.currentPage() is self.commit_page: + self.button(self.CancelButton).setDisabled(True) + self.button(self.FinishButton).setDisabled(True) + + self.thread_monitor = ThreadMonitor() + thread = threading.Thread (target= self.__do_restore__ , args=(self.thread_monitor,)) + thread.daemon = True + thread.start() + + while not self.thread_monitor.is_finished(): + self.app.processEvents() + time.sleep (0.1) + + #if not self.thread_monitor.success: + #QMessageBox.warning (None, "Backup error!", "ERROR: {1}".format(self.vm.name, self.thread_monitor.error_msg)) + + umount_device(self.dev_mount_path) + self.button(self.FinishButton).setEnabled(True) + + + + def has_selected_dir(self): + return self.backup_dir != None + + def has_selected_vms(self): + return self.select_vms_widget.selected_list.count() > 0 + + # Bases on the original code by: # Copyright (c) 2002-2007 Pascal Varet diff --git a/restoredlg.ui b/restoredlg.ui index 9c9d244..3c908e9 100644 --- a/restoredlg.ui +++ b/restoredlg.ui @@ -47,7 +47,7 @@ - + 0 @@ -84,10 +84,10 @@ - + - + ... @@ -96,6 +96,48 @@ + + + + + 50 + false + + + + Restore options + + + + + + Ignore missing templates or netvms, restore VMs anyway. + + + ignore missing + + + + + + + skip dom0 + + + + + + + Ignore dom0 username mismatch while restoring homedir. + + + ignore username mismatch + + + + + + @@ -114,72 +156,11 @@ - + VMs to restore - - - - - - - - 50 - false - - - - Restore options - - - - - - - 50 - false - - - - Do not restore VMs that have missing templates or netvms. - - - skip broken - - - - - - - Ignore missing templates or netvms, restore VMs anyway. - - - ignore missing - - - - - - - Do not restore VMs that are already present on the host. - - - skip conflicting - - - - - - - Ignore dom0 username mismatch while restoring homedir. - - - ignore username mismatch - - - - + @@ -203,7 +184,7 @@ - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> @@ -238,33 +219,7 @@ p, li { white-space: pre-wrap; } - - - - 9 - 50 - false - false - false - - - - Restore in progress... - - - - - - - 24 - - - Qt::AlignCenter - - - false - - + From 68d73dd0143bc479ba244b4b57e48e0094ecde7c Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Wed, 22 Feb 2012 06:15:11 +0100 Subject: [PATCH 28/31] Fixed netvm column. --- qubesmanager/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 5f578cc..0fa0cdc 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -189,8 +189,8 @@ class VmNetvmItem (QTableWidgetItem): def __init__(self, vm): super(VmNetvmItem, self).__init__() - if vm.is_netvm(): - self.setText("self") + if vm.is_netvm() and not vm.is_proxyvm(): + self.setText("n/a") elif vm.netvm_vm is not None: self.setText(vm.netvm_vm.name) else: @@ -199,7 +199,6 @@ class VmNetvmItem (QTableWidgetItem): self.setTextAlignment(Qt.AlignHCenter) - class VmUsageBarWidget (QWidget): class VmUsageBarItem (QTableWidgetItem): From 481e6147343c66c38e9b21bfad2f845dd2573e0c Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Wed, 22 Feb 2012 06:21:46 +0100 Subject: [PATCH 29/31] Unimplemented gui elemenets grayed out --- globalsettingsdlg.ui | 9 +++++++++ settingsdlg.ui | 24 +++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/globalsettingsdlg.ui b/globalsettingsdlg.ui index 0795e1f..8718e50 100644 --- a/globalsettingsdlg.ui +++ b/globalsettingsdlg.ui @@ -16,6 +16,9 @@ + + false + System defaults @@ -77,6 +80,9 @@ + + false + Default memory settings @@ -148,6 +154,9 @@ + + false + Kernel diff --git a/settingsdlg.ui b/settingsdlg.ui index 59d0c1b..99ece3d 100644 --- a/settingsdlg.ui +++ b/settingsdlg.ui @@ -22,11 +22,14 @@ + + true + - 2 + 5 @@ -86,7 +89,7 @@ - true + false Include in backups by default @@ -101,6 +104,9 @@ + + false + Info @@ -153,6 +159,9 @@ + + false + Disk storage @@ -160,7 +169,7 @@ - true + false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -206,6 +215,9 @@ + + false + Advanced @@ -604,6 +616,9 @@ + + false + Devices @@ -628,6 +643,9 @@ + + false + Services From b51f130701ee0b51c80b2451382ebc55bbedbd9c Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Wed, 22 Feb 2012 10:32:48 +0100 Subject: [PATCH 30/31] updated rmp_spec --- rpm_spec/qmgr.spec | 68 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/rpm_spec/qmgr.spec b/rpm_spec/qmgr.spec index 012c2c8..e45f853 100644 --- a/rpm_spec/qmgr.spec +++ b/rpm_spec/qmgr.spec @@ -33,12 +33,25 @@ cp qubes-appmenu-select $RPM_BUILD_ROOT/usr/bin mkdir -p $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager/ cp qubesmanager/main.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/appmenu_select.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/backup.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/backup_utils.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/firewall.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager -cp qubesmanager/qrc_resources.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/global_settings.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/multiselectwidget.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/restore.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/settings.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/thread_monitor.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/resources_rc.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/__init__.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_backupdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_editfwrulesdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_globalsettingsdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_mainwindow.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_multiselectwidget.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/ui_newappvmdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager cp qubesmanager/ui_newfwruledlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager -cp qubesmanager/ui_editfwrulesdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_restoredlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager +cp qubesmanager/ui_settingsdlg.py{,c,o} $RPM_BUILD_ROOT%{python_sitearch}/qubesmanager mkdir -p $RPM_BUILD_ROOT/usr/share/applications cp qubes-manager.desktop $RPM_BUILD_ROOT/usr/share/applications @@ -67,21 +80,60 @@ rm -rf $RPM_BUILD_ROOT %{python_sitearch}/qubesmanager/appmenu_select.py %{python_sitearch}/qubesmanager/appmenu_select.pyc %{python_sitearch}/qubesmanager/appmenu_select.pyo +%{python_sitearch}/qubesmanager/backup.py +%{python_sitearch}/qubesmanager/backup.pyc +%{python_sitearch}/qubesmanager/backup.pyo +%{python_sitearch}/qubesmanager/backup_utils.py +%{python_sitearch}/qubesmanager/backup_utils.pyc +%{python_sitearch}/qubesmanager/backup_utils.pyo %{python_sitearch}/qubesmanager/firewall.py %{python_sitearch}/qubesmanager/firewall.pyc %{python_sitearch}/qubesmanager/firewall.pyo -%{python_sitearch}/qubesmanager/qrc_resources.py -%{python_sitearch}/qubesmanager/qrc_resources.pyc -%{python_sitearch}/qubesmanager/qrc_resources.pyo +%{python_sitearch}/qubesmanager/global_settings.py +%{python_sitearch}/qubesmanager/global_settings.pyc +%{python_sitearch}/qubesmanager/global_settings.pyo +%{python_sitearch}/qubesmanager/multiselectwidget.py +%{python_sitearch}/qubesmanager/multiselectwidget.pyc +%{python_sitearch}/qubesmanager/multiselectwidget.pyo +%{python_sitearch}/qubesmanager/restore.py +%{python_sitearch}/qubesmanager/restore.pyc +%{python_sitearch}/qubesmanager/restore.pyo +%{python_sitearch}/qubesmanager/settings.py +%{python_sitearch}/qubesmanager/settings.pyc +%{python_sitearch}/qubesmanager/settings.pyo +%{python_sitearch}/qubesmanager/thread_monitor.py +%{python_sitearch}/qubesmanager/thread_monitor.pyc +%{python_sitearch}/qubesmanager/thread_monitor.pyo +%{python_sitearch}/qubesmanager/resources_rc.py +%{python_sitearch}/qubesmanager/resources_rc.pyc +%{python_sitearch}/qubesmanager/resources_rc.pyo +%{python_sitearch}/qubesmanager/ui_backupdlg.py +%{python_sitearch}/qubesmanager/ui_backupdlg.pyc +%{python_sitearch}/qubesmanager/ui_backupdlg.pyo +%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.py +%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.pyc +%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.pyo +%{python_sitearch}/qubesmanager/ui_globalsettingsdlg.py +%{python_sitearch}/qubesmanager/ui_globalsettingsdlg.pyc +%{python_sitearch}/qubesmanager/ui_globalsettingsdlg.pyo +%{python_sitearch}/qubesmanager/ui_mainwindow.py +%{python_sitearch}/qubesmanager/ui_mainwindow.pyc +%{python_sitearch}/qubesmanager/ui_mainwindow.pyo +%{python_sitearch}/qubesmanager/ui_multiselectwidget.py +%{python_sitearch}/qubesmanager/ui_multiselectwidget.pyc +%{python_sitearch}/qubesmanager/ui_multiselectwidget.pyo %{python_sitearch}/qubesmanager/ui_newappvmdlg.py %{python_sitearch}/qubesmanager/ui_newappvmdlg.pyc %{python_sitearch}/qubesmanager/ui_newappvmdlg.pyo %{python_sitearch}/qubesmanager/ui_newfwruledlg.py %{python_sitearch}/qubesmanager/ui_newfwruledlg.pyc %{python_sitearch}/qubesmanager/ui_newfwruledlg.pyo -%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.py -%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.pyc -%{python_sitearch}/qubesmanager/ui_editfwrulesdlg.pyo +%{python_sitearch}/qubesmanager/ui_restoredlg.py +%{python_sitearch}/qubesmanager/ui_restoredlg.pyc +%{python_sitearch}/qubesmanager/ui_restoredlg.pyo +%{python_sitearch}/qubesmanager/ui_settingsdlg.py +%{python_sitearch}/qubesmanager/ui_settingsdlg.pyc +%{python_sitearch}/qubesmanager/ui_settingsdlg.pyo /usr/share/applications/qubes-manager.desktop From 42902a4360b15ca8d6c8e83eb4ffe89697d88e1e Mon Sep 17 00:00:00 2001 From: Agnieszka Kostrzewa Date: Wed, 22 Feb 2012 23:25:53 +0100 Subject: [PATCH 31/31] v1.2.0 --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 0664a8f..26aaba0 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.1.6 +1.2.0