executors/virtualbox/executor_virtualbox.go (278 lines of code) (raw):

package virtualbox import ( "errors" "fmt" "gitlab.com/gitlab-org/gitlab-ci-multi-runner/common" "gitlab.com/gitlab-org/gitlab-ci-multi-runner/executors" "gitlab.com/gitlab-org/gitlab-ci-multi-runner/helpers/ssh" vbox "gitlab.com/gitlab-org/gitlab-ci-multi-runner/helpers/virtualbox" "time" ) type executor struct { executors.AbstractExecutor vmName string sshCommand ssh.Client sshPort string provisioned bool machineVerified bool } func (s *executor) verifyMachine(vmName string, sshPort string) error { if s.machineVerified { return nil } // Create SSH command sshCommand := ssh.Client{ Config: *s.Config.SSH, Stdout: s.Trace, Stderr: s.Trace, ConnectRetries: 30, } sshCommand.Port = sshPort sshCommand.Host = "localhost" s.Debugln("Connecting to SSH...") err := sshCommand.Connect() if err != nil { return err } defer sshCommand.Cleanup() err = sshCommand.Run(s.Context, ssh.Command{Command: []string{"exit"}}) if err != nil { return err } s.machineVerified = true return nil } func (s *executor) restoreFromSnapshot() error { s.Debugln("Reverting VM to current snapshot...") err := vbox.RevertToSnapshot(s.vmName) if err != nil { return err } return nil } func (s *executor) determineBaseSnapshot(baseImage string) string { var err error baseSnapshot := s.Config.VirtualBox.BaseSnapshot if baseSnapshot == "" { baseSnapshot, err = vbox.GetCurrentSnapshot(baseImage) if err != nil { if s.Config.VirtualBox.DisableSnapshots { s.Debugln("No snapshots found for base VM", baseImage) return "" } baseSnapshot = "Base State" } } if baseSnapshot != "" && !vbox.HasSnapshot(baseImage, baseSnapshot) { if s.Config.VirtualBox.DisableSnapshots { s.Warningln("Snapshot", baseSnapshot, "not found in base VM", baseImage) return "" } s.Debugln("Creating snapshot", baseSnapshot, "from current base VM", baseImage, "state...") err = vbox.CreateSnapshot(baseImage, baseSnapshot) if err != nil { s.Warningln("Failed to create snapshot", baseSnapshot, "from base VM", baseImage) return "" } } return baseSnapshot } // virtualbox doesn't support templates func (s *executor) createVM(vmName string) (err error) { baseImage := s.Config.VirtualBox.BaseName if baseImage == "" { return errors.New("Missing Image setting from VirtualBox configuration") } _, err = vbox.Status(vmName) if err != nil { vbox.Unregister(vmName) } if !vbox.Exist(vmName) { baseSnapshot := s.determineBaseSnapshot(baseImage) if baseSnapshot == "" { s.Debugln("Creating testing VM from VM", baseImage, "...") } else { s.Debugln("Creating testing VM from VM", baseImage, "snapshot", baseSnapshot, "...") } err = vbox.CreateOsVM(baseImage, vmName, baseSnapshot) if err != nil { return err } } s.Debugln("Identify SSH Port...") s.sshPort, err = vbox.FindSSHPort(s.vmName) if err != nil { s.Debugln("Creating localhost ssh forwarding...") vmSSHPort := s.Config.SSH.Port if vmSSHPort == "" { vmSSHPort = "22" } s.sshPort, err = vbox.ConfigureSSH(vmName, vmSSHPort) if err != nil { return err } } s.Debugln("Using local", s.sshPort, "SSH port to connect to VM...") s.Debugln("Bootstraping VM...") err = vbox.Start(s.vmName) if err != nil { return err } s.Debugln("Waiting for VM to become responsive...") time.Sleep(10) err = s.verifyMachine(s.vmName, s.sshPort) if err != nil { return err } return nil } func (s *executor) Prepare(options common.ExecutorPrepareOptions) error { err := s.AbstractExecutor.Prepare(options) if err != nil { return err } if s.BuildShell.PassFile { return errors.New("virtualbox doesn't support shells that require script file") } if s.Config.SSH == nil { return errors.New("Missing SSH config") } if s.Config.VirtualBox == nil { return errors.New("Missing VirtualBox configuration") } if s.Config.VirtualBox.BaseName == "" { return errors.New("Missing BaseName setting from VirtualBox configuration") } version, err := vbox.Version() if err != nil { return err } s.Println("Using VirtualBox version", version, "executor...") if s.Config.VirtualBox.DisableSnapshots { s.vmName = s.Config.VirtualBox.BaseName + "-" + s.Build.ProjectUniqueName() if vbox.Exist(s.vmName) { s.Debugln("Deleting old VM...") vbox.Kill(s.vmName) vbox.Delete(s.vmName) vbox.Unregister(s.vmName) } } else { s.vmName = fmt.Sprintf("%s-runner-%s-concurrent-%d", s.Config.VirtualBox.BaseName, s.Build.Runner.ShortDescription(), s.Build.RunnerID) } if vbox.Exist(s.vmName) { s.Println("Restoring VM from snapshot...") err := s.restoreFromSnapshot() if err != nil { s.Println("Previous VM failed. Deleting, because", err) vbox.Kill(s.vmName) vbox.Delete(s.vmName) vbox.Unregister(s.vmName) } } if !vbox.Exist(s.vmName) { s.Println("Creating new VM...") err := s.createVM(s.vmName) if err != nil { return err } if !s.Config.VirtualBox.DisableSnapshots { s.Println("Creating default snapshot...") err = vbox.CreateSnapshot(s.vmName, "Started") if err != nil { return err } } } s.Debugln("Checking VM status...") status, err := vbox.Status(s.vmName) if err != nil { return err } if !vbox.IsStatusOnlineOrTransient(status) { s.Println("Starting VM...") err := vbox.Start(s.vmName) if err != nil { return err } } if status != vbox.Running { s.Debugln("Waiting for VM to run...") err = vbox.WaitForStatus(s.vmName, vbox.Running, 60) if err != nil { return err } } s.Debugln("Identify SSH Port...") sshPort, err := vbox.FindSSHPort(s.vmName) s.sshPort = sshPort if err != nil { return err } s.Println("Waiting VM to become responsive...") err = s.verifyMachine(s.vmName, s.sshPort) if err != nil { return err } s.provisioned = true s.Println("Starting SSH command...") s.sshCommand = ssh.Client{ Config: *s.Config.SSH, Stdout: s.Trace, Stderr: s.Trace, } s.sshCommand.Port = s.sshPort s.sshCommand.Host = "localhost" s.Debugln("Connecting to SSH server...") err = s.sshCommand.Connect() if err != nil { return err } return nil } func (s *executor) Run(cmd common.ExecutorCommand) error { err := s.sshCommand.Run(cmd.Context, ssh.Command{ Environment: s.BuildShell.Environment, Command: s.BuildShell.GetCommandWithArguments(), Stdin: cmd.Script, }) if _, ok := err.(*ssh.ExitError); ok { err = &common.BuildError{Inner: err} } return err } func (s *executor) Cleanup() { s.sshCommand.Cleanup() if s.vmName != "" { vbox.Kill(s.vmName) if s.Config.VirtualBox.DisableSnapshots || !s.provisioned { vbox.Delete(s.vmName) } } } func init() { options := executors.ExecutorOptions{ DefaultBuildsDir: "builds", SharedBuildsDir: false, Shell: common.ShellScriptInfo{ Shell: "bash", Type: common.LoginShell, RunnerCommand: "gitlab-runner", }, ShowHostname: true, } creator := func() common.Executor { return &executor{ AbstractExecutor: executors.AbstractExecutor{ ExecutorOptions: options, }, } } featuresUpdater := func(features *common.FeaturesInfo) { features.Variables = true } common.RegisterExecutor("virtualbox", executors.DefaultExecutorProvider{ Creator: creator, FeaturesUpdater: featuresUpdater, }) }