├── requirements.txt ├── README.md ├── LICENSE ├── .gitignore └── qemuman.py /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeppiidev/qemu-manager/HEAD/requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qemu-manager 2 | A lightweight GUI Frontend for QEMU written in Python 3 | 4 | ![image](https://user-images.githubusercontent.com/52355164/151694029-24e0ca80-a866-4986-b0f5-fe8cc98fb71f.png) 5 | 6 | ## Requirements 7 | The following tools have to be installed to run this: 8 | - QEMU (Refer to the [QEMU Download Page](https://www.qemu.org/download/) for the latest QEMU binaries) 9 | - Python 3.8 or above 10 | 11 | ## How to use? 12 | - Download the repository as a `.zip` by clicking on the code button and selecting the `Download ZIP` option 13 | 14 | ![image](https://user-images.githubusercontent.com/52355164/151688379-ae850cce-c244-48bc-b287-5824a572c63c.png) 15 | - Un-zip the archive to a folder, and open a terminal there (On most Linux file managers, right-click > Open in terminal. On Windows, shift+right-click > Open in Windows Powershell/Command Prompt) 16 | - Type `pip install -r requirements.txt` to install the modules required for this program 17 | - Finally, type `python qemuman.py` to run this program. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, yeppiidev 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /qemuman.py: -------------------------------------------------------------------------------- 1 | # A small manager program for QEMU. Tested on Windows 2 | # https://github.com/yeppiidev/qemu-manager 3 | 4 | import os 5 | import tkinter as tk 6 | import atexit 7 | 8 | from subprocess import PIPE 9 | from subprocess import Popen, CalledProcessError 10 | 11 | from tkinter.messagebox import showinfo as alert 12 | from tkinter.messagebox import askyesno as confirm 13 | 14 | from ttkthemes import ThemedTk 15 | from tkinter import ACTIVE, E, LEFT, N, RIGHT, S, W 16 | from tkinter import ttk, simpledialog 17 | from shutil import which 18 | 19 | qemu_process = None 20 | qemu_type_box = None 21 | cdrom_path = None 22 | 23 | # TODO: Pls fix this :( 24 | class NewImageDialog(object): 25 | root = None 26 | 27 | def __init__(self, msg, dict_key=None): 28 | self.top = tk.Toplevel(NewImageDialog.root) 29 | self.top.title("HDD Image Wizard") 30 | self.top.geometry("450x300") 31 | self.top.wm_resizable(False, False) 32 | 33 | frame = ttk.Frame(self.top, borderwidth=4) 34 | frame.pack(fill="both", expand=True) 35 | 36 | label = tk.Label( 37 | frame, 38 | text="Create a new HDD Image", 39 | anchor="w", 40 | background=self.top.cget("background"), 41 | font=("Tahoma", 18), 42 | ) 43 | label.pack(padx=4, pady=4) 44 | label.place(x=15, y=15) 45 | 46 | self.hdd_name_text = tk.StringVar() 47 | 48 | self.hdd_name = ttk.Entry(self.top, textvariable=self.hdd_name_text) 49 | self.hdd_name.pack(fill="x", padx=15) 50 | self.hdd_name.focus() 51 | 52 | b_cancel = ttk.Button(self.top, text="Cancel", command=self.top.destroy) 53 | b_cancel.pack() 54 | b_cancel.place(x=260, y=260) 55 | 56 | b_submit = ttk.Button( 57 | self.top, text="Create", command=lambda: self.entry_to_dict(dict_key) 58 | ) 59 | b_submit.pack() 60 | b_submit.place(x=350, y=260) 61 | 62 | def entry_to_dict(self, dict_key): 63 | data = self.entry.get() 64 | if data: 65 | d, key = dict_key 66 | d[key] = data 67 | self.top.destroy() 68 | 69 | 70 | class Manager: 71 | def __init__(self) -> None: 72 | self.root = ThemedTk() 73 | self.root.title("QEMU Manager") 74 | self.root.geometry("600x500") 75 | self.root.wm_resizable(False, False) 76 | 77 | self.root.style = ttk.Style() 78 | self.root.style.theme_use("arc") 79 | self.root.style.configure("raised.TButton", borderwidth=1) 80 | 81 | # Variables 82 | self.qemu_kill_on_exit = tk.BooleanVar() 83 | self.qemu_kill_on_exit.set(True) 84 | 85 | self.qemu_sdl_window = tk.BooleanVar() 86 | self.qemu_sdl_window.set(False) 87 | 88 | self.qemu_use_haxm = tk.BooleanVar() 89 | self.qemu_use_haxm.set(False) 90 | 91 | atexit.register(self.exit_handler) 92 | 93 | self.create_menu_items() 94 | self.create_widgets() 95 | 96 | self.running = True 97 | self.root.config(menu=self.menubar) 98 | 99 | self.root.mainloop() 100 | 101 | def is_tool(self, name): 102 | # Check whether `name` is on PATH and marked as executable 103 | # https://stackoverflow.com/a/34177358/15871490 104 | 105 | return which(name) is not None 106 | 107 | def exit_handler(self): 108 | if self.qemu_kill_on_exit.get() and hasattr(self, 'qemu_process'): 109 | self.qemu_process.kill() 110 | 111 | def kill_vm(self): 112 | # Show an error message if the VM is not running 113 | try: 114 | if not self.qemu_process.poll() is None: 115 | alert( 116 | title="VM is not running", message="The VM is not running", 117 | ) 118 | return 1 119 | except: 120 | alert( 121 | title="VM is not running", message="The VM is not running", 122 | ) 123 | return 1 124 | 125 | # Do you really want to terminate the VM? 126 | if ( 127 | confirm( 128 | "Terminate QEMU", 129 | "Are you sure you want to terminate the QEMU process? Any unsaved changes inside the virtual machine will be lost!", 130 | icon="warning", 131 | ) 132 | == "yes" 133 | ): 134 | # TODO: Make this work lol 135 | self.qemu_process.kill() 136 | 137 | def create_image(self): 138 | dlg = NewImageDialog 139 | dlg.root = self.root 140 | 141 | NewImageDialog("Hello", (None, "user")) 142 | 143 | def start_vm(self): 144 | try: 145 | # Check if QEMU is installed 146 | if not self.is_tool(self.qemu_type_box.get()): 147 | alert( 148 | "Unable to start the VM", 149 | "It seems like QEMU is not installed on your system. Please install it and try again.", 150 | icon="error", 151 | ) 152 | return 1 153 | 154 | # Check if the CD-ROM file exists 155 | if not os.path.exists(self.cdrom_path.get()): 156 | alert( 157 | "Unable to start the VM", 158 | "The specified CD-ROM file does not exist", 159 | icon="error", 160 | ) 161 | return 1 162 | 163 | # Open QEMU in the background using subprocess.Popen() 164 | self.qemu_process = Popen( 165 | f"{self.qemu_type_box.get()} -cdrom {self.cdrom_path.get()} {'-sdl' if self.qemu_sdl_window.get() else ''} {'-accel hax' if self.qemu_use_haxm.get() else ''}", 166 | stdout=PIPE, 167 | stderr=PIPE, 168 | ) 169 | 170 | except CalledProcessError as e: 171 | # TODO: Improve error messages 172 | alert( 173 | title="QEMU Returned an error", 174 | message=f"QEMU Returned an error: {str(e.output)}", 175 | ) 176 | 177 | def get_first_file_with_ext(self, path, ext): 178 | # Loop through the specified directory and 179 | # find a file that has the specified extension 180 | for root, dirs, files in os.walk(path): 181 | for file in files: 182 | if file.endswith(ext): 183 | return file 184 | 185 | # Return an empty string if no file with the 186 | # specified extension was found 187 | return "" 188 | 189 | def not_implemented(self): 190 | alert("Not Implemented", "This feature has not been implemented yet :P") 191 | 192 | def create_menu_items(self): 193 | # Create the menu bar 194 | self.menubar = tk.Menu(self.root) 195 | 196 | # Adding File Menu and commands 197 | file = tk.Menu(self.menubar, tearoff=0) 198 | self.menubar.add_cascade(label="File", menu=file) 199 | file.add_command(label="Preferences...", command=self.not_implemented) 200 | file.add_separator() 201 | file.add_command(label="Exit", command=self.root.destroy) 202 | 203 | virtual_machine = tk.Menu(self.menubar, tearoff=0) 204 | self.menubar.add_cascade(label="Machine", menu=virtual_machine) 205 | virtual_machine.add_command(label="Start", command=self.start_vm) 206 | virtual_machine.add_command(label="Terminate", command=self.kill_vm) 207 | 208 | tools = tk.Menu(self.menubar, tearoff=0) 209 | self.menubar.add_cascade(label="Tools", menu=tools) 210 | tools.add_command(label="Create HDD Image", command=self.not_implemented) 211 | 212 | about = tk.Menu(self.menubar, tearoff=0) 213 | self.menubar.add_cascade(label="Help", menu=about) 214 | about.add_command( 215 | label="About", 216 | command=lambda: alert( 217 | "About", 218 | "QEMU Manager is a simple GUI frontend for QEMU written in TKinter and Python.\n\nCreated by yeppiidev", 219 | ), 220 | ) 221 | 222 | def create_widgets(self): 223 | # Which formatter should I use to make this look better? 224 | title_label = ttk.Label( 225 | self.root, 226 | text="QEMU Manager", 227 | anchor="w", 228 | background=self.root.cget("background"), 229 | font=("Tahoma", 30), 230 | ) 231 | title_label.pack(fill="both", padx=18, pady=18) 232 | 233 | # Add a button to start the VM 234 | self.start_vm_btn = ttk.Button( 235 | self.root, text="Start VM", command=self.start_vm 236 | ) 237 | self.start_vm_btn.pack(ipadx=10, ipady=10, padx=10, pady=10) 238 | self.start_vm_btn.place(x=490, y=445) 239 | 240 | # Add a button to kill the VM 241 | self.kill_vm_btn = ttk.Button( 242 | self.root, text="Terminate QEMU", command=self.kill_vm 243 | ) 244 | self.kill_vm_btn.pack(ipadx=10, ipady=10, padx=10, pady=10) 245 | self.kill_vm_btn.place(x=360, y=445) 246 | 247 | qemu_type_box_label = ttk.Label( 248 | self.root, text="CPU Architecture:", background=self.root.cget("background") 249 | ) 250 | qemu_type_box_label.pack(fill="x", padx=15) 251 | 252 | self.qemu_type_box_value = tk.StringVar() 253 | self.qemu_type_box_value.set("qemu-system-i386") 254 | 255 | self.qemu_type_box = ttk.Combobox( 256 | self.root, state="readonly", textvariable=self.qemu_type_box_value 257 | ) 258 | self.qemu_type_box["values"] = ( 259 | "qemu-system-i386", 260 | "qemu-system-x86_64", 261 | "qemu-system-ppc", 262 | "qemu-system-ppc64", 263 | ) 264 | self.qemu_type_box.current(1) 265 | self.qemu_type_box.pack(fill=tk.X, padx=15, pady=5) 266 | 267 | cdrom_path_label = ttk.Label( 268 | self.root, 269 | text="CD-ROM (ISO) File Path:", 270 | background=self.root.cget("background"), 271 | ) 272 | cdrom_path_label.pack(fill="x", padx=15, pady=5) 273 | 274 | self.cdrom_path_text = tk.StringVar() 275 | self.cdrom_path_text.set(self.get_first_file_with_ext(os.getcwd(), ".iso")) 276 | 277 | self.cdrom_path = ttk.Entry(self.root, textvariable=self.cdrom_path_text) 278 | self.cdrom_path.pack(fill="x", padx=15) 279 | self.cdrom_path.focus() 280 | 281 | hdd_path_label = ttk.Label( 282 | self.root, 283 | text="HDD (QCOW2) File Path:", 284 | background=self.root.cget("background"), 285 | ) 286 | hdd_path_label.pack(fill="x", padx=15, pady=5) 287 | 288 | self.hdd_path_text = tk.StringVar() 289 | self.hdd_path_text.set(self.get_first_file_with_ext(os.getcwd(), ".qcow2")) 290 | 291 | self.hdd_path_frame = tk.Frame( 292 | self.root, bg=self.root.cget("background"), width=450, height=50 293 | ) 294 | self.hdd_path_frame.grid_columnconfigure(0, weight=1) 295 | 296 | self.hdd_path = ttk.Entry(self.hdd_path_frame, textvariable=self.hdd_path_text) 297 | self.hdd_path.grid(padx=(15, 0), row=0, column=0, columnspan=1, sticky="we") 298 | self.hdd_path.focus() 299 | 300 | self.create_hdd_btn = ttk.Button( 301 | self.hdd_path_frame, text="Create HDD", command=self.not_implemented 302 | ) 303 | self.create_hdd_btn.grid(row=0, column=1, padx=(5, 15)) 304 | 305 | self.hdd_path_frame.pack(fill="x") 306 | 307 | qemu_sdl_window = ttk.Checkbutton( 308 | self.root, 309 | text="Use SDL as the window library?", 310 | variable=self.qemu_sdl_window, 311 | offvalue=False, 312 | onvalue=True, 313 | ) 314 | qemu_sdl_window.pack(fill="x", padx=15, pady=(10, 2)) 315 | 316 | qemu_use_haxm = ttk.Checkbutton( 317 | self.root, 318 | text="Use Intel HAXM? (Requires you to have HAXM installed)", 319 | variable=self.qemu_use_haxm, 320 | offvalue=False, 321 | onvalue=True, 322 | ) 323 | qemu_use_haxm.pack(fill="x", padx=15, pady=2) 324 | 325 | qemu_kill_on_exit_box = ttk.Checkbutton( 326 | self.root, 327 | text="Terminate QEMU on Exit?", 328 | variable=self.qemu_kill_on_exit, 329 | offvalue=False, 330 | onvalue=True, 331 | ) 332 | qemu_kill_on_exit_box.pack(fill="x", padx=15, pady=2) 333 | 334 | # Uncomment for fun :] 335 | # self.selected_theme = tk.StringVar() 336 | 337 | # for theme_name in self.root.get_themes(): 338 | # self.rb = ttk.Radiobutton( 339 | # self.root, 340 | # text=theme_name, 341 | # value=theme_name, 342 | # variable=self.selected_theme, 343 | # command=(lambda: self.root.set_theme(theme_name=self.selected_theme.get())) 344 | # ) 345 | # self.rb.pack(expand=True, fill='both') 346 | 347 | 348 | # Start the manager by constructing a new class 349 | manager = Manager() 350 | --------------------------------------------------------------------------------