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'], +)