| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- // 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)
- }
|