fix(glance): support prob iso image (#24133)

This commit is contained in:
屈轩
2026-01-28 15:28:56 +08:00
committed by GitHub
parent 7f54e038c0
commit fa08f8fd8f
261 changed files with 38575 additions and 9158 deletions
+2 -1
View File
@@ -19,13 +19,14 @@ import (
"net/http"
"strings"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apigateway/clientman"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/util/netutils2"
"yunion.io/x/pkg/errors"
)
const (
+1
View File
@@ -60,6 +60,7 @@ const (
IMAGE_OS_DISTRO = "os_distribution"
IMAGE_OS_TYPE = "os_type"
IMAGE_OS_VERSION = "os_version"
IMAGE_OS_LANGUAGE = "os_language"
IMAGE_DISK_FORMAT = "disk_format"
IMAGE_UEFI_SUPPORT = "uefi_support"
IMAGE_BIOS_SUPPORT = "bios_support"
@@ -24,9 +24,9 @@ import (
"strings"
"time"
"github.com/coredns/coredns/plugin/pkg/log"
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/cloudmux/pkg/multicloud/objectstore"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/streamutils"
)
+54 -5
View File
@@ -63,6 +63,7 @@ import (
"yunion.io/x/onecloud/pkg/mcclient/modules/notify"
"yunion.io/x/onecloud/pkg/util/cephutils"
"yunion.io/x/onecloud/pkg/util/fileutils2"
"yunion.io/x/onecloud/pkg/util/isoutils"
"yunion.io/x/onecloud/pkg/util/logclient"
"yunion.io/x/onecloud/pkg/util/procutils"
"yunion.io/x/onecloud/pkg/util/qemuimg"
@@ -1734,8 +1735,24 @@ func (m *SImageManager) PerformVmwareAccountAdded(ctx context.Context, userCred
func (image *SImage) doProbeImageInfo(ctx context.Context, userCred mcclient.TokenCredential) (bool, error) {
if image.IsIso() {
// no need to probe
return false, nil
imagePath := image.GetLocalLocation()
if len(imagePath) == 0 {
return false, errors.Wrapf(httperrors.ErrNotFound, "image file %s not found", image.Location)
}
fp, err := os.Open(imagePath)
if err != nil {
return false, errors.Wrap(err, "Open image file")
}
defer fp.Close()
isoInfo, err := isoutils.DetectOSFromISO(fp)
if err != nil {
return false, errors.Wrap(err, "DetectOSFromISO")
}
err = image.updateIsoInfo(ctx, userCred, isoInfo)
if err != nil {
return false, errors.Wrap(err, "updateIsoInfo")
}
return true, nil
}
if image.IsData.IsTrue() {
// no need to probe
@@ -1789,9 +1806,9 @@ func (image *SImage) updateImageInfo(
imageProperties := jsonutils.Marshal(imageInfo.OsInfo).(*jsonutils.JSONDict)
imageProperties.Set(api.IMAGE_OS_ARCH, jsonutils.NewString(imageInfo.OsInfo.Arch))
imageProperties.Set("os_version", jsonutils.NewString(imageInfo.OsInfo.Version))
imageProperties.Set("os_distribution", jsonutils.NewString(imageInfo.OsInfo.Distro))
imageProperties.Set("os_language", jsonutils.NewString(imageInfo.OsInfo.Language))
imageProperties.Set(api.IMAGE_OS_VERSION, jsonutils.NewString(imageInfo.OsInfo.Version))
imageProperties.Set(api.IMAGE_OS_DISTRO, jsonutils.NewString(imageInfo.OsInfo.Distro))
imageProperties.Set(api.IMAGE_OS_LANGUAGE, jsonutils.NewString(imageInfo.OsInfo.Language))
imageProperties.Set(api.IMAGE_OS_TYPE, jsonutils.NewString(imageInfo.OsType))
imageProperties.Set(api.IMAGE_PARTITION_TYPE, jsonutils.NewString(imageInfo.PhysicalPartitionType))
@@ -1804,6 +1821,38 @@ func (image *SImage) updateImageInfo(
return ImagePropertyManager.SaveProperties(ctx, userCred, image.Id, imageProperties)
}
func (image *SImage) updateIsoInfo(ctx context.Context, userCred mcclient.TokenCredential, imageInfo *isoutils.ISOInfo) error {
if gotypes.IsNil(imageInfo) || len(imageInfo.Distro) == 0 {
return nil
}
change := false
imageProperties := jsonutils.Marshal(imageInfo).(*jsonutils.JSONDict)
if len(imageInfo.Arch) > 0 {
imageProperties.Set(api.IMAGE_OS_ARCH, jsonutils.NewString(imageInfo.Arch))
change = true
db.Update(image, func() error {
image.OsArch = imageInfo.Arch
return nil
})
}
if len(imageInfo.Version) > 0 {
imageProperties.Set(api.IMAGE_OS_VERSION, jsonutils.NewString(imageInfo.Version))
change = true
}
if len(imageInfo.Distro) > 0 {
imageProperties.Set(api.IMAGE_OS_DISTRO, jsonutils.NewString(imageInfo.Distro))
change = true
}
if len(imageInfo.Language) > 0 {
imageProperties.Set(api.IMAGE_OS_LANGUAGE, jsonutils.NewString(imageInfo.Language))
change = true
}
if change {
return ImagePropertyManager.SaveProperties(ctx, userCred, image.Id, imageProperties)
}
return nil
}
func (image *SImage) updateChecksum() error {
imagePath := image.GetLocalLocation()
if len(imagePath) == 0 {
+1
View File
@@ -12,6 +12,7 @@ import (
"time"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/llm"
+1
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/mark3labs/mcp-go/mcp"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/llm"
+2 -3
View File
@@ -18,9 +18,11 @@ import (
commonapis "yunion.io/x/onecloud/pkg/apis"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
imageapi "yunion.io/x/onecloud/pkg/apis/image"
apis "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/llm/options"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
computemodules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
@@ -29,9 +31,6 @@ import (
"yunion.io/x/onecloud/pkg/util/logclient"
"yunion.io/x/onecloud/pkg/util/procutils"
"yunion.io/x/onecloud/pkg/util/stringutils2"
apis "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/llm/options"
)
var instantModelManager *SInstantModelManager
+1 -2
View File
@@ -7,10 +7,9 @@ import (
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
apis "yunion.io/x/onecloud/pkg/apis/llm"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
apis "yunion.io/x/onecloud/pkg/apis/llm"
)
type SMountedModelsResourceManager struct {
@@ -18,6 +18,7 @@ import (
"context"
"yunion.io/x/log"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/mcclient"
-1
View File
@@ -26,7 +26,6 @@ import (
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcp-server/adapters"
"yunion.io/x/onecloud/pkg/mcp-server/options"
"yunion.io/x/onecloud/pkg/mcp-server/registry"
+1
View File
@@ -0,0 +1 @@
package isoutils // import "yunion.io/x/onecloud/pkg/util/isoutils"
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package isoutils
import (
"fmt"
"io"
"strings"
"github.com/kdomanski/iso9660"
"yunion.io/x/log"
)
// findISO9660File 在ISO9660中查找文件,返回 *iso9660.File
func (r *ISOFileReader) findISO9660File(path string) (*iso9660.File, error) {
if r.format != ISOFormatISO9660 || r.iso9660Img == nil {
return nil, fmt.Errorf("ISO9660格式未初始化")
}
rootDir, err := r.iso9660Img.RootDir()
if err != nil {
return nil, fmt.Errorf("获取根目录失败: %v", err)
}
// 规范化路径
path = strings.Trim(path, "/")
if path == "" {
return rootDir, nil
}
parts := strings.Split(path, "/")
currentDir := rootDir
for i, part := range parts {
if part == "" {
continue
}
// 获取当前目录的子项
children, err := currentDir.GetChildren()
if err != nil {
return nil, fmt.Errorf("读取目录失败: %v", err)
}
// 查找匹配的文件或目录(不区分大小写)
var found *iso9660.File
for _, child := range children {
if strings.EqualFold(child.Name(), part) {
found = child
break
}
}
if found == nil {
return nil, fmt.Errorf("文件或目录不存在: %s", part)
}
// 如果是最后一个部分,返回找到的文件
if i == len(parts)-1 {
return found, nil
}
// 检查是否为目录
if !found.IsDir() {
return nil, fmt.Errorf("路径中的%s不是目录", part)
}
currentDir = found
}
return currentDir, nil
}
// listISO9660Dir 列出ISO9660格式指定目录下的所有文件和子目录
func (r *ISOFileReader) listISO9660Dir(path string) ([]ISO9660FileInfo, error) {
if r.format != ISOFormatISO9660 {
return nil, fmt.Errorf("此方法仅支持ISO9660格式")
}
// 获取目录
dir, err := r.findISO9660File(path)
if err != nil {
return nil, err
}
if !dir.IsDir() {
return nil, fmt.Errorf("路径%s不是目录", path)
}
// 获取子项
children, err := dir.GetChildren()
if err != nil {
return nil, fmt.Errorf("读取目录失败: %v", err)
}
var files []ISO9660FileInfo
for _, child := range children {
fileInfo := ISO9660FileInfo{
Name: child.Name(),
IsDir: child.IsDir(),
Size: child.Size(),
Location: 0, // 使用库时不需要直接访问位置
}
files = append(files, fileInfo)
}
return files, nil
}
// readISO9660FileContent 读取ISO9660格式文件内容
func (r *ISOFileReader) readISO9660FileContent(path string) (string, error) {
file, err := r.findISO9660File(path)
if err != nil {
return "", fmt.Errorf("文件%s不存在: %v", path, err)
}
if file.IsDir() {
return "", fmt.Errorf("路径%s是目录,不是文件", path)
}
reader := file.Reader()
if reader == nil {
return "", fmt.Errorf("无法读取文件%s", path)
}
// 读取前10KB内容(足够识别特征)
buf := make([]byte, 10*1024)
n, err := reader.Read(buf)
if err != nil && err != io.EOF {
log.Errorf("读取ISO9660文件%s失败: %v", path, err)
return "", fmt.Errorf("读取文件%s失败: %v", path, err)
}
return strings.TrimSpace(string(buf[:n])), nil
}
+428
View File
@@ -0,0 +1,428 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package isoutils
import (
"bytes"
"fmt"
"io"
"regexp"
"strings"
"sync"
"github.com/kdomanski/iso9660"
"github.com/mogaika/udf"
"gopkg.in/ini.v1"
"yunion.io/x/log"
"yunion.io/x/pkg/util/imagetools"
)
// ========== 2. 新增结构化返回结果(包含发行版、版本号、架构) ==========
type ISOInfo struct {
Distro string // 发行版(如 CentOS、Ubuntu Server)
Version string // 版本号(如 7.9、22.04 LTS、2022)
Arch string // 架构(如 x86_64、riscv64、arm64)
Language string // 语言(如 en-US、zh-CN)
}
// ISO格式类型
type ISOFormat string
const (
ISOFormatUnknown ISOFormat = "unknown"
ISOFormatUDF ISOFormat = "udf"
ISOFormatISO9660 ISOFormat = "iso9660"
)
// ========== 3. 优化ISOFileReader:增加缓存、日志、架构识别 ==========
type ISOFileReader struct {
format ISOFormat
img *udf.Udf
iso9660Img *iso9660.Image // ISO9660格式的读取器
reader io.Reader
cache sync.Map // 缓存已读取的文件内容:key=文件路径,value=文件内容
}
// isIsoFile 检测ISO格式(UDF或ISO9660)
func isIsoFile(readerAt io.ReaderAt) (bool, error) {
// 读取0x8000地址的内容(ISO9660的Primary Volume Descriptor位置)
buf := make([]byte, 6)
n, err := readerAt.ReadAt(buf, 0x8000)
if err != nil && err != io.EOF {
return false, fmt.Errorf("读取ISO格式标识失败: %v", err)
}
if n < 6 {
return false, fmt.Errorf("读取数据不足")
}
// ISO9660格式:偏移0x8001-0x8005应该是"CD001"
if bytes.Equal(buf[1:6], []byte("CD001")) {
return true, nil
}
return false, nil
}
// NewISOFileReader 初始化ISO读取器(新增格式检测和日志配置)
func NewISOFileReader(reader io.Reader) (*ISOFileReader, error) {
readerAt, ok := reader.(io.ReaderAt)
if !ok {
return nil, fmt.Errorf("reader is not io.ReaderAt")
}
// 检测ISO格式
isIso, err := isIsoFile(readerAt)
if err != nil {
return nil, err
}
if !isIso {
return nil, fmt.Errorf("ISO镜像格式不正确")
}
ret := &ISOFileReader{
format: ISOFormatISO9660,
reader: reader,
cache: sync.Map{},
}
if isUdfFile(readerAt) {
ret.format = ISOFormatUDF
ret.img = udf.NewUdfFromReader(readerAt)
return ret, nil
}
isoImg, err := iso9660.OpenImage(readerAt)
if err != nil {
return nil, fmt.Errorf("打开ISO9660镜像失败: %v", err)
}
ret.iso9660Img = isoImg
return ret, nil
}
// ISO9660FileInfo ISO9660文件信息
type ISO9660FileInfo struct {
Name string // 文件名
IsDir bool // 是否为目录
Size int64 // 文件大小(字节)
Location int64 // 文件在ISO中的位置(字节偏移,使用库时可能为0)
}
func (r *ISOFileReader) list(path string) ([]ISO9660FileInfo, error) {
if r.format == ISOFormatISO9660 {
return r.listISO9660Dir(path)
}
return r.listUdfDir(path)
}
// FileExists 检查ISO内指定路径的文件是否存在(支持UDF和ISO9660)
func (r *ISOFileReader) FileExists(path string) bool {
if r.format == ISOFormatISO9660 {
_, err := r.findISO9660File(path)
return err == nil
}
_, err := r.GetFile(path)
return err == nil
}
// ReadFileContent 读取ISO内指定文件的内容(新增缓存、日志,支持UDF和ISO9660)
func (r *ISOFileReader) ReadFileContent(path string) (string, error) {
// 优先从缓存读取
if cacheVal, ok := r.cache.Load(path); ok {
log.Debugf("从缓存读取文件内容: %s", path)
return cacheVal.(string), nil
}
var content string
var err error
// 根据格式选择相应的读取方法
if r.format == ISOFormatISO9660 {
content, err = r.readISO9660FileContent(path)
} else if r.format == ISOFormatUDF {
content, err = r.readUdfFileContent(path)
} else {
return "", fmt.Errorf("未知的ISO格式: %s", r.format)
}
if err != nil {
return "", err
}
// 写入缓存
r.cache.Store(path, content)
log.Debugf("读取文件%s内容(长度: %d)并缓存", path, len(content))
return content, nil
}
// ========== 6. 核心识别函数:整合版本号、架构、日志、缓存 ==========
func DetectOSFromISO(r io.Reader) (*ISOInfo, error) {
defer func() {
if r := recover(); r != nil {
log.Errorf("DetectOSFromISO panic error: %v", r)
}
}()
result := &ISOInfo{}
// 初始化读取器
reader, err := NewISOFileReader(r)
if err != nil {
return nil, err
}
// ========== 识别Windows系列 ==========
if reader.FileExists("sources/install.wim") {
return DetectWindowsEdition(reader)
}
files, err := reader.list("/")
if err != nil {
return nil, err
}
for _, file := range files {
fileName := file.Name
if fileName == ".treeinfo" {
content, _ := reader.ReadFileContent(fileName)
result = getOsInfoByIniFile(content)
break
}
}
if reader.FileExists(".disk/info") {
content, _ := reader.ReadFileContent(".disk/info")
info := imagetools.NormalizeImageInfo(content, "", "", "", "")
result = &ISOInfo{
Distro: info.OsDistro,
Version: info.OsVersion,
Arch: info.OsArch,
Language: info.OsLang,
}
}
if len(result.Distro) == 0 || result.Distro == imagetools.OS_DIST_OTHER_LINUX {
realeaseFile := ""
if reader.FileExists("dists") {
files, err := reader.list("dists")
if err != nil {
return nil, err
}
for _, file := range files {
log.Debugf("file: %s", file.Name)
if !file.IsDir {
continue
}
subFiles, err := reader.list(fmt.Sprintf("dists/%s", file.Name))
if err != nil {
return nil, err
}
for _, subFile := range subFiles {
if subFile.Name == "Release" {
realeaseFile = fmt.Sprintf("dists/%s/%s", file.Name, subFile.Name)
break
}
}
if realeaseFile != "" {
break
}
}
}
if realeaseFile != "" {
content, _ := reader.ReadFileContent(realeaseFile)
result = getOsInfoByReleaseFile(content)
} else if reader.FileExists("boot/grub2/grub.cfg") {
content, _ := reader.ReadFileContent("boot/grub2/grub.cfg")
result = getOsInfoByGrub(content)
} else if reader.FileExists("EFI/BOOT/grub.cfg") {
content, _ := reader.ReadFileContent("EFI/BOOT/grub.cfg")
result = getOsInfoByGrub(content)
} else if reader.FileExists("isolinux/isolinux.cfg") {
content, _ := reader.ReadFileContent("isolinux/isolinux.cfg")
result = getOsInfoByIsoLinux(content)
}
}
return result, nil
}
func getOsInfoByReleaseFile(content string) *ISOInfo {
result := &ISOInfo{}
for _, line := range strings.Split(content, "\n") {
if strings.HasPrefix(line, "Origin:") {
result.Distro = strings.TrimSpace(strings.TrimPrefix(line, "Origin:"))
}
if strings.HasPrefix(line, "Label:") && len(result.Distro) == 0 {
result.Distro = strings.TrimSpace(strings.TrimPrefix(line, "Label:"))
}
if strings.HasPrefix(line, "Version:") {
result.Version = strings.TrimSpace(strings.TrimPrefix(line, "Version:"))
}
if strings.HasPrefix(line, "Architectures:") {
result.Arch = strings.TrimSpace(strings.TrimPrefix(line, "Architectures:"))
result.Arch = detectArchitecture(strings.ToLower(result.Arch), result.Arch)
}
}
return result
}
func getOsInfoByIniFile(content string) *ISOInfo {
cfg, err := ini.Load([]byte(content))
if err != nil {
// 兼容手动解析(应对部分非标准 INI 格式的 .treeinfo)
return parseTreeInfoFallback(content)
}
release := cfg.Section("release")
ret := &ISOInfo{}
ret.Distro = release.Key("name").String()
ret.Version = release.Key("version").String()
general := cfg.Section("general")
ret.Arch = general.Key("arch").String()
if len(ret.Version) == 0 {
ret.Version = general.Key("version").String()
}
return ret
}
func parseTreeInfoFallback(content string) *ISOInfo {
result := &ISOInfo{}
info := strings.Split(content, "\n")
for _, line := range info {
if strings.HasPrefix(line, "arch =") {
result.Arch = strings.TrimPrefix(line, "arch = ")
}
if strings.HasPrefix(line, "version =") {
result.Version = strings.TrimPrefix(line, "version = ")
}
if strings.HasPrefix(line, "name =") {
result.Distro = strings.TrimPrefix(line, "name = ")
}
}
return result
}
func getOsInfoByIsoLinux(content string) *ISOInfo {
result := &ISOInfo{}
lowerContent := strings.ToLower(content)
// 5.1 识别发行版(关键词匹配)
result.Distro = detectDistro(lowerContent)
// 5.2 识别版本号(正则提取)
result.Version = detectVersion(content)
// 5.3 识别 CPU 架构(关键词+正则)
result.Arch = detectArchitecture(lowerContent, content)
return result
}
func getOsInfoByGrub(content string) *ISOInfo {
result := &ISOInfo{}
lowerContent := strings.ToLower(content)
result.Distro = detectDistro(lowerContent)
result.Version = detectGrubVersion(content)
result.Arch = detectArchitecture(lowerContent, content)
return result
}
func detectDistro(lowerContent string) string {
info := imagetools.NormalizeImageInfo(lowerContent, "", "", "", "")
return info.OsDistro
}
// detectVersion 从配置内容中提取版本号
func detectVersion(content string) string {
// 匹配版本号的正则(支持 x x.y、x.y.z、x.y-LTS 等格式)
versionRegex := regexp.MustCompile(`(\d+(\.\d+(\.\d+)?)?(-[A-Za-z0-9]+)?)`)
// 优先从启动标题(label/menu label)中提取
labelLines := regexp.MustCompile(`(?i)menu label .+Install.+`).FindAllStringSubmatch(content, -1)
for _, line := range labelLines {
log.Debugf("line: %s", line)
if len(line) >= 1 {
version := versionRegex.FindString(line[0])
if version != "" {
return version
}
}
}
// 从整个内容中提取第一个匹配的版本号
return versionRegex.FindString(content)
}
// detectArchitecture 识别 CPU 架构
func detectArchitecture(lowerContent, rawContent string) string {
// 架构关键词映射
archKeywords := map[string][]string{
"x86_64": {"x86_64", "amd64"},
"aarch64": {"aarch64", "arm64"},
"i386": {"i386", "i686"},
"armhfp": {"armhfp", "armv7"},
"ppc64le": {"ppc64le"},
"s390x": {"s390x"},
}
for arch, keywords := range archKeywords {
for _, kw := range keywords {
if strings.Contains(lowerContent, kw) {
return arch
}
}
}
// 从内核文件名(vmlinuz/initrd)中提取
kernelRegex := regexp.MustCompile(`vmlinuz-([a-zA-Z0-9_]+)`)
match := kernelRegex.FindStringSubmatch(rawContent)
if len(match) >= 2 {
return match[1]
}
return ""
}
// detectGrubVersion 从 grub.cfg 提取版本号
func detectGrubVersion(content string) string {
// 匹配版本号的正则(支持 x x.y、x.y.z、x.y-LTS、x.y.z-xxx 等格式)
versionRegex := regexp.MustCompile(`(\d+(\.\d+(\.\d+)?)?(-[A-Za-z0-9]+)?)`)
// 优先从 GRUB 菜单标题(menuentry)中提取(准确性更高)
menuEntryRegex := regexp.MustCompile(`(?i)menuentry\s+["'](.+?)["']`)
menuEntries := menuEntryRegex.FindAllStringSubmatch(content, -1)
for _, entry := range menuEntries {
log.Debugf("entry: %s", entry)
if len(entry) >= 1 {
version := versionRegex.FindString(entry[0])
if version != "" {
return version
}
}
}
// 从内核文件名/参数中提取
kernelLines := regexp.MustCompile(`linux\s+.+`).FindAllString(content, -1)
for _, line := range kernelLines {
version := versionRegex.FindString(line)
if version != "" {
return version
}
}
// 最后从整个内容中提取第一个匹配的版本号
return versionRegex.FindString(content)
}
+187
View File
@@ -0,0 +1,187 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package isoutils
import (
"fmt"
"io"
"strings"
"github.com/mogaika/udf"
"yunion.io/x/log"
)
// isUdfFile 检测是否为UDF格式
func isUdfFile(readerAt io.ReaderAt) bool {
defer func() {
recover()
}()
img := udf.NewUdfFromReader(readerAt)
files := img.ReadDir(nil)
if len(files) == 0 {
return false
}
return true
}
// findUdfDir 在UDF中查找目录,返回目录的 FileEntry
func (r *ISOFileReader) findUdfDir(path string) ([]udf.File, error) {
if r.format != ISOFormatUDF || r.img == nil {
return nil, fmt.Errorf("UDF格式未初始化")
}
// 规范化路径
path = strings.Trim(path, "/")
if path == "" {
// 根目录
return r.img.ReadDir(nil), nil
}
// 查找目录路径
parts := strings.Split(path, "/")
var entry *udf.FileEntry = nil
currentDirEntry := r.img.ReadDir(entry) // 从根目录开始
for i, part := range parts {
if part == "" {
continue
}
var found *udf.File
// 在当前目录中查找
for idx := range currentDirEntry {
child := &currentDirEntry[idx]
childName := child.Name()
// 匹配文件名(不区分大小写)
if strings.EqualFold(childName, part) {
found = child
break
}
}
if found == nil {
return nil, fmt.Errorf("目录不存在: %s", part)
}
// 如果是最后一个部分,返回该目录的内容
if i == len(parts)-1 {
return found.ReadDir(), nil
}
// 继续查找下一级目录
currentDirEntry = found.ReadDir()
}
return nil, fmt.Errorf("未找到目录: %s", path)
}
// listUdfDir 列出UDF格式指定目录下的所有文件和子目录
func (r *ISOFileReader) listUdfDir(path string) ([]ISO9660FileInfo, error) {
if r.format != ISOFormatUDF {
return nil, fmt.Errorf("此方法仅支持UDF格式")
}
// 获取目录内容
children, err := r.findUdfDir(path)
if err != nil {
return nil, err
}
var files []ISO9660FileInfo
for _, child := range children {
fileInfo := ISO9660FileInfo{
Name: child.Name(),
IsDir: child.IsDir(),
Size: child.Size(),
Location: 0, // 使用库时不需要直接访问位置
}
files = append(files, fileInfo)
}
return files, nil
}
// GetFile 在UDF中查找指定路径的文件
func (r *ISOFileReader) GetFile(path string) (*udf.File, error) {
if r.format != ISOFormatUDF {
return nil, fmt.Errorf("此方法仅支持UDF格式")
}
// UDF格式的原有逻辑
parts := strings.Split(strings.Trim(path, "/"), "/")
var entry *udf.FileEntry = nil
currentDirEntry := r.img.ReadDir(entry) // 从根目录开始
for i, part := range parts {
if part == "" {
continue
}
var found *udf.File
// 在当前目录中查找
for idx := range currentDirEntry {
child := &currentDirEntry[idx]
childName := child.Name()
// 匹配文件名(UDF 文件名通常不包含版本号后缀)
if childName == part {
found = child
break
}
}
if found == nil {
return nil, fmt.Errorf("文件或目录不存在: %s", part)
}
// 如果是最后一个部分,返回文件
if i == len(parts)-1 {
return found, nil
}
currentDirEntry = found.ReadDir()
}
return nil, fmt.Errorf("未找到文件: %s", path)
}
// readUdfFileContent 读取UDF格式文件内容
func (r *ISOFileReader) readUdfFileContent(path string) (string, error) {
file, err := r.GetFile(path)
if err != nil {
return "", fmt.Errorf("文件%s不存在: %v", path, err)
}
if file.IsDir() {
return "", fmt.Errorf("路径%s是目录,不是文件", path)
}
reader := file.NewReader()
if reader == nil {
return "", fmt.Errorf("无法读取文件%s", path)
}
// 读取前10KB内容(足够识别特征)
buf := make([]byte, 10*1024)
n, err := reader.Read(buf)
if err != nil && err != io.EOF {
log.Errorf("读取UDF文件%s失败: %v", path, err)
return "", fmt.Errorf("读取文件%s失败: %v", path, err)
}
return strings.TrimSpace(string(buf[:n])), nil
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !linux
// +build !linux
package isoutils
import (
"yunion.io/x/pkg/errors"
)
func DetectWindowsEdition(r *ISOFileReader) (*ISOInfo, error) {
return nil, errors.Wrap(errors.ErrNotSupported, "not supported")
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
// +build linux
package isoutils
import (
"fmt"
"github.com/Microsoft/go-winio/wim"
"yunion.io/x/log"
"yunion.io/x/pkg/util/imagetools"
)
// ========== 7. 保留Windows版本识别函数(适配新结构) ==========
func DetectWindowsEdition(r *ISOFileReader) (*ISOInfo, error) {
wimFile, err := r.GetFile("sources/install.wim")
if err != nil {
return nil, err
}
wim, err := wim.NewReader(wimFile.NewReader())
if err != nil {
return nil, err
}
result := &ISOInfo{}
for _, image := range wim.Image {
version := fmt.Sprintf("%d.%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor, image.Windows.Version.Build)
if image.Windows != nil {
if image.Windows.Arch == 9 {
result.Arch = "x86_64"
} else if image.Windows.Arch == 12 {
result.Arch = "arm64"
} else if image.Windows.Arch == 0 {
result.Arch = "x86"
}
result.Distro = imagetools.OS_DIST_WINDOWS
result.Language = image.Windows.DefaultLanguage
switch fmt.Sprintf("%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor) {
case "6.0":
result.Version = "Windows Vista"
case "6.1":
result.Version = "Windows 7"
case "6.2":
result.Version = "Windows 8"
case "6.3":
result.Version = "Windows 8.1"
case "10.0":
if image.Windows.Version.Build >= 27500 {
result.Version = "Windows 12"
} else if image.Windows.Version.Build >= 22000 {
result.Version = "Windows 11"
} else {
result.Version = "Windows 10"
}
}
if image.Windows.ProductType == "ServerNT" {
result.Distro = imagetools.OS_DIST_WINDOWS_SERVER
switch fmt.Sprintf("%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor) {
case "6.0":
result.Version = "Windows Server 2008"
case "6.1":
result.Version = "Windows Server 2008 R2"
case "6.2":
result.Version = "Windows Server 2012"
case "6.3":
result.Version = "Windows Server 2012 R2"
case "10.0":
if image.Windows.Version.Build >= 26040 {
result.Version = "Windows Server 2025"
} else if image.Windows.Version.Build >= 20348 {
result.Version = "Windows Server 2022"
} else if image.Windows.Version.Build >= 17763 {
result.Version = "Windows Server 2019"
} else if image.Windows.Version.Build >= 14393 {
result.Version = "Windows Server 2016"
}
}
}
log.Debugf("识别到 %s 版本: %s -> %s", result.Distro, version, result.Version)
break
}
}
return result, nil
}