Автоматизация тихой установки программ через MSI на 1000 компьютеров
Я разрабатываю установщик программ для массового развёртывания ПО на 1000 компьютеров. Изначально планировал использовать PowerShell-скрипты для установки программ, но столкнулся с проблемами с безопасностью и блокировками (предупреждения безопасности PowerShell). Поэтому я решил отказаться от использования PowerShell-скриптов и перейти на установку программ с помощью MSI-пакетов в тихом режиме.
import shutil
import os
import subprocess
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
QWidget, QScrollArea, QHBoxLayout, QLineEdit, QGroupBox,
QFileDialog, QCheckBox, QSizePolicy, QSpacerItem, QStackedWidget)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QIcon
class InstallThread(QThread):
completed = pyqtSignal(str)
def __init__(self, msi_path):
super().__init__()
self.msi_path = msi_path
def run(self):
try:
# Формируем команду для запуска MSI установки в тихом режиме
command = ["msiexec", "/i", self.msi_path, "/quiet", "/norestart"]
result = subprocess.run(command, capture_output=True, text=True)
# Проверяем статус выполнения
if result.returncode == 0:
self.completed.emit(f"Программа {self.msi_path} установлена успешно.")
else:
self.completed.emit(f"Ошибка при установке {self.msi_path}: {result.stderr}")
except Exception as e:
self.completed.emit(f"Ошибка выполнения: {str(e)}")
class InstallerApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('Установщик программ')
self.setFixedSize(1000, 800)
self.setWindowIcon(QIcon('im.png')) # Иконка программы
self.installation_path = "C:\\Program Files" # Путь по умолчанию
self._tab_indicator = {}
# Основная компоновка
central_widget = QWidget()
main_layout = QVBoxLayout()
central_widget.setLayout(main_layout)
self.setCentralWidget(central_widget)
# Создание стека вкладок
self.stacked_widget = QStackedWidget()
main_layout.addWidget(self.stacked_widget)
# Добавляем вкладки
self.create_program_selection_tab()
self.create_installation_tab()
self.create_completion_tab()
# Добавляем вкладки в QStackedWidget
self.stacked_widget.addWidget(self.program_selection_tab)
self.stacked_widget.addWidget(self.installation_tab)
self.stacked_widget.addWidget(self.completion_tab)
# По умолчанию открыта первая вкладка
self.stacked_widget.setCurrentIndex(0)
# Обновляем информацию о свободном месте на диске при старте программы
self.update_disk_space_label()
self.update_tab_indicator(1)
def create_program_selection_tab(self):
self.program_selection_tab = QWidget()
layout = QVBoxLayout(self.program_selection_tab)
# Верхний текст и строка поиска
header_layout = QHBoxLayout()
self.header_label = QLabel('Чтобы продолжить, нажмите "Далее"')
header_layout.addWidget(self.header_label)
header_layout.addStretch()
# Создаем строку поиска
self.search_bar = QLineEdit()
self.search_bar.setPlaceholderText("Поиск программ...")
self.search_bar.setFixedWidth(300)
self.search_bar.textChanged.connect(self.filter_programs)
header_layout.addWidget(self.search_bar)
layout.addLayout(header_layout)
# Индикатор вкладок
self.create_tab_indicator(layout, 1)
# Программы
self.program_group = QGroupBox()
self.program_group.setStyleSheet("QGroupBox { padding: 10px; margin: 0px; }")
self.program_layout = QVBoxLayout()
self.program_layout.setContentsMargins(0, 0, 0, 0)
self.program_layout.setSpacing(5)
self.program_group.setLayout(self.program_layout)
self.program_layout.insertStretch(99, 1)
self.all_programs = [
('Google Chrome Setup', 'Установщик Google Chrome', 'Интернет', '10.0 MB'),
('Mozilla Firefox', 'Открытый и бесплатный браузер с множеством возможностей', 'Интернет', '2.75 MB'),
('Microsoft Edge', 'Браузер от Microsoft с интеграцией с Windows', 'Интернет', '2.90 MB'),
('VLC Media Player', 'Проигрыватель мультимедиа для всех форматов', 'Мультимедиа', '4.15 MB'),
('7-Zip', 'Архиватор для работы с файлами', 'Система', '3.36 MB'),
('Notepad++', 'Продвинутый текстовый редактор для разработчиков', 'Разработка', '1.65 MB'),
('Skype', 'Программа для видеозвонков и сообщений', 'Коммуникации', '1.85 MB'),
('Spotify', 'Платформа для прослушивания музыки', 'Мультимедиа', '3.50 MB'),
('Visual Studio Code', 'Редактор кода от Microsoft для разработчиков', 'Разработка', '2.95 MB'),
('Битрикс24', 'Корпоративный портал для управления бизнесом', 'Бизнес приложения', '15 MB'),
('1С', 'Система для бухгалтерии и управления предприятием', 'Бизнес приложения', '100 MB'),
]
# Используем MSI файлы вместо PowerShell
self.installers = {
'Google Chrome Setup': r'\\10.10.10.190\it\36602\Program\ChromeSetup.msi',
'Mozilla Firefox': r'\\10.10.10.190\it\36602\Program\FirefoxSetup.msi',
'Microsoft Edge': r'\\10.10.10.190\it\36602\Program\EdgeSetup.msi',
'VLC Media Player': r'\\10.10.10.190\it\36602\Program\VLCSetup.msi',
'7-Zip': r'\\10.10.10.190\it\36602\Program\7ZipSetup.msi',
'Notepad++': r'\\10.10.10.190\it\36602\Program\Notepad++Setup.msi',
'Skype': r'\\10.10.10.190\it\36602\Program\SkypeSetup.msi',
'Spotify': r'\\10.10.10.190\it\36602\Program\SpotifySetup.msi',
'Visual Studio Code': r'\\10.10.10.190\it\36602\Program\VSCodeSetup.msi',
'Битрикс24': r'\\10.10.10.190\it\36602\Program\bitrix24.msi',
'1С': r'\\10.10.10.190\it\36602\Program\1CSetup.msi',
}
self.programs = self.all_programs.copy()
self.check_boxes = []
self.create_program_list()
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setWidget(self.program_group)
layout.addWidget(self.scroll_area)
bottom_layout = QHBoxLayout()
left_side_layout = QVBoxLayout()
self.selected_size_label = QLabel('Выбрано для установки: 0 программ (0 MB)')
self.selected_size_label.setFixedWidth(500)
self.disk_space_label = QLabel('Доступно на диске: 36 ГБ')
left_side_layout.addWidget(self.selected_size_label)
left_side_layout.addWidget(self.disk_space_label)
center_side_layout = QVBoxLayout()
path_title_label = QLabel('Путь установки:')
center_side_layout.addWidget(path_title_label, alignment=Qt.AlignLeft)
self.installation_path_label = QLabel(self.format_path(self.installation_path))
center_side_layout.addWidget(self.installation_path_label, alignment=Qt.AlignLeft)
right_side_layout = QVBoxLayout()
self.clear_selection_button = QPushButton('Очистить выбор программ')
self.clear_selection_button.clicked.connect(self.clear_program_selection)
self.install_path_button = QPushButton('Выбрать путь установки')
self.install_path_button.clicked.connect(self.select_installation_path)
right_side_layout.addWidget(self.clear_selection_button)
right_side_layout.addWidget(self.install_path_button)
bottom_layout.addLayout(left_side_layout)
bottom_layout.addStretch()
bottom_layout.addLayout(center_side_layout)
bottom_layout.addStretch()
bottom_layout.addLayout(right_side_layout)
layout.addLayout(bottom_layout)
self.next_button = QPushButton('Далее')
self.next_button.clicked.connect(self.go_to_installation_tab)
layout.addWidget(self.next_button)
def filter_programs(self, search_text):
search_text = search_text.lower()
if search_text:
self.programs = [
(name, description, category, size) for name, description, category, size in self.all_programs
if name.lower().startswith(search_text)
]
else:
self.programs = self.all_programs.copy()
self.create_program_list()
self.program_group.adjustSize()
self.scroll_area.verticalScrollBar().setValue(self.scroll_area.verticalScrollBar().minimum())
def create_tab_indicator(self, layout, ind):
steps_layout = QHBoxLayout()
step1_label = QLabel('1. Выбор программ')
step1_label.setObjectName('step1_label')
step1_label.setAlignment(Qt.AlignCenter)
step1_label.setStyleSheet("""
background-color: red;
color: white;
padding: 10px;
font-size: 14px;
min-width: 150px;
""")
step2_label = QLabel('2. Процесс установки')
step2_label.setObjectName('step2_label')
step2_label.setAlignment(Qt.AlignCenter)
step2_label.setStyleSheet("""
background-color: blue;
color: black;
padding: 10px;
font-size: 14px;
min-width: 150px;
""")
step3_label = QLabel('3. Завершение')
step3_label.setObjectName('step3_label')
step3_label.setAlignment(Qt.AlignCenter)
step3_label.setStyleSheet("""
background-color: lightgray;
color: black;
padding: 10px;
font-size: 14px;
min-width: 150px;
""")
steps_layout.addWidget(step1_label)
steps_layout.addWidget(step2_label)
steps_layout.addWidget(step3_label)
self._tab_indicator[ind] = [step1_label, step2_label, step3_label]
layout.addLayout(steps_layout)
def update_tab_indicator(self, step):
step1_label, step2_label, step3_label = self._tab_indicator[step]
if step == 1:
step1_label.setStyleSheet("background-color: red; color: white; padding: 10px;")
step2_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
step3_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
elif step == 2:
step1_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
step2_label.setStyleSheet("background-color: red; color: white; padding: 10px;")
step3_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
elif step == 3:
step1_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
step2_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
step3_label.setStyleSheet("background-color: red; color: white; padding: 10px;")
def create_installation_tab(self):
self.installation_tab = QWidget()
layout = QVBoxLayout(self.installation_tab)
header_layout = QHBoxLayout()
self.header_label_install = QLabel('Идет процесс установки программ...')
header_layout.addWidget(self.header_label_install)
header_layout.addStretch()
layout.addLayout(header_layout)
self.create_tab_indicator(layout, 2)
self.installation_group = QGroupBox()
self.installation_group.setStyleSheet("QGroupBox { padding: 10px; margin: 0px; }")
self.installation_layout = QVBoxLayout()
self.installation_layout.setContentsMargins(0, 0, 0, 0)
self.installation_layout.setSpacing(5)
self.installation_group.setLayout(self.installation_layout)
self.scroll_area_install = QScrollArea()
self.scroll_area_install.setWidgetResizable(True)
self.scroll_area_install.setWidget(self.installation_group)
layout.addWidget(self.scroll_area_install)
bottom_layout = QHBoxLayout()
self.back_button = QPushButton('Назад')
self.back_button.clicked.connect(self.go_to_program_selection_tab)
bottom_layout.addStretch()
bottom_layout.addWidget(self.back_button)
layout.addLayout(bottom_layout)
def create_completion_tab(self):
self.completion_tab = QWidget()
layout = QVBoxLayout(self.completion_tab)
self.create_tab_indicator(layout, 3)
self.completion_label = QLabel('Установка завершена!')
layout.addWidget(self.completion_label)
self.finish_button = QPushButton('Завершить')
self.finish_button.clicked.connect(self.close)
layout.addWidget(self.finish_button)
def go_to_installation_tab(self):
# Обновляем индикатор и переходим на вкладку установки
self.update_tab_indicator(2)
self.stacked_widget.setCurrentIndex(1)
# Только после перехода на вкладку начинается установка программ
self.install_programs()
def install_programs(self):
for check_box, (name, description, category, size) in zip(self.check_boxes, self.programs):
if check_box.isChecked():
msi_path = self.installers.get(name)
if msi_path:
# Запуск MSI установки
self.install_thread = InstallThread(msi_path)
self.install_thread.completed.connect(self.on_installation_complete)
self.install_thread.start()
def on_installation_complete(self, message):
self.go_to_completion_tab() # Переходим на вкладку завершения установки
print(message) # Отображаем сообщение об окончании установки
def go_to_completion_tab(self):
# Обновляем индикатор вкладок на шаг 3 (Завершение)
self.update_tab_indicator(3)
# Переключаемся на вкладку завершения
self.stacked_widget.setCurrentIndex(2)
# Обновляем текст завершения, если необходимо
self.completion_label.setText("Установка завершена!")
def go_to_program_selection_tab(self):
self.update_tab_indicator(1)
self.stacked_widget.setCurrentIndex(0)
def update_disk_space_label(self):
try:
total, used, free = shutil.disk_usage(self.installation_path)
free_gb = free // (2**30)
self.disk_space_label.setText(f'Доступно на диске: {free_gb} ГБ')
except Exception as e:
self.disk_space_label.setText('Ошибка получения информации о диске')
print(f"Ошибка: {e}")
def select_installation_path(self):
path = QFileDialog.getExistingDirectory(self, "Выберите папку для установки")
if path:
self.installation_path = path
self.installation_path_label.setText(self.format_path(self.installation_path))
self.update_disk_space_label()
def format_path(self, path):
if len(path) > 20:
return f"...\\{os.path.basename(path)}"
return path
def create_program_list(self):
self.check_boxes.clear()
for i in reversed(range(self.program_layout.count())):
widget = self.program_layout.itemAt(i).widget()
if widget:
widget.deleteLater()
if not self.programs:
empty_label = QLabel("Программы не найдены")
self.program_layout.addWidget(empty_label, alignment=Qt.AlignCenter)
else:
for i, (name, description, category, size) in enumerate(self.programs):
row_widget = QWidget()
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(0, 5, 0, 5)
row_layout.setSpacing(10)
row_layout.setAlignment(Qt.AlignVCenter)
program_layout_widget = QWidget()
program_layout_vbox = QVBoxLayout(program_layout_widget)
program_layout_vbox.setContentsMargins(0, 0, 0, 0)
program_layout_vbox.setSpacing(2)
check_box = QCheckBox(name)
check_box.setFixedWidth(200)
desc_label = QLabel(description)
desc_label.setStyleSheet("color: gray;")
desc_label.setWordWrap(True)
desc_label.setFixedWidth(650)
program_layout_vbox.addWidget(check_box, alignment=Qt.AlignLeft)
program_layout_vbox.addWidget(desc_label, alignment=Qt.AlignLeft)
row_layout.addWidget(program_layout_widget)
category_size_layout = QVBoxLayout()
category_size_layout.setContentsMargins(0, 0, 0, 0)
category_size_layout.setSpacing(2)
category_label = QLabel(category)
category_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
size_label = QLabel(size)
size_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
category_size_layout.addWidget(category_label)
category_size_layout.addWidget(size_label)
spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
row_layout.addItem(spacer)
row_layout.addLayout(category_size_layout)
self.program_layout.insertWidget(i, row_widget)
self.check_boxes.append(check_box)
check_box.stateChanged.connect(self.update_selected_size)
check_box.stateChanged.connect(self.update_header_label)
self.program_group.adjustSize()
def update_selected_size(self):
total_size = 0
selected_count = 0
for check_box, (_, _, _, size) in zip(self.check_boxes, self.programs):
if check_box.isChecked():
selected_count += 1
size_value, size_unit = size.split()
size_in_mb = float(size_value)
if size_unit == 'KB':
size_in_mb /= 1024
total_size += size_in_mb
if selected_count > 0:
self.selected_size_label.setText(f'Выбрано для установки: {selected_count} программ ({total_size:.2f} MB)')
else:
self.selected_size_label.setText('Выбрано для установки: 0 программ (0 MB)')
def update_header_label(self):
if any(cb.isChecked() for cb in self.check_boxes):
self.header_label.setText('Чтобы продолжить, нажмите "Далее"')
else:
self.header_label.setText('Выберите желаемые программы и нажмите "Далее"')
def clear_program_selection(self):
if any(check_box.isChecked() for check_box in self.check_boxes):
for check_box in self.check_boxes:
check_box.setChecked(False)
self.update_selected_size()
self.update_header_label()
else:
self.selected_size_label.setText('Выбрано для установки: 0 программ (0 MB)')
self.header_label.setText('Выберите желаемые программы и нажмите "Далее"')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = InstallerApp()
window.show()
sys.exit(app.exec_())
Проблема такая.
Проблема с установкой MSI-пакета через msiexec в тихом режиме
Вывод в консоли: Ошибка при установке \10.10.10.190\it\36602\Program\bitrix24.msi:
Основная проблема заключается в том, что при запуске команды для тихой установки MSI-пакета через msiexec установка не происходит, хотя ошибок командлет не выводит. Команда, которую я использую для запуска: command = ["msiexec", "/i", msi_path, "/quiet", "/norestart"]
MSI-пакет находится на сетевом ресурсе, и путь к нему передаю правильно (проверил доступ вручную). Тем не менее, после выполнения команды установка не происходит, и никакого явного сообщения об ошибке не выводится.