qemuimg.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package qemuimg
  15. import (
  16. "fmt"
  17. "os"
  18. "runtime"
  19. "strconv"
  20. "strings"
  21. "yunion.io/x/cloudmux/pkg/cloudprovider"
  22. "yunion.io/x/jsonutils"
  23. "yunion.io/x/log"
  24. "yunion.io/x/pkg/errors"
  25. "yunion.io/x/pkg/util/qemuimgfmt"
  26. "yunion.io/x/pkg/utils"
  27. api "yunion.io/x/onecloud/pkg/apis/compute"
  28. imageapi "yunion.io/x/onecloud/pkg/apis/image"
  29. "yunion.io/x/onecloud/pkg/util/fileutils2"
  30. "yunion.io/x/onecloud/pkg/util/procutils"
  31. "yunion.io/x/onecloud/pkg/util/qemutils"
  32. "yunion.io/x/onecloud/pkg/util/seclib2"
  33. )
  34. var (
  35. ErrUnsupportedFormat = errors.Error("unsupported format")
  36. convertWorkInOrder = false
  37. convertCoroutines = 16
  38. )
  39. func SetConvertWorkInOrder(workInOrder bool) {
  40. convertWorkInOrder = workInOrder
  41. }
  42. func SetConvertCoroutines(coroutines int) error {
  43. if coroutines < 1 || coroutines > 16 {
  44. return errors.Errorf("coroutines %d out of range 1-16", coroutines)
  45. }
  46. convertCoroutines = coroutines
  47. return nil
  48. }
  49. type TIONiceLevel int
  50. const (
  51. // The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
  52. IONiceNone = TIONiceLevel(0)
  53. IONiceRealTime = TIONiceLevel(1)
  54. IONiceBestEffort = TIONiceLevel(2)
  55. IONiceIdle = TIONiceLevel(3)
  56. )
  57. const DefaultConvertCorutines = 8
  58. const DefaultQcow2ClusterSize = 65536
  59. var preallocation = "metadata"
  60. func SetPreallocation(prealloc string) error {
  61. if !utils.IsInStringArray(prealloc, []string{"", "disable", "metadata", "falloc", "full"}) {
  62. return errors.Errorf("unsupported preallocation %s", prealloc)
  63. }
  64. if prealloc == "disable" {
  65. prealloc = ""
  66. }
  67. preallocation = prealloc
  68. return nil
  69. }
  70. type SQemuImage struct {
  71. Path string
  72. Password string
  73. Format qemuimgfmt.TImageFormat
  74. SizeBytes int64
  75. ActualSizeBytes int64
  76. ClusterSize int
  77. BackFilePath string
  78. Compat string
  79. Encrypted bool
  80. Subformat string
  81. IoLevel TIONiceLevel
  82. EncryptFormat TEncryptFormat
  83. EncryptAlg seclib2.TSymEncAlg
  84. }
  85. func NewQemuImage(path string) (*SQemuImage, error) {
  86. return NewQemuImageWithIOLevel(path, IONiceNone)
  87. }
  88. func NewQemuImageWithIOLevel(path string, ioLevel TIONiceLevel) (*SQemuImage, error) {
  89. qemuImg := SQemuImage{Path: path, IoLevel: ioLevel}
  90. err := qemuImg.parse()
  91. if err != nil {
  92. return nil, err
  93. }
  94. return &qemuImg, nil
  95. }
  96. func (img *SQemuImage) parse() error {
  97. if len(img.Path) == 0 {
  98. return fmt.Errorf("empty image path")
  99. }
  100. if strings.HasPrefix(img.Path, "nbd") {
  101. // nbd TCP -> nbd:<server-ip>:<port>
  102. // nbd Unix Domain Sockets -> nbd:unix:<domain-socket-file>
  103. img.ActualSizeBytes = 0
  104. } else if strings.HasPrefix(img.Path, "iscsi") {
  105. // iSCSI LUN -> iscsi://<target-ip>[:<port>]/<target-iqn>/<lun>
  106. return cloudprovider.ErrNotImplemented
  107. } else if strings.HasPrefix(img.Path, "sheepdog") {
  108. // sheepdog -> sheepdog[+tcp|+unix]://[host:port]/vdiname[?socket=path][#snapid|#tag]
  109. return cloudprovider.ErrNotImplemented
  110. } else if strings.HasPrefix(img.Path, api.STORAGE_RBD) {
  111. img.ActualSizeBytes = 0
  112. } else {
  113. // check file existence
  114. fileInfo, err := procutils.RemoteStat(img.Path)
  115. if err != nil {
  116. if !os.IsNotExist(err) {
  117. return errors.Wrapf(err, "remote stat of %s", img.Path)
  118. } else {
  119. // not created yet
  120. return nil
  121. }
  122. } else {
  123. img.ActualSizeBytes = fileInfo.Size()
  124. }
  125. }
  126. resp, err := func() (jsonutils.JSONObject, error) {
  127. output, err := procutils.NewRemoteCommandAsFarAsPossible(qemutils.GetQemuImg(), "info", "-U", img.Path, "--output", "json").Output()
  128. if err != nil {
  129. return nil, errors.Wrapf(err, "qemu-img info: %s", output)
  130. }
  131. return jsonutils.Parse(output)
  132. }()
  133. if err != nil {
  134. return err
  135. }
  136. /*
  137. {
  138. "virtual-size": 107374182400,
  139. "filename": "/opt/cloud/workspace/data/glance/images/3ccecd2b-0ab4-4525-8e64-6f1d3c3a2457",
  140. "cluster-size": 65536,
  141. "format": "vmdk",
  142. "actual-size": 2173186048,
  143. "format-specific": {
  144. "type": "vmdk",
  145. "data": {
  146. "cid": 3046516340,
  147. "parent-cid": 4294967295,
  148. "create-type": "streamOptimized",
  149. "extents": [
  150. {
  151. "compressed": true,
  152. "virtual-size": 107374182400,
  153. "filename": "/opt/cloud/workspace/data/glance/images/3ccecd2b-0ab4-4525-8e64-6f1d3c3a2457",
  154. "cluster-size": 65536,
  155. "format": ""
  156. }
  157. ]
  158. }
  159. },
  160. "dirty-flag": false
  161. }
  162. */
  163. info := struct {
  164. VirtualSizeBytes int64 `json:"virtual-size"`
  165. Filename string `json:"filename"`
  166. Format string `json:"format"`
  167. ActualSizeBytes int64 `json:"actual-size"`
  168. ClusterSize int `json:"cluster-size"`
  169. BackingFilename string `json:"backing-filename"`
  170. FullBackingFilename string `json:"full-backing-filename"`
  171. BackingFilenameFormat string `json:"backing-filename-format"`
  172. Encrypted bool `json:"encrypted"`
  173. FormatSpecific struct {
  174. Type string `json:"type"`
  175. Data struct {
  176. Cid uint64 `json:"cid"`
  177. ParentCid uint64 `json:"parent-cid"`
  178. CreateType string `json:"create-type"`
  179. Extents []struct {
  180. Compressed bool `json:"compressed"`
  181. VirtualSize uint64 `json:"virtual-size"`
  182. Filename string `json:"filename"`
  183. ClusterSize uint64 `json:"cluster-size"`
  184. Format string `json:"format"`
  185. } `json:"extents"`
  186. Compat string `json:"compat"`
  187. LazyRefcounts int `json:"lazy-refcounts"`
  188. RefcountBits int `json:"refcount-bits"`
  189. Corrupt bool `json:"corrupt"`
  190. Encrypt struct {
  191. IvgenAlg string `json:"ivgen-alg"`
  192. HashAlg string `json:"hash-alg"`
  193. CipherAlg string `json:"cipher-alg"`
  194. Uuid string `json:"uuid"`
  195. Format string `json:"format"`
  196. CipherMod string `json:"cipher-mode"`
  197. } `json:"encrypt"`
  198. } `json:"data"`
  199. } `json:"format-specific"`
  200. CreateType string `json:"create-type"`
  201. DirtyFlag bool `json:"dirty-flag"`
  202. }{}
  203. err = resp.Unmarshal(&info)
  204. if err != nil {
  205. return errors.Wrapf(err, "resp.Unmarshal")
  206. }
  207. img.Format = qemuimgfmt.TImageFormat(info.Format)
  208. img.SizeBytes = info.VirtualSizeBytes
  209. img.ClusterSize = info.ClusterSize
  210. img.Compat = info.FormatSpecific.Data.Compat
  211. img.Encrypted = info.Encrypted
  212. img.BackFilePath, err = ParseQemuFilepath(info.FullBackingFilename)
  213. if err != nil {
  214. return errors.Wrap(err, "ParseQemuFilepath")
  215. }
  216. img.Subformat = info.CreateType
  217. if img.Subformat == "" {
  218. img.Subformat = info.FormatSpecific.Data.CreateType
  219. }
  220. if img.Encrypted {
  221. img.EncryptFormat = TEncryptFormat(info.FormatSpecific.Data.Encrypt.Format)
  222. img.EncryptAlg = seclib2.TSymEncAlg(info.FormatSpecific.Data.Encrypt.CipherAlg)
  223. }
  224. // test if it is an ISO
  225. if img.Format == qemuimgfmt.RAW && fileutils2.IsFile(img.Path) && fileutils2.IsIsoFile(img.Path) {
  226. img.Format = qemuimgfmt.ISO
  227. }
  228. if img.Format == qemuimgfmt.RAW && fileutils2.IsFile(img.Path) && (fileutils2.IsTarGzipFile(img.Path) || fileutils2.IsTarFile(img.Path)) {
  229. img.Format = imageapi.IMAGE_DISK_FORMAT_TGZ
  230. }
  231. return nil
  232. }
  233. // base64 password
  234. func (img *SQemuImage) SetPassword(password string) {
  235. img.Password = password
  236. }
  237. func (img *SQemuImage) IsValid() bool {
  238. return len(img.Format) > 0
  239. }
  240. func (img *SQemuImage) IsChained() bool {
  241. return len(img.BackFilePath) > 0
  242. }
  243. func (img *SQemuImage) GetBackingChain() ([]string, error) {
  244. if len(img.BackFilePath) > 0 {
  245. backImg, err := NewQemuImage(img.BackFilePath)
  246. if err != nil {
  247. return nil, err
  248. }
  249. backingChain, err := backImg.GetBackingChain()
  250. if err != nil {
  251. return nil, err
  252. }
  253. return append(backingChain, img.BackFilePath), nil
  254. } else {
  255. return []string{}, nil
  256. }
  257. }
  258. type TEncryptFormat string
  259. const (
  260. EncryptFormatLuks = "luks"
  261. )
  262. type SImageInfo struct {
  263. Path string
  264. Format qemuimgfmt.TImageFormat
  265. IoLevel TIONiceLevel
  266. Password string
  267. ClusterSize int
  268. // only luks supported
  269. EncryptFormat TEncryptFormat
  270. // aes-256, sm4
  271. EncryptAlg seclib2.TSymEncAlg
  272. secId string
  273. }
  274. func (info *SImageInfo) SetSecId(id string) {
  275. info.secId = id
  276. }
  277. func (info SImageInfo) ImageOptions() string {
  278. opts := make([]string, 0)
  279. format := info.Format
  280. if len(format) == 0 {
  281. format = qemuimgfmt.QCOW2
  282. }
  283. opts = append(opts, fmt.Sprintf("driver=%s", format))
  284. opts = append(opts, fmt.Sprintf("file.filename=%s", info.Path))
  285. if info.Encrypted() {
  286. encFormat := info.EncryptFormat
  287. if len(encFormat) == 0 {
  288. encFormat = EncryptFormatLuks
  289. }
  290. opts = append(opts, fmt.Sprintf("encrypt.format=%s", encFormat))
  291. secId := info.secId
  292. if len(secId) == 0 {
  293. secId = "sec0"
  294. }
  295. opts = append(opts, fmt.Sprintf("encrypt.key-secret=%s", secId))
  296. // if info.EncryptFormat == EncryptFormatLuks {
  297. // opts = append(opts, fmt.Sprintf("encrypt.cipher-alg=%s", info.EncryptAlg))
  298. // }
  299. }
  300. return strings.Join(opts, ",")
  301. }
  302. func (info SImageInfo) SecretOptions() string {
  303. if info.Encrypted() {
  304. opts := make([]string, 0)
  305. secId := info.secId
  306. if len(secId) == 0 {
  307. secId = "sec0"
  308. }
  309. opts = append(opts, "secret")
  310. opts = append(opts, fmt.Sprintf("id=%s", secId))
  311. opts = append(opts, fmt.Sprintf("data=%s", info.Password))
  312. opts = append(opts, "format=base64")
  313. return strings.Join(opts, ",")
  314. }
  315. return ""
  316. }
  317. func (info SImageInfo) Encrypted() bool {
  318. return len(info.Password) > 0
  319. }
  320. func Convert(srcInfo, destInfo SImageInfo, compact bool, workerOpions []string) error {
  321. if srcInfo.Encrypted() || destInfo.Encrypted() {
  322. return convertEncrypt(srcInfo, destInfo, compact, workerOpions)
  323. } else {
  324. return convertOther(srcInfo, destInfo, compact, workerOpions)
  325. }
  326. }
  327. func convertOther(srcInfo, destInfo SImageInfo, compact bool, workerOpions []string) error {
  328. cmdline := []string{"-c", strconv.Itoa(int(srcInfo.IoLevel)),
  329. qemutils.GetQemuImg(), "convert"}
  330. if compact {
  331. cmdline = append(cmdline, "-c")
  332. }
  333. if workerOpions == nil {
  334. // https://bugzilla.redhat.com/show_bug.cgi?id=1969848
  335. // https://bugs.launchpad.net/qemu/+bug/1805256
  336. // qemu-img convert may hang on aarch64, fix: add -m 1
  337. // no need to limit 1 any more, the bug has been fixed! - QIUJIAN
  338. // cmdline = append(cmdline, "-m", "1")
  339. } else {
  340. cmdline = append(cmdline, workerOpions...)
  341. }
  342. if !utils.IsInStringArray("-W", workerOpions) && !convertWorkInOrder {
  343. cmdline = append(cmdline, "-W")
  344. }
  345. if !utils.IsInStringArray("-m", workerOpions) && convertCoroutines != DefaultConvertCorutines {
  346. cmdline = append(cmdline, "-m", strconv.Itoa(convertCoroutines))
  347. }
  348. if compact {
  349. cmdline = append(cmdline, "-c")
  350. if destInfo.ClusterSize <= 0 {
  351. destInfo.ClusterSize = DefaultQcow2ClusterSize
  352. }
  353. }
  354. cmdline = append(cmdline, "-f", srcInfo.Format.String(), "-O", destInfo.Format.String())
  355. options := []string{}
  356. if destInfo.Format.String() == "vmdk" { // for esxi vmdk
  357. options = append(options, vmdkOptions(compact)...)
  358. }
  359. if destInfo.Format == qemuimgfmt.QCOW2 {
  360. if destInfo.ClusterSize > 0 {
  361. options = append(options, fmt.Sprintf("cluster_size=%d", destInfo.ClusterSize))
  362. } else if srcInfo.ClusterSize > 0 {
  363. options = append(options, fmt.Sprintf("cluster_size=%d", srcInfo.ClusterSize))
  364. }
  365. }
  366. if len(options) > 0 {
  367. cmdline = append(cmdline, "-o", strings.Join(options, ","))
  368. }
  369. cmdline = append(cmdline, srcInfo.Path, destInfo.Path)
  370. log.Infof("XXXX qemu-img command: %s", cmdline)
  371. cmd := procutils.NewRemoteCommandAsFarAsPossible("ionice", cmdline...)
  372. if runtime.GOOS == "darwin" {
  373. cmdline = cmdline[2:]
  374. cmd = procutils.NewRemoteCommandAsFarAsPossible(cmdline[0], cmdline[1:]...)
  375. }
  376. output, err := cmd.Output()
  377. if err != nil {
  378. os.Remove(destInfo.Path)
  379. return errors.Wrapf(err, "convert: %s", output)
  380. }
  381. return nil
  382. }
  383. func convertEncrypt(srcInfo, destInfo SImageInfo, compact bool, workerOpions []string) error {
  384. source, err := NewQemuImageWithIOLevel(srcInfo.Path, srcInfo.IoLevel)
  385. if err != nil {
  386. return errors.Wrapf(err, "NewQemuImage source %s", srcInfo.Path)
  387. }
  388. target, err := NewQemuImage(destInfo.Path)
  389. if err != nil {
  390. return errors.Wrapf(err, "NewQemuImage dest %s", destInfo.Path)
  391. }
  392. if target.Format != qemuimgfmt.QCOW2 {
  393. err = target.CreateQcow2(source.GetSizeMB(), compact, "", destInfo.Password, destInfo.EncryptFormat, destInfo.EncryptAlg)
  394. if err != nil {
  395. return errors.Wrapf(err, "Create target image %s", destInfo.Path)
  396. }
  397. }
  398. cmdline := []string{"-c", strconv.Itoa(int(srcInfo.IoLevel)), qemutils.GetQemuImg(), "convert"}
  399. if compact {
  400. cmdline = append(cmdline, "-c")
  401. }
  402. if workerOpions == nil {
  403. // https://bugzilla.redhat.com/show_bug.cgi?id=1969848
  404. // https://bugs.launchpad.net/qemu/+bug/1805256
  405. // qemu-img convert may hang on aarch64, fix: add -m 1
  406. // no need to limit 1 any more, the bug has been fixed! - QIUJIAN
  407. // cmdline = append(cmdline, "-m", "1")
  408. } else {
  409. cmdline = append(cmdline, workerOpions...)
  410. }
  411. if !utils.IsInStringArray("-W", workerOpions) && !convertWorkInOrder {
  412. cmdline = append(cmdline, "-W")
  413. }
  414. if !utils.IsInStringArray("-m", workerOpions) && convertCoroutines != DefaultConvertCorutines {
  415. cmdline = append(cmdline, "-m", strconv.Itoa(convertCoroutines))
  416. }
  417. if srcInfo.Encrypted() {
  418. if srcInfo.Format != qemuimgfmt.QCOW2 {
  419. return errors.Wrap(errors.ErrNotSupported, "source image not support encryption")
  420. }
  421. }
  422. if destInfo.Encrypted() {
  423. if destInfo.Format != qemuimgfmt.QCOW2 {
  424. return errors.Wrap(errors.ErrNotSupported, "target image not support encryption")
  425. }
  426. }
  427. if srcInfo.Encrypted() && destInfo.Encrypted() {
  428. if srcInfo.Password == destInfo.Password {
  429. srcInfo.secId = "sec0"
  430. destInfo.secId = "sec0"
  431. cmdline = append(cmdline, "--object", srcInfo.SecretOptions())
  432. } else {
  433. srcInfo.secId = "sec0"
  434. cmdline = append(cmdline, "--object", srcInfo.SecretOptions())
  435. destInfo.secId = "sec1"
  436. cmdline = append(cmdline, "--object", destInfo.SecretOptions())
  437. }
  438. } else if srcInfo.Encrypted() {
  439. srcInfo.secId = "sec0"
  440. cmdline = append(cmdline, "--object", srcInfo.SecretOptions())
  441. } else if destInfo.Encrypted() {
  442. destInfo.secId = "sec0"
  443. cmdline = append(cmdline, "--object", destInfo.SecretOptions())
  444. } else {
  445. // dead branch
  446. }
  447. cmdline = append(cmdline, "--image-opts", srcInfo.ImageOptions())
  448. cmdline = append(cmdline, "--target-image-opts", destInfo.ImageOptions())
  449. cmdline = append(cmdline, "-n")
  450. cmd := procutils.NewRemoteCommandAsFarAsPossible("ionice", cmdline...)
  451. if runtime.GOOS == "darwin" {
  452. cmdline = cmdline[2:]
  453. cmd = procutils.NewRemoteCommandAsFarAsPossible(cmdline[0], cmdline[1:]...)
  454. }
  455. output, err := cmd.Output()
  456. log.Infof("XXXX qemu-img convert command: %s output: %s", cmdline, output)
  457. if err != nil {
  458. os.Remove(destInfo.Path)
  459. return errors.Wrapf(err, "convert: %s", string(output))
  460. }
  461. return nil
  462. }
  463. func (img *SQemuImage) doConvert(targetPath string, format qemuimgfmt.TImageFormat, compact bool, password string, encryptFormat TEncryptFormat, encryptAlg seclib2.TSymEncAlg) error {
  464. if !img.IsValid() {
  465. return fmt.Errorf("self is not valid")
  466. }
  467. destClusterSize := img.ClusterSize
  468. if compact {
  469. destClusterSize = DefaultQcow2ClusterSize
  470. }
  471. return Convert(SImageInfo{
  472. Path: img.Path,
  473. Format: img.Format,
  474. IoLevel: img.IoLevel,
  475. Password: img.Password,
  476. EncryptFormat: img.EncryptFormat,
  477. EncryptAlg: img.EncryptAlg,
  478. ClusterSize: img.ClusterSize,
  479. }, SImageInfo{
  480. Path: targetPath,
  481. Format: format,
  482. Password: password,
  483. EncryptFormat: encryptFormat,
  484. EncryptAlg: encryptAlg,
  485. ClusterSize: destClusterSize,
  486. }, compact, nil)
  487. }
  488. func (img *SQemuImage) Clone(name string, format qemuimgfmt.TImageFormat, compact bool) (*SQemuImage, error) {
  489. switch format {
  490. case qemuimgfmt.QCOW2:
  491. return img.CloneQcow2(name, compact)
  492. case qemuimgfmt.VMDK:
  493. return img.CloneVmdk(name, compact)
  494. case qemuimgfmt.RAW:
  495. return img.CloneRaw(name)
  496. case qemuimgfmt.VHD:
  497. return img.CloneVhd(name)
  498. default:
  499. return nil, ErrUnsupportedFormat
  500. }
  501. }
  502. func (img *SQemuImage) clone(target string, format qemuimgfmt.TImageFormat, compact bool, password string, encryptFormat TEncryptFormat, encryptAlg seclib2.TSymEncAlg) (*SQemuImage, error) {
  503. err := img.doConvert(target, format, compact, password, encryptFormat, encryptAlg)
  504. if err != nil {
  505. return nil, errors.Wrap(err, "doConvert")
  506. }
  507. return NewQemuImage(target)
  508. }
  509. func (img *SQemuImage) convert(format qemuimgfmt.TImageFormat, compact bool, password string, encryptFormat TEncryptFormat, alg seclib2.TSymEncAlg) error {
  510. tmpPath := fmt.Sprintf("%s.%s", img.Path, utils.GenRequestId(36))
  511. err := img.doConvert(tmpPath, format, compact, password, encryptFormat, alg)
  512. if err != nil {
  513. return errors.Wrap(err, "doConvert")
  514. }
  515. cmd := procutils.NewRemoteCommandAsFarAsPossible("mv", "-f", tmpPath, img.Path)
  516. output, err := cmd.Output()
  517. if err != nil {
  518. os.Remove(tmpPath)
  519. return errors.Wrapf(err, "move %s", string(output))
  520. }
  521. img.Password = password
  522. img.EncryptFormat = encryptFormat
  523. img.EncryptAlg = alg
  524. return img.parse()
  525. }
  526. func (img *SQemuImage) convertTo(
  527. format qemuimgfmt.TImageFormat, compact bool, password string, output string, encFormat TEncryptFormat, encAlg seclib2.TSymEncAlg,
  528. ) error {
  529. err := img.doConvert(output, format, compact, password, encFormat, encAlg)
  530. if err != nil {
  531. return errors.Wrap(err, "doConvert")
  532. }
  533. img.Password = password
  534. return img.parse()
  535. }
  536. func (img *SQemuImage) Copy(name string) (*SQemuImage, error) {
  537. if !img.IsValid() {
  538. return nil, fmt.Errorf("self is not valid")
  539. }
  540. cmd := procutils.NewRemoteCommandAsFarAsPossible("cp", "--sparse=always", img.Path, name)
  541. output, err := cmd.Output()
  542. if err != nil {
  543. os.Remove(name)
  544. return nil, errors.Wrapf(err, "cp: %s", string(output))
  545. }
  546. newImg, err := NewQemuImage(name)
  547. if err != nil {
  548. return nil, errors.Wrap(err, "NewQemuImage")
  549. }
  550. newImg.Password = img.Password
  551. return newImg, nil
  552. }
  553. func (img *SQemuImage) Convert2Qcow2To(output string, compact bool, password string, encFormat TEncryptFormat, encAlg seclib2.TSymEncAlg) error {
  554. return img.convertTo(qemuimgfmt.QCOW2, compact, password, output, encFormat, encAlg)
  555. }
  556. func (img *SQemuImage) Convert2Qcow2(compact bool, password string, encFormat TEncryptFormat, encAlg seclib2.TSymEncAlg) error {
  557. if len(password) == 0 && len(img.Password) > 0 {
  558. password = img.Password
  559. encFormat = img.EncryptFormat
  560. encAlg = img.EncryptAlg
  561. }
  562. return img.convert(qemuimgfmt.QCOW2, compact, password, encFormat, encAlg)
  563. }
  564. func (img *SQemuImage) Convert2Vmdk(compact bool) error {
  565. return img.convert(qemuimgfmt.VMDK, compact, "", "", "")
  566. }
  567. func (img *SQemuImage) Convert2Vhd() error {
  568. return img.convert(qemuimgfmt.VHD, false, "", "", "")
  569. }
  570. func (img *SQemuImage) Convert2Raw() error {
  571. return img.convert(qemuimgfmt.RAW, false, "", "", "")
  572. }
  573. func (img *SQemuImage) IsRaw() bool {
  574. return img.Format == qemuimgfmt.RAW
  575. }
  576. func (img *SQemuImage) IsSparseQcow2() bool {
  577. return img.Format == qemuimgfmt.QCOW2 && img.ClusterSize >= 1024*1024*2
  578. }
  579. func (img *SQemuImage) IsSparseVmdk() bool {
  580. return img.Format == qemuimgfmt.VMDK && img.Subformat != "streamOptimized"
  581. }
  582. func (img *SQemuImage) IsSparse() bool {
  583. return img.IsRaw() || img.IsSparseQcow2() || img.IsSparseVmdk()
  584. }
  585. func (img *SQemuImage) Expand() error {
  586. if img.IsSparse() {
  587. return nil
  588. }
  589. return img.Convert2Qcow2(false, img.Password, img.EncryptFormat, img.EncryptAlg)
  590. }
  591. func (img *SQemuImage) CloneQcow2(name string, compact bool) (*SQemuImage, error) {
  592. return img.clone(name, qemuimgfmt.QCOW2, compact, img.Password, img.EncryptFormat, img.EncryptAlg)
  593. }
  594. func vmdkOptions(compact bool) []string {
  595. if compact {
  596. return []string{"subformat=streamOptimized"}
  597. } else {
  598. return []string{"subformat=monolithicSparse"}
  599. }
  600. }
  601. // func vhdOptions(compact bool) []string {
  602. // if compact {
  603. // return []string{"subformat=dynamic"}
  604. // } else {
  605. // return []string{"subformat=fixed"}
  606. // }
  607. // }
  608. func (img *SQemuImage) CloneVmdk(name string, compact bool) (*SQemuImage, error) {
  609. return img.clone(name, qemuimgfmt.VMDK, compact, "", "", "")
  610. }
  611. func (img *SQemuImage) CloneVhd(name string) (*SQemuImage, error) {
  612. return img.clone(name, qemuimgfmt.VHD, false, "", "", "")
  613. }
  614. func (img *SQemuImage) CloneRaw(name string) (*SQemuImage, error) {
  615. return img.clone(name, qemuimgfmt.RAW, false, "", "", "")
  616. }
  617. func (img *SQemuImage) create(sizeMB int, format qemuimgfmt.TImageFormat, options []string, extraArgs []string) error {
  618. if img.IsValid() && img.Format != qemuimgfmt.RAW {
  619. return fmt.Errorf("create: the image is valid??? %s", img.Format)
  620. }
  621. args := []string{"-c", strconv.Itoa(int(img.IoLevel)),
  622. qemutils.GetQemuImg(), "create", "-f", format.String()}
  623. if len(options) > 0 {
  624. args = append(args, "-o", strings.Join(options, ","))
  625. }
  626. if len(extraArgs) > 0 {
  627. args = append(args, extraArgs...)
  628. }
  629. args = append(args, img.Path)
  630. if sizeMB > 0 {
  631. args = append(args, fmt.Sprintf("%dM", sizeMB))
  632. }
  633. cmd := procutils.NewRemoteCommandAsFarAsPossible("ionice", args...)
  634. if runtime.GOOS == "darwin" {
  635. args = args[2:]
  636. cmd = procutils.NewRemoteCommandAsFarAsPossible(args[0], args[1:]...)
  637. }
  638. output, err := cmd.Output()
  639. if err != nil {
  640. return errors.Wrapf(err, "create image failed: %s", output)
  641. }
  642. return img.parse()
  643. }
  644. type SFileInfo struct {
  645. Driver string `json:"driver"`
  646. Filename string `json:"filename"`
  647. Locking string `json:"locking"`
  648. }
  649. type SQcow2FileInfo struct {
  650. Driver string `json:"driver"`
  651. EncryptKeySecret string `json:"encrypt.key-secret"`
  652. EncryptFormat string `json:"encrypt.format"`
  653. File SFileInfo `json:"file"`
  654. }
  655. func newQcow2FileInfo(filePath string) SQcow2FileInfo {
  656. return SQcow2FileInfo{
  657. Driver: "qcow2",
  658. File: SFileInfo{
  659. Driver: "file",
  660. Filename: filePath,
  661. Locking: "off",
  662. },
  663. }
  664. }
  665. func GetQemuFilepath(path string, encKey string, encFormat TEncryptFormat) string {
  666. if len(encKey) == 0 {
  667. return path
  668. }
  669. info := newQcow2FileInfo(path)
  670. info.EncryptKeySecret = encKey
  671. info.EncryptFormat = string(encFormat)
  672. return fmt.Sprintf("json:%s", jsonutils.Marshal(info))
  673. }
  674. func (img *SQemuImage) CreateQcow2(sizeMB int, compact bool, backPath string, password string, encFormat TEncryptFormat, encAlg seclib2.TSymEncAlg) error {
  675. options := make([]string, 0)
  676. extraArgs := make([]string, 0)
  677. if len(password) > 0 {
  678. extraArgs = append(extraArgs, "--object", fmt.Sprintf("secret,id=sec0,data=%s,format=base64", password))
  679. }
  680. if len(backPath) > 0 {
  681. // options = append(options, fmt.Sprintf("backing_file=%s", backPath))
  682. backQemu, err := NewQemuImage(backPath)
  683. if err != nil {
  684. return errors.Wrap(err, "parse backing file")
  685. }
  686. if backQemu.Encrypted {
  687. extraArgs = append(extraArgs, "-b", GetQemuFilepath(backPath, "sec0", EncryptFormatLuks))
  688. } else {
  689. extraArgs = append(extraArgs, "-b", backPath)
  690. }
  691. if len(string(backQemu.Format)) > 0 {
  692. extraArgs = append(extraArgs, "-F", string(backQemu.Format))
  693. }
  694. if !compact {
  695. options = append(options, "cluster_size=2M")
  696. }
  697. if sizeMB == 0 {
  698. sizeMB = backQemu.GetSizeMB()
  699. }
  700. } else if !compact {
  701. sparseOpts := qcow2SparseOptions()
  702. if preallocation != "" {
  703. if sizeMB <= 1024*1024*4 {
  704. options = append(options, fmt.Sprintf("preallocation=%s", preallocation))
  705. }
  706. }
  707. options = append(options, sparseOpts...)
  708. }
  709. if len(password) > 0 {
  710. if len(encFormat) == 0 {
  711. encFormat = EncryptFormatLuks
  712. }
  713. options = append(options, fmt.Sprintf("encrypt.format=%s", encFormat))
  714. options = append(options, "encrypt.key-secret=sec0")
  715. if encFormat == EncryptFormatLuks {
  716. options = append(options, fmt.Sprintf("encrypt.cipher-alg=%s", encAlg))
  717. }
  718. }
  719. return img.create(sizeMB, qemuimgfmt.QCOW2, options, extraArgs)
  720. }
  721. func (img *SQemuImage) CreateVmdk(sizeMB int, compact bool) error {
  722. return img.create(sizeMB, qemuimgfmt.VMDK, vmdkOptions(compact), nil)
  723. }
  724. func (img *SQemuImage) CreateVhd(sizeMB int) error {
  725. return img.create(sizeMB, qemuimgfmt.VHD, nil, nil)
  726. }
  727. func (img *SQemuImage) CreateRaw(sizeMB int) error {
  728. return img.create(sizeMB, qemuimgfmt.RAW, nil, nil)
  729. }
  730. func (img *SQemuImage) GetSizeMB() int {
  731. return int(img.SizeBytes / 1024 / 1024)
  732. }
  733. func (img *SQemuImage) GetActualSizeMB() int {
  734. return int(img.ActualSizeBytes / 1024 / 1024)
  735. }
  736. func (img *SQemuImage) Resize(sizeMB int) error {
  737. if !img.IsValid() {
  738. return fmt.Errorf("self is not valid")
  739. }
  740. encInfo := SImageInfo{
  741. Path: img.Path,
  742. Format: img.Format,
  743. IoLevel: img.IoLevel,
  744. }
  745. args := make([]string, 0)
  746. args = append(args, "-c", strconv.Itoa(int(img.IoLevel)), qemutils.GetQemuImg(), "resize")
  747. if len(img.Password) > 0 {
  748. encInfo.Password = img.Password
  749. encInfo.EncryptFormat = img.EncryptFormat
  750. encInfo.EncryptAlg = img.EncryptAlg
  751. encInfo.secId = "sec0"
  752. args = append(args, "--object", encInfo.SecretOptions())
  753. }
  754. args = append(args, "--image-opts", encInfo.ImageOptions())
  755. args = append(args, fmt.Sprintf("%dM", sizeMB))
  756. cmd := procutils.NewRemoteCommandAsFarAsPossible("ionice", args...)
  757. if runtime.GOOS == "darwin" {
  758. args = args[2:]
  759. cmd = procutils.NewRemoteCommandAsFarAsPossible(args[0], args[1:]...)
  760. }
  761. output, err := cmd.Output()
  762. if err != nil {
  763. return errors.Wrapf(err, "resize: %s", string(output))
  764. }
  765. return img.parse()
  766. }
  767. func (img *SQemuImage) Rebase(backPath string, force bool) error {
  768. if !img.IsValid() {
  769. return fmt.Errorf("self is not valid")
  770. }
  771. encInfo := SImageInfo{
  772. Path: img.Path,
  773. Format: img.Format,
  774. IoLevel: img.IoLevel,
  775. }
  776. args := []string{"-c", strconv.Itoa(int(img.IoLevel)),
  777. qemutils.GetQemuImg(), "rebase"}
  778. if len(img.Password) > 0 {
  779. encInfo.Password = img.Password
  780. encInfo.EncryptFormat = img.EncryptFormat
  781. encInfo.EncryptAlg = img.EncryptAlg
  782. encInfo.secId = "sec0"
  783. args = append(args, "--object", encInfo.SecretOptions())
  784. }
  785. args = append(args, "--image-opts", encInfo.ImageOptions())
  786. if force {
  787. args = append(args, "-u")
  788. }
  789. args = append(args, "-b", backPath)
  790. cmd := procutils.NewRemoteCommandAsFarAsPossible("ionice", args...)
  791. if runtime.GOOS == "darwin" {
  792. args = args[2:]
  793. cmd = procutils.NewRemoteCommandAsFarAsPossible(args[0], args[1:]...)
  794. }
  795. output, err := cmd.Output()
  796. if err != nil {
  797. return errors.Wrapf(err, "rebase %s", string(output))
  798. }
  799. return img.parse()
  800. }
  801. func (img *SQemuImage) Delete() error {
  802. if !img.IsValid() {
  803. return nil
  804. }
  805. err := os.Remove(img.Path)
  806. if err != nil {
  807. log.Errorf("delete fail %s", err)
  808. return err
  809. }
  810. img.Format = ""
  811. img.ActualSizeBytes = 0
  812. img.SizeBytes = 0
  813. return nil
  814. }
  815. func (img *SQemuImage) Fallocate() error {
  816. if !img.IsValid() {
  817. return fmt.Errorf("self is not valid")
  818. }
  819. cmd := procutils.NewCommand("fallocate", "-l", fmt.Sprintf("%dm", img.GetSizeMB()), img.Path)
  820. output, err := cmd.Output()
  821. if err != nil {
  822. return errors.Wrapf(err, "fallocate: %s", string(output))
  823. }
  824. return nil
  825. }
  826. func (img *SQemuImage) String() string {
  827. return fmt.Sprintf("Qemu %s %d(%d) %s", img.Format, img.GetSizeMB(), img.GetActualSizeMB(), img.Path)
  828. }
  829. func (img *SQemuImage) String2ImageFormat() qemuimgfmt.TImageFormat {
  830. return qemuimgfmt.String2ImageFormat(string(img.Format))
  831. }
  832. func (img *SQemuImage) WholeChainFormatIs(format string) (bool, error) {
  833. if img.Format.String() != format {
  834. return false, nil
  835. }
  836. if len(img.BackFilePath) > 0 {
  837. backImg, err := NewQemuImage(img.BackFilePath)
  838. if err != nil {
  839. return false, err
  840. }
  841. return backImg.WholeChainFormatIs(format)
  842. }
  843. return true, nil
  844. }
  845. func (img *SQemuImage) Check() error {
  846. args := []string{"-c", strconv.Itoa(int(img.IoLevel)), qemutils.GetQemuImg(), "check"}
  847. info := SImageInfo{
  848. Path: img.Path,
  849. Format: img.Format,
  850. IoLevel: img.IoLevel,
  851. Password: img.Password,
  852. EncryptFormat: img.EncryptFormat,
  853. EncryptAlg: img.EncryptAlg,
  854. secId: "sec0",
  855. }
  856. if info.Encrypted() {
  857. args = append(args, "--object", info.SecretOptions())
  858. }
  859. args = append(args, "--image-opts", info.ImageOptions())
  860. cmd := procutils.NewRemoteCommandAsFarAsPossible("ionice", args...)
  861. if runtime.GOOS == "darwin" {
  862. args = args[2:]
  863. cmd = procutils.NewRemoteCommandAsFarAsPossible(args[0], args[1:]...)
  864. }
  865. output, err := cmd.Output()
  866. if err != nil {
  867. return errors.Wrapf(err, "check: %s", string(output))
  868. }
  869. return nil
  870. }
  871. // "json:{\"driver\":\"qcow2\",\"file\":{\"driver\":\"file\",\"filename\":\"/opt/cloud/workspace/disks/snapshots/72a2383d-e980-486f-816c-6c562e1757f3_snap/f39f225a-921f-492e-8fb6-0a4167d6ed91\"}}"
  872. func ParseQemuFilepath(pathInfo string) (string, error) {
  873. if strings.HasPrefix(pathInfo, "json:{") {
  874. pathJson, err := jsonutils.ParseString(pathInfo[len("json:"):])
  875. if err != nil {
  876. return "", errors.Wrap(err, "jsonutils.ParseString")
  877. }
  878. path, err := pathJson.GetString("file", "filename")
  879. if err != nil {
  880. return "", errors.Wrap(err, "GetString file.filename")
  881. }
  882. return path, nil
  883. } else {
  884. return pathInfo, nil
  885. }
  886. }