PageRenderTime 1935ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/helpers/parallels/control.go

https://gitlab.com/tnir/gitlab-runner
Go | 233 lines | 185 code | 40 blank | 8 comment | 35 complexity | 19d2313e21d18d028a109bb278186666 MD5 | raw file
  1. package parallels
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "io/ioutil"
  7. "os/exec"
  8. "regexp"
  9. "runtime"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/sirupsen/logrus"
  14. )
  15. type StatusType string
  16. const (
  17. NotFound StatusType = "notfound"
  18. Invalid StatusType = "invalid"
  19. Stopped StatusType = "stopped"
  20. Suspended StatusType = "suspended"
  21. Running StatusType = "running"
  22. // TODO: more statuses
  23. )
  24. const (
  25. prlctlPath = "prlctl"
  26. dhcpLeases = "/Library/Preferences/Parallels/parallels_dhcp_leases"
  27. )
  28. func PrlctlOutput(args ...string) (string, error) {
  29. if runtime.GOOS != "darwin" {
  30. return "", fmt.Errorf("parallels works only on \"darwin\" platform")
  31. }
  32. var stdout, stderr bytes.Buffer
  33. logrus.Debugf("Executing PrlctlOutput: %#v", args)
  34. cmd := exec.Command(prlctlPath, args...)
  35. cmd.Stdout = &stdout
  36. cmd.Stderr = &stderr
  37. err := cmd.Run()
  38. stderrString := strings.TrimSpace(stderr.String())
  39. if _, ok := err.(*exec.ExitError); ok {
  40. err = fmt.Errorf("calling prlctl: %s", stderrString)
  41. }
  42. return stdout.String(), err
  43. }
  44. func Prlctl(args ...string) error {
  45. _, err := PrlctlOutput(args...)
  46. return err
  47. }
  48. func Exec(vmName string, args ...string) (string, error) {
  49. args2 := append([]string{"exec", vmName}, args...)
  50. return PrlctlOutput(args2...)
  51. }
  52. func Version() (string, error) {
  53. out, err := PrlctlOutput("--version")
  54. if err != nil {
  55. return "", err
  56. }
  57. versionRe := regexp.MustCompile(`prlctl version (\d+\.\d+.\d+)`)
  58. matches := versionRe.FindStringSubmatch(out)
  59. if matches == nil {
  60. return "", fmt.Errorf("could not find Parallels Desktop version in output:\n%s", out)
  61. }
  62. version := matches[1]
  63. logrus.Debugf("Parallels Desktop version: %s", version)
  64. return version, nil
  65. }
  66. func Exist(name string) bool {
  67. err := Prlctl("list", name, "--no-header", "--output", "status")
  68. return err == nil
  69. }
  70. func CreateTemplate(vmName, templateName string) error {
  71. return Prlctl("clone", vmName, "--name", templateName, "--template", "--linked")
  72. }
  73. func CreateOsVM(vmName, templateName string) error {
  74. return Prlctl("create", vmName, "--ostemplate", templateName)
  75. }
  76. func CreateSnapshot(vmName, snapshotName string) error {
  77. return Prlctl("snapshot", vmName, "--name", snapshotName)
  78. }
  79. func GetDefaultSnapshot(vmName string) (string, error) {
  80. output, err := PrlctlOutput("snapshot-list", vmName)
  81. if err != nil {
  82. return "", err
  83. }
  84. lines := strings.Split(output, "\n")
  85. for _, line := range lines {
  86. pos := strings.Index(line, " *")
  87. if pos >= 0 {
  88. snapshot := line[pos+2:]
  89. snapshot = strings.TrimSpace(snapshot)
  90. if len(snapshot) > 0 { // It uses UUID so it should be 38
  91. return snapshot, nil
  92. }
  93. }
  94. }
  95. return "", errors.New("no snapshot")
  96. }
  97. func RevertToSnapshot(vmName, snapshotID string) error {
  98. return Prlctl("snapshot-switch", vmName, "--id", snapshotID)
  99. }
  100. func Start(vmName string) error {
  101. return Prlctl("start", vmName)
  102. }
  103. func Status(vmName string) (StatusType, error) {
  104. output, err := PrlctlOutput("list", vmName, "--no-header", "--output", "status")
  105. if err != nil {
  106. return NotFound, err
  107. }
  108. return StatusType(strings.TrimSpace(output)), nil
  109. }
  110. func WaitForStatus(vmName string, vmStatus StatusType, seconds int) error {
  111. var status StatusType
  112. var err error
  113. for i := 0; i < seconds; i++ {
  114. status, err = Status(vmName)
  115. if err != nil {
  116. return err
  117. }
  118. if status == vmStatus {
  119. return nil
  120. }
  121. time.Sleep(time.Second)
  122. }
  123. return errors.New("VM " + vmName + " is in " + string(status) + " where it should be in " + string(vmStatus))
  124. }
  125. func TryExec(vmName string, seconds int, cmd ...string) error {
  126. var err error
  127. for i := 0; i < seconds; i++ {
  128. _, err = Exec(vmName, cmd...)
  129. if err == nil {
  130. return nil
  131. }
  132. time.Sleep(time.Second)
  133. }
  134. return err
  135. }
  136. func Kill(vmName string) error {
  137. return Prlctl("stop", vmName, "--kill")
  138. }
  139. func Delete(vmName string) error {
  140. return Prlctl("delete", vmName)
  141. }
  142. func Unregister(vmName string) error {
  143. return Prlctl("unregister", vmName)
  144. }
  145. func Mac(vmName string) (string, error) {
  146. output, err := PrlctlOutput("list", "-i", vmName)
  147. if err != nil {
  148. return "", err
  149. }
  150. stdoutString := strings.TrimSpace(output)
  151. re := regexp.MustCompile("net0.* mac=([0-9A-F]{12}) card=.*")
  152. macMatch := re.FindAllStringSubmatch(stdoutString, 1)
  153. if len(macMatch) != 1 {
  154. return "", fmt.Errorf("MAC address for NIC: nic0 on Virtual Machine: %s not found", vmName)
  155. }
  156. mac := macMatch[0][1]
  157. logrus.Debugf("Found MAC address for NIC: net0 - %s\n", mac)
  158. return mac, nil
  159. }
  160. // IPAddress finds the IP address of a VM connected that uses DHCP by its MAC address
  161. //
  162. // Parses the file /Library/Preferences/Parallels/parallels_dhcp_leases
  163. // file contain a list of DHCP leases given by Parallels Desktop
  164. // Example line:
  165. // 10.211.55.181="1418921112,1800,001c42f593fb,ff42f593fb000100011c25b9ff001c42f593fb"
  166. // IP Address ="Lease expiry, Lease time, MAC, MAC or DUID"
  167. func IPAddress(mac string) (string, error) {
  168. if len(mac) != 12 {
  169. return "", fmt.Errorf("not a valid MAC address: %s. It should be exactly 12 digits", mac)
  170. }
  171. leases, err := ioutil.ReadFile(dhcpLeases)
  172. if err != nil {
  173. return "", err
  174. }
  175. re := regexp.MustCompile("(.*)=\"(.*),(.*)," + strings.ToLower(mac) + ",.*\"")
  176. mostRecentIP := ""
  177. mostRecentLease := uint64(0)
  178. for _, l := range re.FindAllStringSubmatch(string(leases), -1) {
  179. ip := l[1]
  180. expiry, _ := strconv.ParseUint(l[2], 10, 64)
  181. leaseTime, _ := strconv.ParseUint(l[3], 10, 32)
  182. logrus.Debugf("Found lease: %s for MAC: %s, expiring at %d, leased for %d s.\n", ip, mac, expiry, leaseTime)
  183. if mostRecentLease <= expiry-leaseTime {
  184. mostRecentIP = ip
  185. mostRecentLease = expiry - leaseTime
  186. }
  187. }
  188. if mostRecentIP == "" {
  189. return "", fmt.Errorf("IP lease not found for MAC address %s in: %s", mac, dhcpLeases)
  190. }
  191. logrus.Debugf("Found IP lease: %s for MAC address %s\n", mostRecentIP, mac)
  192. return mostRecentIP, nil
  193. }