vagrant-tart 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+ require "i18n"
5
+ require "vagrant"
6
+
7
+ module VagrantPlugins
8
+ module Tart
9
+ # Holds the configuration for the Tart provider.
10
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
11
+ class Config < Vagrant.plugin("2", :config)
12
+ # @return [String] The image registry
13
+ attr_accessor :registry
14
+ # @return [String] The image registry username
15
+ attr_accessor :username
16
+ # @return [String] The image registry password
17
+ attr_accessor :password
18
+
19
+ # @return [String] The image source
20
+ attr_accessor :image
21
+ # @return [String] The name
22
+ attr_accessor :name
23
+
24
+ # @return [Integer] Number of CPUs
25
+ attr_accessor :cpu
26
+ # @return [Integer] Memory size in MB
27
+ attr_accessor :memory
28
+ # @return [Integer] Disk storage size in GB
29
+ attr_accessor :disk
30
+ # @return [Boolean] Show a GUI window on boot, or run headless
31
+ attr_accessor :gui
32
+ # @return [String] Display screen resolution
33
+ attr_accessor :display
34
+ # @return [Boolean] Whether the machine is suspendable
35
+ attr_accessor :suspendable
36
+
37
+ # @return [Array<String>] List of volumes to mount
38
+ attr_accessor :volumes
39
+
40
+ # Initialize the configuration with unset values
41
+ def initialize
42
+ super
43
+
44
+ @registry = UNSET_VALUE
45
+ @username = UNSET_VALUE
46
+ @password = UNSET_VALUE
47
+
48
+ @image = UNSET_VALUE
49
+ @name = UNSET_VALUE
50
+
51
+ @cpu = UNSET_VALUE
52
+ @memory = UNSET_VALUE
53
+ @disk = UNSET_VALUE
54
+ @gui = UNSET_VALUE
55
+ @display = UNSET_VALUE
56
+ @suspendable = UNSET_VALUE
57
+
58
+ @volumes = []
59
+ end
60
+
61
+ # Make sure the configuration has defined all the necessary values
62
+ def finalize!
63
+ @registry = nil if @registry == UNSET_VALUE
64
+ @username = nil if @username == UNSET_VALUE
65
+ @password = nil if @password == UNSET_VALUE
66
+
67
+ @image = nil if @image == UNSET_VALUE
68
+ @name = nil if @name == UNSET_VALUE
69
+
70
+ @cpu = 1 if @cpu == UNSET_VALUE
71
+ @memory = 1024 if @memory == UNSET_VALUE
72
+ @disk = 10 if @disk == UNSET_VALUE
73
+ @gui = false if @gui == UNSET_VALUE
74
+ @display = "1024x768" if @display == UNSET_VALUE
75
+ @suspendable = false if @suspendable == UNSET_VALUE
76
+ end
77
+
78
+ # Validate the configuration
79
+ def validate(_machine)
80
+ errors = _detected_errors
81
+
82
+ # Sanity checks for the registry
83
+ unless @registry.nil?
84
+ errors << I18n.t("vagrant_tart.config.username_required") if @username.nil? || @username.empty?
85
+ errors << I18n.t("vagrant_tart.config.password_required") if @password.nil? || @password.empty?
86
+ end
87
+
88
+ # Sanity checks for the image and the name
89
+ errors << I18n.t("vagrant_tart.config.image_required") if @image.nil? || @image.empty?
90
+ errors << I18n.t("vagrant_tart.config.name_required") if @name.nil? || @name.empty?
91
+
92
+ # Check that the GUI flag is a valid boolean
93
+ errors << I18n.t("vagrant_tart.config.gui_invalid") unless @gui == true || @gui == false
94
+
95
+ # Check that CPU is a valid number and between 1 and the maximum available CPUs
96
+ max_cpus = Etc.nprocessors
97
+ unless (@cpu.is_a? Integer) && @cpu >= 1 && @cpu <= max_cpus
98
+ errors << I18n.t("vagrant_tart.config.cpu_invalid", max_cpus: max_cpus)
99
+ end
100
+
101
+ # Check that memory is a valid number and between 1 and the maximum available memory
102
+ max_memory = `sysctl -n hw.memsize`.to_i / 1024 / 1024
103
+ unless (@memory.is_a? Integer) && @memory >= 1 && @memory <= max_memory
104
+ errors << I18n.t("vagrant_tart.config.memory_invalid", max_memory: max_memory)
105
+ end
106
+
107
+ # Check that disk is a valid number and greater than 1
108
+ errors << I18n.t("vagrant_tart.config.disk_invalid") unless (@disk.is_a? Integer) && @disk >= 1
109
+
110
+ # Check that the display resolution is a valid string conforming to the format "WIDTHxHEIGHT"
111
+ errors << I18n.t("vagrant_tart.config.display_invalid") unless @display.match?(/^\d+x\d+$/)
112
+
113
+ { "Tart Provider" => errors }
114
+ end
115
+
116
+ def suspendable?
117
+ @suspendable
118
+ end
119
+
120
+ def use_registry?
121
+ !@registry.nil? && !@username.nil? && !@password.nil?
122
+ end
123
+ end
124
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
125
+ end
126
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module Tart
5
+ module Errors
6
+ # The base class for all plugin errors.
7
+ class TartError < Vagrant::Errors::VagrantError
8
+ error_namespace("vagrant_tart.errors")
9
+ end
10
+
11
+ # This error is raised if the platform is not MacOS.
12
+ class MacOSRequired < TartError
13
+ error_key(:macos_required)
14
+ end
15
+
16
+ # This error is raised if the Tart binary is not found.
17
+ class TartRequired < TartError
18
+ error_key(:tart_required)
19
+ end
20
+
21
+ # This error is raised if a Tart command fails.
22
+ class CommandError < TartError
23
+ error_key(:command_error)
24
+ end
25
+
26
+ # This error is raised if the virutal machine is not created.
27
+ class InstanceNotCreatedError < TartError
28
+ error_key(:instance_not_created)
29
+ end
30
+
31
+ # This error is raised if the virtual machine is not running.
32
+ class InstanceNotRunningError < TartError
33
+ error_key(:instance_not_running)
34
+ end
35
+
36
+ # This error is raised if the synced folder are not for Tart.
37
+ class SyncedFolderNonTart < TartError
38
+ error_key(:synced_folder_not_tart)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module Tart
5
+ module Model
6
+ # Represents the result of a Tart 'get' operation.
7
+ class GetResult
8
+ # @return [Integer] The number of CPUs of the machine.
9
+ attr_accessor :cpu
10
+ # @return [Integer] The size of the machine's disk.
11
+ attr_accessor :disk
12
+ # @return [Boolean] Whether the machine is running.
13
+ attr_accessor :running
14
+ # @return [Integer] The memory of the machine.
15
+ attr_accessor :memory
16
+ # @return [String] The operating system of the machine.
17
+ attr_accessor :os
18
+ # @return [String] The size of the machine.
19
+ attr_accessor :size
20
+ # @return [String] The state of the machine.
21
+ attr_accessor :state
22
+ # @return [String] The size of the display
23
+ attr_accessor :display
24
+
25
+ # Initialize the result from raw data.
26
+ # @param [Hash] data The raw data.
27
+ def initialize(data)
28
+ @cpu = data["CPU"]
29
+ @disk = data["Disk"]
30
+ @running = data["Running"]
31
+ @memory = data["Memory"]
32
+ @os = data["OS"]
33
+ @size = data["Size"]
34
+ @state = data["State"]
35
+ @display = data["Display"]
36
+ end
37
+
38
+ # Returns the state of the machine using Vagrant symbols.
39
+ def vagrant_state
40
+ case @state
41
+ when "running"
42
+ :running
43
+ when "stopped", "suspended"
44
+ :stopped
45
+ else
46
+ :host_state_unknown
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module Tart
5
+ module Model
6
+ # Represents the result of a Tart 'list' operation.
7
+ class ListResult
8
+ # @return [Array<ListResultItem>] The list of machines.
9
+ attr_accessor :machines
10
+
11
+ # Initialize the result from raw data.
12
+ # @param [Array<Hash>] data The raw data.
13
+ def initialize(data)
14
+ @machines = []
15
+ data.each do |machine|
16
+ item = ListResultItem.new(machine)
17
+ @machines << item
18
+ end
19
+ end
20
+
21
+ # Checks if a machine with the given name exists.
22
+ # @param [String] name The name of the machine.
23
+ # @return [Boolean]
24
+ def any?(name)
25
+ @machines.any? { |i| i.name == name }
26
+ end
27
+
28
+ # Finds a machine with the given name.
29
+ # @param [String] name The name of the machine.
30
+ # @return [ListResultItem]
31
+ def find(name)
32
+ @machines.find { |i| i.name == name }
33
+ end
34
+
35
+ # Represents an item in the list result.
36
+ class ListResultItem
37
+ # @return [Integer] The size of the machine's disk.
38
+ attr_accessor :disk
39
+ # @return [String] The name of the machine.
40
+ attr_accessor :name
41
+ # @return [Boolean] Whether the machine is running.
42
+ attr_accessor :running
43
+ # @return [Integer] The size of the machine's image.
44
+ attr_accessor :size
45
+ # @return [String] The source of the machine (local or oci).
46
+ attr_accessor :source
47
+ # @return [String] The state of the machine.
48
+ attr_accessor :state
49
+
50
+ # Initialize the result from raw data.
51
+ # @param [Hash] data The raw data.
52
+ def initialize(data)
53
+ @disk = data["Disk"]
54
+ @name = data["Name"]
55
+ @running = data["Running"]
56
+ @size = data["Size"]
57
+ @source = data["Source"]
58
+ @state = data["State"]
59
+ end
60
+
61
+ # Returns the state of the machine using Vagrant symbols.
62
+ def vagrant_state
63
+ case @state
64
+ when "running"
65
+ :running
66
+ when "stopped", "suspended"
67
+ :stopped
68
+ else
69
+ :host_state_unknown
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VagrantPlugins
4
+ module Tart
5
+ module Model
6
+ # Represents a synced folder for the Tart provider.
7
+ class TartDisk
8
+ # The default tag for auto-mounted folders.
9
+ AUTO_MOUNT ||= "com.apple.virtio-fs.automount" # rubocop:disable Lint/OrAssignmentToConstant
10
+ # The default path for auto-mounted folders on Darwin.
11
+ DARWIN_SHARED_FILES ||= "/Volumes/My Shared Files" # rubocop:disable Lint/OrAssignmentToConstant
12
+
13
+ # The path on the host machine.
14
+ attr_accessor :host_path
15
+ # The path on the guest machine.
16
+ attr_accessor :guest_path
17
+ # The mode of the folder (either "ro" or "rw"). Default is "rw".
18
+ attr_accessor :mode
19
+ # The tag of the folder.
20
+ attr_accessor :tag
21
+ # The prefix of the folder.
22
+ attr_accessor :prefix
23
+
24
+ # Initialize the folder from configuration data.
25
+ # @param [Hash] data The configuration data.
26
+ def initialize(data)
27
+ @host_path = data[:hostpath]
28
+ @guest_path = data[:guestpath]
29
+ parse_mount_options(data[:mount_options] || [])
30
+ end
31
+
32
+ # Check if the disk is auto-mounted.
33
+ def automount?
34
+ @tag == AUTO_MOUNT
35
+ end
36
+
37
+ # Convert the disk to a Tart command line option.
38
+ def to_tart_disk
39
+ # Create options
40
+ options = []
41
+ options << "ro" if @mode == "ro"
42
+ options << "tag=#{tag}" if @tag
43
+
44
+ result = @host_path.to_s
45
+ if @guest_path.start_with?(DARWIN_SHARED_FILES)
46
+ prefix = @guest_path[DARWIN_SHARED_FILES.length + 1..]
47
+ result = "#{prefix}:#{result}" if prefix
48
+ end
49
+ result += ":#{options.join(",")}" if options.any?
50
+ result
51
+ end
52
+
53
+ # Convert the disk to a human readable string.
54
+ def to_s
55
+ "#{@host_path} -> #{@guest_path} (mode=#{@mode || "rw"}, tag=#{@tag || "none"})"
56
+ end
57
+
58
+ private
59
+
60
+ # Parse the mount options.
61
+ def parse_mount_options(mount_options)
62
+ # Mode option
63
+ mode = mount_options.find { |option| option.start_with?("mode=") }
64
+ @mode = mode.split("=")[1] if mode
65
+
66
+ # Tag option
67
+ tag = mount_options.find { |option| option.start_with?("tag=") }
68
+ @tag = if tag
69
+ tag.split("=")[1]
70
+ else
71
+ AUTO_MOUNT
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Check if the Vagrant gem is available
4
+ begin
5
+ require "vagrant"
6
+ rescue LoadError
7
+ raise "The Vagrant Tart plugin must be run within Vagrant."
8
+ end
9
+
10
+ # Make sure no one is attempting to install this plugin into an early Vagrant version.
11
+ raise "The Vagrant Tart plugin is only compatible with Vagrant 2.3 or higher" if Vagrant::VERSION < "2.3.0"
12
+
13
+ module VagrantPlugins
14
+ module Tart
15
+ # Provides the capabilities to manage Tart virtual machines.
16
+ class Plugin < Vagrant.plugin("2")
17
+ name "tart"
18
+ description "Allows Vagrant to manage Tart virtual machines."
19
+
20
+ # Register the configuration.
21
+ config(:tart, :provider) do
22
+ require_relative "config"
23
+ Config
24
+ end
25
+
26
+ # Register the provider.
27
+ provider(:tart, box_optional: true, parallel: true) do
28
+ setup_i18n
29
+
30
+ require_relative "provider"
31
+ Provider
32
+ end
33
+
34
+ # Register the synced folder implementation.
35
+ synced_folder(:tart) do
36
+ require_relative "synced_folder"
37
+ SyncedFolder
38
+ end
39
+
40
+ # Load the translation files
41
+ def self.setup_i18n
42
+ I18n.load_path << File.expand_path("locales/en.yml", Tart.source_root)
43
+ I18n.reload!
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "log4r"
4
+ require_relative "util/driver"
5
+
6
+ module VagrantPlugins
7
+ module Tart
8
+ # Provider that is responsible for managing the virtual machine and exposing it to Vagrant.
9
+ class Provider < Vagrant.plugin("2", :provider)
10
+ # The associated driver
11
+ attr_reader :driver
12
+
13
+ # Initialize the provider with the given machine.
14
+ def initialize(machine)
15
+ super
16
+ @logger = Log4r::Logger.new("vagrant::provider::tart")
17
+ @machine = machine
18
+ @driver = Util::Driver.new
19
+ end
20
+
21
+ # Check if the provider can be used.
22
+ # rubocop:disable Style/OptionalBooleanParameter
23
+ def self.usable?(raise_error = false)
24
+ raise Errors::MacOSRequired unless Vagrant::Util::Platform.darwin?
25
+
26
+ tart_present = Vagrant::Util::Which.which("tart")
27
+ raise Errors::TartRequired unless tart_present
28
+
29
+ true
30
+ rescue Errors::TartError
31
+ raise if raise_error
32
+
33
+ false
34
+ end
35
+ # rubocop:enable Style/OptionalBooleanParameter
36
+
37
+ # Execute the action with the given name.
38
+ def action(name)
39
+ action_method = "action_#{name}"
40
+ return Action.send(action_method) if Action.respond_to?(action_method)
41
+
42
+ nil
43
+ end
44
+
45
+ # Return the state of the virtual machine.
46
+ def state
47
+ @logger.info("Getting state '#{@machine.id}'")
48
+
49
+ state_id = nil
50
+ state_id = :not_created unless @machine.id
51
+
52
+ unless state_id
53
+ env = @machine.action(:get_state)
54
+ state_id = env[:machine_state_id]
55
+ end
56
+
57
+ # Get the short and long description
58
+ short = state_id.to_s
59
+ long = ""
60
+
61
+ # If we're not created, then specify the special ID flag
62
+ state_id = Vagrant::MachineState::NOT_CREATED_ID if state_id == :not_created
63
+
64
+ Vagrant::MachineState.new(state_id, short, long)
65
+ end
66
+
67
+ # Return the SSH info for the virtual machine.
68
+ def ssh_info
69
+ # We can only SSH into a running machine
70
+ return nil if state.id != :running
71
+
72
+ # Retrieve the IP address
73
+ instance_ip = nil
74
+ begin
75
+ instance_ip = @driver.ip(@machine.provider_config.name)
76
+ rescue Errors::CommandError
77
+ @logger.warn("Failed to read guest IP #{$ERROR_INFO}")
78
+ end
79
+
80
+ return nil if instance_ip.nil?
81
+
82
+ @logger.info("IP: #{instance_ip}")
83
+
84
+ # Return the information
85
+ {
86
+ host: instance_ip,
87
+ port: 22
88
+ }
89
+ end
90
+
91
+ def to_s
92
+ id = @machine.id.nil? ? "nil" : @machine.id
93
+ "Tart[#{id}]"
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env zsh
2
+ set -eo pipefail
3
+
4
+ HOST="$1"
5
+ USERNAME="$2"
6
+ PASSWORD="$3"
7
+
8
+ # Login to the registry with the provided credentials
9
+ echo "$PASSWORD" | tart login "$HOST" --username "$USERNAME" --password-stdin
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env zsh
2
+ set -eo pipefail
3
+
4
+ # Run the virtual machine in the background with the arguments passed to the script
5
+ nohup tart run "$@" &
6
+
7
+ # Save the PID of the virtual machine
8
+ TART_VM_PID=$!
9
+
10
+ # Give the process 3 seconds to start.
11
+ sleep 3
12
+
13
+ # If the process is not alive, then exit with an error message
14
+ if ! ps -p $TART_VM_PID > /dev/null; then
15
+ echo "Failed to start the virtual machine." 1>&2
16
+ exit 1
17
+ fi
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "log4r"
4
+ require "vagrant/util/platform"
5
+ require_relative "model/tart_disk"
6
+
7
+ module VagrantPlugins
8
+ module Tart
9
+ # Manages the synced folders for the Tart provider.
10
+ class SyncedFolder < Vagrant.plugin("2", :synced_folder)
11
+ def initialize(*args)
12
+ super
13
+ @logger = Log4r::Logger.new("vagrant::provider::tart")
14
+ @disks = []
15
+ end
16
+
17
+ # rubocop:disable Style/OptionalBooleanParameter
18
+ def usable?(machine, raise_errors = false)
19
+ # These synced folders only work if the provider if Tart
20
+ if machine.provider_name != :tart
21
+ raise Errors::SyncedFolderNonTart, provider: machine.provider_name.to_s if raise_errors
22
+
23
+ return false
24
+ end
25
+
26
+ true
27
+ end
28
+ # rubocop:enable Style/OptionalBooleanParameter
29
+
30
+ def prepare(machine, folders, _opts)
31
+ @logger.info("Preparing synced folders for Tart provider.")
32
+
33
+ # Get the shared folder options
34
+ folders.each_value do |data|
35
+ disk = Model::TartDisk.new(data)
36
+ @disks << disk
37
+ end
38
+
39
+ # Add the synced folders to the configuration
40
+ @disks.each do |disk|
41
+ machine.ui.output("Adding disk '#{disk}'.")
42
+ machine.provider_config.volumes << disk.to_tart_disk
43
+ end
44
+
45
+ @logger.info("#{@disks.size} synced folders added to the configuration.")
46
+ end
47
+
48
+ def enable(machine, _folders, _opts)
49
+ darwin = machine.guest.capability?(:darwin_version)
50
+
51
+ @disks.each do |disk|
52
+ enable_disk(machine, disk, darwin)
53
+ end
54
+ end
55
+
56
+ def disable(_machine, _folders, _opts)
57
+ # Do nothing
58
+ end
59
+
60
+ def cleanup(_machine, _opts)
61
+ @disks.clear
62
+ end
63
+
64
+ private
65
+
66
+ def enable_disk(machine, disk, darwin)
67
+ machine.ui.output("Mounting disk '#{disk.host_path}' -> '#{disk.guest_path}'...")
68
+ if darwin
69
+ enable_disk_darwin(machine, disk)
70
+ else
71
+ enable_disk_linux(machine, disk)
72
+ end
73
+ machine.ui.success("Disk mounted.")
74
+ end
75
+
76
+ def enable_disk_darwin(machine, disk)
77
+ # Create the guest path if it doesn't exist
78
+ machine.communicate.sudo("mkdir -p #{disk.guest_path}") unless disk.automount?
79
+
80
+ return if disk.automount? || machine.communicate.test("mount | grep #{disk.guest_path}")
81
+
82
+ # Mount the disk
83
+ machine.communicate.sudo("mount_virtiofs #{disk.tag} #{disk.guest_path}")
84
+ end
85
+
86
+ def enable_disk_linux(machine, disk)
87
+ # Create the guest path if it doesn't exist
88
+ machine.communicate.sudo("mkdir -p #{disk.guest_path}")
89
+
90
+ return if machine.communicate.test("mountpoint -q #{disk.guest_path}")
91
+
92
+ # Mount the disk
93
+ machine.communicate.sudo("mount -t virtiofs #{disk.tag} #{disk.guest_path}")
94
+ end
95
+ end
96
+ end
97
+ end