PageRenderTime 163ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/helpers/virtualbox/control.go

https://gitlab.com/tnir/gitlab-runner
Go | 282 lines | 247 code | 34 blank | 1 comment | 44 complexity | 753ef4bd3fe28de43fd319acd1592398 MD5 | raw file
  1. package virtualbox
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "net"
  7. "os"
  8. "os/exec"
  9. "regexp"
  10. "strings"
  11. "time"
  12. "github.com/sirupsen/logrus"
  13. )
  14. type StatusType string
  15. const (
  16. NotFound StatusType = "notfound"
  17. PoweredOff StatusType = "poweroff"
  18. Saved StatusType = "saved"
  19. Teleported StatusType = "teleported"
  20. Aborted StatusType = "aborted"
  21. Running StatusType = "running"
  22. Paused StatusType = "paused"
  23. Stuck StatusType = "gurumeditation"
  24. Teleporting StatusType = "teleporting"
  25. LiveSnapshotting StatusType = "livesnapshotting"
  26. Starting StatusType = "starting"
  27. Stopping StatusType = "stopping"
  28. Saving StatusType = "saving"
  29. Restoring StatusType = "restoring"
  30. TeleportingPausedVM StatusType = "teleportingpausedvm"
  31. TeleportingIn StatusType = "teleportingin"
  32. FaultTolerantSyncing StatusType = "faulttolerantsyncing"
  33. DeletingSnapshotOnline StatusType = "deletingsnapshotlive"
  34. DeletingSnapshotPaused StatusType = "deletingsnapshotlivepaused"
  35. OnlineSnapshotting StatusType = "onlinesnapshotting"
  36. RestoringSnapshot StatusType = "restoringsnapshot"
  37. DeletingSnapshot StatusType = "deletingsnapshot"
  38. SettingUp StatusType = "settingup"
  39. Snapshotting StatusType = "snapshotting"
  40. Unknown StatusType = "unknown"
  41. // TODO: update as new VM states are added
  42. )
  43. func IsStatusOnlineOrTransient(vmStatus StatusType) bool {
  44. switch vmStatus {
  45. case Running,
  46. Paused,
  47. Stuck,
  48. Teleporting,
  49. LiveSnapshotting,
  50. Starting,
  51. Stopping,
  52. Saving,
  53. Restoring,
  54. TeleportingPausedVM,
  55. TeleportingIn,
  56. FaultTolerantSyncing,
  57. DeletingSnapshotOnline,
  58. DeletingSnapshotPaused,
  59. OnlineSnapshotting,
  60. RestoringSnapshot,
  61. DeletingSnapshot,
  62. SettingUp,
  63. Snapshotting:
  64. return true
  65. }
  66. return false
  67. }
  68. func VboxManageOutput(exe string, args ...string) (string, error) {
  69. var stdout, stderr bytes.Buffer
  70. logrus.Debugf("Executing VBoxManageOutput: %#v", args)
  71. cmd := exec.Command(exe, args...)
  72. cmd.Stdout = &stdout
  73. cmd.Stderr = &stderr
  74. err := cmd.Run()
  75. stderrString := strings.TrimSpace(stderr.String())
  76. if _, ok := err.(*exec.ExitError); ok {
  77. err = fmt.Errorf("VBoxManageOutput error: %s", stderrString)
  78. }
  79. return stdout.String(), err
  80. }
  81. func VBoxManage(args ...string) (string, error) {
  82. return VboxManageOutput("vboxmanage", args...)
  83. }
  84. func Version() (string, error) {
  85. version, err := VBoxManage("--version")
  86. if err != nil {
  87. return "", err
  88. }
  89. return strings.TrimSpace(version), nil
  90. }
  91. func FindSSHPort(vmName string) (port string, err error) {
  92. info, err := VBoxManage("showvminfo", vmName)
  93. if err != nil {
  94. return
  95. }
  96. portRe := regexp.MustCompile(`guestssh.*host port = (\d+)`)
  97. sshPort := portRe.FindStringSubmatch(info)
  98. if len(sshPort) >= 2 {
  99. port = sshPort[1]
  100. } else {
  101. err = errors.New("failed to find guestssh port")
  102. }
  103. return
  104. }
  105. func Exist(vmName string) bool {
  106. _, err := VBoxManage("showvminfo", vmName)
  107. return err == nil
  108. }
  109. func CreateOsVM(vmName string, templateName string, templateSnapshot string, baseFolder string) error {
  110. args := []string{"clonevm", vmName, "--mode", "machine", "--name", templateName, "--register"}
  111. if templateSnapshot != "" {
  112. args = append(args, "--snapshot", templateSnapshot, "--options", "link")
  113. }
  114. if baseFolder != "" {
  115. args = append(args, "--basefolder", baseFolder)
  116. }
  117. _, err := VBoxManage(args...)
  118. return err
  119. }
  120. func isPortUnassigned(testPort string, usedPorts [][]string) bool {
  121. for _, port := range usedPorts {
  122. if testPort == port[1] {
  123. return false
  124. }
  125. }
  126. return true
  127. }
  128. func getUsedVirtualBoxPorts() (usedPorts [][]string, err error) {
  129. output, err := VBoxManage("list", "vms", "-l")
  130. if err != nil {
  131. return
  132. }
  133. allPortsRe := regexp.MustCompile(`host port = (\d+)`)
  134. usedPorts = allPortsRe.FindAllStringSubmatch(output, -1)
  135. return
  136. }
  137. func allocatePort(handler func(port string) error) (port string, err error) {
  138. ln, err := net.Listen("tcp", ":0")
  139. if err != nil {
  140. logrus.Debugln("VirtualBox ConfigureSSH:", err)
  141. return
  142. }
  143. defer func() { _ = ln.Close() }()
  144. usedPorts, err := getUsedVirtualBoxPorts()
  145. if err != nil {
  146. logrus.Debugln("VirtualBox ConfigureSSH:", err)
  147. return
  148. }
  149. addressElements := strings.Split(ln.Addr().String(), ":")
  150. port = addressElements[len(addressElements)-1]
  151. if isPortUnassigned(port, usedPorts) {
  152. err = handler(port)
  153. } else {
  154. err = os.ErrExist
  155. }
  156. return
  157. }
  158. func ConfigureSSH(vmName string, vmSSHPort string) (port string, err error) {
  159. for {
  160. port, err = allocatePort(
  161. func(port string) error {
  162. rule := fmt.Sprintf("guestssh,tcp,127.0.0.1,%s,,%s", port, vmSSHPort)
  163. _, err = VBoxManage("modifyvm", vmName, "--natpf1", rule)
  164. return err
  165. },
  166. )
  167. if err == nil || err != os.ErrExist {
  168. return
  169. }
  170. }
  171. }
  172. func CreateSnapshot(vmName string, snapshotName string) error {
  173. _, err := VBoxManage("snapshot", vmName, "take", snapshotName)
  174. return err
  175. }
  176. func RevertToSnapshot(vmName string) error {
  177. _, err := VBoxManage("snapshot", vmName, "restorecurrent")
  178. return err
  179. }
  180. func matchSnapshotName(snapshotName string, snapshotList string) bool {
  181. snapshotRe := regexp.MustCompile(
  182. fmt.Sprintf(`(?m)^Snapshot(Name|UUID)[^=]*="(%s)"\r?$`, regexp.QuoteMeta(snapshotName)),
  183. )
  184. snapshot := snapshotRe.FindStringSubmatch(snapshotList)
  185. return snapshot != nil
  186. }
  187. func HasSnapshot(vmName string, snapshotName string) bool {
  188. output, err := VBoxManage("snapshot", vmName, "list", "--machinereadable")
  189. if err != nil {
  190. return false
  191. }
  192. return matchSnapshotName(snapshotName, output)
  193. }
  194. func matchCurrentSnapshotName(snapshotList string) []string {
  195. snapshotRe := regexp.MustCompile(`(?m)^CurrentSnapshotName="([^"]*)"\r?$`)
  196. return snapshotRe.FindStringSubmatch(snapshotList)
  197. }
  198. func GetCurrentSnapshot(vmName string) (string, error) {
  199. output, err := VBoxManage("snapshot", vmName, "list", "--machinereadable")
  200. if err != nil {
  201. return "", err
  202. }
  203. snapshot := matchCurrentSnapshotName(output)
  204. if snapshot == nil {
  205. return "", errors.New("failed to match current snapshot name")
  206. }
  207. return snapshot[1], nil
  208. }
  209. func Start(vmName string) error {
  210. _, err := VBoxManage("startvm", vmName, "--type", "headless")
  211. return err
  212. }
  213. func Kill(vmName string) error {
  214. _, err := VBoxManage("controlvm", vmName, "poweroff")
  215. return err
  216. }
  217. func Delete(vmName string) error {
  218. _, err := VBoxManage("unregistervm", vmName, "--delete")
  219. return err
  220. }
  221. func Status(vmName string) (StatusType, error) {
  222. output, err := VBoxManage("showvminfo", vmName, "--machinereadable")
  223. statusRe := regexp.MustCompile(`VMState="(\w+)"`)
  224. status := statusRe.FindStringSubmatch(output)
  225. if err != nil {
  226. return NotFound, err
  227. }
  228. return StatusType(status[1]), nil
  229. }
  230. func WaitForStatus(vmName string, vmStatus StatusType, seconds int) error {
  231. var status StatusType
  232. var err error
  233. for i := 0; i < seconds; i++ {
  234. status, err = Status(vmName)
  235. if err != nil {
  236. return err
  237. }
  238. if status == vmStatus {
  239. return nil
  240. }
  241. time.Sleep(time.Second)
  242. }
  243. return errors.New("VM " + vmName + " is in " + string(status) + " where it should be in " + string(vmStatus))
  244. }
  245. func Unregister(vmName string) error {
  246. _, err := VBoxManage("unregistervm", vmName)
  247. return err
  248. }