風雲論壇后台开发 → 浏览:帖子主题
分页: 1 2, 共 2 页
* 帖子主题:利用 WMI 创建 Hyper-V 虚拟机
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 1 楼 ] 回复
Gitee 源码参考 或者 查看演示
这里以 JScript 的 ASP/ASPX 代码为例,以便更好的理解 WMI 管理 Hyper-V 的基本流程。

提示1:ASP.NET 需要将 IIS AppPool\DefaultAppPool(ASP 也可以用 IUSR)添加到 Hyper-V Administrators 组才有权限执行操作。
提示2:操作账号需要对配置中出现的文件和目录有读写权限,否则异步操作(code: 4096)过程中有可能会出错。

// 定义 wmi 方法
function wmi() { return !wmi.ins ? wmi.ins = GetObject("winmgmts://localhost/root/virtualization/v2") : wmi.ins; }
// wmi 数组转 js 数组
function wmiToArr(rs, fields) {
    var cols = fields.split(/[\,\s]+/);
    var enm = new Enumerator(rs), arr = new Array;
    while(!enm.atEnd()) {
        var x = enm.item(), o = new Object;
        for(var i = 0; i < cols.length; i++) o[cols[i]] = x[cols[i]];
        arr.push(o); enm.moveNext();
    }
    sys.wmiRs = rs;
    return arr;
}
// 时间格式解析
function parseTime(str) { return str.replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2}).*/, "$1-$2-$3 $4:$5:$6"); }

// 第一个示例,检查服务状态
,ServiceStatusDoc: [ "服务运行状态" ]
,servicestatus: function() {
    var rs = wmi().instancesOf("Msvm_VirtualSystemManagementService");
    if(!rs.Count) return { err: "未找到服务" };
    rs = wmiToArr(rs, "Caption, DefineSystem, Description, StatusDescriptions, InstallDate")[0];
    rs.InstallDate = parseTime(rs.InstallDate);
    rs.StatusDescriptions = rs.StatusDescriptions.getItem(0);    // ASP
    // rs.StatusDescriptions = rs.StatusDescriptions[0];    // ASPX
    return rs;
}
2022/6/14 0:03:53 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 2 楼 ] 回复
创建虚拟磁盘,为了能快速启动系统,我们在这里使用了创建“差异磁盘”的方式,以一个已经初始化好的系统作为底盘:

,VHDCreateDoc: [ "新建 VHD", "vhdName" ]
,vhdcreate: function() {
    var vhdName = form("vhdName") || "NewVHD";
    var rs = wmi().Get("Msvm_VirtualHardDiskSettingData").SpawnInstance_();
    rs.Format = 3;    // 2: VHD, 3: VHDX, 4: VHDSet
    rs.Type = 4;    // 2: 固定大小, 3: 动态, 4: 差分硬盘
    rs.ParentPath = "F:\\Disks\\HyperV\\WinXP\\Virtual Hard Disks\\Source.vhdx";
    rs.Path = "F:\\Disks\\VhdSys\\Free\\" + vhdName + ".vhdx";
    var hyper = wmi().instancesOf("Msvm_ImageManagementService").ItemIndex(0);
    var inParam = hyper.Methods_("CreateVirtualHardDisk").InParameters.SpawnInstance_();
    inParam.VirtualDiskSettingData = rs.GetText_(2);
    var outParam = hyper.ExecMethod_("CreateVirtualHardDisk", inParam);
    // 如果返回 32773,那就是参数错误。异步操作,会返回4096。
    if(outParam.ReturnValue != 4096) return { code: outParam.ReturnValue };
    var job = wmi().Get(outParam.Job);
    return { InstanceID: job.InstanceID, Description: job.Description, StatusDescriptions: job.StatusDescriptions };
}
2022/6/14 0:14:25 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 3 楼 ] 回复
简单的创建虚拟机示例代码,更高级一些的请参考 5 楼:

,VpsCreateDoc: [ "VPS 设置", "name, system, cpu, mem", "支持的 system: Alpine, Windows, Centos", "内存 mem 单位为 GB" ]
,vpscreate: function() {
    var hyper = wmi().instancesOf("Msvm_VirtualSystemManagementService");
    if(!hyper.Count) return { err: "服务未启用" };
    var rs = wmi().Get("Msvm_VirtualSystemSettingData").SpawnInstance_();
    rs.ElementName = form("name") || "测试虚拟机";
    rs.Version = "9.0";    // 坑:非管理员权限时,版本号必填且格式必须正确,否则 4096 错误
    rs.ConfigurationDataRoot = "F:\\Disks\\HyperV\\Free";    // 坑:操作账号必须有读写权限,否则第二次请求报 4096
    var code = hyper.ItemIndex(0).DefineSystem(rs.GetText_(2));
    return { code: code };
}
2022/6/14 19:54:06 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 4 楼 ] 回复
查看虚拟机列表示例代码:
,VpsListDoc: [ "VPS 列表" ]
,vpslist: function() {
    var rs = wmi().execQuery("select * from CIM_System where Caption='虚拟机'");
    // 得到虚拟机列表(名称,安装时间,运行状态,开机时间毫秒数)
    rs = wmiToArr(rs, "ElementName, InstallDate, EnabledState, OnTimeInMilliseconds");
    var status = [ "", "", "正在运行", "关机" ];
    for(var i = 0; i < rs.length; i++) {
        rs[i].StateDescription = status[rs[i].EnabledState];
        rs[i].InstallDate = parseTime(rs[i].InstallDate);
        if(rs[i].OnTimeInMilliseconds > 0) {
            rs[i].BootTime = new Date(sys.sTime - rs[i].OnTimeInMilliseconds).getVarDate();
        }
    };
    return rs;
}

但是这里的虚拟机列表没有 IP 地址,可以使用 Msvm_GuestNetworkAdapterConfiguration 得到所有虚拟机的 IP 地址,然后根据 InstanceID 以“\”分隔,索引 1 即 关联虚拟机的 Name,IPAddresses 即 IP 地址数组。
Alpine 里面的IPAddresses 数组为空,是因为 Alpine 默认没有内置 Hyper-V 集成服务,可以使用如下命令开启:
apk add hvtools
rc-update add hv_kvp_daemon
rc-update add hv_vss_daemon
rc-update add hv_fcopy_daemon
service hv_kvp_daemon start
service hv_vss_daemon start
service hv_fcopy_daemon start

然后,Hyper-V 控制台和 wmi 都能获取到 Alpine 的 IP 地址了。

安装 dhcpcd (apk add dhcpcd)后重启, 就能通过域名 hostname.mshome.net 访问虚拟机。
2022/6/14 19:55:41 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 5 楼 ] 回复
还可以使用反射调用,功能更强大一点,可以获得虚拟机信息:
,VpsCreate2Doc: [ "第二种创建虚拟机的方式", "name" ]
,vpscreate2: function() {
    var hyper = wmi().instancesOf("Msvm_VirtualSystemManagementService").ItemIndex(0);
    var newVM = wmi().Get("Msvm_VirtualSystemSettingData").SpawnInstance_();
    newVM.ElementName = form("name") || "测试虚拟机";
    newVM.Version = "9.0";    // 坑:非管理员权限时,版本号必填且格式必须正确,否则 4096 错误
    newVM.ConfigurationDataRoot = "F:\\Disks\\HyperV\\Free";    // 坑:操作账号必须有读写权限,否则第二次请求报 4096
    var inParam = hyper.Methods_("DefineSystem").InParameters.SpawnInstance_();
    inParam.SystemSettings = newVM.GetText_(2);    // 需要去官方文档查询参数名
    var outParam = hyper.ExecMethod_("DefineSystem", inParam);
    if(outParam.ReturnValue) return { err: outParam.ReturnValue };
    newVM = wmi().Get(outParam.ResultingSystem);    // 得到新创建的虚拟机
    return { Name: newVM.Name, ElementName: newVM.ElementName, ResultingSystem: outParam.ResultingSystem };
}
2022/6/14 21:17:16 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 6 楼 ] 回复
接下来是重头了,将硬盘附加到虚拟机!这个功能我花了1.5天才调通。微软的零件说明书真蛋疼,每个零件都有参数和作用,但是你想实现一个功能,望着一堆零件你不知道从哪下手,关键是,有些零件参数还标错了。百度谷歌各种搜索,感觉我是第一个做这个功能的,搜到的要么只有创建虚拟机,要么还是 wmi v1 的方法;请教 Copilot 大神,给你各种看起来很佩服但实际跑不起来的代码 😂😂😂。然后,微软官方文档里面 资源类标注的是 CIM_VirtualSystemSettingData,实际必须使用 Msvm_ResourceAllocationSettingData ,还有就是 Type 和 SubType 居然都必须提供。我是在不断尝试各种错误的时候,偶然将 CIM 改成 Msvm 就神奇的成功了,这得多幸运!

,VpsAttachVhdDoc: [ "挂载 VHD", "vmName, vhdPath", "vmName 为虚拟机 GUID" ]
,vpsattachvhd: function(vmName, vhdPath) {
    if(!vmName) vmName = form("vmName");
    if(!vhdPath) vhdPath = form("vhdPath");
    // 查找 IDE 存储控制器
    var ide = wmi().execQuery("select * from Msvm_ResourceAllocationSettingData where ResourceType=5 and InstanceID like 'Microsoft:" + vmName + "%' and Address=0");
    if(!ide.Count) return { err: "未找到 IDE0 存储控制器" };
    // 向IDE控制器添加驱动器
    var drv = wmi().Get("Msvm_ResourceAllocationSettingData").SpawnInstance_();    // 坑:微软官方文档上是 CIM,实际必须用Msvm
    drv.AddressOnParent = "0";    // IDE0 的 0 号插槽
    drv.ResourceType = 17;        // 硬盘驱动器
    drv.ResourceSubType = "Microsoft:Hyper-V:Synthetic Disk Drive";    // 坑:Type 和 SubType 两个都必须提供!
    drv.Parent = ide.ItemIndex(0).Path_.Path;
    var hyper = wmi().instancesOf("Msvm_VirtualSystemManagementService").ItemIndex(0);
    var inParam = hyper.Methods_("AddResourceSettings").InParameters.SpawnInstance_();
    var vps = wmi().execQuery("select * from CIM_ComputerSystem where Name='" + vmName + "'");
    inParam.AffectedConfiguration = vps.ItemIndex(0).Path_.Path;
    inParam.ResourceSettings = [ drv.GetText_(2) ];
    var outParam = hyper.ExecMethod_("AddResourceSettings", inParam);
    if(outParam.ReturnValue == 4096) return { code: outParam.ReturnValue, msg: "硬盘驱动器参数可能有问题。", Job: outParam.Job };
    if(outParam.ReturnValue) return { code: outParam.ReturnValue, msg: "添加驱动器失败" };
    // 向硬盘驱动器添加虚拟硬盘
    var vhd = wmi().Get("Msvm_ResourceAllocationSettingData").SpawnInstance_();
    vhd.ResourceType = 31;
    vhd.ResourceSubType = "Microsoft:Hyper-V:Virtual Hard Disk";
    vhd.HostResource = [ vhdPath ];
    vhd.Parent = outParam.ResultingResourceSettings[0];    // 刚刚附加的驱动器
    inParam = hyper.Methods_("AddResourceSettings").InParameters.SpawnInstance_();
    inParam.AffectedConfiguration = vps.ItemIndex(0).Path_.Path;
    inParam.ResourceSettings = [ vhd.GetText_(2) ];
    outParam = hyper.ExecMethod_("AddResourceSettings", inParam);
    if(outParam.ReturnValue == 4096) return { code: outParam.ReturnValue, msg: "硬盘映像参数可能有问题。", Job: outParam.Job };
    if(outParam.ReturnValue) return { code: outParam.ReturnValue, msg: "添加硬盘映像失败" };
    return { code: 0, msg: "硬盘附加成功" };
}
2022/6/15 20:23:31 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 7 楼 ] 回复
然后是挂载网卡,方法又不一样了!不能直接新建一个 Msvm_ResourceAllocationSettingData 实例,必须使用 Msvm_SyntheticEthernetPortSettingData 已存在的默认值。调通此功能花费一整天。

,VpsAttachEthDoc: [ "挂载网卡", "vpsName, switchName", "vpsName 为 虚拟机 GUID", "switchName 默认为“Default Switch”" ]
,vpsattacheth: function(vpsName, switchName) {
    if(!vpsName) vpsName = form("vpsName");
    if(!switchName) switchName = form("switchName") || "Default Switch";
    var vps = wmi().execQuery("select * from CIM_ComputerSystem where Name='" + vpsName + "'");
    if(!vps.Count) return { err: "没有找到虚拟机" };
    var sw = wmi().execQuery("select * from Msvm_VirtualEthernetSwitch where ElementName='" + switchName + "'");
    if(!sw.Count) return { err: "没有找到交换机" };
    var vsms = wmi().instancesOf("Msvm_VirtualSystemManagementService").ItemIndex(0);    // 用于操作虚拟机的工具,Msvm 比 CIM 稳定
    // 装网卡
    var sep = wmi().Get("Msvm_SyntheticEthernetPortSettingData.InstanceID='Microsoft:Definition\\6A45335D-4C3A-44B7-B61F-C9808BBDF8ED\\Default'");
    var inParam = vsms.Methods_("AddResourceSettings").InParameters.SpawnInstance_();
    inParam.AffectedConfiguration = vps.ItemIndex(0).Path_.Path;
    inParam.ResourceSettings = [ sep.GetText_(2) ];
    var outParam = vsms.ExecMethod_("AddResourceSettings", inParam);
    var code = outParam.ReturnValue;
    if(code == 4096) return { code: 4096, msg: "正在操作", Job: outParam.Job };
    if(code != 0) return { err: "以太网适配器参数可能有错", code: outParam.ReturnValue };
    // 插网线
    var etc = wmi().Get("Msvm_EthernetPortAllocationSettingData.InstanceID='Microsoft:Definition\\72027ECE-E44A-446E-AF2B-8D8C4B8A2279\\Default'");
    etc.Parent = outParam.ResultingResourceSettings.getItem(0);
    etc.HostResource = [ sw.ItemIndex(0).Path_.Path ];
    var code = vsms.AddResourceSettings(vps.ItemIndex(0).Path_.Path, [ etc.GetText_(2) ]);
    if(code != 0) return { err: "网卡设置参数可能有错", code: code };
    return { msg: "以太网适配器附加成功", Path: etc.Parent };
}
2022/6/15 22:51:19 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 8 楼 ] 回复
上面的代码中,“网卡(Msvm_SyntheticEthernetPortSettingData)”,“网线(Msvm_EthernetPortAllocationSettingData)”分别用了"6A45335D-4C3A-44B7-B61F-C9808BBDF8ED" 和 "72027ECE-E44A-446E-AF2B-8D8C4B8A2279"两个ID,这两个ID应该是固定的。万一因为操作系统版本不一致(Win 10 和 Win 11 是一致的),请按照常规的啰嗦做法实现:

var sep = getDefaultSetting("Synthetic Ethernet Port");
var etc = getDefaultSetting("Ethernet Connection");

// 获取参数的默认设置
function getDefaultSetting(strType) {
    // 对应池\分配功能\设置项\默认网卡设置
    var pool = wmi().execQuery("select * from Msvm_ResourcePool where ResourceSubType='Microsoft:Hyper-V:" + strType + "' and Primordial=True");    // 虚拟以太网端口池
    var acs = wmi().execQuery("select * from Msvm_AllocationCapabilities where InstanceID='" + pool.ItemIndex(0).InstanceID + "'");    // 虚拟以太网分配功能
    var sdc = new Enumerator(acs.ItemIndex(0).References_("Msvm_SettingsDefineCapabilities"));    // 虚拟以太网分配功能关联的设置项
    while(!sdc.atEnd()) {
        var x = sdc.item();
        if(x.ValueRole == 0) return wmi().Get(x.PartComponent);
        sdc.moveNext();
    }
}
2022/6/16 21:26:46 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 9 楼 ] 回复
然后,就是最后一步,开关机:

,StartUpDoc: [ "虚拟机开机", "vmName", "vmName 为虚拟机 GUID" ]
,startup: function(vmName) {
    if(!vmName) vmName = form("vmName");
    var vps = wmi().execQuery("select * from CIM_System where Name='" + vmName + "'");
    if(!vps.Count) return { err: "没有找到虚拟机" };
    var state = vps.ItemIndex(0).EnabledState;
    if(state != 3) return { code: 0, msg: "当前状态[" + state + "]暂时不支持开机" };
    var code = vps.ItemIndex(0).RequestStateChange(2);
    if(code != 4096) return { err: "操作失败", code: code };
    return { code: code, msg: "命令发送成功" };
}

,ShutdownDoc: [ "虚拟机关机", "vmName, force", "vmName 为虚拟机 GUID", "force=1时 强制关机" ]
,shutdown: function(vmName, force) {
    if(!vmName) vmName = form("vmName");
    if(!force) force = form("force");
    var vps = wmi().execQuery("select * from CIM_System where Name='" + vmName + "'");
    if(!vps.Count) return { err: "没有找到虚拟机" };
    var state = vps.ItemIndex(0).EnabledState;
    if(state != 2) return { code: 0, msg: "当前状态[" + state + "]暂时不支持关机" };
    var code = vps.ItemIndex(0).RequestStateChange(force == 1 ? 3 : 4);
    if(code != 4096) return { err: "操作失败", code: code };
    return { code: code, msg: "命令发送成功" };
}

// RequestStateChange 参数说明官方参考
2022/6/16 22:44:11 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 10 楼 ] 回复
然后来一个完全自动化创建 VPS 并开机的接口:

,NewVpsDoc: [ "新建虚拟机", "vpsName, system, cpu, mem", "内存 mem 单位为 GB" ]
,newvps: function() {
    var vpsName = form("vpsName") || "NewVPS";
    var system = (form("system") || "alpine").toLowerCase();
    var cpu = ~~form("cpu") || 1;
    var mem = ~~form("mem") || 1;
    if(/^[^\w \-\.]$/.test(vpsName)) return { err: "虚拟机名称不能包含特殊字符" };
    if(!sys.source[system]) return { err: "不支持的系统类型" };
    // 判断此名称的虚拟机是否创建过了
    var vps = wmi().ExecQuery("select * from CIM_System where ElementName='" + vpsName + "'");
    if(vps.Count) return { err: "虚拟机名称【" + vpsName + "】已被使用" };
    // 创建硬盘是异步操作,先创建硬盘镜像
    var rs = this.vhdcreate(vpsName, system);
    if(rs.err) return { err: rs.err, step: "创建硬盘" };
    // 创建虚拟机
    rs = this.vpscreate(vpsName);
    if(rs.err) return { err: rs.err, step: "创建虚拟机" };
    // 挂载硬盘到虚拟机
    var vpsid = wmi().Get(rs.ResultingSystem).Name;
    rs = this.vpsattachvhd(vpsid, sys.dir.vhdRoot + "\\Free\\" + vpsName + ".vhdx");
    if(rs.err) return { err: rs.err, step: "挂载硬盘" };
    // 挂载网络适配器
    rs = this.vpsattacheth(vpsid, "Default Switch");
    if(rs.err) return { err: rs.err, step: "挂载网络适配器" };
    // 开机
    rs = this.startup(vpsid);
    if(rs.err) return { err: rs.err, step: "开机" };
    return { msg: "虚拟机【" + vpsName + "】创建成功,默认密码为:******" };
}
2022/6/17 17:52:43 IP:已设置保密
風雲 (ID: 3)
头衔:论坛版主
等级:天使
积分:1351
发帖:59
来自:保密
注册:2022/3/30 15:28:53
造访:2024/4/19 21:24:50
[ 第 11 楼 ] 回复
创建了太多测试的虚拟机,可以删除。注意,建议自动创建的虚拟机使用某一个指定版本,接口只能删除这个版本的虚拟机,以保证正常虚拟机数据安全。

,DropVpsDoc: [ "删除虚拟机", "vpsName", "此 vpsName 为虚拟机友好名称" ]
,dropvps: function() {
    var vpsName = form("vpsName");
    if(/^[^\w \-\.]$/.test(vpsName)) return { err: "虚拟机名称不能包含特殊字符" };
    var vps = wmi().ExecQuery("select * from CIM_ComputerSystem where ElementName='" + vpsName + "'");
    if(vps.Count < 1) return { err: "虚拟机【" + vpsName + "】不存在" };
    vps = vps.ItemIndex(0);
    var vss = wmi().Get("Msvm_VirtualSystemSettingData.InstanceID='Microsoft:" + vps.Name + "'");
    if(vss.Version != "9.0") return { err: "此虚拟机暂时只能由管理员删除。" };
    if(vps.EnabledState != 3) {
        vps.RequestStateChange(4);
        return { err: "已尝试关闭虚拟机【" + vps.Name + "】,请稍后重试。" };
    }
    var vsms = wmi().ExecQuery("select * from Msvm_VirtualSystemManagementService").ItemIndex(0);
    // 先删除虚拟机会导致虚拟硬盘文件被占用,所以先删虚拟硬盘。
    var fso = new ActiveXObject("Scripting.FileSystemObject");
    fso.DeleteFile(sys.dir.vhdRoot + "\\Free\\" + vpsName + ".vhdx");
    vsms.DestroySystem(vps.Path_.Path);
    return { msg: "虚拟机【" + vpsName + "】删除完成" };
}
2022/6/17 17:55:00 IP:已设置保密
pojin (ID: 2)
等级:精灵王
积分:244
发帖:1
来自:保密
注册:2022/3/30 11:42:27
造访:2024/4/19 8:46:13
[ 第 12 楼 ] 回复
很牛的样子,不知道怎么用?
2022/6/17 21:29:17 IP:已设置保密
分页: 1 2, 共 2 页
快速回复主题
账号/密码
用户: 没有注册? 密码:
评论内容