github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/parallels/executor_parallels.go (about)

     1  package parallels
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os/exec"
     7  	"time"
     8  
     9  	"gitlab.com/gitlab-org/gitlab-runner/common"
    10  	"gitlab.com/gitlab-org/gitlab-runner/executors"
    11  	"gitlab.com/gitlab-org/gitlab-runner/helpers/ssh"
    12  
    13  	prl "gitlab.com/gitlab-org/gitlab-runner/helpers/parallels"
    14  )
    15  
    16  type executor struct {
    17  	executors.AbstractExecutor
    18  	cmd             *exec.Cmd
    19  	vmName          string
    20  	sshCommand      ssh.Client
    21  	provisioned     bool
    22  	ipAddress       string
    23  	machineVerified bool
    24  }
    25  
    26  func (s *executor) waitForIPAddress(vmName string, seconds int) (string, error) {
    27  	var lastError error
    28  
    29  	if s.ipAddress != "" {
    30  		return s.ipAddress, nil
    31  	}
    32  
    33  	s.Debugln("Looking for MAC address...")
    34  	macAddr, err := prl.Mac(vmName)
    35  	if err != nil {
    36  		return "", err
    37  	}
    38  
    39  	s.Debugln("Requesting IP address...")
    40  	for i := 0; i < seconds; i++ {
    41  		ipAddr, err := prl.IPAddress(macAddr)
    42  		if err == nil {
    43  			s.Debugln("IP address found", ipAddr, "...")
    44  			s.ipAddress = ipAddr
    45  			return ipAddr, nil
    46  		}
    47  		lastError = err
    48  		time.Sleep(time.Second)
    49  	}
    50  	return "", lastError
    51  }
    52  
    53  func (s *executor) verifyMachine(vmName string) error {
    54  	if s.machineVerified {
    55  		return nil
    56  	}
    57  
    58  	ipAddr, err := s.waitForIPAddress(vmName, 120)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	// Create SSH command
    64  	sshCommand := ssh.Client{
    65  		Config:         *s.Config.SSH,
    66  		Stdout:         s.Trace,
    67  		Stderr:         s.Trace,
    68  		ConnectRetries: 30,
    69  	}
    70  	sshCommand.Host = ipAddr
    71  
    72  	s.Debugln("Connecting to SSH...")
    73  	err = sshCommand.Connect()
    74  	if err != nil {
    75  		return err
    76  	}
    77  	defer sshCommand.Cleanup()
    78  	err = sshCommand.Run(s.Context, ssh.Command{Command: []string{"exit"}})
    79  	if err != nil {
    80  		return err
    81  	}
    82  	s.machineVerified = true
    83  	return nil
    84  }
    85  
    86  func (s *executor) restoreFromSnapshot() error {
    87  	s.Debugln("Requesting default snapshot for VM...")
    88  	snapshot, err := prl.GetDefaultSnapshot(s.vmName)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	s.Debugln("Reverting VM to snapshot", snapshot, "...")
    94  	err = prl.RevertToSnapshot(s.vmName, snapshot)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  func (s *executor) createVM() error {
   103  	baseImage := s.Config.Parallels.BaseName
   104  	if baseImage == "" {
   105  		return errors.New("Missing Image setting from Parallels config")
   106  	}
   107  
   108  	templateName := s.Config.Parallels.TemplateName
   109  	if templateName == "" {
   110  		templateName = baseImage + "-template"
   111  	}
   112  
   113  	// remove invalid template (removed?)
   114  	templateStatus, _ := prl.Status(templateName)
   115  	if templateStatus == prl.Invalid {
   116  		prl.Unregister(templateName)
   117  	}
   118  
   119  	if !prl.Exist(templateName) {
   120  		s.Debugln("Creating template from VM", baseImage, "...")
   121  		err := prl.CreateTemplate(baseImage, templateName)
   122  		if err != nil {
   123  			return err
   124  		}
   125  	}
   126  
   127  	s.Debugln("Creating runner from VM template...")
   128  	err := prl.CreateOsVM(s.vmName, templateName)
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	s.Debugln("Bootstraping VM...")
   134  	err = prl.Start(s.vmName)
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	// TODO: integration tests do fail on this due
   140  	// Unable to open new session in this virtual machine.
   141  	// Make sure the latest version of Parallels Tools is installed in this virtual machine and it has finished bootingg
   142  	s.Debugln("Waiting for VM to start...")
   143  	err = prl.TryExec(s.vmName, 120, "exit", "0")
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	s.Debugln("Waiting for VM to become responsive...")
   149  	err = s.verifyMachine(s.vmName)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	return nil
   154  }
   155  
   156  func (s *executor) updateGuestTime() error {
   157  	s.Debugln("Updating VM date...")
   158  	timeServer := s.Config.Parallels.TimeServer
   159  	if timeServer == "" {
   160  		timeServer = "time.apple.com"
   161  	}
   162  
   163  	// Check either ntpdate command exists or not before trying to execute it
   164  	// Starting from Mojave ntpdate was removed
   165  	_, err := prl.Exec(s.vmName, "which", "ntpdate")
   166  	if err != nil {
   167  		// Fallback to sntp
   168  		return prl.TryExec(s.vmName, 20, "sudo", "sntp", "-sS", timeServer)
   169  	}
   170  
   171  	return prl.TryExec(s.vmName, 20, "sudo", "ntpdate", "-u", timeServer)
   172  }
   173  
   174  func (s *executor) Prepare(options common.ExecutorPrepareOptions) error {
   175  	err := s.AbstractExecutor.Prepare(options)
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	if s.BuildShell.PassFile {
   181  		return errors.New("Parallels doesn't support shells that require script file")
   182  	}
   183  
   184  	if s.Config.SSH == nil {
   185  		return errors.New("Missing SSH configuration")
   186  	}
   187  
   188  	if s.Config.Parallels == nil {
   189  		return errors.New("Missing Parallels configuration")
   190  	}
   191  
   192  	if s.Config.Parallels.BaseName == "" {
   193  		return errors.New("Missing BaseName setting from Parallels config")
   194  	}
   195  
   196  	version, err := prl.Version()
   197  	if err != nil {
   198  		return err
   199  	}
   200  
   201  	s.Println("Using Parallels", version, "executor...")
   202  
   203  	// remove invalid VM (removed?)
   204  	vmStatus, _ := prl.Status(s.vmName)
   205  	if vmStatus == prl.Invalid {
   206  		prl.Unregister(s.vmName)
   207  	}
   208  
   209  	if s.Config.Parallels.DisableSnapshots {
   210  		s.vmName = s.Config.Parallels.BaseName + "-" + s.Build.ProjectUniqueName()
   211  		if prl.Exist(s.vmName) {
   212  			s.Debugln("Deleting old VM...")
   213  			prl.Kill(s.vmName)
   214  			prl.Delete(s.vmName)
   215  			prl.Unregister(s.vmName)
   216  		}
   217  	} else {
   218  		s.vmName = fmt.Sprintf("%s-runner-%s-concurrent-%d",
   219  			s.Config.Parallels.BaseName,
   220  			s.Build.Runner.ShortDescription(),
   221  			s.Build.RunnerID)
   222  	}
   223  
   224  	if prl.Exist(s.vmName) {
   225  		s.Println("Restoring VM from snapshot...")
   226  		err := s.restoreFromSnapshot()
   227  		if err != nil {
   228  			s.Println("Previous VM failed. Deleting, because", err)
   229  			prl.Kill(s.vmName)
   230  			prl.Delete(s.vmName)
   231  			prl.Unregister(s.vmName)
   232  		}
   233  	}
   234  
   235  	if !prl.Exist(s.vmName) {
   236  		s.Println("Creating new VM...")
   237  		err := s.createVM()
   238  		if err != nil {
   239  			return err
   240  		}
   241  
   242  		if !s.Config.Parallels.DisableSnapshots {
   243  			s.Println("Creating default snapshot...")
   244  			err = prl.CreateSnapshot(s.vmName, "Started")
   245  			if err != nil {
   246  				return err
   247  			}
   248  		}
   249  	}
   250  
   251  	s.Debugln("Checking VM status...")
   252  	status, err := prl.Status(s.vmName)
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	// Start VM if stopped
   258  	if status == prl.Stopped || status == prl.Suspended {
   259  		s.Println("Starting VM...")
   260  		err := prl.Start(s.vmName)
   261  		if err != nil {
   262  			return err
   263  		}
   264  	}
   265  
   266  	if status != prl.Running {
   267  		s.Debugln("Waiting for VM to run...")
   268  		err = prl.WaitForStatus(s.vmName, prl.Running, 60)
   269  		if err != nil {
   270  			return err
   271  		}
   272  	}
   273  
   274  	s.Println("Waiting VM to become responsive...")
   275  	err = s.verifyMachine(s.vmName)
   276  	if err != nil {
   277  		return err
   278  	}
   279  
   280  	s.provisioned = true
   281  
   282  	// TODO: integration tests do fail on this due
   283  	// Unable to open new session in this virtual machine.
   284  	// Make sure the latest version of Parallels Tools is installed in this virtual machine and it has finished booting
   285  	err = s.updateGuestTime()
   286  	if err != nil {
   287  		s.Println("Could not sync with timeserver!")
   288  		return err
   289  	}
   290  
   291  	ipAddr, err := s.waitForIPAddress(s.vmName, 60)
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	s.Debugln("Starting SSH command...")
   297  	s.sshCommand = ssh.Client{
   298  		Config: *s.Config.SSH,
   299  		Stdout: s.Trace,
   300  		Stderr: s.Trace,
   301  	}
   302  	s.sshCommand.Host = ipAddr
   303  
   304  	s.Debugln("Connecting to SSH server...")
   305  	err = s.sshCommand.Connect()
   306  	if err != nil {
   307  		return err
   308  	}
   309  	return nil
   310  }
   311  
   312  func (s *executor) Run(cmd common.ExecutorCommand) error {
   313  	err := s.sshCommand.Run(cmd.Context, ssh.Command{
   314  		Environment: s.BuildShell.Environment,
   315  		Command:     s.BuildShell.GetCommandWithArguments(),
   316  		Stdin:       cmd.Script,
   317  	})
   318  	if _, ok := err.(*ssh.ExitError); ok {
   319  		err = &common.BuildError{Inner: err}
   320  	}
   321  	return err
   322  }
   323  
   324  func (s *executor) Cleanup() {
   325  	s.sshCommand.Cleanup()
   326  
   327  	if s.vmName != "" {
   328  		prl.Kill(s.vmName)
   329  
   330  		if s.Config.Parallels.DisableSnapshots || !s.provisioned {
   331  			prl.Delete(s.vmName)
   332  		}
   333  	}
   334  
   335  	s.AbstractExecutor.Cleanup()
   336  }
   337  
   338  func init() {
   339  	options := executors.ExecutorOptions{
   340  		DefaultCustomBuildsDirEnabled: false,
   341  		DefaultBuildsDir:              "builds",
   342  		DefaultCacheDir:               "cache",
   343  		SharedBuildsDir:               false,
   344  		Shell: common.ShellScriptInfo{
   345  			Shell:         "bash",
   346  			Type:          common.LoginShell,
   347  			RunnerCommand: "gitlab-runner",
   348  		},
   349  		ShowHostname: true,
   350  	}
   351  
   352  	creator := func() common.Executor {
   353  		return &executor{
   354  			AbstractExecutor: executors.AbstractExecutor{
   355  				ExecutorOptions: options,
   356  			},
   357  		}
   358  	}
   359  
   360  	featuresUpdater := func(features *common.FeaturesInfo) {
   361  		features.Variables = true
   362  	}
   363  
   364  	common.RegisterExecutor("parallels", executors.DefaultExecutorProvider{
   365  		Creator:          creator,
   366  		FeaturesUpdater:  featuresUpdater,
   367  		DefaultShellName: options.Shell.Shell,
   368  	})
   369  }