diff --git a/.gitignore b/.gitignore
index 7c9d0f2ebe2b0f9c6cf7986c8a2ec63695701469..8cf4c65773dd9126edb2f509918547f39135d37c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,7 @@ MANIFEST
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
-*.spec
+# *.spec
# Installer logs
pip-log.txt
@@ -158,4 +158,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+.idea/
diff --git a/docs/developer-manual.md b/docs/developer-manual.md
index 45a60a495a30098461ab2efcf3a2131873de709a..cc39801402792109caa6fe1f92db2046f4240853 100644
--- a/docs/developer-manual.md
+++ b/docs/developer-manual.md
@@ -49,7 +49,8 @@ EulerLauncher可执行文件包括以下几个部分:
1. EulerLauncherd: EulerLauncher守护进程,以root权限运行在后台,与调用虚拟化组件(Qemu、HyperV、KVM等)及镜像组件进行相关操作;
2. EulerLauncher.app: EulerLauncher服务端主程序,将EulerLauncher及其他相关程序、数据、文件等打包为MacOS APP软件包,便于分发和使用。
3. EulerLauncher: MacOS可执行文件,EulerLauncher客户端CLI工具,用于与服务端交互。
-4. install: MacOS可执行文件,将EulerLauncher运行所需配置文件及相关数据文件安装至`Application Support`文件夹。
+4. eulerlauncherGUI.app: EulerLauncher的客户端GUI程序,用于与服务端交互。
+5. install: MacOS可执行文件,将EulerLauncher运行所需配置文件及相关数据文件安装至`Application Support`文件夹。
由于`EulerLauncher.app`对`EulerLauncherd`有依赖关系,请严格按照以下顺序构建`EulerLauncherd`及`EulerLauncher.app`:
@@ -74,6 +75,11 @@ pyinstaller --clean --noconfirm specs/cli-mac.spec
pyinstaller --clean --noconfirm specs/install.spec
```
+`eulerlauncherGUI.app`与其他文件之间无依赖关系,可以单独构建:
+``` Shell
+pyinstaller --clean --noconfirm specs/eulerlauncherGUI-Mac.spec
+```
+
### 制作`.dmg`:
首先,我们创建一个新目录并将文件移动到其中。
@@ -82,12 +88,19 @@ mkdir -p dist/dmg
cp -R dist/EulerLauncher.app dist/dmg
```
-然后,我们可以使用下面的命令来制作磁盘镜像文件:
+然后,我们可以使用下面的命令来制作主程序`EulerLauncher.app`的磁盘镜像文件:
``` Shell
create-dmg --volname "EulerLauncher" --volicon "etc/images/favicon.png" --window-pos 200 120 --window-size 600 300 --icon-size 100 --icon "EulerLauncher.app" 175 120 --hide-extension "EulerLauncher.app" --app-drop-link 425 120 "dist/EulerLauncher.dmg" "dist/dmg/"
```
-`EulerLauncher.dmg`中将只包含`EulerLauncher.app`主程序,需要将`install`脚本及`EulerLauncher` CLI工具一并压缩后再进行分发。
+类似的,也可以制作出GUI`eulerlauncherGUI.app`的`.dmg`文件:
+``` Shell
+mkdir -p dist/GUIdmg
+cp -R dist/eulerlauncherGUI.app dist/GUIdmg
+create-dmg --volname "eulerlauncherGUI" --volicon "logos/favicon.png" --window-pos 200 120 --window-size 600 300 --icon-size 100 --icon "eulerlauncherGUI.app" 175 120 --hide-extension "eulerlauncherGUI.app" --app-drop-link 425 120 "dist/eulerlauncherGUI.dmg" "dist/GUIdmg/"
+```
+
+`EulerLauncher.dmg`中将只包含`EulerLauncher.app`主程序,`eulerlauncherGUI.dmg`中将只含`eulerlauncherGUI.app`前端界面程序,需要将`install`脚本及`EulerLauncher` CLI工具一并压缩后再进行分发。
## 在Windows上构建EulerLauncher
@@ -122,15 +135,16 @@ EulerLauncher可执行文件包括以下几个部分:
- eulerlauncherd.exe:EulerLauncher的主进程,是运行在后台的守护进程,负责与各类虚拟化后端交互,管理虚拟机、容器以及镜像的生命周期,eulerlauncherd.exe是运行在后台的守护进程。
- eulerlauncher.exe:EulerLauncher的CLI客户端,用户通过该客户端与eulerlauncherd守护进程交互,对虚拟机、镜像等进行相关操作。
+- eulerlauncherGUI.exe:EulerLauncher的GUI客户端,用户也可以通过该图形化界面客户端与eulerlauncherd守护进程交互,对虚拟机、镜像等进行相关操作。
- config-env.bat: 帮助用户快速配置环境变量
1. 构建`eulerlauncherd.exe`:
-项目源码中已包含用于构建EulerLauncherd的Spec脚本`EulerLauncherd-win.spec`, 若非必要,请勿修改该文件,使用一下命令开始构建:
+项目源码中已包含用于构建EulerLauncherd的Spec脚本`EulerLauncherd-win.spec`, 若非必要,请勿修改该文件,使用以下命令开始构建:
- ``` Shell
- pyinstaller --clean --noconfirm specs\\EulerLauncherd-win.spec
- ```
+``` Shell
+pyinstaller --clean --noconfirm specs\\EulerLauncherd-win.spec
+```
2. 构建`eulerlauncher.exe`:
@@ -138,9 +152,15 @@ EulerLauncher可执行文件包括以下几个部分:
pyinstaller --clean --noconfirm specs\\cli-win.spec
```
-3. 将`etc\bin`目录下的`config-env.bat`,`qemu`及`qemu-img`文件夹拷贝到制品目录。
+3. 构建`eulerlauncherGUI.exe`:
+
+``` Shell
+pyinstaller --clean --noconfirm specs\\eulerlauncherGUI.spec
+```
+
+4. 将`etc\bin`目录下的`config-env.bat`,`qemu`及`qemu-img`文件夹拷贝到制品目录。
-4. 在制品目录创建`etc`文件夹,将`logos\favicon.png`复制到该目录下,并在该目录下创建`eulerlauncher.conf`文件,最后将制品目录压缩打包
+5. 在制品目录创建`etc`文件夹,将`logos\favicon.png`与`logos\favicon.ico`复制到该目录下,并在该目录下创建`eulerlauncher.conf`文件,最后将制品目录压缩打包
[1]: https://www.python.org/
[2]: https://brew.sh/
\ No newline at end of file
diff --git a/docs/images/mac-GUIcontent.png b/docs/images/mac-GUIcontent.png
new file mode 100644
index 0000000000000000000000000000000000000000..dc5a234549e9dbf9b4c1cadb9dadf9e29d738e5b
Binary files /dev/null and b/docs/images/mac-GUIcontent.png differ
diff --git a/docs/images/mac-GUIinstall.png b/docs/images/mac-GUIinstall.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0644bd99b9faea5232f13910d18211164994198
Binary files /dev/null and b/docs/images/mac-GUIinstall.png differ
diff --git a/docs/mac-user-manual.md b/docs/mac-user-manual.md
index 2e7b82e4a9802d930f5cbdc3d15cbcd51426bc88..352ebfbd3b671df89936ee7ef6708017bc0ec84e 100644
--- a/docs/mac-user-manual.md
+++ b/docs/mac-user-manual.md
@@ -50,10 +50,12 @@ brew install wget
解压后的目录包含以下文件:
-
+
其中`install`可执行文件为安装文件,用于将**EulerLauncher**所需支持文件安装到指定位置,`EulerLauncher.dmg`为主程序的磁盘映象。
+如果要安装GUI的话,还需要用到`eulerlauncherGUI.dmg`。
+
1. 安装支持文件(本操作需要sudo权限,请先完成前面的步骤):双击`install`可执行文件,等待程序完成执行。
2. 配置**EulerLauncher**:
@@ -98,6 +100,12 @@ brew install wget
+4. 安装**eulerlauncherGUI.app**(选装):
+
+ - 参考上一步,双击`eulerlauncherGUI.dmg`,在弹出的窗口中用鼠标将`eulerlauncherGUI.app`拖动到`Applications`中,即可完成安装,并可在应用程序中找到`eulerlauncherGUI.app`
+
+
+
## 使用EulerLauncher
1. 在应用程序中找到`EulerLauncher.app`,单击启动程序。
@@ -106,166 +114,237 @@ brew install wget
-3. EulerLauncher当前仅支持命令行方式进行访问,请打开`终端.app`,使用命令行进行操作。
+3. EulerLauncher当前支持命令行方式和使用GUI进行访问,请打开`终端.app`或者`eulerlauncherGUI.app`,进行操作。
### 镜像操作
1. 获取可用镜像列表:
-```Shell
-eulerlauncher images
-
-+-----------+----------+--------------+
-| Images | Location | Status |
-+-----------+----------+--------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 2203-load | Local | Ready |
-+-----------+----------+--------------+
-```
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher images
+> +-----------+----------+--------------+
+> | Images | Location | Status |
+> +-----------+----------+--------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 2203-load | Local | Ready |
+> +-----------+----------+--------------+
+> ```
+
+> ***GUI客户端***
+>
+> 左边栏选择`镜像管理`,右边下方点击`刷新`,即可获取可用镜像列表,内容包括镜像名称、位置、状态等信息。
**EulerLauncher**镜像有两种位置属性:1)远端镜像 2)本地镜像,只有处于本地且状态为 `Ready` 的镜像可以直接用来创建虚拟机,位于远端的镜像需要下载后才能够使用;你也可以加载已经预先下载好的本地镜像到**EulerLauncher**中,具体操作方法可以参考接下来的操作指导。
2. 下载远端镜像
-```Shell
-eulerlauncher download-image 22.03-LTS
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher download-image 22.03-LTS
+>
+> Downloading: 22.03-LTS, this might take a while, please check image status with "images" command.
+> ```
-Downloading: 22.03-LTS, this might take a while, please check image status with "images" command.
-```
-
-镜像下载请求是一个异步请求,具体的下载动作将在后台完成,具体耗时与你的网络情况相关,整体的镜像下载流程包括下载、解压缩、格式转换等相关子流程,在下载过程中可以通过 `image` 命令随时查看下载进展与镜像状态,进度条格式为`([downloaded_bytes] [percentage] [download_speed] [remaining_download_time])`:
+> ***GUI客户端***
+>
+> 左边栏选择`镜像管理`,右边列表中选中要下载的镜像,然后在右边下方点击`下载`,即可开始下载镜像,下载过程中点击`刷新`按钮可以查看当前状态。
-```Shell
-eulerlauncher images
-+-----------+----------+------------------------------------+
-| Images | Location | Status |
-+-----------+----------+------------------------------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 22.03-LTS | Local | Downloading: 33792K 8% 4.88M 55s |
-+-----------+----------+------------------------------------+
-```
+镜像下载请求是一个异步请求,具体的下载动作将在后台完成,具体耗时与你的网络情况相关,整体的镜像下载流程包括下载、解压缩、格式转换等相关子流程,在下载过程中可以通过 `image` 命令随时查看下载进展与镜像状态,进度条格式为`([downloaded_bytes] [percentage] [download_speed] [remaining_download_time])`:
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher images
+>
+> +-----------+----------+------------------------------------+
+> | Images | Location | Status |
+> +-----------+----------+------------------------------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 22.03-LTS | Local | Downloading: 33792K 8% 4.88M 55s |
+> +-----------+----------+------------------------------------+
+> ```
+
+> ***GUI客户端***
+>
+> 点击`刷新`按钮查看当前状态,列表中也会显示上面的三列。
当镜像状态转变为 `Ready` 时,表示镜像下载完成,处于 `Ready` 状态的镜像可被用来创建虚拟机:
-```Shell
-eulerlauncher images
-
-+-----------+----------+--------------+
-| Images | Location | Status |
-+-----------+----------+--------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 22.03-LTS | Local | Ready |
-+-----------+----------+--------------+
-```
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher images
+>
+> +-----------+----------+--------------+
+> | Images | Location | Status |
+> +-----------+----------+--------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 22.03-LTS | Local | Ready |
+> +-----------+----------+--------------+
+> ```
+
+> ***GUI客户端***
+>
+> 点击`刷新`按钮查看当前状态,列表中也会显示上面的三列,最后一个值为`Ready`时说明下载完成。
3. 加载本地镜像
用户也可以加载自定义镜像或预先下载到本地的镜像到EulerLauncher中用于创建自定义虚拟机:
-```Shell
-eulerlauncher load-image --path {image_file_path} IMAGE_NAME
-```
-
-当前支持加载的镜像格式有 `xxx.{qcow2, raw, vmdk, vhd, vhdx, qcow, vdi}.[xz]`
-
-例如:
-
-```Shell
-eulerlauncher load-image --path /opt/openEuler-22.03-LTS-x86_64.qcow2.xz 2203-load
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher load-image --path {image_file_path} IMAGE_NAME
+> ```
-Loading: 2203-load, this might take a while, please check image status with "images" command.
-```
+> ***GUI客户端***
+>
+> 左边栏选择`镜像管理`,右边下方点击`加载本地镜像`,会弹出文件选择窗口,按照弹出窗口的引导选择本地镜像文件,输入镜像名称,即可完成加载。
-将位于 `/opt` 目录下的 `openEuler-22.03-LTS-x86_64.qcow2.xz` 加载到EulerLauncher系统中,并命名为 `2203-load`,与下载命令一样,加载命令也是一个异步命令,用户需要用镜像列表命令查询镜像状态直到显示为 `Ready`, 但相对于直接下载镜像,加载镜像的速度会快很多:
-```Shell
-eulerlauncher images
+当前支持加载的镜像格式有 `xxx.{qcow2, raw, vmdk, vhd, vhdx, qcow, vdi}.[xz]`
-+-----------+----------+----------------------------+
-| Images | Location | Status |
-+-----------+----------+----------------------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 2203-load | Local | Loading: (24.00/100%) |
-+-----------+----------+----------------------------+
+例如:
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher load-image --path /opt/openEuler-22.03-LTS-x86_64.qcow2.xz 2203-load
+>
+> Loading: 2203-load, this might take a while, please check image status with "images" command.
+> ```
+>
+> 将位于 `/opt` 目录下的 `openEuler-22.03-LTS-x86_64.qcow2.xz` 加载到EulerLauncher系统中,并命名为 `2203-load`,与下载命令一样,加载命令也是一个异步命令,用户需要用镜像列表命令查询镜像状态直到显示为 `Ready`, 但相对于直接下载镜像,加载镜像的速度会快很多:
+>
+> ```Shell
+> eulerlauncher images
+>
+> +-----------+----------+----------------------------+
+> | Images | Location | Status |
+> +-----------+----------+----------------------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 2203-load | Local | Loading: (24.00/100%) |
+> +-----------+----------+----------------------------+
+>
+> eulerlauncher images
+>
+> +-----------+----------+--------------+
+> | Images | Location | Status |
+> +-----------+----------+--------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 2203-load | Local | Ready |
+> +-----------+----------+--------------+
+> ```
+
+> ***GUI客户端***
+>
+> 同样以上面的例子为例,点击`加载本地镜像`,在文件选择窗口内选择`/opt/openEuler-22.03-LTS-x86_64.qcow2.xz`,镜像名称输入`2203-load`,点击`确定`,即可完成加载。获取加载情况请点击`刷新`按钮查看。
-eulerlauncher images
-
-+-----------+----------+--------------+
-| Images | Location | Status |
-+-----------+----------+--------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 2203-load | Local | Ready |
-+-----------+----------+--------------+
-```
4. 删除镜像:
-通过下面的命令将镜像从EulerLauncher系统中删除:
-
-```Shell
-eulerlauncher delete-image 2203-load
+> ***CLI客户端***
+>
+> 通过下面的命令将镜像从EulerLauncher系统中删除:
+>
+> ```Shell
+> eulerlauncher delete-image 2203-load
+>
+> Image: 2203-load has been successfully deleted.
+> ```
-Image: 2203-load has been successfully deleted.
-```
+> ***GUI客户端***
+>
+> 左边栏选择`镜像管理`,右边列表中选中要删除的镜像,然后在右边下方点击`删除镜像`,即可删除镜像。
### 虚拟机操作
1. 获取虚拟机列表:
-```shell
-eulerlauncher list
-
-+----------+-----------+---------+---------------+
-| Name | Image | State | IP |
-+----------+-----------+---------+---------------+
-| test1 | 2203-load | Running | 172.22.57.220 |
-+----------+-----------+---------+---------------+
-| test2 | 2203-load | Running | N/A |
-+----------+-----------+---------+---------------+
-```
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher list
+>
+> +----------+-----------+---------+---------------+
+> | Name | Image | State | IP |
+> +----------+-----------+---------+---------------+
+> | test1 | 2203-load | Running | 172.22.57.220 |
+> +----------+-----------+---------+---------------+
+> | test2 | 2203-load | Running | N/A |
+> +----------+-----------+---------+---------------+
+> ```
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边上方点击`刷新`,即可获取虚拟机列表,内容包括虚拟机名称、镜像、状态、IP等信息。
若虚拟机IP地址显示为 `N/A` ,若这台虚拟机的状态为 `Running` 则表示这台虚拟机为新创建的虚拟机,网络还未配置完成,网络配置过程大概需要若干秒,请稍后重新尝试获取相关虚拟机信息。
2. 登录虚拟机:
-若虚拟机已成功分配到IP地址,可以直接使用 `SSH` 命令进行登录:
+> ***CLI客户端***
+>
+>若虚拟机已成功分配到IP地址,可以直接使用 `SSH` 命令进行登录:
+>
+>
+> ```Shell
+> ssh root@{instance_ip}
+> ```
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边列表中选中要登录的虚拟机,然后在右边下方点击`连接`,界面会自动跳转到`虚拟机连接`。`虚拟机连接`界面上方左侧会显示虚拟机的各种信息,上方右侧有三个按钮,提供两种SSH连接方式。
+> - 选择`调用终端进行SSH连接`,则会调用系统终端进行SSH连接,会自动弹出终端窗口,在里面操作即可,关闭终端窗口即可断开连接(最好先输入exit指令)。
+> - 选择`在GUI中进行SSH连接`,如果使用导入的镜像则需要在弹出窗口中输入密码,连接成功则会在界面下方的文本框内显示信息,在输入栏中输入命令后按输入条右侧的`Send`或者按键盘上的`Enter`键即可发送命令。要断开连接,可以点击上方右侧的`断开连接`按钮,或者在输入栏中输入`exit`命令,还可以在左边栏直接切换到其他页面(不推荐),这三种方式均可断开连接。
-```Shell
-ssh root@{instance_ip}
-```
若使用的是openEuler社区提供的官方镜像,则默认用户为 `root` 默认密码为 `openEuler12#$`
3. 创建虚拟机
-```Shell
-eulerlauncher launch --image {image_name} {instance_name}
-```
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher launch --image {image_name} {instance_name}
+> ```
+>
+> 通过 `--image` 指定镜像,同时指定虚拟机名称,EulerLauncher会根据所指定的镜像默认创建一个规格为`2U4G`的openEuler虚拟机。
-通过 `--image` 指定镜像,同时指定虚拟机名称。
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边下方点击`创建虚拟机`,弹出窗口中选择镜像,输入虚拟机名称,点击`确定`,即可创建虚拟机,创建过程中请耐心等待`请等待`弹窗消失。
4. 删除虚拟机
-```Shell
-eulerlauncher delete-instance {instance_name}
-```
-根据虚拟机名称删除指定的虚拟机。
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher delete-instance {instance_name}
+> ```
+> 根据虚拟机名称删除指定的虚拟机。
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边列表中选中要删除的虚拟机,然后在右边下方点击`删除虚拟机`,即可删除虚拟机。
5. 为虚拟机打快照,并导出为镜像
-```Shell
-eulerlauncher take-snapshot --snapshot_name snap --export_path path vm_name
-```
-通过`--snapshot_name`指定快照名称,`--export_path`指定导出镜像存放位置,`vm_name`为虚拟机名。
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher take-snapshot --snapshot_name snap --export_path path vm_name
+> ```
+> 通过`--snapshot_name`指定快照名称,`--export_path`指定导出镜像存放位置,`vm_name`为虚拟机名。
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边列表中选中要打快照的虚拟机,然后在右边下方点击`生成快照`,弹出窗口中选择文件存放位置,输入快照名称,点击`确定`,即可完成快照,其会存放在选择的路径下。
6. 将虚拟机导出为主流编程框架开发镜像
-```Shell
-eulerlauncher export-development-image --image_name image --export_path path vm_name
-```
-通过`--image_name`指定导出镜像名称,`--export_path`指定导出镜像存放位置,`vm_name`为虚拟机名,默认导出为Python/Go/Java主流编程框架开发镜像。
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher export-development-image --image_name image --export_path path vm_name
+> ```
+> 通过`--image_name`指定导出镜像名称,`--export_path`指定导出镜像存放位置,`vm_name`为虚拟机名,默认导出为Python/Go/Java主流编程框架开发镜像。
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边列表中选中要导出的虚拟机,然后在右边下方点击`导出为开发镜像`,弹出窗口中选择文件存放位置,输入镜像名称,点击`确定`,即可完成导出,其会存放在选择的路径下。
[1]: https://developer.apple.com/documentation/vmnet
[2]: https://gitee.com/openeuler/eulerlauncher/releases
\ No newline at end of file
diff --git a/docs/win-user-manual.md b/docs/win-user-manual.md
index 7fafc279029b98e34b9770d1671694af5e8d4a57..0385da3d2ea040aced99ed3dd04e55f7c054ef96 100644
--- a/docs/win-user-manual.md
+++ b/docs/win-user-manual.md
@@ -12,6 +12,7 @@
- eulerlauncherd.exe:EulerLauncher的主进程,是运行在后台的守护进程,负责与各类虚拟化后端交互,管理虚拟机、容器以及镜像的生命周期,eulerlauncherd.exe是运行在后台的守护进程。
- eulerlauncher.exe:EulerLauncher的CLI客户端,用户通过该客户端与eulerlauncherd守护进程交互,对虚拟机、镜像等进行相关操作。
+- eulerlauncherGUI.exe:EulerLauncher的GUI客户端,用户也可以通过该图形化界面客户端与eulerlauncherd守护进程交互,对虚拟机、镜像等进行相关操作。
- eulerlauncher.conf:EulerLauncher配置文件,需与eulerlauncherd.exe放置于同一目录下,参考下面配置进行相应配置:
```Conf
@@ -31,173 +32,243 @@ memory = 8G
配置完成后请右键点击eulerlauncherd.exe,选择以管理员身份运行,点击后eulerlauncherd.exe将以守护进程的形式在后台运行。
-打开 `PowerShell` 或 `Terminal` ,准备进行对应的操作。
+打开 `PowerShell` 或 `Terminal` (对应后文的***CLI客户端***),或者打开`eulerlauncherGUI.exe`(对应后文的***GUI客户端***),准备进行对应的操作。
### Windows下退出EulerLauncherd后台进程
当eulerlauncherd.exe运行后,会在操作系统右下角托盘区域生成eulerlauncherd托盘图标:
-
+
鼠标右键点击托盘图标,并选择 `Exit EulerLauncher` 即可退出EulerLauncherd后台进程。
### 镜像操作
1. 获取可用镜像列表:
-```PowerShell
-eulerlauncher images
-
-+-----------+----------+--------------+
-| Images | Location | Status |
-+-----------+----------+--------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 2203-load | Local | Ready |
-+-----------+----------+--------------+
-```
+> ***CLI客户端***
+> ```PowerShell
+> eulerlauncher images
+> +-----------+----------+--------------+
+> | Images | Location | Status |
+> +-----------+----------+--------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 2203-load | Local | Ready |
+> +-----------+----------+--------------+
+> ```
+
+> ***GUI客户端***
+>
+> 左边栏选择`镜像管理`,右边下方点击`刷新`,即可获取可用镜像列表,内容包括镜像名称、位置、状态等信息。
+
**EulerLauncher**镜像有两种位置属性:1)远端镜像 2)本地镜像,只有处于本地且状态为 `Ready` 的镜像可以直接用来创建虚拟机,位于远端的镜像需要下载后才能够使用;你也可以加载已经预先下载好的本地镜像到**EulerLauncher**中,具体操作方法可以参考接下来的操作指导。
2. 下载远端镜像
-```PowerShell
-eulerlauncher download-image 22.03-LTS
-
-Downloading: 22.03-LTS, this might take a while, please check image status with "images" command.
-```
-
-镜像下载请求是一个异步请求,具体的下载动作将在后台完成,具体耗时与你的网络情况相关,整体的镜像下载流程包括下载、解压缩、格式转换等相关子流程,在下载过程中可以通过 `image` 命令随时查看下载进展与镜像状态,进度条格式为`[downloaded_bytes]/[total_bytes] (percentage)`:
-
-```PowerShell
-eulerlauncher images
-
-+-----------+----------+------------------------------------+
-| Images | Location | Status |
-+-----------+----------+------------------------------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 22.03-LTS | Local | Downloading: 97.76/ 386.74MB (25%) |
-+-----------+----------+------------------------------------+
-```
-
+> ***CLI客户端***
+> ```PowerShell
+> eulerlauncher download-image 22.03-LTS
+>
+> Downloading: 22.03-LTS, this might take a while, please check image status with "images" command.
+> ```
+
+> ***GUI客户端***
+>
+> 左边栏选择`镜像管理`,右边列表中选中要下载的镜像,然后在右边下方点击`下载`,即可开始下载镜像,下载过程中点击`刷新`按钮可以查看当前状态。
+
+镜像下载请求是一个异步请求,具体的下载动作将在后台完成,具体耗时与你的网络情况相关,整体的镜像下载流程包括下载、解压缩、格式转换等相关子流程,在下载过程中可以通过 `image` 命令(CLI客户端)或者点击`刷新`(GUI客户端)随时查看下载进展与镜像状态,进度条格式为`[downloaded_bytes]/[total_bytes] (percentage)`:
+> ***CLI客户端***
+> ```PowerShell
+> eulerlauncher images
+>
+> +-----------+----------+------------------------------------+
+> | Images | Location | Status |
+> +-----------+----------+------------------------------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 22.03-LTS | Local | Downloading: 97.76/ 386.74MB (25%) |
+> +-----------+----------+------------------------------------+
+> ```
+
+> ***GUI客户端***
+>
+> 点击`刷新`按钮查看当前状态,列表中也会显示上面的三列。
当镜像状态转变为 `Ready` 时,表示镜像下载完成,处于 `Ready` 状态的镜像可被用来创建虚拟机:
-```PowerShell
-eulerlauncher images
+> ***CLI客户端***
+> ```PowerShell
+> eulerlauncher images
+>
+> +-----------+----------+--------------+
+> | Images | Location | Status |
+> +-----------+----------+--------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 22.03-LTS | Local | Ready |
+> +-----------+----------+--------------+
+> ```
+
+> ***GUI客户端***
+>
+> 点击`刷新`按钮查看当前状态,列表中也会显示上面的三列,最后一个值为`Ready`时说明下载完成。
-+-----------+----------+--------------+
-| Images | Location | Status |
-+-----------+----------+--------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 22.03-LTS | Local | Ready |
-+-----------+----------+--------------+
-```
3. 加载本地镜像
用户也可以加载自定义镜像或预先下载到本地的镜像到EulerLauncher中用于创建自定义虚拟机:
-```PowerShell
-eulerlauncher load-image --path {image_file_path} IMAGE_NAME
-```
+> ***CLI客户端***
+> ```PowerShell
+> eulerlauncher load-image --path {image_file_path} IMAGE_NAME
+> ```
+
+> ***GUI客户端***
+>
+> 左边栏选择`镜像管理`,右边下方点击`加载本地镜像`,会弹出文件选择窗口,按照弹出窗口的引导选择本地镜像文件,输入镜像名称,即可完成加载。
当前支持加载的镜像格式有 `xxx.{qcow2, raw, vmdk, vhd, vhdx, qcow, vdi}.[xz]`
例如:
-```PowerShell
-eulerlauncher load-image --path D:\openEuler-22.03-LTS-x86_64.qcow2.xz 2203-load
-
-Loading: 2203-load, this might take a while, please check image status with "images" command.
-```
-
-将位于 `D:\` 目录下的 `openEuler-22.03-LTS-x86_64.qcow2.xz` 加载到OmniVirt系统中,并命名为 `2203-load`,与下载命令一样,加载命令也是一个异步命令,用户需要用镜像列表命令查询镜像状态直到显示为 `Ready`, 但相对于直接下载镜像,加载镜像的速度会快很多:
-
-```PowerShell
-eulerlauncher images
-
-+-----------+----------+----------------------------+
-| Images | Location | Status |
-+-----------+----------+----------------------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 2203-load | Local | Loading: (24.00/100%) |
-+-----------+----------+----------------------------+
-
-eulerlauncher images
-
-+-----------+----------+--------------+
-| Images | Location | Status |
-+-----------+----------+--------------+
-| 22.03-LTS | Remote | Downloadable |
-| 21.09 | Remote | Downloadable |
-| 2203-load | Local | Ready |
-+-----------+----------+--------------+
-```
+> ***CLI客户端***
+> ```PowerShell
+> eulerlauncher load-image --path D:\openEuler-22.03-LTS-x86_64.qcow2.xz 2203-load
+>
+> Loading: 2203-load, this might take a while, please check image status with "images" command.
+> ```
+>
+> 将位于 `D:\` 目录下的 `openEuler-22.03-LTS-x86_64.qcow2.xz` 加载到OmniVirt系统中,并命名为 `2203-load`,与下载命令一样,加载命令也是一个异步命令,用户需要用镜像列表命令查询镜像状态直到显示为 `Ready`, 但相对于直接下载镜像,加载镜像的速度会快很多:
+>
+> ```PowerShell
+> eulerlauncher images
+>
+> +-----------+----------+----------------------------+
+> | Images | Location | Status |
+> +-----------+----------+----------------------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 2203-load | Local | Loading: (24.00/100%) |
+> +-----------+----------+----------------------------+
+>
+> eulerlauncher images
+>
+> +-----------+----------+--------------+
+> | Images | Location | Status |
+> +-----------+----------+--------------+
+> | 22.03-LTS | Remote | Downloadable |
+> | 21.09 | Remote | Downloadable |
+> | 2203-load | Local | Ready |
+> +-----------+----------+--------------+
+> ```
+
+> ***GUI客户端***
+>
+> 同样以上面的例子为例,点击`加载本地镜像`,在文件选择窗口内选择`D:\openEuler-22.03-LTS-x86_64.qcow2.xz`,镜像名称输入`2203-load`,点击`确定`,即可完成加载。获取加载情况请点击`刷新`按钮查看。
4. 删除镜像:
-通过下面的命令将镜像从EulerLauncher系统中删除:
-
-```PowerShell
-eulerlauncher delete-image 2203-load
+> ***CLI客户端***
+>
+> 通过下面的命令将镜像从EulerLauncher系统中删除:
+>
+> ```PowerShell
+> eulerlauncher delete-image 2203-load
+>
+> Image: 2203-load has been successfully deleted.
+> ```
-Image: 2203-load has been successfully deleted.
-```
+> ***GUI客户端***
+>
+> 左边栏选择`镜像管理`,右边列表中选中要删除的镜像,然后在右边下方点击`删除镜像`,即可删除镜像。
### 虚拟机操作
1. 获取虚拟机列表:
-```Powershell
-eulerlauncher list
-
-+----------+-----------+---------+---------------+
-| Name | Image | State | IP |
-+----------+-----------+---------+---------------+
-| test1 | 2203-load | Running | 172.22.57.220 |
-+----------+-----------+---------+---------------+
-| test2 | 2203-load | Running | N/A |
-+----------+-----------+---------+---------------+
-```
+> ***CLI客户端***
+> ```Powershell
+> eulerlauncher list
+>
+> +----------+-----------+---------+---------------+
+> | Name | Image | State | IP |
+> +----------+-----------+---------+---------------+
+> | test1 | 2203-load | Running | 172.22.57.220 |
+> +----------+-----------+---------+---------------+
+> | test2 | 2203-load | Running | N/A |
+> +----------+-----------+---------+---------------+
+> ```
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边上方点击`刷新`,即可获取虚拟机列表,内容包括虚拟机名称、镜像、状态、IP等信息。
若虚拟机IP地址显示为 `N/A` ,若这台虚拟机的状态为 `Running` 则表示这台虚拟机为新创建的虚拟机,网络还未配置完成,网络配置过程大概需要若干秒,请稍后重新尝试获取相关虚拟机信息。
2. 登录虚拟机:
-若虚拟机已成功分配到IP地址,可以直接使用 `SSH` 命令进行登录:
+> ***CLI客户端***
+>
+>若虚拟机已成功分配到IP地址,可以直接使用 `SSH` 命令进行登录:
+>
+>
+> ```PowerShell
+> ssh root@{instance_ip}
+> ```
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边列表中选中要登录的虚拟机,然后在右边下方点击`连接`,界面会自动跳转到`虚拟机连接`。`虚拟机连接`界面上方左侧会显示虚拟机的各种信息,上方右侧有三个按钮,提供两种SSH连接方式。
+> - 选择`调用终端进行SSH连接`,则会调用系统终端进行SSH连接,会自动弹出终端窗口,在里面操作即可,关闭终端窗口即可断开连接(最好先输入exit指令)。
+> - 选择`在GUI中进行SSH连接`,如果使用导入的镜像则需要在弹出窗口中输入密码,连接成功则会在界面下方的文本框内显示信息,在输入栏中输入命令后按输入条右侧的`Send`或者按键盘上的`Enter`键即可发送命令。要断开连接,可以点击上方右侧的`断开连接`按钮,或者在输入栏中输入`exit`命令,还可以在左边栏直接切换到其他页面(不推荐),这三种方式均可断开连接。
-```PowerShell
-ssh root@{instance_ip}
-```
若使用的是openEuler社区提供的官方镜像,则默认用户为 `root` 默认密码为 `openEuler12#$`
3. 创建虚拟机
-```PowerShell
-eulerlauncher launch --image {image_name} {instance_name}
-```
+> ***CLI客户端***
+> ```PowerShell
+> eulerlauncher launch --image {image_name} {instance_name}
+> ```
+>
+> 通过 `--image` 指定镜像,同时指定虚拟机名称,EulerLauncher会根据所指定的镜像默认创建一个规格为`2U4G`的openEuler虚拟机。
-通过 `--image` 指定镜像,同时指定虚拟机名称,EulerLauncher会根据所指定的镜像默认创建一个规格为`2U4G`的openEuler虚拟机。
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边下方点击`创建虚拟机`,弹出窗口中选择镜像,输入虚拟机名称,点击`确定`,即可创建虚拟机,创建过程中请耐心等待`请等待`弹窗消失。
4. 删除虚拟机
-```PowerShell
-eulerlauncher delete-instance {instance_name}
-```
-根据虚拟机名称删除指定的虚拟机。
+> ***CLI客户端***
+> ```PowerShell
+> eulerlauncher delete-instance {instance_name}
+> ```
+> 根据虚拟机名称删除指定的虚拟机。
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边列表中选中要删除的虚拟机,然后在右边下方点击`删除虚拟机`,即可删除虚拟机。
5. 为虚拟机打快照,并导出为镜像
-```Shell
-eulerlauncher take-snapshot --snapshot_name snap --export_path path vm_name
-```
-通过`--snapshot_name`指定快照名称,`--export_path`指定导出镜像存放位置,`vm_name`为虚拟机名。
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher take-snapshot --snapshot_name snap --export_path path vm_name
+> ```
+> 通过`--snapshot_name`指定快照名称,`--export_path`指定导出镜像存放位置,`vm_name`为虚拟机名。
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边列表中选中要打快照的虚拟机,然后在右边下方点击`生成快照`,弹出窗口中选择文件存放位置,输入快照名称,点击`确定`,即可完成快照,其会存放在选择的路径下。
6. 将虚拟机导出为主流编程框架开发镜像
-```Shell
-eulerlauncher export-development-image --image_name image --export_path path vm_name
-```
-通过`--image_name`指定导出镜像名称,`--export_path`指定导出镜像存放位置,`vm_name`为虚拟机名,默认导出为Python/Go/Java主流编程框架开发镜像。
+> ***CLI客户端***
+> ```Shell
+> eulerlauncher export-development-image --image_name image --export_path path vm_name
+> ```
+> 通过`--image_name`指定导出镜像名称,`--export_path`指定导出镜像存放位置,`vm_name`为虚拟机名,默认导出为Python/Go/Java主流编程框架开发镜像。
+
+> ***GUI客户端***
+>
+> 左边栏选择`虚拟机管理`,右边列表中选中要导出的虚拟机,然后在右边下方点击`导出为开发镜像`,弹出窗口中选择文件存放位置,输入镜像名称,点击`确定`,即可完成导出,其会存放在选择的路径下。
[1]: https://gitee.com/openeuler/omnivirt/releases
[2]: https://learn.microsoft.com/zh-cn/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v
\ No newline at end of file
diff --git a/eulerlauncher/eulerlauncherGUI.py b/eulerlauncher/eulerlauncherGUI.py
new file mode 100644
index 0000000000000000000000000000000000000000..00efe2cf265bde1e31a67c1775900e65ef51bf22
--- /dev/null
+++ b/eulerlauncher/eulerlauncherGUI.py
@@ -0,0 +1,1050 @@
+"""
+Author: Xiang Chenyu
+Description: EulerLauncher前端界面
+Tips:
+- take-snapshot与export-development-image功能由于主程序尚未实现,该前端界面在这两部分留空,实现后请补齐
+ 具体需要修改的位置为button_vmsnapshot_clicked和button_vmout_clicked两个函数
+ Line 620 未实现的提示,主程序实现该功能后请删除此行代码
+ Line 637 实现后请在实际情况下修改该判断条件,就是按照正确执行后命令行的返回值特点来进行判断
+ Line 672 未实现的提示,主程序实现该功能后请删除此行代码
+ Line 690 实现后请在实际情况下修改该判断条件,就是按照正确执行后命令行的返回值特点来进行判断
+- 由于主程序暂未对Linux提供支持,因此Linux系统下的终端SSH连接功能还未进行验证,修改请在Line 739处进行
+- 增删改功能请在主界面init_ui或四个界面生成函数(以widget_of开头)中增删改按钮等控件,然后增删改对应的槽函数即可。
+- 本代码中有许多通用方法,如do_cmd、table_process、create_massage_box、create_line等,可以复用
+ 它们在Line 374、415、961、970
+- 请注意,本代码仅为前端界面,后端功能由EulerLauncherd提供,因此请确保整个EulerLauncher已经正确安装并配置好conf文件
+"""
+
+import sys
+from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QStackedWidget, QFrame, \
+ QPushButton, QLabel, QListWidget, QFileDialog, QInputDialog, QMessageBox, QTextEdit, \
+ QLineEdit, QCheckBox, QDialog
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QIcon
+from paramiko import SSHClient, AutoAddPolicy
+import subprocess
+import time
+import re
+
+
+class EulerLauncherGUI(QMainWindow):
+ def __init__(self):
+ super().__init__()
+
+ # 创建一个堆叠布局
+ self.stack = QStackedWidget()
+
+ # 创建边栏
+ self.sidebar = QFrame()
+ self.sidebar.setObjectName("sidebar")
+ self.sidebar_layout = QVBoxLayout(self.sidebar)
+
+ # 创建内容区域
+ self.content = QFrame()
+ self.content.setObjectName("content")
+ self.content_layout = QVBoxLayout(self.content)
+
+ # 设置刷新提示与刷新时间记录文本
+ self.refresh_time = QLabel('请点击刷新按钮获取镜像列表')
+ self.refresh_vm_time = QLabel('请点击刷新按钮获取虚拟机列表')
+
+ # 边栏选中的按钮
+ self.current_button = None
+
+ # 虚拟机连接界面的信息
+ self.vm_tip = QLabel('请点击虚拟机管理界面选择虚拟机进行连接', self)
+ self.vm_tip.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ self.vm_name = QLabel('', self)
+ self.vm_name.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ self.vm_image = QLabel('', self)
+ self.vm_image.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ self.vm_state = QLabel('', self)
+ self.vm_state.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ self.vm_ip = QLabel('', self)
+ self.vm_ip.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+
+ # ssh连接
+ self.ssh = None
+ self.shell = None
+
+ # 创建主窗口
+ self.init_ui()
+
+ def init_ui(self):
+ # 创建主窗口
+ main_widget = QWidget()
+
+ # 创建布局
+ main_layout = QHBoxLayout(self)
+
+ # 将堆叠布局添加到内容区域
+ self.content_layout.addWidget(self.stack)
+
+ # 将各个页面添加到堆叠布局
+ self.stack.addWidget(self.widget_of_iso())
+ self.stack.addWidget(self.widget_of_management())
+ self.stack.addWidget(self.widget_of_connecting())
+ self.stack.addWidget(self.widget_of_setting())
+
+ # 切换显示用的按钮, 它们的name属性为menu
+ self.button1 = QPushButton('镜像管理', self)
+ self.button1.setProperty('name', 'menu')
+ self.button1.clicked.connect(self.switch_page)
+ self.sidebar_layout.addWidget(self.button1)
+
+ self.button2 = QPushButton('虚拟机管理', self)
+ self.button2.setProperty('name', 'menu')
+ self.button2.clicked.connect(self.switch_page)
+ self.sidebar_layout.addWidget(self.button2)
+
+ self.button3 = QPushButton('虚拟机连接', self)
+ self.button3.setProperty('name', 'menu')
+ self.button3.clicked.connect(self.switch_page)
+ self.sidebar_layout.addWidget(self.button3)
+
+ self.button4 = QPushButton('设置', self)
+ self.button4.setProperty('name', 'menu')
+ self.button4.clicked.connect(self.switch_page)
+
+ # 设置按钮的位置
+ self.sidebar_layout.addStretch(1)
+ self.sidebar_layout.addWidget(self.create_line(True))
+ self.sidebar_layout.addWidget(self.button4)
+
+ # 设置按钮的可选择状态
+ self.button1.setCheckable(True)
+ self.button2.setCheckable(True)
+ self.button3.setCheckable(True)
+ self.button4.setCheckable(True)
+
+ # 设置按钮的默认状态
+ self.current_button = self.button1
+ self.button1.setChecked(True)
+
+ # 将按钮布局和堆叠布局添加到主布局中
+ main_layout.addWidget(self.sidebar)
+ main_layout.addWidget(self.stack)
+
+ # 设置主窗口的布局
+ main_widget.setLayout(main_layout)
+
+ # 设置主窗口的中央部件
+ self.setCentralWidget(main_widget)
+
+ # 设置窗口的标题
+ self.setWindowTitle('EulerLauncherGUI')
+
+ # 设置窗口的图标
+ self.setWindowIcon(QIcon('etc/favicon.ico'))
+
+ # 加载样式表
+ self.load_qss_style()
+
+ # 设置窗口的大小
+ self.resize(800, 450)
+
+ def widget_of_iso(self): # 创建镜像管理页面
+ wid = QWidget()
+
+ # 定义布局
+ main_layout = QVBoxLayout(wid)
+ button_layout = QHBoxLayout(wid)
+
+ # 镜像列表的标题
+ label1 = QLabel(f'镜像名称 | 位置 | 状态', wid)
+ label1.setAlignment(Qt.AlignLeft)
+ main_layout.addWidget(label1)
+
+ # 镜像列表 使用QListWidget
+ self.list_of_iso = QListWidget(wid)
+
+ # 四个按钮
+ button_fresh = QPushButton('刷新', wid)
+ button_load = QPushButton('加载本地镜像', wid)
+ button_del = QPushButton('删除镜像', wid)
+ button_download = QPushButton('下载镜像', wid)
+
+ # 各个button的位置布局 放入HBox button_layout 中
+ button_layout.addStretch(1)
+ button_layout.addWidget(button_fresh)
+ button_layout.addStretch(1)
+ button_layout.addWidget(button_load)
+ button_layout.addStretch(1)
+ button_layout.addWidget(button_del)
+ button_layout.addStretch(1)
+ button_layout.addWidget(button_download)
+ button_layout.addStretch(1)
+
+ # 设置按钮的信号与槽
+ button_fresh.clicked.connect(self.button_fresh_clicked)
+ button_load.clicked.connect(self.button_load_clicked)
+ button_del.clicked.connect(self.button_del_clicked)
+ button_download.clicked.connect(self.button_download_clicked)
+
+ # 将文本、镜像列表和按钮布局放入VBox main_layout 中
+ main_layout.addWidget(self.list_of_iso)
+ main_layout.addWidget(self.refresh_time)
+ main_layout.addLayout(button_layout)
+
+ # 设置窗口的布局
+ wid.setLayout(main_layout)
+
+ return wid
+
+ def widget_of_management(self): # 创建虚拟机管理页面
+ wid = QWidget()
+
+ # 定义布局
+ main_layout = QVBoxLayout(wid)
+ button_layout = QHBoxLayout(wid)
+ head_layout = QHBoxLayout(wid)
+
+ # 虚拟机列表的标题
+ label1 = QLabel(f'虚拟机名称 | 使用的镜像 | 状态 | IP', wid)
+ label1.setAlignment(Qt.AlignLeft | Qt.AlignBottom)
+ head_layout.addWidget(label1)
+
+ # 镜像列表 使用QListWidget
+ self.list_of_vm = QListWidget(wid)
+
+ # 六个按钮
+ button_vmfresh = QPushButton('刷新', wid)
+ button_vmcreate = QPushButton('创建虚拟机', wid)
+ button_vmsnapshot = QPushButton('生成快照', wid)
+ button_vmdel = QPushButton('删除虚拟机', wid)
+ button_vmout = QPushButton('导出为开发镜像', wid)
+ button_vmlink = QPushButton('连接', wid)
+
+ # 刷新按钮与Label一行
+ head_layout.addWidget(button_vmfresh)
+ main_layout.addLayout(head_layout)
+
+ # 其他各个button的位置布局 放入HBox button_layout 中
+ button_layout.addStretch(1)
+ button_layout.addWidget(button_vmcreate)
+ button_layout.addStretch(1)
+ button_layout.addWidget(button_vmdel)
+ button_layout.addStretch(1)
+ button_layout.addWidget(button_vmsnapshot)
+ button_layout.addStretch(1)
+ button_layout.addWidget(button_vmout)
+ button_layout.addStretch(1)
+ button_layout.addWidget(button_vmlink)
+ button_layout.addStretch(1)
+
+ # 设置按钮的信号与槽
+ button_vmfresh.clicked.connect(self.button_vmfresh_clicked)
+ button_vmcreate.clicked.connect(self.button_vmcreate_clicked)
+ button_vmsnapshot.clicked.connect(self.button_vmsnapshot_clicked)
+ button_vmdel.clicked.connect(self.button_vmdel_clicked)
+ button_vmout.clicked.connect(self.button_vmout_clicked)
+ button_vmlink.clicked.connect(self.button_vmlink_clicked)
+
+ # 将文本、镜像列表和按钮布局放入VBox main_layout 中
+ main_layout.addWidget(self.list_of_vm)
+ main_layout.addWidget(self.refresh_vm_time)
+ main_layout.addLayout(button_layout)
+
+ # 设置窗口的布局
+ wid.setLayout(main_layout)
+
+ return wid
+
+ def widget_of_connecting(self): # 创建虚拟机连接页面
+ wid = QWidget()
+
+ # 定义按钮
+ btn = QPushButton('调用终端进行SSH连接', wid)
+ btn.clicked.connect(self.button_connection_clicked)
+ btn2 = QPushButton('在GUI中进行SSH连接', wid)
+ btn2.clicked.connect(self.connect_ssh)
+ btn3 = QPushButton('断开连接', wid)
+ btn3.clicked.connect(self.close_ssh)
+ self.ck = QCheckBox('保留上一句命令', wid)
+
+ # 定义内容输出框
+ self.output_text = QTextEdit(wid)
+ self.output_text.setReadOnly(True)
+
+ # 定义输入框与发送按钮
+ self.command_input = QLineEdit(wid)
+ self.command_input.returnPressed.connect(self.execute_command)
+ self.command_input.setPlaceholderText("Command Press Enter to send")
+ send_button = QPushButton('Send', wid)
+ send_button.clicked.connect(self.execute_command)
+
+ # 定义布局
+ main_layout = QVBoxLayout(wid)
+ upper_layout = QHBoxLayout(wid)
+ bottom_layout = QHBoxLayout(wid)
+
+ button_layout = QVBoxLayout(wid)
+ button_layout.addWidget(btn)
+ button_layout.addWidget(btn2)
+ button_layout.addWidget(btn3)
+
+ layout = QVBoxLayout(wid)
+ layout.addWidget(self.vm_tip)
+ layout.addWidget(self.vm_name)
+ layout.addWidget(self.vm_image)
+ layout.addWidget(self.vm_state)
+ layout.addWidget(self.vm_ip)
+
+ upper = QWidget()
+ upper_layout.addLayout(layout)
+ upper_layout.addLayout(button_layout)
+ upper.setLayout(upper_layout)
+ upper.setMaximumHeight(200)
+
+ bottom_layout.addWidget(self.command_input)
+ bottom_layout.addWidget(send_button)
+
+ main_layout.addWidget(upper)
+ main_layout.addWidget(self.output_text)
+ main_layout.addLayout(bottom_layout)
+ main_layout.addWidget(self.ck)
+
+ wid.setLayout(main_layout)
+
+ return wid
+
+ def widget_of_setting(self): # 创建设置页面
+ wid = QWidget()
+ main_layout = QVBoxLayout(wid)
+
+ # conf文件选择区域
+ conf_layout = QHBoxLayout(wid)
+
+ conf_button = QPushButton('选择conf文件', wid)
+ conf_button.clicked.connect(self.fetch_conf_path)
+
+ self.conf_label = QLabel('conf文件未选中', wid)
+
+ conf_layout.addWidget(self.conf_label)
+ conf_layout.addWidget(conf_button)
+
+ # conf文件显示内容修改区域
+ bottom_layout = QHBoxLayout(wid)
+ btn_layout = QVBoxLayout(wid)
+
+ self.conf_text = QTextEdit(wid)
+ self.conf_text.setReadOnly(True)
+
+ edit_button = QPushButton('修改', wid)
+ edit_button.clicked.connect(self.write_conf)
+ re_button = QPushButton('恢复默认', wid)
+ re_button.clicked.connect(self.initialize_conf)
+
+ btn_layout.addStretch(1)
+ btn_layout.addWidget(edit_button)
+ btn_layout.addWidget(re_button)
+ btn_layout.addStretch(1)
+
+ bottom_layout.addWidget(self.conf_text)
+ bottom_layout.addLayout(btn_layout)
+
+ # 设置布局
+ main_layout.addLayout(conf_layout)
+ main_layout.addWidget(self.create_line())
+ main_layout.addLayout(bottom_layout)
+
+ wid.setLayout(main_layout)
+
+ return wid
+
+ def switch_page(self): # 切换页面
+ button = self.sender()
+ if self.current_button: # 取消之前选中的按钮
+ self.current_button.setChecked(False)
+ button.setChecked(True) # 设置当前选中的按钮
+ self.current_button = button
+ index = 0
+ if button.text() == '镜像管理':
+ index = 0
+ elif button.text() == '虚拟机管理':
+ index = 1
+ elif button.text() == '虚拟机连接':
+ index = 2
+ elif button.text() == '设置':
+ index = 3
+ self.stack.setCurrentIndex(index) # 切换页面
+ if self.ssh:
+ self.close_ssh() # 关闭ssh连接, 防止在连接界面切换到其他界面时出现问题
+
+ def do_cmd(self, cmd):
+ # 执行命令行语句, 并将输出以列表形式返回, check为True表示正常执行, False表示出现错误
+ outputs = []
+ check = True
+ try: # 捕获异常
+ if sys.platform == 'win32':
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
+ creationflags=subprocess.CREATE_NO_WINDOW)
+ else:
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+
+ while True:
+ output = p.stdout.readline()
+ if output == '' and p.poll() is not None:
+ break
+ elif output:
+ outputs.append(output.strip())
+
+ # 等待进程结束
+ p.wait()
+
+ # 错误输出
+ stderr = p.stderr.read()
+ if stderr:
+ self.create_message_box('错误', stderr)
+ check = False
+
+ # 异常处理
+ except FileNotFoundError:
+ self.create_message_box('错误', '命令行未知命令: eulerlauncher\n请检查是否安装了EulerLauncher\n'
+ '\n需要以管理员身份运行'
+ '\nWindows:是否将EulerLauncher添加到环境变量PATH中'
+ '\nLinux & MacOS:EulerLauncher是否按说明将conf文件配置正确')
+ check = False
+ except Exception as e:
+ self.create_message_box('错误', str(e))
+ check = False
+ finally:
+ return outputs, check
+
+ @staticmethod
+ def table_process(column_num, table_list, target_q_list_widget): # 该函数处理返回的表格
+ for item in table_list:
+ if item[0] == '+': # 去除表格线
+ continue
+ else:
+ pattern1 = r'.+Images.+Location.+Status.*'
+ pattern2 = r'.+Name.+Image.+State.+IP.*'
+ if re.match(pattern1, item) or re.match(pattern2, item): # 去除表头
+ continue
+ else: # 处理表格内容
+ item = item.split()
+ flag = 0
+ item_process = ''
+ for i in item:
+ if not i == '|':
+ flag += 1
+ item_process += i
+ if flag < column_num:
+ item_process += ' | '
+ target_q_list_widget.addItem(item_process)
+
+ def button_fresh_clicked(self): # 镜像刷新按钮的槽函数
+ self.list_of_iso.clear()
+
+ # 构建命令行语句
+ cmd = ['eulerlauncher', 'images']
+
+ # 执行命令行语句, 并将输出添加到列表中
+ iso_list, check = self.do_cmd(cmd)
+ if check:
+ if not iso_list[0][0] == '+': # 返回的不是一个表格, 说明出现了错误
+ if iso_list[0] == 'Calling to EulerLauncherd daemon failed, please check ' \
+ 'EulerLauncherd daemon status ...':
+ self.create_message_box('错误', '{}\n请检查是否运行了EulerLauncherd后台程序'.format(iso_list[0]))
+ else:
+ self.create_message_box('错误', iso_list[0])
+ else:
+ # 处理表格
+ self.table_process(3, iso_list, self.list_of_iso)
+ self.list_of_iso.update()
+
+ # 刷新时间
+ self.refresh_time.setText('刷新时间: ' + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))
+
+ def button_load_clicked(self): # 加载本地镜像按钮的槽函数
+ # 打开文件选择对话框
+ filepath, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "镜像文件 (*.qcow2 *.raw *.vmdk *.vhd *.vhdx "
+ "*.qcow *.vdi *.qcow2.xz *.raw.xz *.vmdk.xz *.vhd.xz *.vhdx.xz "
+ "*.qcow.xz *.vdi.xz)")
+ if filepath:
+ # 选中镜像后给镜像命名
+ image_name, ok = QInputDialog.getText(self, '镜像命名', '请输入镜像名称:')
+ if ok:
+ # 构建命令行语句
+ cmd = ['eulerlauncher', 'load-image', '--path', filepath, image_name]
+
+ # 执行命令行语句, 并将输出添加到列表中
+ outputs, check = self.do_cmd(cmd)
+ if check:
+ self.refresh_time.setText(
+ 'Loading: {}, 可能需要一段时间, 请通过刷新按键查看加载状态'.format(image_name))
+ self.create_message_box('信息', 'Loading: {}, 可能需要一段时间\n'
+ '请通过刷新按键查看加载状态, Ready时加载完毕'.format(image_name))
+ self.button_fresh_clicked()
+
+ def button_del_clicked(self): # 删除镜像按钮的槽函数
+ item = self.list_of_iso.currentItem()
+ if item:
+ # 获取选中的镜像名称已经其状态
+ iso_info = item.text().split(' | ')
+ iso_name = iso_info[0]
+
+ # 构建命令行语句
+ cmd = ['eulerlauncher', 'delete-image', iso_name]
+
+ # 执行命令行语句, 并将输出添加到列表中
+ outputs, check = self.do_cmd(cmd)
+ pattern = r'.+has been successfully deleted'
+ if check and re.match(pattern, outputs[0]):
+ self.refresh_time.setText('镜像{}被成功删除'.format(iso_name))
+ self.create_message_box('信息', '镜像{}被成功删除'.format(iso_name))
+ self.button_fresh_clicked()
+ elif check:
+ self.create_message_box('错误', outputs[0])
+ else:
+ self.create_message_box('错误', '删除失败')
+
+ else:
+ self.create_message_box('错误', '未选中镜像')
+
+ def button_download_clicked(self): # 下载镜像按钮的槽函数
+ item = self.list_of_iso.currentItem()
+ if item:
+ # 获取选中的镜像名称已经其状态
+ iso_info = item.text().split(' | ')
+ iso_name = iso_info[0]
+ iso_status = iso_info[2]
+
+ if iso_status == 'Downloadable':
+ # 构建命令行语句
+ cmd = ['eulerlauncher', 'download-image', iso_name]
+
+ # 执行命令行语句, 并将输出添加到列表中
+ outputs, check = self.do_cmd(cmd)
+ if check:
+ self.refresh_time.setText(
+ 'Downloading: {}, 可能需要一段时间, 请通过刷新按键查看下载状态'.format(iso_name))
+ self.create_message_box('信息', 'Downloading: {}, 可能需要一段时间\n'
+ '请通过刷新按键查看下载状态, Ready时为下载完毕'.format(iso_name))
+ self.button_fresh_clicked()
+ else:
+ self.create_message_box('错误', '未选中镜像')
+
+ def button_vmfresh_clicked(self): # 虚拟机刷新按钮的槽函数
+ self.list_of_vm.clear()
+
+ # 构建命令行语句
+ cmd = ['eulerlauncher', 'list']
+
+ # 执行命令行语句, 并将输出添加到列表中
+ vm_list, check = self.do_cmd(cmd)
+ if check:
+ if not vm_list[0][0] == '+':
+ if vm_list[0] == 'Calling to EulerLauncherd daemon failed, please check ' \
+ 'EulerLauncherd daemon status ...':
+ self.create_message_box('错误', '{}\n请检查是否运行了EulerLauncherd后台程序\n'.format(vm_list[0]))
+ else:
+ self.create_message_box('错误', vm_list[0])
+ else:
+ # 处理表格
+ self.table_process(4, vm_list, self.list_of_vm)
+ self.list_of_vm.update()
+
+ # 如果要连接的虚拟机不存在了, 则将连接界面的信息清空
+ if self.vm_ip.text() != '':
+ vm_ip = self.vm_ip.text()
+ for i in range(self.list_of_vm.count()):
+ now = self.list_of_vm.item(i).text()
+ ip = now.split(' | ')[3]
+ if ip == vm_ip:
+ break
+ else:
+ self.vm_tip.setText('请点击虚拟机管理界面选择虚拟机进行连接')
+ self.vm_name.setText('')
+ self.vm_image.setText('')
+ self.vm_state.setText('')
+ self.vm_ip.setText('')
+
+ # 刷新时间
+ self.refresh_vm_time.setText('刷新时间: ' + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))
+
+ def button_vmcreate_clicked(self): # 创建虚拟机按钮的槽函数
+ self.button_fresh_clicked()
+
+ # 获取可用镜像列表
+ list_of_iso_copy = []
+ for i in range(self.list_of_iso.count()):
+ now = self.list_of_iso.item(i).text()
+ name = now.split(' | ')[0]
+ status = now.split(' | ')[2]
+ if status == 'Ready':
+ list_of_iso_copy.append(name)
+
+ # 如果没有可用镜像, 则'无可用镜像'
+ if len(list_of_iso_copy) == 0:
+ list_of_iso_copy.append('无可用镜像')
+
+ # 弹出对话框, 选择镜像
+ text, ok = QInputDialog.getItem(self, '选择一个镜像', '镜像:', list_of_iso_copy, 0, False)
+ if ok and text:
+ if text == '无可用镜像':
+ self.create_message_box('错误', '无可用镜像\n请先加载镜像或下载镜像')
+ else:
+ # 弹出对话框, 输入虚拟机名称
+ vm_name, ok = QInputDialog.getText(self, '虚拟机命名', '请输入虚拟机名称:')
+
+ if ok:
+ # 等待提示
+ dialog = QDialog(self)
+ dialog.setWindowTitle('请等待...')
+ dialog.setWindowModality(Qt.WindowModal)
+ dialog.setWindowFlags(self.windowFlags() & ~Qt.WindowCloseButtonHint)
+ dialog_layout = QVBoxLayout(dialog)
+ dialog.setLayout(dialog_layout)
+ dialog_label = QLabel('创建虚拟机{}中, 需要一段时间响应, 请稍后'.format(vm_name))
+ dialog_layout.addWidget(dialog_label)
+ dialog.show()
+
+ # 构建命令行语句
+ cmd = ['eulerlauncher', 'launch', '--image', text, vm_name]
+
+ # 执行命令行语句, 并将输出添加到列表中
+ outputs, check = self.do_cmd(cmd)
+ if check:
+ if outputs[0][0] == '+': # 返回的是一个表格, 说明创建成功
+ self.create_message_box('信息', '创建虚拟机{}成功\n'
+ '需要一定时间进行网络配置,请等待\n'.format(vm_name))
+ else:
+ self.create_message_box('错误', outputs[0])
+ else:
+ self.create_message_box('错误', '创建失败')
+ dialog.close()
+ self.button_vmfresh_clicked()
+
+ def button_vmsnapshot_clicked(self): # 生成快照按钮的槽函数
+ self.create_message_box('提示', '该功能在主程序尚未实现,请自行取消后续操作') # 该功能在主程序尚未实现,实现后请删除此行代码
+
+ item = self.list_of_vm.currentItem()
+ if item:
+ snapshot_name, ok = QInputDialog.getText(self, '快照命名', '请输入快照名称:')
+ if ok:
+ folder_path = QFileDialog.getExistingDirectory(None, '选择保存路径', './', QFileDialog.ShowDirsOnly)
+ if folder_path:
+ # 获取选中的虚拟机名称
+ vm_name = item.text().split(' | ')[0]
+
+ # 构建命令行语句
+ cmd = ['eulerlauncher', 'take-snapshot', snapshot_name, 'snap', folder_path, 'path', vm_name]
+
+ # 执行命令行语句, 并将输出添加到列表中
+ outputs, check = self.do_cmd(cmd)
+ if check:
+ if re.match(r'Unknown', outputs[0]): # 注:此处无对应的正确输出,因为指令不存在,请在实际情况下修改该判断条件
+ self.create_message_box('信息', '创建快照{}成功'.format(snapshot_name))
+ else:
+ self.create_message_box('错误', outputs[0])
+ else:
+ self.create_message_box('错误', '快照生成失败')
+ self.button_vmfresh_clicked()
+ else:
+ self.create_message_box('错误', '未选中虚拟机')
+
+ def button_vmdel_clicked(self): # 删除虚拟机按钮的槽函数
+ item = self.list_of_vm.currentItem()
+ if item:
+ # 获取选中的虚拟机名称
+ vm_name = item.text().split(' | ')[0]
+
+ # 构建命令行语句
+ cmd = ['eulerlauncher', 'delete-instance', vm_name]
+
+ # 执行命令行语句, 并将输出添加到列表中
+ outputs, check = self.do_cmd(cmd)
+ if check:
+ if re.match(r'Successfully deleted instance.+', outputs[0]):
+ self.create_message_box('信息', '删除虚拟机{}成功'.format(vm_name))
+ self.button_vmfresh_clicked()
+ else:
+ self.create_message_box('错误', outputs[0])
+ else:
+ self.create_message_box('错误', '删除失败')
+ self.button_vmfresh_clicked()
+
+ else:
+ self.create_message_box('错误', '未选中虚拟机')
+
+ def button_vmout_clicked(self): # 导出为开发镜像按钮的槽函数
+ self.create_message_box('提示', '该功能在主程序尚未实现,请自行取消后续操作') # 该功能在主程序尚未实现,实现后请删除此行代码
+
+ item = self.list_of_vm.currentItem()
+ if item:
+ image_name, ok = QInputDialog.getText(self, '导出镜像命名', '请输入导出镜像名称:')
+ if ok:
+ folder_path = QFileDialog.getExistingDirectory(None, '选择保存路径', './', QFileDialog.ShowDirsOnly)
+ if folder_path:
+ # 获取选中的虚拟机名称
+ vm_name = item.text().split(' | ')[0]
+
+ # 构建命令行语句
+ cmd = ['eulerlauncher', 'export-development-image', image_name, 'image', folder_path, 'path',
+ vm_name]
+
+ # 执行命令行语句, 并将输出添加到列表中
+ outputs, check = self.do_cmd(cmd)
+ if check:
+ if re.match(r'Unknown', outputs[0]): # 注:此处无对应的正确输出,因为指令不存在,请在实际情况下修改修改该判断条件
+ self.create_message_box('信息', '导出为开发镜像{}成功'.format(image_name))
+ else:
+ self.create_message_box('错误', outputs[0])
+ else:
+ self.create_message_box('错误', '导出失败')
+ self.button_vmfresh_clicked()
+ else:
+ self.create_message_box('错误', '未选中虚拟机')
+
+ def button_vmlink_clicked(self): # 连接按钮的槽函数
+ # 获取选中的虚拟机信息
+ vm = self.list_of_vm.currentItem()
+ if vm:
+ vm_info = vm.text().split(' | ')
+ # 如果虚拟机IP正在配置中, 则无法连接
+ if vm_info[3] == 'N/A':
+ self.create_message_box('错误', '虚拟机IP正在配置中,请稍后')
+ else:
+ # 更新虚拟机连接界面的信息
+ self.vm_tip.setText('更换需要连接的虚拟机\n请在虚拟机管理中重新选择\n\n'
+ '将要连接的虚拟机信息: ')
+ self.vm_name.setText('虚拟机名称: ' + vm_info[0])
+ self.vm_image.setText('使用的镜像: ' + vm_info[1])
+ self.vm_state.setText('状态: ' + vm_info[2])
+ self.vm_ip.setText(vm_info[3])
+
+ # 自动切到虚拟机连接界面
+ self.button3.click()
+ else:
+ self.create_message_box('错误', '未选中虚拟机')
+
+ def button_connection_clicked(self): # 虚拟机连接界面按钮的槽函数
+ now_ip = self.vm_ip.text()
+ if now_ip == '': # 未选中虚拟机
+ self.create_message_box('错误', '请点击虚拟机管理界面选择虚拟机进行连接')
+ else: # 打开终端, 并连接虚拟机
+ self.open_terminal(self, 'ssh root@{}'.format(now_ip))
+
+ @staticmethod
+ def open_terminal(self, command): # 打开终端并执行命令,用于执行终端ssh连接的第一种方式
+ if sys.platform == "win32":
+ # Windows系统
+ subprocess.run(f'start cmd /k {command}', shell=True)
+ elif sys.platform == "darwin":
+ # macOS系统
+ applescript = '''
+ tell application "Terminal"
+ activate
+ do script "{}"
+ end tell
+ '''.format(command)
+ subprocess.run(['osascript'], text=True, input=applescript)
+ elif sys.platform == "linux" or sys.platform == "linux2":
+ # Linux系统
+ terminals = ['terminal', 'gnome-terminal', 'xterm', 'konsole', 'terminator', 'tilix']
+ for terminal in terminals:
+ try:
+ subprocess.run([terminal, '--', 'bash', '-c', f'{command}'])
+ break
+ except FileNotFoundError:
+ continue
+ else:
+ self.create_message_box('错误', '未找到合适的终端')
+
+ def connect_ssh(self): # GUI连接SSH
+ host = self.vm_ip.text()
+ user = 'root'
+ password = 'openEuler12#$'
+
+ if host == '': # 未选中虚拟机
+ self.create_message_box('错误', '请点击虚拟机管理界面选择虚拟机进行连接')
+ return
+
+ # 检查是否为官方镜像,如果是则使用默认密码
+ self.button_fresh_clicked()
+ for i in range(self.list_of_iso.count()):
+ now = self.list_of_iso.item(i).text()
+ name = now.split(' | ')[0]
+ location = now.split(' | ')[1]
+ tar = self.vm_image.text().split(': ')[1]
+ if name == tar and location == 'Remote':
+ break
+ else:
+ self.create_message_box('信息', '检测到该虚拟机为非官方镜像\n请手动输入密码连接')
+ password, ok = QInputDialog.getText(self, '输入密码', '请输入密码:')
+ if not ok:
+ return
+
+ # 清空输出框
+ self.output_text.clear()
+
+ try:
+ # 创建SSH连接
+ self.ssh = SSHClient()
+ self.ssh.set_missing_host_key_policy(AutoAddPolicy())
+ self.ssh.connect(host, username=user, password=password)
+
+ # 创建伪shell
+ self.shell = self.ssh.invoke_shell()
+
+ # 读取输出
+ if self.shell and self.shell.recv_ready():
+ self.output_text.append(f"Connected to {host}\n")
+ self.read_output()
+
+ except Exception as e:
+ self.output_text.append(f"Error: {str(e)}\n")
+
+ def read_output(self): # 输出读取函数
+ if self.shell:
+ try:
+ while True:
+ if self.shell.recv_ready():
+ # 去除输出中ANSI转义字符
+ data = re.sub(r'\x1b\[\??([0-9a-z]+)(;[0-9]+)*([A-Za-z])?', '',
+ self.shell.recv(1024).decode('utf-8'))
+ # 输出到文本框
+ self.output_text.append(data)
+ if '$' in data or '# ' in data: # 检查命令提示符,结束读取
+ break
+ except Exception as e:
+ self.output_text.append(f"Error reading output: {str(e)}\n")
+
+ def execute_command(self): # 执行命令,即发送命令
+ if self.shell:
+ command = self.command_input.text()
+ if command == 'exit':
+ self.close_ssh()
+ return
+ self.shell.send(command + '\n')
+ self.read_output()
+ if not self.ck.isChecked():
+ self.command_input.clear()
+ else:
+ self.output_text.clear()
+ self.output_text.append("Not connected\n")
+
+ def close_ssh(self):
+ if self.ssh:
+ self.ssh.close()
+ self.ssh = None
+ if self.shell:
+ self.shell.close()
+ self.shell = None
+ self.output_text.clear()
+ self.command_input.clear()
+ self.output_text.append("Disconnected\n")
+
+ def read_conf(self):
+ try:
+ with open(self.conf_label.text(), 'r') as f:
+ self.conf_text.setText(f.read())
+ except Exception as e:
+ self.create_message_box('错误', '读取conf文件失败,{}'.format(e))
+ self.conf_text.setText(str(e))
+
+ def write_conf(self):
+ if self.conf_label.text() == 'conf文件未选中':
+ self.create_message_box('错误', '请先选择conf文件')
+ return
+
+ # 确认弹窗
+ reply = QMessageBox.question(self, '确认', '确认修改conf文件?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
+ if reply == QMessageBox.No:
+ return
+
+ # 修改收集弹窗
+ dialog = QDialog(self)
+ dialog.setWindowTitle('修改conf文件')
+ dialog.resize(400, 200)
+
+ # 创建布局
+ layout = QVBoxLayout(dialog)
+ self.write_text = QTextEdit(dialog)
+ self.write_text.setText(self.conf_text.toPlainText())
+ layout.addWidget(self.write_text)
+ button = QPushButton('确认', dialog)
+ button.clicked.connect(self.write_conf_to_file)
+ layout.addWidget(button)
+
+ # 设置布局
+ dialog.setLayout(layout)
+
+ # 显示弹窗
+ dialog.exec()
+
+ # 完成后再次读取conf文件
+ self.read_conf()
+
+ def write_conf_to_file(self):
+ try:
+ with open(self.conf_label.text(), 'w') as f:
+ f.write(self.write_text.toPlainText())
+ self.create_message_box('信息', '修改成功')
+ except Exception as e:
+ self.create_message_box('错误', '写入conf文件失败,{}'.format(e))
+ finally:
+ dialog = self.sender().parent()
+ dialog.close()
+
+ def initialize_conf(self):
+ if self.conf_label.text() == 'conf文件未选中':
+ self.create_message_box('错误', '请先选择conf文件')
+ return
+
+ # 确认弹窗
+ reply = QMessageBox.question(self, '确认', '确认将conf文件恢复为初始状态?', QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.No)
+ if reply == QMessageBox.No:
+ return
+
+ # 按系统选择恢复内容
+ if sys.platform == "win32":
+ # Windows系统
+ index = r'''[default]
+log_dir = C:\workdir\logs
+debug = True
+work_dir = C:\workdir
+image_dir = images
+instance_dir = instances
+qemu_dir =
+pattern = hyper-v
+
+[vm]
+cpu_num = 2
+memory = 8G'''
+ else:
+ # Linux & MacOS系统
+ index = '''[default]
+log_dir =
+work_dir =
+wget_dir =
+qemu_dir =
+qemu_img_dir =
+debug = True
+
+[vm]
+cpu_num = 1
+memory = 1024'''
+
+ # 确认弹窗
+ reply = QMessageBox.question(self, '确认', '确认将conf文件恢复为以下内容?\n{}\n注意:该内容不完整,'
+ '请稍后自行根据情况填入和更改各参数'.format(index),
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
+ if reply == QMessageBox.No:
+ return
+
+ # 恢复默认
+ try:
+ with open(self.conf_label.text(), 'w') as f:
+ f.write(index)
+ self.create_message_box('信息', '恢复初始状态成功')
+ except Exception as e:
+ self.create_message_box('错误', '写入conf文件失败,{}'.format(e))
+
+ # 完成后读取conf文件内容
+ self.read_conf()
+
+ def fetch_conf_path(self):
+ # 打开文件选择对话框
+ filepath, _ = QFileDialog.getOpenFileName(self, "选择对应conf文件", "",
+ "eulerlauncher.conf文件 (eulerlauncher.conf)")
+ if filepath:
+ # 选中conf文件后显示conf文件路径
+ self.conf_label.setText(filepath)
+
+ # 读取conf文件内容
+ self.read_conf()
+
+ # 创建该程序中标准的消息框
+ def create_message_box(self, title, text):
+ message_box = QMessageBox(self)
+ message_box.setIcon(QMessageBox.Information)
+ message_box.setText(text)
+ message_box.setWindowTitle(title)
+ message_box.setStandardButtons(QMessageBox.Ok)
+ message_box.exec()
+
+ @staticmethod # 创建该程序中标准的分割线 True为水平 False为垂直
+ def create_line(horizon=True):
+ line = QFrame()
+ if horizon:
+ line.setFrameShape(QFrame.HLine)
+ else:
+ line.setFrameShape(QFrame.VLine)
+ line.setFrameShadow(QFrame.Sunken)
+ return line
+
+ def load_qss_style(self): # 读取 QSS 文件
+ # 本字符串为GUI的样式表,用于设置GUI的样式,其为qss格式
+ # 进行修改请在style字符串中进行
+ style = """
+ QWidget
+ {
+ font-family: "Arial", "SimHei", sans-serif;
+ font-size: 14px;
+ }
+ QPushButton[name = 'menu']
+ {
+ color : black;
+ padding-top : 10px;
+ padding-bottom : 10px;
+ padding-left : 10px;
+ padding-right : 50px;
+ min-width : 100px;
+ border-left : 0;
+ font-size : 18px;
+ background-color: white;
+ border-radius: 10px;
+ text-align: left;
+ }
+ QPushButton[name = 'menu']:checked
+ {
+ background-color: #f0f0f0;
+ border-left: 5px solid #87CEEB;
+ padding-left : 5px;
+ }
+ QPushButton
+ {
+ color : black;
+ padding : 10px;
+ min-width : 80px;
+ }
+ QListWidget
+ {
+ color: black;
+ border-radius: 10px;
+ }
+ QListWidget::item
+ {
+ padding: 5px;
+ }
+ #sidebar
+ {
+ background-color: white;
+ width: 300px;
+ color: black;
+ border-radius: 10px;
+ }
+ QLineEdit
+ {
+ border-radius: 10px;
+ border: 1px solid #87CEEB;
+ padding: 10px;
+ }
+ QTextEdit
+ {
+ border-radius: 10px;
+ border: 1px solid #87CEEB;
+ padding: 3px;
+ }
+ """
+ self.setStyleSheet(style)
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ ex = EulerLauncherGUI()
+ ex.show()
+ sys.exit(app.exec())
diff --git a/logos/favicon.icns b/logos/favicon.icns
new file mode 100644
index 0000000000000000000000000000000000000000..83fad6db5ab6f082d2b6af88e32c58fcbeb9f386
Binary files /dev/null and b/logos/favicon.icns differ
diff --git a/requirements-win.txt b/requirements-win.txt
index 7ed4143d721f264d574644cdc0c824a682f9cbfa..cb83a5018cfc0209c160943218cd0e3bdcdbeeaa 100644
--- a/requirements-win.txt
+++ b/requirements-win.txt
@@ -23,4 +23,5 @@ pystray
PyYAML
six
urllib3
-wget
\ No newline at end of file
+wget
+PyQt5
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 93e887394dbb3d4b2535f2d8c0fa27608ee4d2b5..54bd753b3a04d97a27f5b4e6c5a510accc6d03b4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -27,4 +27,5 @@ urllib3
wget
pywebview
sqlalchemy
-httpx
\ No newline at end of file
+httpx
+PyQt5
\ No newline at end of file
diff --git a/specs/eulerlauncherGUI-Mac.spec b/specs/eulerlauncherGUI-Mac.spec
new file mode 100644
index 0000000000000000000000000000000000000000..e11aba0586b64cae4698d1756a3fe5ec0464f5c2
--- /dev/null
+++ b/specs/eulerlauncherGUI-Mac.spec
@@ -0,0 +1,48 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+block_cipher = None
+
+a = Analysis(
+ ['../eulerlauncher/eulerlauncherGUI.py'],
+ pathex=[],
+ binaries=[],
+ datas=[],
+ hiddenimports=[],
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ cipher=block_cipher,
+ noarchive=False,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ [],
+ name='eulerlauncherGUI',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=False,
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+ uac_admin=True,
+ icon=['../logos/favicon.icns'],
+)
+app = BUNDLE(
+ exe,
+ name='eulerlauncherGUI.app',
+ icon='../logos/favicon.icns',
+ bundle_identifier=None,
+)
diff --git a/specs/eulerlauncherGUI-win.spec b/specs/eulerlauncherGUI-win.spec
new file mode 100644
index 0000000000000000000000000000000000000000..88ce5ffbf231393937abb58318f8fbaf04b745f0
--- /dev/null
+++ b/specs/eulerlauncherGUI-win.spec
@@ -0,0 +1,40 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+block_cipher = None
+
+a = Analysis(
+ ['..\\eulerlauncher\\eulerlauncherGUI.py'],
+ pathex=[],
+ binaries=[],
+ datas=[],
+ hiddenimports=[],
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ noarchive=False,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ [],
+ name='eulerlauncherGUI',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=False,
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+ icon=['..\\logos\\favicon.ico'],
+)