mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-24 16:03:43 +08:00
fix(glance): support prob iso image (#24133)
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package isoutils // import "yunion.io/x/onecloud/pkg/util/isoutils"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 := ¤tDirEntry[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 := ¤tDirEntry[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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user