diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000000000000000000000000000000000000..ebd701f99e5591140bf7e4ff46223fc91392bf91 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Zhenyu Zheng +Zhipeng Huang diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000000000000000000000000000000000000..b0bce081aba28388c3317572617d9856ef7539d4 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,6 @@ +CHANGES +======= + +* fix requirement bugs on urllib3 and libvirt-python +* Make the tool work on Mac +* Initial Commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ee5839968a2bf86c93283efc09d40fd050b7cfa2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,127 @@ + 木兰宽松许可证, 第2版 + + 木兰宽松许可证, 第2版 + 2020年1月 http://license.coscl.org.cn/MulanPSL2 + + + 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: + + 0. 定义 + + “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 + + “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 + + “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 + + “法人实体”是指提交贡献的机构及其“关联实体”。 + + “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 + + 1. 授予版权许可 + + 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 + + 2. 授予专利许可 + + 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 + + 3. 无商标许可 + + “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 + + 4. 分发限制 + + 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 + + 5. 免责声明与责任限制 + + “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 + + 6. 语言 + “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 + + 条款结束 + + 如何将木兰宽松许可证,第2版,应用到您的软件 + + 如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: + + 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; + + 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; + + 3, 请将如下声明文本放入每个源文件的头部注释中。 + + Copyright (c) [Year] [name of copyright holder] + [Software Name] is licensed under Mulan PSL v2. + You can use this software according to the terms and conditions of the Mulan PSL v2. + You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 + THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + + Mulan Permissive Software License,Version 2 + + Mulan Permissive Software License,Version 2 (Mulan PSL v2) + January 2020 http://license.coscl.org.cn/MulanPSL2 + + Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: + + 0. Definition + + Software means the program and related documents which are licensed under this License and comprise all Contribution(s). + + Contribution means the copyrightable work licensed by a particular Contributor under this License. + + Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. + + Legal Entity means the entity making a Contribution and all its Affiliates. + + Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. + + 1. Grant of Copyright License + + Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. + + 2. Grant of Patent License + + Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. + + 3. No Trademark License + + No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4. + + 4. Distribution Restriction + + You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. + + 5. Disclaimer of Warranty and Limitation of Liability + + THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 6. Language + + THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. + + END OF THE TERMS AND CONDITIONS + + How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software + + To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps: + + i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; + + ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package; + + iii Attach the statement to the appropriate annotated syntax at the beginning of each source file. + + + Copyright (c) [Year] [name of copyright holder] + [Software Name] is licensed under Mulan PSL v2. + You can use this software according to the terms and conditions of the Mulan PSL v2. + You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 + THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. diff --git a/OmniVirt-MacOS.spec b/OmniVirt-MacOS.spec new file mode 100644 index 0000000000000000000000000000000000000000..12a5e6107cc60713cdc3f5876a71d7d687391a6f --- /dev/null +++ b/OmniVirt-MacOS.spec @@ -0,0 +1,52 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['omnivirt/macos-gui.py'], + pathex=[], + binaries=[('dist/OmniVirtd', './bin')], + datas=[('etc/omnivirt.conf', './etc'), ('etc/images/favicon.png', './etc'), ('resources/qemu/edk2-aarch64-code.fd', './etc'), ('resources/qemu/edk2-x86_64-code.fd', './etc')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + 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='OmniVirt', + 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=['etc/images/favicon.ico'], +) +app = BUNDLE( + exe, + name='OmniVirt.app', + icon='etc/images/favicon.ico', + bundle_identifier=None, +) diff --git a/OmniVirtd-Mac.spec b/OmniVirtd-Mac.spec new file mode 100644 index 0000000000000000000000000000000000000000..3a8748b2e3f00adb0942f8a96cfbcc92ae4d44c0 --- /dev/null +++ b/OmniVirtd-Mac.spec @@ -0,0 +1,44 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['omnivirt/omnivirtd.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + 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='OmniVirtd', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/cli.spec b/cli.spec new file mode 100644 index 0000000000000000000000000000000000000000..bfaf66db082b0bba4cfc518da6555f5f396e1c21 --- /dev/null +++ b/cli.spec @@ -0,0 +1,44 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['omnivirt/cli.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + 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='omnivirt', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/docs/developer-manual.md b/docs/developer-manual.md new file mode 100644 index 0000000000000000000000000000000000000000..ba3c76504834b85f9dc886b2e28d4ae4afcc24b3 --- /dev/null +++ b/docs/developer-manual.md @@ -0,0 +1,93 @@ +# OmniVirt开发者文档 + +## 构建OmniVirt + +OmniVirt使用Python语言编写,源代码可以跨平台运行,但需要安装Python运行时。为了方便用户使用,可以将源代码编译为二进制形式。在不同的操作系统上构建的步骤略有不同,具体请参照下述指南。 + +## 在MacOS上构建OmniVirt + +### 准备阶段 + +**安装Python:** + +参考[Python社区首页][1]完成Python安装,推荐安装Python 3.10及以上版本 + +**安装Homebrew** + +参考[Homebrew官网][2]完成Homebrew安装 + +OmniVirt使用`Pyinstaller`将源码编译为MacOS可执行文件(Unix二进制文件)及`.app`文件,使用`create-dmg`工具将`.app`构建成为`.dmg`磁盘文件以方便软件安装。 + +使用下面命令安装`Pyinstaller` + +``` Shell +pip3 install pyinstaller +``` + +使用下面命令安装`create-dmg` + +``` Shell +brew install create-dmg +``` + +进入项目目录并准备开始工作 + +``` Shell +cd /path/to/OmniVirt +``` + +安装项目依赖 + +``` Shell +pip3 install -r requirements.txt +``` + +### 构建 + +OmniVirt可执行文件包括以下几个部分: + +1. OmniVirtd: omnivirt守护进程,以root权限运行在后台,与调用虚拟化组件(Qemu、HyperV、KVM等)及镜像组件进行相关操作; +2. OmniVirt.app: OmniVirt服务端主程序,将omnivirtd及其他相关程序、数据、文件等打包为MacOS APP软件包,便于分发和使用。 +3. omnivirt: MacOS可执行文件,OmniVirt客户端CLI工具,用于与服务端交互。 +4. install: MacOS可执行文件,将OmniVirt运行所需配置文件及相关数据文件安装至`Application Support`文件夹。 + +由于`OmniVirt.app`对`OmniVirtd`有依赖关系,请严格按照以下顺序构建`OmniVirtd`及`OmniVirt.app`: + +1. OmniVirtd: + + 项目源码中已包含用于构建OmniVirtd的Spec脚本`OmniVirtd-Mac.spec`, 若非必要,请勿修改该文件,使用一下命令开始构建: + ``` Shell + pyinstaller --clean --noconfirm OmniVirtd-Mac.spec + ``` + +2. OmniVirt.app: + + 项目源码中已包含用于构建OmniVirt的Spec脚本`OmniVirt-MacOS.spec`, 若非必要,请勿修改该文件,使用一下命令开始构建: + ``` Shell + pyinstaller --clean --noconfirm OmniVirt-MacOS.spec + ``` + +构建`omnivirt` CLI 及 `install` 脚本, cli与install之间有依赖关系,请严格按照下面的顺序进行构建: + +``` Shell +pyinstaller --clean --noconfirm cli.spec +pyinstaller --clean --noconfirm install.spec +``` + +### 制作`.dmg`: + +首先,我们创建一个新目录并将文件移动到其中。 +``` Shell +mkdir -p dist/dmg +cp -R dist/OmniVirt.app dist/dmg +``` + +然后,我们可以使用下面的命令来制作磁盘镜像文件: +``` Shell +create-dmg --volname "OmniVirt" --volicon "etc/images/favicon.png" --window-pos 200 120 --window-size 600 300 --icon-size 100 --icon "OmniVirt.app" 175 120 --hide-extension "OmniVirt.app" --app-drop-link 425 120 "dist/OmniVirt.dmg" "dist/dmg/" +``` + +`OmniVirt.dmg`中将只包含`OmniVirt.app`主程序,需要将`install`脚本及`omnivirt` CLI工具一并压缩后再进行分发。 + +[1]: https://www.python.org/ +[2]: https://brew.sh/ \ No newline at end of file diff --git a/etc/homebrewcn.sh b/etc/homebrewcn.sh new file mode 100644 index 0000000000000000000000000000000000000000..1ff86e20475a385ffa2ab45f32b056eddcce63e1 --- /dev/null +++ b/etc/homebrewcn.sh @@ -0,0 +1,816 @@ +#HomeBrew自动安装脚本 +#cunkai.wang@foxmail.com +#brew brew brew brew + +#获取硬件信息 判断inter还是苹果M +UNAME_MACHINE="$(uname -m)" +#在X86电脑上测试arm电脑 +# UNAME_MACHINE="arm64" + +# 判断是Linux还是Mac os +OS="$(uname)" +if [[ "$OS" == "Linux" ]]; then + HOMEBREW_ON_LINUX=1 +elif [[ "$OS" != "Darwin" ]]; then + echo "Homebrew 只运行在 Mac OS 或 Linux." +fi + +# 字符串染色程序 +if [[ -t 1 ]]; then + tty_escape() { printf "\033[%sm" "$1"; } +else + tty_escape() { :; } +fi +tty_universal() { tty_escape "0;$1"; } #正常显示 +tty_mkbold() { tty_escape "1;$1"; } #设置高亮 +tty_underline="$(tty_escape "4;39")" #下划线 +tty_blue="$(tty_universal 34)" #蓝色 +tty_red="$(tty_universal 31)" #红色 +tty_green="$(tty_universal 32)" #绿色 +tty_yellow="$(tty_universal 33)" #黄色 +tty_bold="$(tty_universal 39)" #加黑 +tty_cyan="$(tty_universal 36)" #青色 +tty_reset="$(tty_escape 0)" #去除颜色 + +#用户输入极速安装speed,git克隆只取最近新版本 +#但是update会出错,提示需要下载全部数据 +GIT_SPEED="" + +if [[ $0 == "speed" ]]; then + GIT_SPEED="--depth=1" +else + for dir in $@; do + echo $dir + if [[ $dir == "speed" ]]; then + GIT_SPEED="--depth=1" + fi + done +fi + +if [[ $GIT_SPEED != "" ]]; then +echo "${tty_red} + 检测到参数speed,只拉取最新数据,可以正常install使用! + 腾讯和阿里不支持speed拉取,需要腾讯阿里需要完全模式。 + 但是以后brew update的时候会报错,运行报错提示的两句命令即可修复 + ${tty_reset}" +fi + +#获取前面两个.的数据 +major_minor() { + echo "${1%%.*}.$(x="${1#*.}"; echo "${x%%.*}")" +} + +#设置一些平台地址 +if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then + #Mac + if [[ "$UNAME_MACHINE" == "arm64" ]]; then + #M1 + HOMEBREW_PREFIX="/opt/homebrew" + HOMEBREW_REPOSITORY="${HOMEBREW_PREFIX}" + else + #Inter + HOMEBREW_PREFIX="/usr/local" + HOMEBREW_REPOSITORY="${HOMEBREW_PREFIX}/Homebrew" + fi + + HOMEBREW_CACHE="${HOME}/Library/Caches/Homebrew" + HOMEBREW_LOGS="${HOME}/Library/Logs/Homebrew" + + #国内没有homebrew-services,手动在gitee创建了一个,有少数人用到。 + USER_SERVICES_GIT=https://gitee.com/cunkai/homebrew-services.git + + STAT="stat -f" + CHOWN="/usr/sbin/chown" + CHGRP="/usr/bin/chgrp" + GROUP="admin" + TOUCH="/usr/bin/touch" + + #获取Mac系统版本 + macos_version="$(major_minor "$(/usr/bin/sw_vers -productVersion)")" +else + #Linux + UNAME_MACHINE="$(uname -m)" + + HOMEBREW_PREFIX="/home/linuxbrew/.linuxbrew" + HOMEBREW_REPOSITORY="${HOMEBREW_PREFIX}/Homebrew" + + HOMEBREW_CACHE="${HOME}/.cache/Homebrew" + HOMEBREW_LOGS="${HOME}/.logs/Homebrew" + + STAT="stat --printf" + CHOWN="/bin/chown" + CHGRP="/bin/chgrp" + GROUP="$(id -gn)" + TOUCH="/bin/touch" +fi + + + +#获取系统时间 +TIME=$(date "+%Y-%m-%d %H:%M:%S") + +JudgeSuccess() +{ + if [ $? -ne 0 ];then + echo "${tty_red}此步骤失败 '$1'${tty_reset}" + if [[ "$2" == 'out' ]]; then + exit 0 + fi + else + echo "${tty_green}此步骤成功${tty_reset}" + + fi +} +# 判断是否有系统权限 +have_sudo_access() { + if [[ -z "${HAVE_SUDO_ACCESS-}" ]]; then + /usr/bin/sudo -l mkdir &>/dev/null + HAVE_SUDO_ACCESS="$?" + fi + + if [[ "$HAVE_SUDO_ACCESS" -ne 0 ]]; then + echo "${tty_red}开机密码输入错误,获取权限失败!${tty_reset}" + fi + + return "$HAVE_SUDO_ACCESS" +} + + +abort() { + printf "%s\n" "$1" + # exit 1 +} + +shell_join() { + local arg + printf "%s" "$1" + shift + for arg in "$@"; do + printf " " + printf "%s" "${arg// /\ }" + done +} + +execute() { + if ! "$@"; then + abort "$(printf "${tty_red}此命令运行失败: %s${tty_reset}" "$(shell_join "$@")")" + fi +} + + + +ohai() { + printf "${tty_blue}运行代码 ==>${tty_bold} %s${tty_reset}\n" "$(shell_join "$@")" +} + +# 管理员运行 +execute_sudo() +{ + + local -a args=("$@") + if have_sudo_access; then + if [[ -n "${SUDO_ASKPASS-}" ]]; then + args=("-A" "${args[@]}") + fi + ohai "/usr/bin/sudo" "${args[@]}" + execute "/usr/bin/sudo" "${args[@]}" + else + ohai "${args[@]}" + execute "${args[@]}" + fi +} +#添加文件夹权限 +AddPermission() +{ + execute_sudo "/bin/chmod" "-R" "a+rwx" "$1" + execute_sudo "$CHOWN" "$USER" "$1" + execute_sudo "$CHGRP" "$GROUP" "$1" +} +#创建文件夹 +CreateFolder() +{ + echo '-> 创建文件夹' $1 + execute_sudo "/bin/mkdir" "-p" "$1" + JudgeSuccess + AddPermission $1 +} + +RmAndCopy() +{ + if [[ -d $1 ]]; then + echo " ---备份要删除的$1到系统桌面...." + if ! [[ -d $HOME/Desktop/Old_Homebrew/$TIME/$1 ]]; then + sudo mkdir -p "$HOME/Desktop/Old_Homebrew/$TIME/$1" + fi + sudo cp -rf $1 "$HOME/Desktop/Old_Homebrew/$TIME/$1" + echo " ---$1 备份完成" + fi + sudo rm -rf $1 +} + +RmCreate() +{ + RmAndCopy $1 + CreateFolder $1 +} + +#判断文件夹存在但不可写 +exists_but_not_writable() { + [[ -e "$1" ]] && ! [[ -r "$1" && -w "$1" && -x "$1" ]] +} +#文件所有者 +get_owner() { + $(shell_join "$STAT %u $1" ) +} +#文件本人无权限 +file_not_owned() { + [[ "$(get_owner "$1")" != "$(id -u)" ]] +} +#获取所属的组 +get_group() { + $(shell_join "$STAT %g $1" ) +} +#不在所属组 +file_not_grpowned() { + [[ " $(id -G "$USER") " != *" $(get_group "$1") "* ]] +} +#获得当前文件夹权限 例如777 +get_permission() { + $(shell_join "$STAT %A $1" ) +} +#授权当前用户权限 +user_only_chmod() { + [[ -d "$1" ]] && [[ "$(get_permission "$1")" != "755" ]] +} + + +#创建brew需要的目录 直接复制于国外版本,同步 +CreateBrewLinkFolder() +{ + echo "--创建Brew所需要的目录" + directories=(bin etc include lib sbin share opt var + Frameworks + etc/bash_completion.d lib/pkgconfig + share/aclocal share/doc share/info share/locale share/man + share/man/man1 share/man/man2 share/man/man3 share/man/man4 + share/man/man5 share/man/man6 share/man/man7 share/man/man8 + var/log var/homebrew var/homebrew/linked + bin/brew) + group_chmods=() + for dir in "${directories[@]}"; do + if exists_but_not_writable "${HOMEBREW_PREFIX}/${dir}"; then + group_chmods+=("${HOMEBREW_PREFIX}/${dir}") + fi + done + + directories=(share/zsh share/zsh/site-functions) + zsh_dirs=() + for dir in "${directories[@]}"; do + zsh_dirs+=("${HOMEBREW_PREFIX}/${dir}") + done + + directories=(bin etc include lib sbin share var opt + share/zsh share/zsh/site-functions + var/homebrew var/homebrew/linked + Cellar Caskroom Frameworks) + mkdirs=() + for dir in "${directories[@]}"; do + if ! [[ -d "${HOMEBREW_PREFIX}/${dir}" ]]; then + mkdirs+=("${HOMEBREW_PREFIX}/${dir}") + fi + done + + user_chmods=() + if [[ "${#zsh_dirs[@]}" -gt 0 ]]; then + for dir in "${zsh_dirs[@]}"; do + if user_only_chmod "${dir}"; then + user_chmods+=("${dir}") + fi + done + fi + + chmods=() + if [[ "${#group_chmods[@]}" -gt 0 ]]; then + chmods+=("${group_chmods[@]}") + fi + if [[ "${#user_chmods[@]}" -gt 0 ]]; then + chmods+=("${user_chmods[@]}") + fi + + chowns=() + chgrps=() + if [[ "${#chmods[@]}" -gt 0 ]]; then + for dir in "${chmods[@]}"; do + if file_not_owned "${dir}"; then + chowns+=("${dir}") + fi + if file_not_grpowned "${dir}"; then + chgrps+=("${dir}") + fi + done + fi + + if [[ -d "${HOMEBREW_PREFIX}" ]]; then + if [[ "${#chmods[@]}" -gt 0 ]]; then + execute_sudo "/bin/chmod" "u+rwx" "${chmods[@]}" + fi + if [[ "${#group_chmods[@]}" -gt 0 ]]; then + execute_sudo "/bin/chmod" "g+rwx" "${group_chmods[@]}" + fi + if [[ "${#user_chmods[@]}" -gt 0 ]]; then + execute_sudo "/bin/chmod" "755" "${user_chmods[@]}" + fi + if [[ "${#chowns[@]}" -gt 0 ]]; then + execute_sudo "$CHOWN" "$USER" "${chowns[@]}" + fi + if [[ "${#chgrps[@]}" -gt 0 ]]; then + execute_sudo "$CHGRP" "$GROUP" "${chgrps[@]}" + fi + else + execute_sudo "/bin/mkdir" "-p" "${HOMEBREW_PREFIX}" + if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then + execute_sudo "$CHOWN" "root:wheel" "${HOMEBREW_PREFIX}" + else + execute_sudo "$CHOWN" "$USER:$GROUP" "${HOMEBREW_PREFIX}" + fi + fi + + if [[ "${#mkdirs[@]}" -gt 0 ]]; then + execute_sudo "/bin/mkdir" "-p" "${mkdirs[@]}" + execute_sudo "/bin/chmod" "g+rwx" "${mkdirs[@]}" + execute_sudo "$CHOWN" "$USER" "${mkdirs[@]}" + execute_sudo "$CHGRP" "$GROUP" "${mkdirs[@]}" + fi + + if ! [[ -d "${HOMEBREW_REPOSITORY}" ]]; then + execute_sudo "/bin/mkdir" "-p" "${HOMEBREW_REPOSITORY}" + fi + execute_sudo "$CHOWN" "-R" "$USER:$GROUP" "${HOMEBREW_REPOSITORY}" + + if ! [[ -d "${HOMEBREW_CACHE}" ]]; then + if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then + execute_sudo "/bin/mkdir" "-p" "${HOMEBREW_CACHE}" + else + execute "/bin/mkdir" "-p" "${HOMEBREW_CACHE}" + fi + fi + if exists_but_not_writable "${HOMEBREW_CACHE}"; then + execute_sudo "/bin/chmod" "g+rwx" "${HOMEBREW_CACHE}" + fi + if file_not_owned "${HOMEBREW_CACHE}"; then + execute_sudo "$CHOWN" "-R" "$USER" "${HOMEBREW_CACHE}" + fi + if file_not_grpowned "${HOMEBREW_CACHE}"; then + execute_sudo "$CHGRP" "-R" "$GROUP" "${HOMEBREW_CACHE}" + fi + if [[ -d "${HOMEBREW_CACHE}" ]]; then + execute "$TOUCH" "${HOMEBREW_CACHE}/.cleaned" + fi + echo "--依赖目录脚本运行完成" +} + +#git提交 +git_commit(){ + git add . + git commit -m "your del" +} + +#version_gt 判断$1是否大于$2 +version_gt() { + [[ "${1%.*}" -gt "${2%.*}" ]] || [[ "${1%.*}" -eq "${2%.*}" && "${1#*.}" -gt "${2#*.}" ]] +} +#version_ge 判断$1是否大于等于$2 +version_ge() { + [[ "${1%.*}" -gt "${2%.*}" ]] || [[ "${1%.*}" -eq "${2%.*}" && "${1#*.}" -ge "${2#*.}" ]] +} +#version_lt 判断$1是否小于$2 +version_lt() { + [[ "${1%.*}" -lt "${2%.*}" ]] || [[ "${1%.*}" -eq "${2%.*}" && "${1#*.}" -lt "${2#*.}" ]] +} + +#发现错误 关闭脚本 提示如何解决 +error_game_over(){ + echo " + ${tty_red}失败$MY_DOWN_NUM 右键下面地址查看常见错误解决办法 + https://gitee.com/cunkai/HomebrewCN/blob/master/error.md + 如果没有解决,把全部运行过程截图发到 cunkai.wang@foxmail.com ${tty_reset} + " + + exit 0 +} + +#一些警告判断 +warning_if(){ + git_https_proxy=$(git config --global https.proxy) + git_http_proxy=$(git config --global http.proxy) + if [[ -z "$git_https_proxy" && -z "$git_http_proxy" ]]; then + echo "未发现Git代理(属于正常状态)" + else + echo "${tty_yellow} + 提示:发现你电脑设置了Git代理,如果Git报错,请运行下面两句话: + + git config --global --unset https.proxy + + git config --global --unset http.proxy${tty_reset} + " + fi +} + +echo " + ${tty_green} 开始执行Brew自动安装程序 ${tty_reset} + ${tty_cyan} [cunkai.wang@foxmail.com] ${tty_reset} + ['$TIME']['$macos_version'] + ${tty_cyan} https://zhuanlan.zhihu.com/p/111014448 ${tty_reset} +" +#选择一个brew下载源 +echo -n "${tty_green} +请选择一个下载brew本体的序号,例如中科大,输入1回车。 +源有时候不稳定,如果git克隆报错重新运行脚本选择源。 +1、中科大下载源 +2、清华大学下载源 +3、北京外国语大学下载源 ${tty_reset}" +if [[ $GIT_SPEED == "" ]]; then + echo -n "${tty_green} +4、腾讯下载源 +5、阿里巴巴下载源 ${tty_reset}" +fi +echo -n " +${tty_blue}请输入序号: " +read MY_DOWN_NUM +echo "${tty_reset}" +case $MY_DOWN_NUM in +"2") + echo " + 你选择了清华大学brew本体下载源 + " + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/ + #HomeBrew基础框架 + USER_BREW_GIT=https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git + #HomeBrew Core + USER_CORE_GIT=https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git + #HomeBrew Cask + USER_CASK_GIT=https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask.git + USER_CASK_FONTS_GIT=https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask-fonts.git + USER_CASK_DRIVERS_GIT=https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask-drivers.git +;; +"3") + echo " + 北京外国语大学brew本体下载源 + " + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.bfsu.edu.cn/homebrew-bottles + #HomeBrew基础框架 + USER_BREW_GIT=https://mirrors.bfsu.edu.cn/git/homebrew/brew.git + #HomeBrew Core + USER_CORE_GIT=https://mirrors.bfsu.edu.cn/git/homebrew/homebrew-core.git + #HomeBrew Cask + USER_CASK_GIT=https://mirrors.bfsu.edu.cn/git/homebrew/homebrew-cask.git + USER_CASK_FONTS_GIT=https://mirrors.bfsu.edu.cn/git/homebrew/homebrew-cask-fonts.git + USER_CASK_DRIVERS_GIT=https://mirrors.bfsu.edu.cn/git/homebrew/homebrew-cask-drivers.git +;; +"4") + echo " + 你选择了腾讯brew本体下载源 + " + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.cloud.tencent.com/homebrew-bottles + #HomeBrew基础框架 + USER_BREW_GIT=https://mirrors.cloud.tencent.com/homebrew/brew.git + #HomeBrew Core + USER_CORE_GIT=https://mirrors.cloud.tencent.com/homebrew/homebrew-core.git + #HomeBrew Cask + USER_CASK_GIT=https://mirrors.cloud.tencent.com/homebrew/homebrew-cask.git +;; +"5") + echo " + 你选择了阿里巴巴brew本体下载源 + " + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles + #HomeBrew基础框架 + USER_BREW_GIT=https://mirrors.aliyun.com/homebrew/brew.git + #HomeBrew Core + USER_CORE_GIT=https://mirrors.aliyun.com/homebrew/homebrew-core.git + #HomeBrew Cask + USER_CASK_GIT=https://mirrors.aliyun.com/homebrew/homebrew-cask.git +;; +*) + echo " + 你选择了中国科学技术大学brew本体下载源 + " + #HomeBrew 下载源 install + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles + #HomeBrew基础框架 + USER_BREW_GIT=https://mirrors.ustc.edu.cn/brew.git + #HomeBrew Core + USER_CORE_GIT=https://mirrors.ustc.edu.cn/homebrew-core.git + #HomeBrew Cask + USER_CASK_GIT=https://mirrors.ustc.edu.cn/homebrew-cask.git +;; +esac +echo -n "${tty_green}!!!此脚本将要删除之前的brew(包括它下载的软件),请自行备份。 +->是否现在开始执行脚本(N/Y) " +read MY_Del_Old +echo "${tty_reset}" +case $MY_Del_Old in +"y") +echo "--> 脚本开始执行" +;; +"Y") +echo "--> 脚本开始执行" +;; +*) +echo "你输入了 $MY_Del_Old ,自行备份老版brew和它下载的软件, 如果继续运行脚本应该输入Y或者y +" +exit 0 +;; +esac + + +if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then +#MAC + echo "${tty_yellow} Mac os设置开机密码方法: + (设置开机密码:在左上角苹果图标->系统偏好设置->"用户与群组"->更改密码) + (如果提示This incident will be reported. 在"用户与群组"中查看是否管理员) ${tty_reset}" +fi + +echo "==> 通过命令删除之前的brew、创建一个新的Homebrew文件夹 +${tty_cyan}请输入开机密码,输入过程不显示,输入完后回车${tty_reset}" + +sudo echo '开始执行' +#删除以前的Homebrew +RmCreate ${HOMEBREW_REPOSITORY} +RmAndCopy $HOMEBREW_CACHE +RmAndCopy $HOMEBREW_LOGS + +# 让环境暂时纯粹,脚本运行结束后恢复 +if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then + export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${HOMEBREW_REPOSITORY}/bin +fi +git --version +if [ $? -ne 0 ];then + + if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then + sudo rm -rf "/Library/Developer/CommandLineTools/" + echo "${tty_cyan}安装Git${tty_reset}后再运行此脚本,${tty_red}在系统弹窗中点击“安装”按钮 + 如果没有弹窗的老系统,需要自己下载安装:https://sourceforge.net/projects/git-osx-installer/ ${tty_reset}" + xcode-select --install + exit 0 + else + echo "${tty_red} 发现缺少git,开始安装,请输入Y ${tty_reset}" + sudo apt install git + fi +fi + +echo " +${tty_cyan}下载速度觉得慢可以ctrl+c或control+c重新运行脚本选择下载源${tty_reset} +==> 从 $USER_BREW_GIT 克隆Homebrew基本文件 +" +warning_if +sudo git clone ${GIT_SPEED} $USER_BREW_GIT ${HOMEBREW_REPOSITORY} +JudgeSuccess 尝试再次运行自动脚本选择其他下载源或者切换网络 out + +#依赖目录创建 授权等等 +CreateBrewLinkFolder + +echo '==> 创建brew的替身' +if [[ "${HOMEBREW_REPOSITORY}" != "${HOMEBREW_PREFIX}" ]]; then + find ${HOMEBREW_PREFIX}/bin -name brew -exec sudo rm -f {} \; + execute "ln" "-sf" "${HOMEBREW_REPOSITORY}/bin/brew" "${HOMEBREW_PREFIX}/bin/brew" +fi + +echo "==> 从 $USER_CORE_GIT 克隆Homebrew Core +${tty_cyan}此处如果显示Password表示需要再次输入开机密码,输入完后回车${tty_reset}" +sudo mkdir -p ${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-core +sudo git clone ${GIT_SPEED} $USER_CORE_GIT ${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-core/ +JudgeSuccess 尝试再次运行自动脚本选择其他下载源或者切换网络 out + +if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then +#MAC + echo "==> 从 $USER_CASK_GIT 克隆Homebrew Cask 图形化软件 + ${tty_cyan}此处如果显示Password表示需要再次输入开机密码,输入完后回车${tty_reset}" + sudo mkdir -p ${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-cask + sudo git clone ${GIT_SPEED} $USER_CASK_GIT ${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-cask/ + if [ $? -ne 0 ];then + sudo rm -rf ${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-cask + echo "${tty_red}尝试切换下载源或者切换网络,不过Cask组件非必须模块。可以忽略${tty_reset}" + else + echo "${tty_green}此步骤成功${tty_reset}" + + fi + + echo "==> 从 $USER_SERVICES_GIT 克隆Homebrew services 管理服务的启停 + " + sudo mkdir -p ${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-cask + sudo git clone ${GIT_SPEED} $USER_SERVICES_GIT ${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-services/ + JudgeSuccess +else +#Linux + echo "${tty_yellow} Linux 不支持Cask图形化软件下载 此步骤跳过${tty_reset}" +fi +echo '==> 配置国内镜像源HOMEBREW BOTTLE' + +#判断下mac os终端是Bash还是zsh +case "$SHELL" in + */bash*) + if [[ -r "$HOME/.bash_profile" ]]; then + shell_profile="${HOME}/.bash_profile" + else + shell_profile="${HOME}/.profile" + fi + ;; + */zsh*) + shell_profile="${HOME}/.zprofile" + ;; + *) + shell_profile="${HOME}/.profile" + ;; +esac + +if [[ -n "${HOMEBREW_ON_LINUX-}" ]]; then + #Linux + shell_profile="/etc/profile" +fi + +if [[ -f ${shell_profile} ]]; then + AddPermission ${shell_profile} +fi +#删除之前的环境变量 +if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then + #Mac + sed -i "" "/ckbrew/d" ${shell_profile} +else + #Linux + sed -i "/ckbrew/d" ${shell_profile} +fi + +#选择一个homebrew-bottles下载源 +echo -n "${tty_green} + + Brew本体已经安装成功,接下来配置国内源。 + +请选择今后brew install的时候访问那个国内镜像,例如阿里巴巴,输入5回车。 + +1、中科大国内源 +2、清华大学国内源 +3、北京外国语大学国内源 +4、腾讯国内源 +5、阿里巴巴国内源 ${tty_reset}" + +echo -n " +${tty_blue}请输入序号: " +read MY_DOWN_NUM +echo "${tty_reset}" +case $MY_DOWN_NUM in +"2") + echo " + 你选择了清华大学国内源 + " + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/ +;; +"3") + echo " + 北京外国语大学国内源 + " + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.bfsu.edu.cn/homebrew-bottles +;; +"4") + echo " + 你选择了腾讯国内源 + " + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.cloud.tencent.com/homebrew-bottles +;; +"5") + echo " + 你选择了阿里巴巴国内源 + " + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles +;; +*) + echo " + 你选择了中国科学技术大学国内源 + " + #HomeBrew 下载源 install + USER_HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles +;; +esac + +#写入环境变量到文件 +echo " + + 环境变量写入->${shell_profile} + +" + +echo " + export HOMEBREW_BOTTLE_DOMAIN=${USER_HOMEBREW_BOTTLE_DOMAIN} #ckbrew + eval \$(${HOMEBREW_REPOSITORY}/bin/brew shellenv) #ckbrew +" >> ${shell_profile} +JudgeSuccess +source "${shell_profile}" +if [ $? -ne 0 ];then + echo "${tty_red}发现错误,${shell_profile} 文件中有错误,建议根据上一句提示修改; + 否则会导致提示 permission denied: brew${tty_reset}" +fi + +AddPermission ${HOMEBREW_REPOSITORY} + +if [[ -n "${HOMEBREW_ON_LINUX-}" ]]; then + #检测linux curl是否有安装 + echo "${tty_red}-检测curl是否安装 留意是否需要输入Y${tty_reset}" + curl -V + if [ $? -ne 0 ];then + sudo apt-get install curl + if [ $? -ne 0 ];then + sudo yum install curl + if [ $? -ne 0 ];then + echo '失败 请自行安装curl 可以参考https://www.howtoing.com/install-curl-in-linux' + error_game_over + fi + fi + fi +fi + +echo ' +==> 安装完成,brew版本 +' +brew -v +if [ $? -ne 0 ];then + echo '发现错误,自动修复一次!' + rm -rf $HOMEBREW_CACHE + export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${HOMEBREW_REPOSITORY}/bin + brew update-reset + brew -v + if [ $? -ne 0 ];then + error_game_over + fi +else + echo "${tty_green}Brew前期配置成功${tty_reset}" +fi + +#brew 3.1.2版本 修改了很多地址,都写死在了代码中,没有调用环境变量。。额。。 +#ruby下载需要改官方文件 +ruby_URL_file=$HOMEBREW_REPOSITORY/Library/Homebrew/cmd/vendor-install.sh + +#判断Mac系统版本 +if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then + if version_gt "$macos_version" "10.14"; then + echo "电脑系统版本:$macos_version" + else + echo "${tty_red}检测到你不是最新系统,会有一些报错,请稍等Ruby下载安装;${tty_reset} + " + fi + + if [[ -f ${ruby_URL_file} ]]; then + sed -i "" "s/ruby_URL=/ruby_URL=\"https:\/\/mirrors.tuna.tsinghua.edu.cn\/homebrew-bottles\/bottles-portable-ruby\/\$ruby_FILENAME\" \#/g" $ruby_URL_file + fi +else + if [[ -f ${ruby_URL_file} ]]; then + sed -i "s/ruby_URL=/ruby_URL=\"https:\/\/mirrors.tuna.tsinghua.edu.cn\/linuxbrew-bottles\/bottles-portable-ruby\/\$ruby_FILENAME\" \#/g" $ruby_URL_file + fi +fi + +brew services cleanup + +if [[ $GIT_SPEED == "" ]];then + echo ' + ==> brew update-reset + ' + brew update-reset + if [[ $? -ne 0 ]];then + brew config + error_game_over + exit 0 + fi +else + #极速模式提示Update修复方法 + echo " +${tty_red} 极速版本安装完成,${tty_reset} install功能正常,如果需要update功能请自行运行下面三句命令 +git -C ${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-core fetch --unshallow +git -C ${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-cask fetch --unshallow +brew update-reset + " +fi + +echo " + ${tty_green}Brew自动安装程序运行完成${tty_reset} + ${tty_green}国内地址已经配置完成${tty_reset} + + 桌面的Old_Homebrew文件夹,大致看看没有你需要的可以删除。 + + 初步介绍几个brew命令 +本地软件库列表:brew ls +查找软件:brew search google(其中google替换为要查找的关键字) +查看brew版本:brew -v 更新brew版本:brew update +安装cask软件:brew install --cask firefox 把firefox换成你要安装的 + ${tty_green} + 欢迎右键点击下方地址-打开URL 来给点个赞${tty_reset} + ${tty_underline} https://zhuanlan.zhihu.com/p/111014448 ${tty_reset} +" + +if [[ -z "${HOMEBREW_ON_LINUX-}" ]]; then + #Mac + echo "${tty_red} 安装成功 但还需要重启终端 或者 运行${tty_bold} source ${shell_profile} ${tty_reset} ${tty_red}否则可能无法使用${tty_reset} + " +else + #Linux + echo "${tty_red} Linux需要重启电脑 或者暂时运行${tty_bold} source ${shell_profile} ${tty_reset} ${tty_red}否则可能无法使用${tty_reset} + " +fi diff --git a/etc/images/blue-horizon.png b/etc/images/blue-horizon.png new file mode 100644 index 0000000000000000000000000000000000000000..465d0010cff62ef6d772e86fbb7887190c392b03 Binary files /dev/null and b/etc/images/blue-horizon.png differ diff --git a/etc/images/blue-vertical.png b/etc/images/blue-vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..c95b3fe1da3cd16bb3232bb01b96754314c22e7d Binary files /dev/null and b/etc/images/blue-vertical.png differ diff --git a/etc/images/color-horizon.png b/etc/images/color-horizon.png new file mode 100644 index 0000000000000000000000000000000000000000..5882e8d8b2d13f3c09103badd7ed215e86cf66db Binary files /dev/null and b/etc/images/color-horizon.png differ diff --git a/etc/images/color-verital.png b/etc/images/color-verital.png new file mode 100644 index 0000000000000000000000000000000000000000..b07cf9d822c5ae62c62a99c55155b5bf65a4f378 Binary files /dev/null and b/etc/images/color-verital.png differ diff --git a/etc/images/favicon.ico b/etc/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d225de9d87e9573c146a854213c9c6bd674dea30 Binary files /dev/null and b/etc/images/favicon.ico differ diff --git a/etc/images/favicon.png b/etc/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d18457353a5904380338580a7ad548a018170071 Binary files /dev/null and b/etc/images/favicon.png differ diff --git a/etc/images/white-horizon.png b/etc/images/white-horizon.png new file mode 100644 index 0000000000000000000000000000000000000000..7b206eeec9a0d7d3bad15cfa0cd1b2e773b5409c Binary files /dev/null and b/etc/images/white-horizon.png differ diff --git a/etc/images/white-vertical.png b/etc/images/white-vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..8299f08b4d1e05fea3a7a3daf454c87c7380b02e Binary files /dev/null and b/etc/images/white-vertical.png differ diff --git a/etc/omnivirt-win.conf b/etc/omnivirt-win.conf new file mode 100644 index 0000000000000000000000000000000000000000..423681ab919544d283a7a7d43ce87a450ab5104a --- /dev/null +++ b/etc/omnivirt-win.conf @@ -0,0 +1,6 @@ +[default] +log_dir = D:\workdir\logs +debug = True +work_dir = D:\workdir +image_dir = images +instance_dir = instances \ No newline at end of file diff --git a/etc/omnivirt.conf b/etc/omnivirt.conf new file mode 100644 index 0000000000000000000000000000000000000000..a101a510a7e4508fa09797c0573d0299cf152f04 --- /dev/null +++ b/etc/omnivirt.conf @@ -0,0 +1,10 @@ +[default] +log_dir = +work_dir = +wget_dir = +qemu_dir = +debug = True + +[vm] +cpu_num = 1 +memory = 1024 \ No newline at end of file diff --git a/install.spec b/install.spec new file mode 100644 index 0000000000000000000000000000000000000000..cac4aec15a0e8ccc5ab82c9791ec42e9b0e9ab8d --- /dev/null +++ b/install.spec @@ -0,0 +1,44 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['omnivirt/install.py'], + pathex=[], + binaries=[('dist/omnivirt', './etc')], + datas=[('etc/omnivirt.conf', './etc'), ('resources/qemu/edk2-aarch64-code.fd', './etc'), ('resources/qemu/edk2-x86_64-code.fd', './etc')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + 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='install', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/omnivirt/__init__.py b/omnivirt/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/omnivirt/__pycache__/__init__.cpython-310.pyc b/omnivirt/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54a26528c7bd3e1d90cc93d1ff93fc2a00ef4c2e Binary files /dev/null and b/omnivirt/__pycache__/__init__.cpython-310.pyc differ diff --git a/omnivirt/backends/__init__.py b/omnivirt/backends/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/omnivirt/backends/__pycache__/__init__.cpython-310.pyc b/omnivirt/backends/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cc9e70986476b2f6e86491b52a66164759458bd Binary files /dev/null and b/omnivirt/backends/__pycache__/__init__.cpython-310.pyc differ diff --git a/omnivirt/backends/mac/image_handler.py b/omnivirt/backends/mac/image_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..c11f03dec66bee18cf347d1108e741b10bcc167a --- /dev/null +++ b/omnivirt/backends/mac/image_handler.py @@ -0,0 +1,118 @@ +import copy +import lzma +import wget +import os +import subprocess +import shutil +import ssl + +from omnivirt.utils import constants +from omnivirt.utils import utils as omni_utils +from omnivirt.utils import objs + + +ssl._create_default_https_context = ssl._create_unverified_context + + +class MacImageHandler(object): + + def __init__(self, conf, work_dir, image_dir, image_record_file, + logger, base_dir) -> None: + self.conf = conf + self.work_dir = work_dir + self.image_dir = image_dir + self.image_record_file = image_record_file + self.base_dir = base_dir + self.wget_bin = conf.conf.get('default', 'wget_dir') + self.LOG = logger + + + def download_and_transform(self, images, img_to_download): + + # Download the image + img_name = wget.filename_from_url(images['remote'][img_to_download]['path']) + img_dict = copy.deepcopy(images['remote'][img_to_download]) + + if not os.path.exists(os.path.join(self.image_dir, img_name)): + self.LOG.debug(f'Downloading image: {img_to_download} from remote repo ...') + img_dict['location'] = constants.IMAGE_LOCATION_LOCAL + img_dict['status'] = constants.IMAGE_STATUS_DOWNLOADING + images['local'][img_to_download] = img_dict + omni_utils.save_json_data(self.image_record_file, images) + + download_cmd = [self.wget_bin, images['remote'][img_to_download]['path'], + '-O', os.path.join(self.image_dir, img_name), '--no-check-certificate'] + self.LOG.debug(' '.join(download_cmd)) + subprocess.call(' '.join(download_cmd), shell=True) + #wget.download(url=images['remote'][img_to_download]['path'], out=os.path.join(self.image_dir, img_name), bar=None) + self.LOG.debug(f'Image: {img_to_download} succesfully downloaded from remote repo ...') + + # Decompress the image + self.LOG.debug(f'Decompressing image file: {img_name} ...') + qcow2_name = img_name[:-3] + with open(os.path.join(self.image_dir, img_name), 'rb') as pr, open(os.path.join(self.image_dir, qcow2_name), 'wb') as pw: + data = pr.read() + data_dec = lzma.decompress(data) + pw.write(data_dec) + + self.LOG.debug(f'Cleanup temp files ...') + os.remove(os.path.join(self.image_dir, img_name)) + + # Record local image + img_dict['status'] = constants.IMAGE_STATUS_READY + img_dict['path'] = os.path.join(self.image_dir, qcow2_name) + images['local'][img_to_download] = img_dict + omni_utils.save_json_data(self.image_record_file, images) + self.LOG.debug(f'Image: {img_to_download} is ready ...') + + + def delete_image(self, images, img_to_delete): + if img_to_delete not in images['local'].keys(): + return 1 + else: + return self._delete_image(images, img_to_delete) + + def _delete_image(self, images, img_to_delete): + img_path = images['local'][img_to_delete]['path'] + # TODO: Raise error message if image file not exists + if os.path.exists(img_path): + self.LOG.debug(f'Deleting: {img_path} ...') + os.remove(img_path) + + self.LOG.debug(f'Deleting: {img_to_delete} from image database ...') + del images['local'][img_to_delete] + omni_utils.save_json_data(self.image_record_file, images) + + return 0 + + def load_and_transform(self, images, img_to_load, path, fmt, update=False): + + if update: + self._delete_image(images, img_to_load) + + image = objs.Image() + image.name = img_to_load + image.path = '' + image.location = constants.IMAGE_LOCATION_LOCAL + image.status = constants.IMAGE_STATUS_LOADING + images['local'][image.name] = image.to_dict() + omni_utils.save_json_data(self.image_record_file, images) + + if fmt == 'qcow2': + qcow2_name = f'{img_to_load}.qcow2' + shutil.copyfile(path, os.path.join(self.image_dir, qcow2_name)) + else: + # Decompress the image + self.LOG.debug(f'Decompressing image file: {path} ...') + qcow2_name = f'{img_to_load}.qcow2' + with open(path, 'rb') as pr, open(os.path.join(self.image_dir, qcow2_name), 'wb') as pw: + data = pr.read() + data_dec = lzma.decompress(data) + pw.write(data_dec) + + # Record local image + image.path = os.path.join(self.image_dir, qcow2_name) + image.status = constants.IMAGE_STATUS_READY + images['local'][image.name] = image.to_dict() + omni_utils.save_json_data(self.image_record_file, images) + self.LOG.debug(f'Image: {qcow2_name} is ready ...') diff --git a/omnivirt/backends/mac/instance_handler.py b/omnivirt/backends/mac/instance_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..a7aa10d83d15165522bd3d47290cbb142133880a --- /dev/null +++ b/omnivirt/backends/mac/instance_handler.py @@ -0,0 +1,177 @@ +import os +import psutil +import shutil +import signal +import subprocess +import sys +import time + +from oslo_utils import uuidutils + +from omnivirt.utils import constants +from omnivirt.utils import utils as omni_utils +from omnivirt.utils import objs +from omnivirt.backends.mac import qemu + + +class MacInstanceHandler(object): + + def __init__(self, conf, work_dir, instance_dir, image_dir, + image_record_file, logger, base_dir) -> None: + self.conf = conf + self.work_dir = work_dir + self.instance_dir = instance_dir + self.instance_record_file = os.path.join(instance_dir, 'instances.json') + self.image_dir = image_dir + self.image_record_file = image_record_file + self.driver = qemu.QemuDriver(self.conf, logger) + self.running_instances = {} + self.instance_pids = [] + self.base_dir = base_dir + self.LOG = logger + + def list_instances(self): + instances = omni_utils.load_json_data(self.instance_record_file)['instances'] + vm_list = [] + + for instance in instances.values(): + vm = objs.Instance(name=instance['name']) + vm.uuid = instance['uuid'] + vm.mac = instance['mac_address'] + vm.info = None + vm.vm_state = self._check_vm_state(instance) + if not instance['ip_address']: + ip_address = self._parse_ip_addr(vm.mac) + vm.ip = ip_address + else: + vm.ip = instance['ip_address'] + vm.image = instance['image'] + vm_list.append(vm) + + return vm_list + + def _check_vm_state(self, instance): + if instance['identification']['type'] == 'pid': + instance_pid = instance['identification']['id'] + if instance_pid in psutil.pids() and \ + psutil.Process(instance_pid).status() == 'running' and \ + psutil.Process(instance_pid).name().startswith('qemu'): + return constants.VM_STATE_MAP[2] + else: + return constants.VM_STATE_MAP[3] + else: + return constants.VM_STATE_MAP[99] + + def _parse_ip_addr(self, mac_addr): + ip = '' + cmd = 'arp -a' + start_time = time.time() + while(ip == '' and time.time() - start_time < 20): + pr = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE) + arp_result = pr.stdout.decode('utf-8').split('\n') + founded = False + for str in arp_result: + # The result for 'arp -a' in MacOS is different with Linux, it erase + # the first 0 if the first digit is 0 for this mac section, add it + # back before compare + try: + arp_ip = str.split(' ')[1].replace("(", "").replace(")", "") + mac = str.split(' ')[3].replace("(", "").replace(")", "") + except IndexError: + continue + mac_list = mac.split(':') + for i in range(0, len(mac_list)): + if len(mac_list[i]) == 1: + mac_list[i] = '0' + mac_list[i] + mac_0 = ':'.join(mac_list) + if mac_addr == mac_0: + ip = arp_ip + founded = True + break + if founded: + break + + return ip + + def check_names(self, name, all_instances): + try: + all_instances['instances'][name] + return 1 + except KeyError: + return 0 + + def create_instance(self, name, image_id, instance_record, all_instances, all_images): + # Create dir for the instance + vm_uuid = uuidutils.generate_uuid() + vm_dict = { + 'name': name, + 'uuid': vm_uuid, + 'image': image_id, + 'vm_state': constants.VM_STATE_MAP[99], + 'ip_address': 'N/A', + 'mac_address': omni_utils.generate_mac(), + 'identification': { + 'type': 'pid', + 'id': None + } + } + + instance_path = os.path.join(self.instance_dir, name) + os.makedirs(instance_path) + img_path = all_images['local'][image_id]['path'] + + root_disk_path = shutil.copyfile(img_path, os.path.join(instance_path, image_id + '.qcow2')) + + vm_process = self.driver.create_vm(name, vm_uuid, vm_dict['mac_address'], root_disk_path) + + self.running_instances[vm_process.pid] = vm_process + self.instance_pids.append(vm_process.pid) + + vm_dict['identification']['id'] = vm_process.pid + + vm_ip = self._parse_ip_addr(vm_dict['mac_address']) + vm_dict['ip_address'] = vm_ip + + instance_record_dict = { + 'name': name, + 'uuid': vm_dict['uuid'], + 'image': image_id, + 'path': instance_path, + 'mac_address': vm_dict['mac_address'], + 'ip_address': vm_dict['ip_address'], + 'identification': vm_dict['identification'] + } + + all_instances['instances'][name] = instance_record_dict + omni_utils.save_json_data(instance_record, all_instances) + + return { + 'name': name, + 'vm_state': self._check_vm_state(vm_dict), + 'image': image_id, + 'ip_address': vm_dict['ip_address'] + } + + def delete_instance(self, name, instance_record, all_instances): + # Delete instance process + instance = all_instances['instances'][name] + if instance['identification']['type'] == 'pid': + instance_pid = instance['identification']['id'] + if instance_pid in psutil.pids() and \ + psutil.Process(instance_pid).is_running(): + psutil.Process(instance_pid).kill() + self.LOG.debug(f'Instance: {name} with PID {instance_pid} succesfully killed ...') + else: + self.LOG.debug(f'Instance: {name} with PID {instance_pid} already stopped, skip ...') + else: + self.LOG.debug(f'Instance: {name} unable to handled, skip ...') + + # Cleanup files and records + instance_dir = instance['path'] + shutil.rmtree(instance_dir) + del all_instances['instances'][name] + + omni_utils.save_json_data(instance_record, all_instances) + + return 0 + diff --git a/omnivirt/backends/mac/qemu.py b/omnivirt/backends/mac/qemu.py new file mode 100644 index 0000000000000000000000000000000000000000..732bd3f459f96f06c8acf39f0c13457c13eb7495 --- /dev/null +++ b/omnivirt/backends/mac/qemu.py @@ -0,0 +1,30 @@ +import platform +import subprocess +import os + +from omnivirt.utils import constants + + +class QemuDriver(object): + + def __init__(self, conf, logger) -> None: + host_arch_raw = platform.uname().machine + host_arch = constants.ARCH_MAP[host_arch_raw] + self.qemu_bin = conf.conf.get('default', 'qemu_dir') + self.uefi_file = os.path.join('/Library/Application\ Support/org.openeuler.omnivirt/','edk2-' + host_arch + '-code.fd') + self.uefi_params = ',if=pflash,format=raw,readonly=on' + self.vm_cpu = conf.conf.get('vm', 'cpu_num') + self.vm_ram = conf.conf.get('vm', 'memory') + self.LOG = logger + + def create_vm(self, vm_name, vm_uuid, vm_mac, vm_root_disk): + qemu_cmd = [ + self.qemu_bin, '-machine', 'virt,highmem=off', '-name', vm_name, '-uuid', vm_uuid, + '-accel hvf', '-drive', 'file=' + self.uefi_file + self.uefi_params, '-cpu host', + '-nic', 'vmnet-shared,model=virtio-net-pci,mac=' + vm_mac, + '-drive', 'file=' + vm_root_disk, '-device', 'virtio-scsi-pci,id=scsi0', + '-smp', self.vm_cpu, '-m', self.vm_ram + 'M', '-monitor none -chardev null,id=char0', + '-serial chardev:char0 -nographic'] + self.LOG.debug(' '.join(qemu_cmd)) + instance_process = subprocess.Popen(' '.join(qemu_cmd), shell=True) + return instance_process diff --git a/omnivirt/backends/win/__init__.py b/omnivirt/backends/win/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/omnivirt/backends/win/__pycache__/__init__.cpython-310.pyc b/omnivirt/backends/win/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b60b7386e1a82169ae79dfcc725df6f3ad15ec9b Binary files /dev/null and b/omnivirt/backends/win/__pycache__/__init__.cpython-310.pyc differ diff --git a/omnivirt/backends/win/image_handler.py b/omnivirt/backends/win/image_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..be040fe857420506afd0ed8db3e902a3e1e3ac6f --- /dev/null +++ b/omnivirt/backends/win/image_handler.py @@ -0,0 +1,123 @@ +import copy +import lzma +import wget +import os +import shutil +import ssl + +from omnivirt.backends.win import powershell +from omnivirt.utils import constants +from omnivirt.utils import utils as omni_utils +from omnivirt.utils import objs + + +ssl._create_default_https_context = ssl._create_unverified_context + + +class WinImageHandler(object): + + def __init__(self, conf, work_dir, image_dir, image_record_file, logger) -> None: + self.conf = conf + self.work_dir = work_dir + self.image_dir = image_dir + self.image_record_file = image_record_file + self.LOG = logger + + def download_and_transform(self, images, img_to_download): + + # Download the image + img_name = wget.filename_from_url(images['remote'][img_to_download]['path']) + img_dict = copy.deepcopy(images['remote'][img_to_download]) + + if not os.path.exists(os.path.join(self.image_dir, img_name)): + self.LOG.debug(f'Downloading image: {img_to_download} from remote repo ...') + img_dict['location'] = constants.IMAGE_LOCATION_LOCAL + img_dict['status'] = constants.IMAGE_STATUS_DOWNLOADING + images['local'][img_to_download] = img_dict + omni_utils.save_json_data(self.image_record_file, images) + wget.download(url=images['remote'][img_to_download]['path'], out=os.path.join(self.image_dir, img_name), bar=None) + self.LOG.debug(f'Image: {img_to_download} succesfully downloaded from remote repo ...') + + # Decompress the image + self.LOG.debug(f'Decompressing image file: {img_name} ...') + qcow2_name = img_name[:-3] + with open(os.path.join(self.image_dir, img_name), 'rb') as pr, open(os.path.join(self.image_dir, qcow2_name), 'wb') as pw: + data = pr.read() + data_dec = lzma.decompress(data) + pw.write(data_dec) + + # Convert the qcow2 img to vhdx + vhdx_name = img_to_download + '.vhdx' + self.LOG.debug(f'Converting image file: {img_name} to {vhdx_name} ...') + with powershell.PowerShell('GBK') as ps: + cmd = 'qemu-img convert -O vhdx {0} {1}' + outs, errs = ps.run(cmd.format(os.path.join(self.image_dir, qcow2_name), os.path.join(self.image_dir, vhdx_name))) + + self.LOG.debug(f'Cleanup temp files ...') + os.remove(os.path.join(self.image_dir, qcow2_name)) + + # Record local image + img_dict['status'] = constants.IMAGE_STATUS_READY + img_dict['path'] = os.path.join(self.image_dir, vhdx_name) + images['local'][img_to_download] = img_dict + omni_utils.save_json_data(self.image_record_file, images) + self.LOG.debug(f'Image: {img_to_download} is ready ...') + + def delete_image(self, images, img_to_delete): + if img_to_delete not in images['local'].keys(): + return 1 + else: + return self._delete_image(images, img_to_delete) + + def _delete_image(self, images, img_to_delete): + img_path = images['local'][img_to_delete]['path'] + if os.path.exists(img_path): + self.LOG.debug(f'Deleting: {img_path} ...') + os.remove(img_path) + + self.LOG.debug(f'Deleting: {img_to_delete} from image database ...') + del images['local'][img_to_delete] + omni_utils.save_json_data(self.image_record_file, images) + + return 0 + + def load_and_transform(self, images, img_to_load, path, fmt, update=False): + + if update: + self._delete_image(images, img_to_load) + + image = objs.Image() + image.name = img_to_load + image.path = '' + image.location = constants.IMAGE_LOCATION_LOCAL + image.status = constants.IMAGE_STATUS_LOADING + images['local'][image.name] = image.to_dict() + omni_utils.save_json_data(self.image_record_file, images) + + if fmt == 'qcow2': + qcow2_name = f'{img_to_load}.qcow2' + shutil.copyfile(path, os.path.join(self.image_dir, qcow2_name)) + else: + # Decompress the image + self.LOG.debug(f'Decompressing image file: {path} ...') + qcow2_name = f'{img_to_load}.qcow2' + with open(path, 'rb') as pr, open(os.path.join(self.image_dir, qcow2_name), 'wb') as pw: + data = pr.read() + data_dec = lzma.decompress(data) + pw.write(data_dec) + + # Convert the qcow2 img to vhdx + vhdx_name = img_to_load + '.vhdx' + self.LOG.debug(f'Converting image file: {qcow2_name} to {vhdx_name} ...') + with powershell.PowerShell('GBK') as ps: + cmd = 'qemu-img convert -O vhdx {0} {1}' + outs, errs = ps.run(cmd.format(os.path.join(self.image_dir, qcow2_name), os.path.join(self.image_dir, vhdx_name))) + self.LOG.debug(f'Cleanup temp files ...') + os.remove(os.path.join(self.image_dir, qcow2_name)) + + # Record local image + image.path = os.path.join(self.image_dir, vhdx_name) + image.status = constants.IMAGE_STATUS_READY + images['local'][image.name] = image.to_dict() + omni_utils.save_json_data(self.image_record_file, images) + self.LOG.debug(f'Image: {vhdx_name} is ready ...') diff --git a/omnivirt/backends/win/instance_handler.py b/omnivirt/backends/win/instance_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..bbcc0e3837d8e137ae2d7d797e5559deb200b217 --- /dev/null +++ b/omnivirt/backends/win/instance_handler.py @@ -0,0 +1,200 @@ +import os +import shutil +import time + +from oslo_utils import uuidutils +from os_win import constants as os_win_const +from os_win import exceptions as os_win_exc + +from omnivirt.backends.win import powershell +from omnivirt.backends.win import vmops +from omnivirt.utils import constants +from omnivirt.utils import utils as omni_utils +from omnivirt.utils import objs + + +_vmops = vmops.VMOps() + +class WinInstanceHandler(object): + + def __init__(self, conf, work_dir, instance_dir, image_dir, image_record_file, logger) -> None: + self.conf = conf + self.work_dir = work_dir + self.instance_dir = instance_dir + self.instance_record_file = os.path.join(instance_dir, 'instances.json') + self.image_dir = image_dir + self.image_record_file = image_record_file + self.LOG = logger + + def list_instances(self): + vms = _vmops.list_instances() + return vms + + def check_names(self, name, all_instances): + ret = _vmops.check_all_instance_names(name) + return ret + + def create_instance(self, name, image_id, instance_record, all_instances, all_images): + # Create dir for the instance + vm_dict = { + 'name': name, + 'uuid': uuidutils.generate_uuid(), + 'image': image_id, + 'vm_state': constants.VM_STATE_MAP[99], + 'ip_address': 'N/A', + 'mac_address': 'N/A', + 'identification': { + 'type': 'name', + 'id': name + } + } + + instance_path = os.path.join(self.instance_dir, name) + os.makedirs(instance_path) + img_path = all_images['local'][image_id]['path'] + + root_disk_path = shutil.copyfile(img_path, os.path.join(instance_path, image_id + '.vhdx')) + _vmops.build_and_run_vm(name, vm_dict['uuid'], image_id, False, 2, instance_path, root_disk_path) + + info = _vmops.get_info(name) + vm_dict['vm_state'] = constants.VM_STATE_MAP[info['EnabledState']] + ip = _vmops.get_instance_ip_addr(name) + if ip: + vm_dict['ip_address'] = ip + + instance_record_dict = { + 'name': name, + 'uuid': vm_dict['uuid'], + 'image': image_id, + 'path': instance_path, + 'mac_address': vm_dict['mac_address'], + 'ip_address': vm_dict['ip_address'], + 'identification': vm_dict['identification'] + } + + all_instances['instances'][name] = instance_record_dict + omni_utils.save_json_data(instance_record, all_instances) + + return { + 'name': name, + 'vm_state': vm_dict['vm_state'], + 'image': image_id, + 'ip_address': vm_dict['ip_address'] + } + + def delete_instance(self, name, instance_record, all_instances): + # Delete instance + _vmops.delete_instance(name) + + # Cleanup files and records + instance_dir = all_instances['instances'][name]['path'] + shutil.rmtree(instance_dir) + del all_instances['instances'][name] + + omni_utils.save_json_data(instance_record, all_instances) + + return 0 + + def reboot(self, instance, reboot_type='soft'): + """Reboot the specified instance.""" + self.LOG.debug("Rebooting instance", instance=instance) + + if reboot_type == 'soft': + if self._soft_shutdown(instance): + self.power_on(instance) + return + + self._set_vm_state(instance, + os_win_const.HYPERV_VM_STATE_REBOOT) + + def _soft_shutdown(self, instance, + timeout=5, + retry_interval=1): + """Perform a soft shutdown on the VM. + + :return: True if the instance was shutdown within time limit, + False otherwise. + """ + self.LOG.debug("Performing Soft shutdown on instance", instance=instance) + + while timeout > 0: + # Perform a soft shutdown on the instance. + # Wait maximum timeout for the instance to be shutdown. + # If it was not shutdown, retry until it succeeds or a maximum of + # time waited is equal to timeout. + wait_time = min(retry_interval, timeout) + try: + self.LOG.debug("Soft shutdown instance, timeout remaining: %d", + timeout, instance=instance) + self._vmutils.soft_shutdown_vm(instance.name) + if self._wait_for_power_off(instance.name, wait_time): + self.LOG.info("Soft shutdown succeeded.", + instance=instance) + return True + except os_win_exc.HyperVException as e: + # Exception is raised when trying to shutdown the instance + # while it is still booting. + self.LOG.debug("Soft shutdown failed: %s", e, instance=instance) + time.sleep(wait_time) + + timeout -= retry_interval + + self.LOG.warning("Timed out while waiting for soft shutdown.", + instance=instance) + return False + + def pause(self, instance): + """Pause VM instance.""" + self.LOG.debug("Pause instance", instance=instance) + self._set_vm_state(instance, + os_win_const.HYPERV_VM_STATE_PAUSED) + + def unpause(self, instance): + """Unpause paused VM instance.""" + self.LOG.debug("Unpause instance", instance=instance) + self._set_vm_state(instance, + os_win_const.HYPERV_VM_STATE_ENABLED) + + def suspend(self, instance): + """Suspend the specified instance.""" + self.LOG.debug("Suspend instance", instance=instance) + self._set_vm_state(instance, + os_win_const.HYPERV_VM_STATE_SUSPENDED) + + def resume(self, instance): + """Resume the suspended VM instance.""" + self.LOG.debug("Resume instance", instance=instance) + self._set_vm_state(instance, + os_win_const.HYPERV_VM_STATE_ENABLED) + + def power_off(self, instance, timeout=0, retry_interval=0): + """Power off the specified instance.""" + self.LOG.debug("Power off instance", instance=instance) + + # We must make sure that the console log workers are stopped, + # otherwise we won't be able to delete or move the VM log files. + self._serial_console_ops.stop_console_handler(instance.name) + + if retry_interval <= 0: + retry_interval = SHUTDOWN_TIME_INCREMENT + + try: + if timeout and self._soft_shutdown(instance, + timeout, + retry_interval): + return + + self._set_vm_state(instance, + os_win_const.HYPERV_VM_STATE_DISABLED) + except os_win_exc.HyperVVMNotFoundException: + # The manager can call the stop API after receiving instance + # power off events. If this is triggered when the instance + # is being deleted, it might attempt to power off an unexisting + # instance. We'll just pass in this case. + self.LOG.debug("Instance not found. Skipping power off", + instance=instance) + + def power_on(self, instance): + """Power on the specified instance.""" + self.LOG.debug("Power on instance", instance=instance) + self._set_vm_state(instance, os_win_const.HYPERV_VM_STATE_ENABLED) diff --git a/omnivirt/backends/win/powershell.py b/omnivirt/backends/win/powershell.py new file mode 100644 index 0000000000000000000000000000000000000000..ac6a60d89e26b004a12905b44d9c7b1f39554ec8 --- /dev/null +++ b/omnivirt/backends/win/powershell.py @@ -0,0 +1,49 @@ +import os +from glob import glob +import subprocess as sp + + +class PowerShell: + # from scapy + def __init__(self, coding, ): + cmd = [self._where('PowerShell.exe'), + "-NoLogo", "-NonInteractive", # Do not print headers + "-Command", "-"] # Listen commands from stdin + startupinfo = sp.STARTUPINFO() + startupinfo.dwFlags |= sp.STARTF_USESHOWWINDOW + self.popen = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE, stderr=sp.STDOUT, startupinfo=startupinfo) + self.coding = coding + + def __enter__(self): + return self + + def __exit__(self, a, b, c): + self.popen.kill() + + def run(self, cmd, timeout=15): + b_cmd = cmd.encode(encoding=self.coding) + try: + b_outs, errs = self.popen.communicate(b_cmd, timeout=timeout) + except sp.TimeoutExpired: + self.popen.kill() + b_outs, errs = self.popen.communicate() + outs = b_outs.decode(encoding=self.coding) + return outs, errs + + @staticmethod + def _where(filename, dirs=None, env="PATH"): + """Find file in current dir, in deep_lookup cache or in system path""" + if dirs is None: + dirs = [] + if not isinstance(dirs, list): + dirs = [dirs] + if glob(filename): + return filename + paths = [os.curdir] + os.environ[env].split(os.path.pathsep) + dirs + try: + return next(os.path.normpath(match) + for path in paths + for match in glob(os.path.join(path, filename)) + if match) + except (StopIteration, RuntimeError): + raise IOError("File not found: %s" % filename) diff --git a/omnivirt/backends/win/vmops.py b/omnivirt/backends/win/vmops.py new file mode 100644 index 0000000000000000000000000000000000000000..6882803cc2a3e2399e0c19fe9e02628d1d6d4da0 --- /dev/null +++ b/omnivirt/backends/win/vmops.py @@ -0,0 +1,181 @@ +import json + +from os_win import constants as os_win_const +from os_win import exceptions as os_win_exc +from os_win.utils.compute import vmutils10 +from os_win import utilsfactory +from oslo_utils import uuidutils + +from omnivirt.utils import objs +from omnivirt.utils import constants +from omnivirt.utils import utils as omni_utils +from omnivirt.backends.win import powershell + +SWITCH_NAME = 'Default Switch' + + +class VMUtils_omni(vmutils10.VMUtils10): + + def __init__(self) -> None: + super().__init__() + + def get_instance_notes(self, instance_name): + instance_notes = self._get_instance_notes(instance_name) + return instance_notes + + def get_vm_nic_uids(self, vm_name): + nics = self._get_vm_nics(vm_name) + return nics + +class VMOps(object): + _ROOT_DISK_CTRL_ADDR = 0 + + def __init__(self, virtapi=None): + self._virtapi = virtapi + self._vmutils = VMUtils_omni() + self._netutils = utilsfactory.get_networkutils() + self._hostutils = utilsfactory.get_hostutils() + + def _set_vm_state(self, instance, req_state): + instance_name = instance.name + self._vmutils.set_vm_state(instance_name, req_state) + + def list_instance_uuids(self): + instance_uuids = [] + for (instance_name, notes) in self._vmutils.list_instance_notes(): + if notes and uuidutils.is_uuid_like(notes[0]): + instance_uuids.append(str(notes[0])) + else: + pass + #LOG.debug("Notes not found or not resembling a GUID for " + # "instance: %s", instance_name) + return instance_uuids + + def check_all_instance_names(self, name): + instance_names = self._vmutils.list_instances() + if name in instance_names: + return 1 + else: + return 0 + + def list_instances(self): + instance_names = self._vmutils.list_instances() + vm_list = [] + for instance_name in instance_names: + vm = objs.Instance(name=instance_name) + meta = self.get_meta(instance_name) + if not meta or not meta['creator'] == 'omnivirt': + continue + else: + vm.metadata = meta + vm.uuid = meta['uuid'] + info = self.get_info(instance_name) + vm.info = info + vm.vm_state = constants.VM_STATE_MAP[info['EnabledState']] + ip_address = self.get_instance_ip_addr(instance_name) + vm.ip = ip_address + vm.image = meta['image'] + vm_list.append(vm) + + return vm_list + + + def get_instance_ip_addr(self, instance_name): + nic_name = instance_name + '_eth0' + nic = self.get_vm_nics(instance_name, nic_name) + mac_address = omni_utils.format_mac_addr(nic.Address) + with powershell.PowerShell('GBK') as ps: + outs, errs = ps.run('arp -a | findstr /i {}'.format(mac_address)) + ip_address = outs.strip(' ').split(' ')[0] + + return ip_address + + + def get_info(self, instance): + """Get information about the VM.""" + # LOG.debug("get_info called for instance", instance=instance) + + instance_name = instance + if not self._vmutils.vm_exists(instance_name): + raise # exception.InstanceNotFound(instance_id=instance.uuid) + + info = self._vmutils.get_vm_summary_info(instance_name) + + return info + + def create_vm(self, vm_name, vnuma_enabled, vm_gen, instance_path, + meta_data): + self._vmutils.create_vm(vm_name, + vnuma_enabled, + vm_gen, + instance_path, + [json.dumps(meta_data)]) + + def build_and_run_vm(self, vm_name, uuid, image_name, vnuma_enabled, vm_gen, instance_path, root_disk_path): + meta_data = { + 'uuid': uuid, + 'image': image_name, + 'creator': 'omnivirt' + } + + # Create an instance + self.create_vm(vm_name, vnuma_enabled, vm_gen, instance_path, meta_data) + # Create a scsi controller for this instance + self._vmutils.create_scsi_controller(vm_name) + # Attach the root disk to the driver + self.attach_disk(vm_name, root_disk_path, constants.DISK) + # Start the instance + self.power_up(vm_name) + nic_name = vm_name + '_eth0' + self.add_nic(vm_name, nic_name) + self.connect_vnic_to_switch(SWITCH_NAME, nic_name) + return 0 + + def get_meta(self, instance_name, expect_existing=False): + try: + instance_notes = self._vmutils.get_instance_notes(instance_name) + if instance_notes: + return json.loads(instance_notes[0]) + else: + return instance_notes + except os_win_exc.HyperVVMNotFoundException: + raise + + def delete_instance(self, vm_name): + # Stop the VM first. + self._vmutils.stop_vm_jobs(vm_name) + self._vmutils.set_vm_state(vm_name, os_win_const.HYPERV_VM_STATE_DISABLED) + self._vmutils.destroy_vm(vm_name) + + while(1): + if not self._vmutils.vm_exists(vm_name): + break + return 0 + + def get_vm_disks(self, vm_name): + return self._vmutils.get_vm_disks(vm_name) + + def attach_disk(self, instance_name, path, drive_type): + self._vmutils.attach_scsi_drive(instance_name, path, drive_type) + + def power_up(self, instance_name): + req_state = os_win_const.HYPERV_VM_STATE_ENABLED + self._vmutils.set_vm_state(instance_name, req_state) + + def add_nic(self, instance_name, nic_name): + self._vmutils.create_nic(instance_name, nic_name) + + def get_vm_nics(self, instance_name, nic_name): + return self._vmutils._get_nic_data_by_name(nic_name) + + def list_switch_ports(self, switch_name): + return self._netutils.get_switch_ports(switch_name) + + def connect_vnic_to_switch(self, switch_name, vnic_name): + self._netutils.connect_vnic_to_vswitch(switch_name, vnic_name) + + def get_switch_port(self, switch_name, port_id): + return self._netutils.get_port_by_id(port_id, switch_name) + + def get_host_ips(self): + return self._hostutils.get_local_ips() diff --git a/omnivirt/cli.py b/omnivirt/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..5d6217bd329fe4471d2dc76f4590be1fef6f5ca0 --- /dev/null +++ b/omnivirt/cli.py @@ -0,0 +1,144 @@ +import click +import prettytable as pt + +from omnivirt.grpcs import client +from omnivirt.utils import utils as omni_utils +from omnivirt.utils import exceptions + + +omnivirt_client = client.Client() + +# List all instances on the host +@click.command() +def list(): + + try: + ret = omnivirt_client.list_instances() + except Exception: + print('Calling to OmniVirtd daemon failed, please check OmniVirtd daemon status ...') + else: + tb = pt.PrettyTable() + + tb.field_names = ["Name", "Image", "State", "IP"] + + try: + for instance in ret['instances']: + tb.add_row( + [instance['name'], + instance['image'], + instance['vmState'], + instance['ipAddress']]) + except KeyError: + pass + + print(tb) + + +# List all usable images +@click.command() +def images(): + + try: + ret = omnivirt_client.list_images() + except Exception: + print('Calling to OmniVirtd daemon failed, please check OmniVirtd daemon status ...') + else: + tb = pt.PrettyTable() + + tb.field_names = ["Images", "Location", "Status"] + + for image in ret['images']: + tb.add_row( + [image['name'], image['location'], image['status']]) + + print(tb) + + +@click.command() +@click.argument('name') +def download_image(name): + + try: + ret = omnivirt_client.download_image(name) + except Exception: + print('Calling to OmniVirtd daemon failed, please check OmniVirtd daemon status ...') + else: + print(ret['msg']) + + +@click.command() +@click.argument('name') +@click.option('--path', help='Image file to load') +def load_image(name, path): + + try: + ret = omnivirt_client.load_image(name, path) + except Exception: + print('Calling to OmniVirtd daemon failed, please check OmniVirtd daemon status ...') + else: + print(ret['msg']) + + +@click.command() +@click.argument('name') +def delete_image(name): + + try: + ret = omnivirt_client.delete_image(name) + except Exception: + print('Calling to OmniVirtd daemon failed, please check OmniVirtd daemon status ...') + else: + print(ret['msg']) + + +@click.command() +@click.argument('name') +def delete_instance(name): + + try: + ret = omnivirt_client.delete_instance(name) + except Exception: + print('Calling to OmniVirtd daemon failed, please check OmniVirtd daemon status ...') + else: + print(ret['msg']) + +@click.command() +@click.argument('vm_name') +@click.option('--image', help='Image to build vm') +def launch(vm_name, image): + + try: + ret = omnivirt_client.create_instance(vm_name, image) + except Exception: + print('Calling to OmniVirtd daemon failed, please check OmniVirtd daemon status ...') + else: + + if ret['ret'] == 1: + tb = pt.PrettyTable() + tb.field_names = ["Name", "Image", "State", "IP"] + tb.add_row( + [ret['instance']['name'], + ret['instance']['image'], + ret['instance']['vmState'], + ret['instance']['ipAddress']]) + + print(tb) + + else: + print(ret['msg']) + + +@click.group() +def cli(): + pass + + +if __name__ == '__main__': + cli.add_command(list) + cli.add_command(images) + cli.add_command(download_image) + cli.add_command(load_image) + cli.add_command(launch) + cli.add_command(delete_image) + cli.add_command(delete_instance) + cli() \ No newline at end of file diff --git a/omnivirt/grpcs/__init__.py b/omnivirt/grpcs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/omnivirt/grpcs/__pycache__/__init__.cpython-310.pyc b/omnivirt/grpcs/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf26fdd57195799ee1da127e81050bcef443c378 Binary files /dev/null and b/omnivirt/grpcs/__pycache__/__init__.cpython-310.pyc differ diff --git a/omnivirt/grpcs/client.py b/omnivirt/grpcs/client.py new file mode 100644 index 0000000000000000000000000000000000000000..fe4c065ad2e78421cef3f130dc4effc968709377 --- /dev/null +++ b/omnivirt/grpcs/client.py @@ -0,0 +1,95 @@ +import grpc +import os + +from omnivirt.grpcs.omnivirt_grpc import images_pb2, images_pb2_grpc +from omnivirt.grpcs.omnivirt_grpc import instances_pb2, instances_pb2_grpc +from omnivirt.grpcs import images, instances +from omnivirt.utils import constants +from omnivirt.utils import utils as omnivirt_utils + + +class Client(object): + def __init__(self, channel_target=None): + if not channel_target: + channel_target = 'localhost:50052' + channel = grpc.insecure_channel(channel_target) + + images_client = images_pb2_grpc.ImageGrpcServiceStub(channel) + instances_client = instances_pb2_grpc.InstanceGrpcServiceStub(channel) + + self._images = images.Image(images_client) + self._instances = instances.Instance(instances_client) + + @omnivirt_utils.response2dict + def list_images(self, filters=None): + """ [IMAGE] List images + + :param filters(list): None + :return: dict -- list of images' info + """ + + return self._images.list() + + @omnivirt_utils.response2dict + def download_image(self, name): + """ Download image + """ + + return self._images.download(name) + + @omnivirt_utils.response2dict + def load_image(self, name, path): + """ Load local image file + """ + + if not os.path.exists(path): + err_msg = { + 'ret': 1, + 'msg': f'No such file or directory: {path}, please check again.' + } + return err_msg + + supported = False + for tp in constants.IMAGE_LOAD_SUPPORTED_TYPES: + if path.endswith(tp): + supported = True + break + + if not supported: + err_msg = { + 'ret': 1, + 'msg': f'Image file format does not supported: {path}, please check again.' + } + return err_msg + + return self._images.load(name, path) + + @omnivirt_utils.response2dict + def delete_image(self, name): + """ Delete the requested image + """ + + return self._images.delete(name) + + @omnivirt_utils.response2dict + def list_instances(self): + """ List instances + :return: dict -- list of instances' info + """ + + return self._instances.list() + + @omnivirt_utils.response2dict + def create_instance(self, name, image): + """ Create instance + :return: dict -- dict of instance's info + """ + + return self._instances.create(name, image) + + @omnivirt_utils.response2dict + def delete_instance(self, name): + """ Delete the requested instance + """ + + return self._instances.delete(name) diff --git a/omnivirt/grpcs/images.py b/omnivirt/grpcs/images.py new file mode 100644 index 0000000000000000000000000000000000000000..ba452d38d3d664a42cbfc9242b35410c03eeaf90 --- /dev/null +++ b/omnivirt/grpcs/images.py @@ -0,0 +1,30 @@ +from omnivirt.grpcs.omnivirt_grpc import images_pb2 + + +class Image(object): + def __init__(self, client): + self.client = client + + def list(self): + """Get list of images""" + request = images_pb2.ListImageRequest() + response = self.client.list_images(request) + return response + + def download(self, name): + """Download the requested image""" + request = images_pb2.DownloadImageRequest(name=name) + response = self.client.download_image(request) + return response + + def load(self, name, path): + """Load local image file""" + request = images_pb2.LoadImageRequest(name=name, path=path) + response = self.client.load_image(request) + return response + + def delete(self, name): + """Delete the requested image""" + request = images_pb2.DeleteImageRequest(name=name) + response = self.client.delete_image(request) + return response \ No newline at end of file diff --git a/omnivirt/grpcs/instances.py b/omnivirt/grpcs/instances.py new file mode 100644 index 0000000000000000000000000000000000000000..b630a7f177d272384897ed0dd2abaee698d074ea --- /dev/null +++ b/omnivirt/grpcs/instances.py @@ -0,0 +1,23 @@ +from omnivirt.grpcs.omnivirt_grpc import instances_pb2 + +class Instance(object): + def __init__(self, client): + self.client = client + + def list(self): + """Get list of instance""" + request = instances_pb2.ListInstancesRequest() + response = self.client.list_instances(request) + return response + + def create(self, name, image): + """Create instance""" + request = instances_pb2.CreateInstanceRequest(name=name, image=image) + response = self.client.create_instance(request) + return response + + def delete(self, name): + """Delete instance""" + request = instances_pb2.DeleteInstanceRequest(name=name) + response = self.client.delete_instance(request) + return response diff --git a/omnivirt/grpcs/omnivirt_grpc/__init__.py b/omnivirt/grpcs/omnivirt_grpc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/omnivirt/grpcs/omnivirt_grpc/__pycache__/__init__.cpython-310.pyc b/omnivirt/grpcs/omnivirt_grpc/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a1637de90b2758d310773547b1292d4a736f316 Binary files /dev/null and b/omnivirt/grpcs/omnivirt_grpc/__pycache__/__init__.cpython-310.pyc differ diff --git a/omnivirt/grpcs/omnivirt_grpc/images.proto b/omnivirt/grpcs/omnivirt_grpc/images.proto new file mode 100644 index 0000000000000000000000000000000000000000..26996d9dfd1c61817d7d3fc9fbc077fe3b02136b --- /dev/null +++ b/omnivirt/grpcs/omnivirt_grpc/images.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; +package omnivirt; + +option cc_generic_services = true; + + +service ImageGrpcService { + rpc list_images (ListImageRequest) returns (ListImageResponse) {} + rpc download_image (DownloadImageRequest) returns (GeneralImageResponse) {} + rpc load_image (LoadImageRequest) returns (GeneralImageResponse) {} + rpc delete_image (DeleteImageRequest) returns (GeneralImageResponse) {} +} + + +message Image { + string name = 1; + string location = 2; + string status = 3; +} + + +message ListImageRequest { +} + + +message ListImageResponse { + repeated Image images = 1; +} + +message DownloadImageRequest { + string name = 1; +} + +message LoadImageRequest { + string name = 1; + string path = 2; +} + +message DeleteImageRequest { + string name = 1; +} + +message GeneralImageResponse { + uint32 ret = 1; + string msg = 2; +} diff --git a/omnivirt/grpcs/omnivirt_grpc/images_pb2.py b/omnivirt/grpcs/omnivirt_grpc/images_pb2.py new file mode 100644 index 0000000000000000000000000000000000000000..6b2f860ebc20149da3530d08e18adeca7a89c574 --- /dev/null +++ b/omnivirt/grpcs/omnivirt_grpc/images_pb2.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: images.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cimages.proto\x12\x08omnivirt\"7\n\x05Image\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08location\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\"\x12\n\x10ListImageRequest\"4\n\x11ListImageResponse\x12\x1f\n\x06images\x18\x01 \x03(\x0b\x32\x0f.omnivirt.Image\"$\n\x14\x44ownloadImageRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\".\n\x10LoadImageRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\"\"\n\x12\x44\x65leteImageRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"0\n\x14GeneralImageResponse\x12\x0b\n\x03ret\x18\x01 \x01(\r\x12\x0b\n\x03msg\x18\x02 \x01(\t2\xcc\x02\n\x10ImageGrpcService\x12H\n\x0blist_images\x12\x1a.omnivirt.ListImageRequest\x1a\x1b.omnivirt.ListImageResponse\"\x00\x12R\n\x0e\x64ownload_image\x12\x1e.omnivirt.DownloadImageRequest\x1a\x1e.omnivirt.GeneralImageResponse\"\x00\x12J\n\nload_image\x12\x1a.omnivirt.LoadImageRequest\x1a\x1e.omnivirt.GeneralImageResponse\"\x00\x12N\n\x0c\x64\x65lete_image\x12\x1c.omnivirt.DeleteImageRequest\x1a\x1e.omnivirt.GeneralImageResponse\"\x00\x42\x03\x80\x01\x01\x62\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'images_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\200\001\001' + _IMAGE._serialized_start=26 + _IMAGE._serialized_end=81 + _LISTIMAGEREQUEST._serialized_start=83 + _LISTIMAGEREQUEST._serialized_end=101 + _LISTIMAGERESPONSE._serialized_start=103 + _LISTIMAGERESPONSE._serialized_end=155 + _DOWNLOADIMAGEREQUEST._serialized_start=157 + _DOWNLOADIMAGEREQUEST._serialized_end=193 + _LOADIMAGEREQUEST._serialized_start=195 + _LOADIMAGEREQUEST._serialized_end=241 + _DELETEIMAGEREQUEST._serialized_start=243 + _DELETEIMAGEREQUEST._serialized_end=277 + _GENERALIMAGERESPONSE._serialized_start=279 + _GENERALIMAGERESPONSE._serialized_end=327 + _IMAGEGRPCSERVICE._serialized_start=330 + _IMAGEGRPCSERVICE._serialized_end=662 +# @@protoc_insertion_point(module_scope) diff --git a/omnivirt/grpcs/omnivirt_grpc/images_pb2_grpc.py b/omnivirt/grpcs/omnivirt_grpc/images_pb2_grpc.py new file mode 100644 index 0000000000000000000000000000000000000000..f3735391eedce4f83ff43310ae64ebf9bc3dac23 --- /dev/null +++ b/omnivirt/grpcs/omnivirt_grpc/images_pb2_grpc.py @@ -0,0 +1,165 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from omnivirt.grpcs.omnivirt_grpc import images_pb2 as images__pb2 + + +class ImageGrpcServiceStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.list_images = channel.unary_unary( + '/omnivirt.ImageGrpcService/list_images', + request_serializer=images__pb2.ListImageRequest.SerializeToString, + response_deserializer=images__pb2.ListImageResponse.FromString, + ) + self.download_image = channel.unary_unary( + '/omnivirt.ImageGrpcService/download_image', + request_serializer=images__pb2.DownloadImageRequest.SerializeToString, + response_deserializer=images__pb2.GeneralImageResponse.FromString, + ) + self.load_image = channel.unary_unary( + '/omnivirt.ImageGrpcService/load_image', + request_serializer=images__pb2.LoadImageRequest.SerializeToString, + response_deserializer=images__pb2.GeneralImageResponse.FromString, + ) + self.delete_image = channel.unary_unary( + '/omnivirt.ImageGrpcService/delete_image', + request_serializer=images__pb2.DeleteImageRequest.SerializeToString, + response_deserializer=images__pb2.GeneralImageResponse.FromString, + ) + + +class ImageGrpcServiceServicer(object): + """Missing associated documentation comment in .proto file.""" + + def list_images(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def download_image(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def load_image(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def delete_image(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_ImageGrpcServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'list_images': grpc.unary_unary_rpc_method_handler( + servicer.list_images, + request_deserializer=images__pb2.ListImageRequest.FromString, + response_serializer=images__pb2.ListImageResponse.SerializeToString, + ), + 'download_image': grpc.unary_unary_rpc_method_handler( + servicer.download_image, + request_deserializer=images__pb2.DownloadImageRequest.FromString, + response_serializer=images__pb2.GeneralImageResponse.SerializeToString, + ), + 'load_image': grpc.unary_unary_rpc_method_handler( + servicer.load_image, + request_deserializer=images__pb2.LoadImageRequest.FromString, + response_serializer=images__pb2.GeneralImageResponse.SerializeToString, + ), + 'delete_image': grpc.unary_unary_rpc_method_handler( + servicer.delete_image, + request_deserializer=images__pb2.DeleteImageRequest.FromString, + response_serializer=images__pb2.GeneralImageResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'omnivirt.ImageGrpcService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class ImageGrpcService(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def list_images(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/omnivirt.ImageGrpcService/list_images', + images__pb2.ListImageRequest.SerializeToString, + images__pb2.ListImageResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def download_image(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/omnivirt.ImageGrpcService/download_image', + images__pb2.DownloadImageRequest.SerializeToString, + images__pb2.GeneralImageResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def load_image(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/omnivirt.ImageGrpcService/load_image', + images__pb2.LoadImageRequest.SerializeToString, + images__pb2.GeneralImageResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def delete_image(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/omnivirt.ImageGrpcService/delete_image', + images__pb2.DeleteImageRequest.SerializeToString, + images__pb2.GeneralImageResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/omnivirt/grpcs/omnivirt_grpc/instances.proto b/omnivirt/grpcs/omnivirt_grpc/instances.proto new file mode 100644 index 0000000000000000000000000000000000000000..c2e751cb2fc354d7f0903a9bc7529f278501167d --- /dev/null +++ b/omnivirt/grpcs/omnivirt_grpc/instances.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; +package omnivirt; + +option cc_generic_services = true; + + +service InstanceGrpcService { + rpc list_instances (ListInstancesRequest) returns (ListInstancesResponse) {} + rpc create_instance (CreateInstanceRequest) returns (CreateInstanceResponse) {} + rpc delete_instance (DeleteInstanceRequest) returns (DeleteInstanceResponse) {} +} + + +message Instance { + string name = 1; + string image = 2; + string vm_state = 3; + string ip_address = 4; +} + + +message ListInstancesRequest { +} + + +message ListInstancesResponse { + repeated Instance instances = 1; +} + +message CreateInstanceRequest { + string name = 1; + string image = 2; +} + +message CreateInstanceResponse { + uint32 ret = 1; + string msg = 2; + optional Instance instance = 3; +} + +message DeleteInstanceRequest { + string name = 1; +} + +message DeleteInstanceResponse { + uint32 ret = 1; + string msg = 2; +} diff --git a/omnivirt/grpcs/omnivirt_grpc/instances_pb2.py b/omnivirt/grpcs/omnivirt_grpc/instances_pb2.py new file mode 100644 index 0000000000000000000000000000000000000000..9877833f058ff6798506e937cee9f7ce7d1e555e --- /dev/null +++ b/omnivirt/grpcs/omnivirt_grpc/instances_pb2.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: instances.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0finstances.proto\x12\x08omnivirt\"M\n\x08Instance\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05image\x18\x02 \x01(\t\x12\x10\n\x08vm_state\x18\x03 \x01(\t\x12\x12\n\nip_address\x18\x04 \x01(\t\"\x16\n\x14ListInstancesRequest\">\n\x15ListInstancesResponse\x12%\n\tinstances\x18\x01 \x03(\x0b\x32\x12.omnivirt.Instance\"4\n\x15\x43reateInstanceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05image\x18\x02 \x01(\t\"j\n\x16\x43reateInstanceResponse\x12\x0b\n\x03ret\x18\x01 \x01(\r\x12\x0b\n\x03msg\x18\x02 \x01(\t\x12)\n\x08instance\x18\x03 \x01(\x0b\x32\x12.omnivirt.InstanceH\x00\x88\x01\x01\x42\x0b\n\t_instance\"%\n\x15\x44\x65leteInstanceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"2\n\x16\x44\x65leteInstanceResponse\x12\x0b\n\x03ret\x18\x01 \x01(\r\x12\x0b\n\x03msg\x18\x02 \x01(\t2\x9a\x02\n\x13InstanceGrpcService\x12S\n\x0elist_instances\x12\x1e.omnivirt.ListInstancesRequest\x1a\x1f.omnivirt.ListInstancesResponse\"\x00\x12V\n\x0f\x63reate_instance\x12\x1f.omnivirt.CreateInstanceRequest\x1a .omnivirt.CreateInstanceResponse\"\x00\x12V\n\x0f\x64\x65lete_instance\x12\x1f.omnivirt.DeleteInstanceRequest\x1a .omnivirt.DeleteInstanceResponse\"\x00\x42\x03\x80\x01\x01\x62\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'instances_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\200\001\001' + _INSTANCE._serialized_start=29 + _INSTANCE._serialized_end=106 + _LISTINSTANCESREQUEST._serialized_start=108 + _LISTINSTANCESREQUEST._serialized_end=130 + _LISTINSTANCESRESPONSE._serialized_start=132 + _LISTINSTANCESRESPONSE._serialized_end=194 + _CREATEINSTANCEREQUEST._serialized_start=196 + _CREATEINSTANCEREQUEST._serialized_end=248 + _CREATEINSTANCERESPONSE._serialized_start=250 + _CREATEINSTANCERESPONSE._serialized_end=356 + _DELETEINSTANCEREQUEST._serialized_start=358 + _DELETEINSTANCEREQUEST._serialized_end=395 + _DELETEINSTANCERESPONSE._serialized_start=397 + _DELETEINSTANCERESPONSE._serialized_end=447 + _INSTANCEGRPCSERVICE._serialized_start=450 + _INSTANCEGRPCSERVICE._serialized_end=732 +# @@protoc_insertion_point(module_scope) diff --git a/omnivirt/grpcs/omnivirt_grpc/instances_pb2_grpc.py b/omnivirt/grpcs/omnivirt_grpc/instances_pb2_grpc.py new file mode 100644 index 0000000000000000000000000000000000000000..b9288b85ebec4249e64a16de253c9c3656c1968c --- /dev/null +++ b/omnivirt/grpcs/omnivirt_grpc/instances_pb2_grpc.py @@ -0,0 +1,132 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from omnivirt.grpcs.omnivirt_grpc import instances_pb2 as instances__pb2 + + +class InstanceGrpcServiceStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.list_instances = channel.unary_unary( + '/omnivirt.InstanceGrpcService/list_instances', + request_serializer=instances__pb2.ListInstancesRequest.SerializeToString, + response_deserializer=instances__pb2.ListInstancesResponse.FromString, + ) + self.create_instance = channel.unary_unary( + '/omnivirt.InstanceGrpcService/create_instance', + request_serializer=instances__pb2.CreateInstanceRequest.SerializeToString, + response_deserializer=instances__pb2.CreateInstanceResponse.FromString, + ) + self.delete_instance = channel.unary_unary( + '/omnivirt.InstanceGrpcService/delete_instance', + request_serializer=instances__pb2.DeleteInstanceRequest.SerializeToString, + response_deserializer=instances__pb2.DeleteInstanceResponse.FromString, + ) + + +class InstanceGrpcServiceServicer(object): + """Missing associated documentation comment in .proto file.""" + + def list_instances(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def create_instance(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def delete_instance(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_InstanceGrpcServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'list_instances': grpc.unary_unary_rpc_method_handler( + servicer.list_instances, + request_deserializer=instances__pb2.ListInstancesRequest.FromString, + response_serializer=instances__pb2.ListInstancesResponse.SerializeToString, + ), + 'create_instance': grpc.unary_unary_rpc_method_handler( + servicer.create_instance, + request_deserializer=instances__pb2.CreateInstanceRequest.FromString, + response_serializer=instances__pb2.CreateInstanceResponse.SerializeToString, + ), + 'delete_instance': grpc.unary_unary_rpc_method_handler( + servicer.delete_instance, + request_deserializer=instances__pb2.DeleteInstanceRequest.FromString, + response_serializer=instances__pb2.DeleteInstanceResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'omnivirt.InstanceGrpcService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class InstanceGrpcService(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def list_instances(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/omnivirt.InstanceGrpcService/list_instances', + instances__pb2.ListInstancesRequest.SerializeToString, + instances__pb2.ListInstancesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def create_instance(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/omnivirt.InstanceGrpcService/create_instance', + instances__pb2.CreateInstanceRequest.SerializeToString, + instances__pb2.CreateInstanceResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def delete_instance(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/omnivirt.InstanceGrpcService/delete_instance', + instances__pb2.DeleteInstanceRequest.SerializeToString, + instances__pb2.DeleteInstanceResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/omnivirt/install.py b/omnivirt/install.py new file mode 100644 index 0000000000000000000000000000000000000000..8e7d6def8bc1f7b035946a3ba04218c4676ff20a --- /dev/null +++ b/omnivirt/install.py @@ -0,0 +1,27 @@ +import os +import subprocess + + +if __name__ == '__main__': + base_dir = os.path.dirname(__file__) + + print('Welcome to use OmniVirt, a tool allows users to develop openEuler on favourate desktop.') + print('Creating folder for supporting files ...') + + folder_cmd = ['sudo', 'mkdir', '-p', '/Library/Application\ Support/org.openeuler.omnivirt'] + subprocess.run(' '.join(folder_cmd), shell=True) + + print('Copy all supporting files to the folder ...') + cp_cmd = ['sudo', 'cp -r', os.path.join(base_dir,'./etc/*'), '/Library/Application\ Support/org.openeuler.omnivirt/'] + subprocess.run(' '.join(cp_cmd), shell=True) + + print('Change supporting file rights ...') + chmod_cmd = ['sudo', 'chmod -R', '775', '/Library/Application\ Support/org.openeuler.omnivirt/'] + subprocess.run(' '.join(chmod_cmd), shell=True) + + print('Create softlink for the CLI binary ...') + ln_cmd = ['sudo', 'ln', '-s', '/Library/Application\ Support/org.openeuler.omnivirt/omnivirt', '/usr/local/bin/'] + subprocess.run(' '.join(ln_cmd), shell=True) + + print('Done ...') + print('Please update omnivirt.conf according to your own environment, enjoy your openEuler trip ...') \ No newline at end of file diff --git a/omnivirt/macos-gui.py b/omnivirt/macos-gui.py new file mode 100644 index 0000000000000000000000000000000000000000..b5b63532dc6bc49cc4fd4cabecf89f4cefa99e23 --- /dev/null +++ b/omnivirt/macos-gui.py @@ -0,0 +1,58 @@ +import os +import PIL.Image +import platform +import pystray +import subprocess +import signal +import sys + +from omnivirt.utils import constants +from omnivirt.utils import objs + + +CONF_DIR_SHELL = '/Library/Application\ Support/org.openeuler.omnivirt/omnivirt.conf' +CONF_DIR = '/Library/Application Support/org.openeuler.omnivirt/omnivirt.conf' + +# Avoid create zombie children in MacOS and Linux +signal.signal(signal.SIGCHLD, signal.SIG_IGN) + +if __name__ == '__main__': + try: + host_arch_raw = platform.uname().machine + host_arch = constants.ARCH_MAP[host_arch_raw] + base_dir = os.path.dirname(__file__) + + + conf_file = CONF_DIR + logo_file = os.path.join(base_dir,'./etc/favicon.png') + + CONF = objs.Conf(conf_file) + + logo = PIL.Image.open(logo_file) + + def on_clicked(icon, item): + + icon.stop() + + icon = pystray.Icon('OmniVirt', logo, menu=pystray.Menu( + pystray.MenuItem('Exit OmniVirt', on_clicked) + )) + + except Exception as e: + print('Error: ' + str(e)) + else: + omnivirtd_cmd = ['sudo', os.path.join(base_dir,'./bin/OmniVirtd'), CONF_DIR_SHELL, base_dir] + omnivirtd = subprocess.Popen(' '.join(omnivirtd_cmd), shell=True, preexec_fn=os.setsid) + + def term_handler(signum, frame): + subprocess.check_call(['sudo', 'kill', str(omnivirtd.pid)]) + + # Avoid create orphan children in MacOS and Linux + signal.signal(signal.SIGTERM, term_handler) + + icon.run() + + # Shutdown omnivirtd, we created it with sudo, so kill it with sudo + subprocess.check_call(['sudo', 'kill', str(omnivirtd.pid)]) + os.waitpid(omnivirtd.pid, 0) + sys.exit(0) \ No newline at end of file diff --git a/omnivirt/omnivirtd.py b/omnivirt/omnivirtd.py new file mode 100644 index 0000000000000000000000000000000000000000..08b7a60e5cde2a14831cc08e9316480ed7ff2ab7 --- /dev/null +++ b/omnivirt/omnivirtd.py @@ -0,0 +1,148 @@ +import argparse +from concurrent import futures +import grpc +import logging +import os +import PIL.Image +import platform +import pystray +import requests +import signal +import subprocess +import sys +import time + +from omnivirt.grpcs.omnivirt_grpc import images_pb2, images_pb2_grpc +from omnivirt.grpcs.omnivirt_grpc import instances_pb2, instances_pb2_grpc +from omnivirt.services import imager_service, instance_service +from omnivirt.utils import constants +from omnivirt.utils import objs +from omnivirt.utils import utils + + +IMG_URL = 'https://gitee.com/openeuler/omnivirt/raw/master/etc/supported_images.json' + +# Avoid create zombie children in MacOS and Linux +signal.signal(signal.SIGCHLD, signal.SIG_IGN) + +parser = argparse.ArgumentParser() +parser.add_argument('conf_file', help='Configuration file for the application', type=str) +parser.add_argument('base_dir', help='The base work directory of the daemon') + + +def config_logging(config): + log_dir = config.conf.get('default', 'log_dir') + debug = config.conf.get('default', 'debug') + + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + log_file = os.path.join(log_dir, 'omnivirt.log') + + if debug == 'True': + log_level = logging.DEBUG + else: + log_level = logging.INFO + logging.basicConfig( + format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s', + filename=log_file, level=log_level, filemode='a+') + + +def init(arch, config, LOG): + work_dir = config.conf.get('default', 'work_dir') + image_dir = os.path.join(work_dir, 'images') + instance_dir = os.path.join(work_dir, 'instances') + instance_record_file = os.path.join(instance_dir, 'instances.json') + img_record_file = os.path.join(image_dir, 'images.json') + + LOG.debug('Initializing OmniVirtd ...') + LOG.debug('Checking for work directory ...') + if not os.path.exists(work_dir): + LOG.debug('Create %s as working directory ...' % work_dir) + os.makedirs(work_dir) + LOG.debug('Checking for instances directory ...') + if not os.path.exists(instance_dir): + LOG.debug('Create %s as working directory ...' % work_dir) + os.makedirs(instance_dir) + LOG.debug('Checking for instance database ...') + if not os.path.exists(instance_record_file): + instances = { + 'instances': {} + } + utils.save_json_data(instance_record_file, instances) + + LOG.debug('Checking for image directory ...') + if not os.path.exists(image_dir): + LOG.debug('Create %s as image directory ...' % image_dir) + os.makedirs(image_dir) + + LOG.debug('Checking for image database ...') + remote_img_resp = requests.get(IMG_URL, verify=False) + remote_imgs = remote_img_resp.json()[arch] + if not os.path.exists(img_record_file): + images = {} + for name, path in remote_imgs.items(): + image = objs.Image() + image.name = name + image.path = path + image.location = constants.IMAGE_LOCATION_REMOTE + image.status = constants.IMAGE_STATUS_DOWLOADABLE + images[image.name] = image.to_dict() + + image_body = { + 'remote': images, + 'local': {} + } + utils.save_json_data(img_record_file, image_body) + +def serve(arch, host_os, CONF, LOG, base_dir): + ''' + Run the Omnivirtd Service + ''' + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + images_pb2_grpc.add_ImageGrpcServiceServicer_to_server(imager_service.ImagerService(arch, host_os, CONF, base_dir), server) + instances_pb2_grpc.add_InstanceGrpcServiceServicer_to_server(instance_service.InstanceService(arch, host_os, CONF, base_dir), server) + server.add_insecure_port('[::]:50052') + server.start() + LOG.debug('OmniVirtd Service Started ...') + + def term_handler(signum, frame): + pid = os.getpid() + os.killpg(os.getpgid(pid), signal.SIGKILL) + + # Avoid create orphan children in MacOS and Linux + signal.signal(signal.SIGTERM, term_handler) + + while True: + time.sleep(1) + +def init_omnivirtd(conf, base_dir): + CONF = objs.Conf(conf) + + config_logging(CONF) + LOG = logging.getLogger(__name__) + + host_arch_raw = platform.uname().machine + host_os_raw = platform.uname().system + + host_arch = constants.ARCH_MAP[host_arch_raw] + host_os = constants.OS_MAP[host_os_raw] + + try: + init(host_arch, CONF, LOG) + except Exception as e: + LOG.debug('Error: ' + str(e)) + return str(e) + else: + serve(host_arch, host_os, CONF, LOG, base_dir) + + +if __name__ == '__main__': + args = parser.parse_args() + conf_file = args.conf_file + try: + pass + except Exception as e: + print('Error: ' + str(e)) + else: + init_omnivirtd(conf_file, args.base_dir) diff --git a/omnivirt/services/__init__.py b/omnivirt/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/omnivirt/services/__pycache__/__init__.cpython-310.pyc b/omnivirt/services/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36d7135c5e14dd9d44dc4bcc18e8c6802bb36f98 Binary files /dev/null and b/omnivirt/services/__pycache__/__init__.cpython-310.pyc differ diff --git a/omnivirt/services/imager_service.py b/omnivirt/services/imager_service.py new file mode 100644 index 0000000000000000000000000000000000000000..c2d6f28d135fcaa2bed4b5056d2e3a33f911aef9 --- /dev/null +++ b/omnivirt/services/imager_service.py @@ -0,0 +1,106 @@ +import logging +import os + +from omnivirt.backends.mac import image_handler as mac_image_handler +from omnivirt.backends.win import image_handler as win_image_handler +from omnivirt.grpcs.omnivirt_grpc import images_pb2, images_pb2_grpc +from omnivirt.utils import constants as omni_constants +from omnivirt.utils import utils as omni_utils + + +LOG = logging.getLogger(__name__) + + +class ImagerService(images_pb2_grpc.ImageGrpcServiceServicer): + ''' + The Imager GRPC Handler + ''' + + def __init__(self, arch, host_os, conf, svc_base_dir) -> None: + self.CONF = conf + self.svc_base_dir = svc_base_dir + self.work_dir = self.CONF.conf.get('default', 'work_dir') + self.image_dir = os.path.join(self.work_dir, 'images') + self.img_record_file = os.path.join(self.image_dir, 'images.json') + if host_os == 'Win': + self.backend = win_image_handler.WinImageHandler( + self.CONF, self.work_dir, self.image_dir, self.img_record_file, LOG) + elif host_os == 'MacOS': + self.backend = mac_image_handler.MacImageHandler( + self.CONF, self.work_dir, self.image_dir, self.img_record_file, + LOG, self.svc_base_dir) + + def list_images(self, request, context): + LOG.debug(f"Get request to list images ...") + all_images = omni_utils.load_json_data(self.img_record_file) + + ret = [] + for _, images in all_images.items(): + for _, img in images.items(): + image = images_pb2.Image() + image.name = img['name'] + image.location = img['location'] + image.status = img['status'] + ret.append(image) + LOG.debug(f"Responded: {ret}") + return images_pb2.ListImageResponse(images=ret) + + def download_image(self, request, context): + LOG.debug(f"Get request to download image: {request.name} ...") + all_images = omni_utils.load_json_data(self.img_record_file) + + if request.name not in all_images['remote'].keys(): + LOG.debug(f'Image: {request.name} not valid for download') + msg = f'Error: Image {request.name} is valid for download, please check image name from REMOTE IMAGE LIST using "images" command ...' + return images_pb2.GeneralImageResponse(ret=1, msg=msg) + + @omni_utils.asyncwrapper + def do_download(images, name): + self.backend.download_and_transform(images, name) + + do_download(all_images, request.name) + + msg = f'Downloading: {request.name}, this might take a while, please check image status with "images" command.' + return images_pb2.GeneralImageResponse(ret=0, msg=msg) + + def load_image(self, request, context): + LOG.debug(f"Get request to load image: {request.name} from path: {request.path} ...") + + supported, fmt = omni_utils.check_file_tail( + request.path, omni_constants.IMAGE_LOAD_SUPPORTED_TYPES) + + if not supported: + supported_fmt = ', '.join(omni_constants.IMAGE_LOAD_SUPPORTED_TYPES) + msg = f'Unsupported image format, the current supported format are: {supported_fmt}.' + + return images_pb2.GeneralImageResponse(ret=1, msg=msg) + + all_images = omni_utils.load_json_data(self.img_record_file) + + msg = f'Loading: {request.name}, this might take a while, please check image status with "images" command.' + update = False + + local_images = all_images['local'] + if request.name in local_images.keys(): + LOG.debug(f"Image: {request.name} already existed, replace it with: {request.path} ...") + msg = f'Replacing: {request.name}, with new image file: {request.path}, this might take a while, please check image status with "images" command.' + update = True + + @omni_utils.asyncwrapper + def do_load(images, name, path, fmt, update): + self.backend.load_and_transform(images, name, path, fmt, update) + + do_load(all_images, request.name, request.path, fmt, update) + + return images_pb2.GeneralImageResponse(ret=0, msg=msg) + + def delete_image(self, request, context): + LOG.debug(f"Get request to delete image: {request.name} ...") + images = omni_utils.load_json_data(self.img_record_file) + ret = self.backend.delete_image(images, request.name) + if ret == 0: + msg = f'Image: {request.name} has been successfully deleted.' + elif ret == 1: + msg = f'Image: {request.name} does not exist, please check again.' + + return images_pb2.GeneralImageResponse(ret=1, msg=msg) diff --git a/omnivirt/services/instance_service.py b/omnivirt/services/instance_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e4125e9c598a1bc40e5d76311911b13424b779bf --- /dev/null +++ b/omnivirt/services/instance_service.py @@ -0,0 +1,81 @@ +import logging +import os + +from omnivirt.grpcs.omnivirt_grpc import instances_pb2, instances_pb2_grpc +from omnivirt.utils import utils + + +LOG = logging.getLogger(__name__) + +class InstanceService(instances_pb2_grpc.InstanceGrpcServiceServicer): + ''' + The Instance GRPC Handler + ''' + + def __init__(self, arch, host_os, conf, svc_base_dir) -> None: + self.CONF = conf + self.svc_base_dir = svc_base_dir + self.work_dir = self.CONF.conf.get('default', 'work_dir') + self.instance_dir = os.path.join(self.work_dir, 'instances') + self.instance_record_file = os.path.join(self.instance_dir, 'instances.json') + self.image_dir = os.path.join(self.work_dir, 'images') + self.img_record_file = os.path.join(self.image_dir, 'images.json') + if host_os == 'Win': + from omnivirt.backends.win import instance_handler as win_instance_handler + self.backend = win_instance_handler.WinInstanceHandler( + self.CONF, self.work_dir, self.instance_dir, self.image_dir, self.img_record_file, LOG) + elif host_os == 'MacOS': + from omnivirt.backends.mac import instance_handler as mac_instance_handler + self.backend = mac_instance_handler.MacInstanceHandler( + self.CONF, self.work_dir, self.instance_dir, self.image_dir, + self.img_record_file, LOG, self.svc_base_dir) + + def list_instances(self, request, context): + LOG.debug(f"Get request to list instances ...") + instances_obj = self.backend.list_instances() + + ret = [] + for vm_obj in instances_obj: + instance_dict = { + 'name': vm_obj.name, + 'image': vm_obj.image, + 'vm_state': vm_obj.vm_state, + 'ip_address': vm_obj.ip if vm_obj.ip else 'N/A' + } + ret.append(instance_dict) + + return instances_pb2.ListInstancesResponse(instances=ret) + + def create_instance(self, request, context): + LOG.debug(f"Get request to create instance: {request.name} with image {request.image} ...") + + all_img = utils.load_json_data(self.img_record_file) + if request.image not in all_img['local'].keys(): + msg = f'Error: Image "{request.image}" is not available locally, please check again or (down)load it before using ...' + return instances_pb2.CreateInstanceResponse(ret=2, msg=msg) + + all_instances = utils.load_json_data(self.instance_record_file) + if request.name in all_instances['instances'].keys(): + msg = f'Error: Instance with name {request.name} already exist, please specify another name.' + return instances_pb2.CreateInstanceResponse(ret=2, msg=msg) + + check_result = self.backend.check_names(request.name, all_instances) + if check_result == 1: + msg = f'Error: Instance with name {request.name} already exist in exixting Hyper-V or Qemu backend, please specify another name.' + return instances_pb2.CreateInstanceResponse(ret=2, msg=msg) + + vm = self.backend.create_instance( + request.name, request.image, self.instance_record_file, all_instances, all_img) + msg = f'Successfully created {request.name} with image {request.image}' + return instances_pb2.CreateInstanceResponse(ret=1, msg=msg, instance=vm) + + def delete_instance(self, request, context): + LOG.debug(f"Get request to delete instance: {request.name} ...") + all_instances = utils.load_json_data(self.instance_record_file) + if request.name not in all_instances['instances'].keys(): + msg = f'Error: Instance with name {request.name} does not exist.' + return instances_pb2.DeleteInstanceResponse(ret=2, msg=msg) + + self.backend.delete_instance(request.name, self.instance_record_file, all_instances) + msg = f'Successfully deleted instance: {request.name}.' + return instances_pb2.DeleteInstanceResponse(ret=1, msg=msg) diff --git a/omnivirt/utils/__init__.py b/omnivirt/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/omnivirt/utils/__pycache__/__init__.cpython-310.pyc b/omnivirt/utils/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bf35aee4bbd06edb8b2cbc3bc15b58b80792819 Binary files /dev/null and b/omnivirt/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/omnivirt/utils/constants.py b/omnivirt/utils/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..9383d2d5f2d0edf75c689631eb017ec0d846b989 --- /dev/null +++ b/omnivirt/utils/constants.py @@ -0,0 +1,37 @@ +STORAGE_PROTOCOL_ISCSI = 'iscsi' +STORAGE_PROTOCOL_FC = 'fibre_channel' +STORAGE_PROTOCOL_SMBFS = 'smbfs' +STORAGE_PROTOCOL_RBD = 'rbd' + +DISK = "VHD" + +IMAGE_LOCATION_REMOTE = 'Remote' +IMAGE_LOCATION_LOCAL = 'Local' + +IMAGE_STATUS_INIT = 'N/A' +IMAGE_STATUS_DOWLOADABLE = 'Downloadable' +IMAGE_STATUS_DOWNLOADING = 'Downloading' +IMAGE_STATUS_LOADING = 'Loading' +IMAGE_STATUS_READY = 'Ready' + +IMAGE_LOAD_SUPPORTED_TYPES = ['qcow2.xz', 'qcow2'] + +ARCH_MAP = { + 'AMD64': 'x86_64', + 'arm64': 'aarch64', + 'x86_64': 'x86_64' +} + +VM_STATE_MAP = { + 2: 'Running', + 3: 'Stopped', + 10: 'Rebooting', + 32768: 'Paused', + 32769: 'Suspended', + 99: 'N/A' + } + +OS_MAP = { + 'Darwin': 'MacOS', + 'Windows': 'Win' +} \ No newline at end of file diff --git a/omnivirt/utils/exceptions.py b/omnivirt/utils/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..3f671e1fbcca83b98d0e02f86d16ce2947f65580 --- /dev/null +++ b/omnivirt/utils/exceptions.py @@ -0,0 +1,61 @@ +"""OmniVirt Base Exceptions. +""" + +import logging + +LOG = logging.getLogger(__name__) + +class OmniVirtException(Exception): + """Base OmniVirt Exception + To correctly use this class, inherit from it and define + a 'msg_fmt' property. That msg_fmt will get printf'd + with the keyword arguments provided to the constructor. + """ + msg_fmt = "An unknown exception occurred." + code = 500 + headers = {} + safe = False + + def __init__(self, message=None, **kwargs): + self.kwargs = kwargs + + if 'code' not in self.kwargs: + try: + self.kwargs['code'] = self.code + except AttributeError: + pass + + try: + if not message: + message = self.msg_fmt % kwargs + else: + message = str(message) + except Exception: + self._log_exception() + message = self.msg_fmt + + self.message = message + super(OmniVirtException, self).__init__(message) + + def _log_exception(self): + LOG.exception('Exception in string format operation') + for name, value in self.kwargs.items(): + LOG.error("%s: %s" % (name, value)) # noqa + + def format_message(self): + return self.args[0] + + def __repr__(self): + dict_repr = self.__dict__ + dict_repr['class'] = self.__class__.__name__ + return str(dict_repr) + + +class NoSuchFile(OmniVirtException): + msg_fmt = "No Such File or Directory: %(file)s" + +class NoConfigFileProvided(OmniVirtException): + msg_fmt = "Config File Should Be Provided" + +class OmniVirtdNotAvailable(OmniVirtException): + msg_fmt = "OmniVirtd Daemon is not available" \ No newline at end of file diff --git a/omnivirt/utils/objs.py b/omnivirt/utils/objs.py new file mode 100644 index 0000000000000000000000000000000000000000..9cb2004c83b56d75f90c0c03e27be956514e32e1 --- /dev/null +++ b/omnivirt/utils/objs.py @@ -0,0 +1,54 @@ +import os +import configparser + +from omnivirt.utils import exceptions +from omnivirt.utils import constants + +class Instance(object): + + def __init__(self, name='') -> None: + self.name = name + self.uuid = '' + self.identifier = {} + self.metadata = None + self.vm_state = None + self.vcpu = None + self.ram = None + self.disk = None + self.info = None + self.image = None + self.ip = 'N/A' + self.mac = 'N/A' + + +class Image(object): + + def __init__(self) -> None: + self.name = '' + self.location = '' + self.status = constants.IMAGE_STATUS_INIT + self.path = '' + + def to_dict(self): + image_dict = { + 'name': self.name, + 'location': self.location, + 'status': self.status, + 'path': self.path + } + return image_dict + + def from_dict(self, img_dict): + self.name = img_dict['name'] + self.location = img_dict['location'] + self.status = img_dict['status'] + self.path = img_dict['path'] + + +class Conf(object): + + def __init__(self, config_file) -> None: + self.conf = configparser.ConfigParser() + if not os.path.exists(config_file): + raise exceptions.NoSuchFile(file=config_file) + self.conf.read(config_file) diff --git a/omnivirt/utils/utils.py b/omnivirt/utils/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b6f10c97872b802bfb1a92f6eea332b0c0899955 --- /dev/null +++ b/omnivirt/utils/utils.py @@ -0,0 +1,94 @@ +import functools +import json +import os +import random +from threading import Thread +import uuid + + +from google.protobuf.json_format import MessageToDict + +from omnivirt.utils import exceptions +from omnivirt.utils import objs + + +def asyncwrapper(fn): + def wrapper(*args, **kwargs): + thr = Thread(target=fn, args=args, kwargs=kwargs) + thr.start() + + return wrapper + + +def response2dict(fn): + @functools.wraps(fn) + def wrap(*args, **kwargs): + response = fn(*args, **kwargs) + response = MessageToDict(response) + return response + + return wrap + + +def parse_config(args): + if len(args) != 2 or args[0] != '--config-file': + raise exceptions.NoConfigFileProvided + if not os.path.exists(args[1]): + raise exceptions.NoSuchFile(file=args[1]) + + return objs.Conf(args[1]) + + +def format_mac_addr(mac_str): + ret = '' + if len(mac_str) != 12: + return ret + mac_low = mac_str.lower() + for i in range(0, 5): + ret = ret + mac_low[2 * i] + mac_low[2 * i + 1] + '-' + ret = ret + mac_low[-2] + mac_low[-1] + + return ret + +def load_json_data(json_file): + with open(json_file, 'r', encoding='utf-8') as fr: + data = json.load(fr) + + return data + +def save_json_data(json_file, data): + with open(json_file, 'w', encoding='utf-8') as fw: + json.dump(data, fw, indent=4, ensure_ascii=False) + +def generate_mac(): + local_mac = uuid.uuid1().hex[-12:] + + mac = [random.randint(0x00, 0xff), random.randint(0x00, 0xff)] + s = [local_mac[0:2], local_mac[2:4], local_mac[4:6], local_mac[6:8]] + for item in mac: + s.append(str("%02x" % item)) + + return (':'.join(s)) + +def catch_exception(func): + + def wrap(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: + raise exceptions.OmniVirtdNotAvailable + + return wrap + +def check_file_tail(file_name, to_check): + + ret = False + ret_fmt = None + + for fmt in to_check: + if file_name.endswith(fmt): + ret = True + ret_fmt = fmt + break + + return ret, ret_fmt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3f666ec2f136a323097d85cd003ad63286cf2602 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +argparse +click +configparser +dnspython +grpcio +grpcio-tools +pkg-config +libvirt-python +lxml +os-win +oslo.concurrency +oslo.config +oslo.context +oslo.i18n +oslo.log +oslo.serialization +oslo.utils +Pillow +prettytable +protobuf +psutil +pystray +PyYAML +six +urllib3 +wget diff --git a/resources/qemu/edk2-aarch64-code.fd b/resources/qemu/edk2-aarch64-code.fd new file mode 100644 index 0000000000000000000000000000000000000000..832bfe21451ce2a2cc71f7ad91a92d20a372e8cf Binary files /dev/null and b/resources/qemu/edk2-aarch64-code.fd differ diff --git a/resources/qemu/edk2-x86_64-code.fd b/resources/qemu/edk2-x86_64-code.fd new file mode 100644 index 0000000000000000000000000000000000000000..b1cb5da36aee2f9b3c243c721cd6e44ae36c121c Binary files /dev/null and b/resources/qemu/edk2-x86_64-code.fd differ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000000000000000000000000000000000..6a95db993d600c0af33139d63675afd10be7e9b5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,32 @@ +[metadata] +name = omnivirt +version = 0.1 +summary = A tool to run openEuler instances on all platforms +description_file = + README.md +author = Zhenyu Zheng +author_email = zheng.zhenyu@outlook.com +python_requires = >=3.8 +url = https://github.com/ZhengZhenyu/omnivirt.git +classifiers = + Development Status :: 3 - Alpha + Environment :: OmniVirt + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Mulan PSL v2 + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: Implementation :: CPython + +[files] + +packages = + omnivirt + +[entry_points] +console_scripts = + omnivirt = omnivirt.cli:main \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..6f3f889c68fb8bd52070ecfb0c137ece43710579 --- /dev/null +++ b/setup.py @@ -0,0 +1,5 @@ +import setuptools + +setuptools.setup( + setup_requires=['pbr>=2.0.0'], + pbr=True)