PageRenderTime 55ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/helpers/parallels/control.go

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