vagrant-mirror 0.1.0.alpha

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,108 @@
1
+ # Monitors changes on the host and guest instance, and propogates any new, changed
2
+ # or deleted files between machines. Note that this will block the vagrant
3
+ # execution on the host.
4
+ #
5
+ # @author Andrew Coulton < andrew@ingerator.com >
6
+ module Vagrant
7
+ module Mirror
8
+ module Middleware
9
+ class Mirror < Base
10
+
11
+ # Loads the rest of the middlewares first, then finishes up by running
12
+ # the mirror middleware. This allows the listener to start after the
13
+ # instance has been provisioned.
14
+ #
15
+ # @param [Vagrant::Action::Environment] The environment
16
+ def call(env)
17
+ @app.call(env)
18
+
19
+ mirrors = env[:vm].config.mirror.folders
20
+ if !mirrors.empty?
21
+ execute(mirrors, env)
22
+ else
23
+ env[:ui].info("No vagrant-mirror mirrored folders configured for this box")
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ # Mirrors the folder pairs configured in the vagrantfile
30
+ #
31
+ # @param [Array] The folder pairs to synchronise
32
+ # @param [Vagrant::Action::Environment] The environment
33
+ def execute(mirrors, env)
34
+ ui = env[:ui]
35
+ ui.info("Beginning directory mirroring")
36
+
37
+ begin
38
+ workers = []
39
+
40
+ # Create a thread to work off the queue for each folder
41
+ each_mirror(mirrors) do | host_path, guest_sf_path, mirror_config |
42
+ workers << Thread.new do
43
+ # Set up the listener and the changes queue
44
+ Thread.current["queue"] = Queue.new
45
+ host_listener = Vagrant::Mirror::Listener::Host.new(host_path, Thread.current["queue"])
46
+ rsync = Vagrant::Mirror::Rsync.new(env[:vm], guest_sf_path, host_path, mirror_config)
47
+
48
+ # Start listening and store the thread reference
49
+ Thread.current["listener"] = host_listener.listen
50
+
51
+ # Just poll indefinitely waiting for changes or to be told to quit
52
+ quit = false
53
+ while !quit
54
+ change = Thread.current["queue"].pop
55
+ if (change[:quit])
56
+ quit = true
57
+ else
58
+ # Handle removed files first - guard sometimes flagged as deleted when they aren't
59
+ # So we first check if the file has been deleted on the host. If so, we delete on
60
+ # the guest, otherwise we add to the list to rsync in case there are changes
61
+ changes = []
62
+ change[:removed].each do | relpath |
63
+ if (File.exists?(File.join(host_path, relpath)))
64
+ changes << relpath
65
+ else
66
+ target = "#{mirror_config[:guest_path]}/#{relpath}"
67
+ ui.warn("XX Deleting #{target}")
68
+ env[:vm].channel.sudo("rm #{target}")
69
+ end
70
+ end
71
+
72
+ # Add to the list of deletions with each of the modified and added files
73
+ changes = changes + change[:added] + change[:modified]
74
+ changes.each do | relpath |
75
+ ui.info(">> #{relpath}")
76
+ rsync.run(relpath)
77
+ end
78
+
79
+ # Beep if configured
80
+ if (mirror_config[:beep])
81
+ print "\a"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # Wait for the listener thread to exit
89
+ workers.each do | thread |
90
+ thread.join
91
+ end
92
+ rescue RuntimeError => e
93
+ # Pass through Vagrant errors
94
+ if e.is_a? Vagrant::Errors::VagrantError
95
+ raise
96
+ end
97
+
98
+ # Convert to a vagrant error descendant so that the box is not cleaned up
99
+ raise Vagrant::Mirror::Errors::Error.new("Vagrant-mirror caught a #{e.class.name} - #{e.message}")
100
+ end
101
+
102
+ ui.success("Completed directory synchronisation")
103
+ end
104
+
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,92 @@
1
+ require 'date'
2
+ require 'fileutils'
3
+
4
+ # Executes a full sync from the host to the guest instance based on the configuration
5
+ # in the vagrantfile, copying new or changed files to the guest as required.
6
+ #
7
+ # @author Andrew Coulton < andrew@ingerator.com >
8
+ module Vagrant
9
+ module Mirror
10
+ module Middleware
11
+ class Sync < Base
12
+
13
+ # Loads the rest of the middlewares first, then finishes up by running
14
+ # the sync middleware. This is required because the core share_folders
15
+ # middleware does not mount the shares until the very end of the process
16
+ # and we need to run after that
17
+ #
18
+ # @param [Vagrant::Action::Environment] The environment
19
+ def call(env)
20
+ @app.call(env)
21
+
22
+ mirrors = env[:vm].config.mirror.folders
23
+ if !mirrors.empty?
24
+ execute(mirrors, env)
25
+ else
26
+ env[:ui].info("No vagrant-mirror mirrored folders configured for this box")
27
+ end
28
+ end
29
+
30
+ protected
31
+
32
+ # Synchronizes the folder pairs configured in the vagrantfile
33
+ #
34
+ # @param [Array] The folder pairs to synchronise
35
+ # @param [Vagrant::Action::Environment] The environment
36
+ def execute(mirrors, env)
37
+ ui = env[:ui]
38
+ ui.info("Beginning directory synchronisation at " + DateTime.now.iso8601)
39
+
40
+ begin
41
+ each_mirror(mirrors) do | host_path, guest_sf_path, mirror_config |
42
+ ui.info("Synchronising for #{host_path}")
43
+
44
+ # Create any required symlinks
45
+ mirror_config[:symlinks].each do | relpath |
46
+ relpath.sub!(/^\//, '')
47
+ source = "#{guest_sf_path}/#{relpath}"
48
+ target = "#{mirror_config[:guest_path]}/#{relpath}"
49
+
50
+ # Find the parent directory - we have to do this with regexp as we don't have the
51
+ # right filesystem to use File.expand
52
+ dirs = /^(.*)(\/.+)$/.match(target)
53
+ if (dirs)
54
+ target_dir = dirs[1]
55
+ else
56
+ target_dir = '/'
57
+ end
58
+
59
+ # Create the host directory if required
60
+ host_dir = "#{host_path}/#{relpath}"
61
+ if ( ! File.exists?(host_dir))
62
+ FileUtils.mkdir_p(host_dir)
63
+ end
64
+
65
+ # Create the parent directory and create the symlink
66
+ ui.info("Creating link from #{target} to #{source}")
67
+ env[:vm].channel.sudo("rm -f #{target} && mkdir -p #{target_dir} && ln -s #{source} #{target}")
68
+ end
69
+
70
+ # Trigger the sync on the remote host
71
+ ui.info("Synchronising from #{guest_sf_path} to #{mirror_config[:guest_path]}")
72
+ rsync = Vagrant::Mirror::Rsync.new(env[:vm], guest_sf_path, host_path, mirror_config)
73
+ rsync.run('/')
74
+ end
75
+
76
+ rescue RuntimeError => e
77
+ # Pass through Vagrant errors
78
+ if e.is_a? Vagrant::Errors::VagrantError
79
+ raise
80
+ end
81
+
82
+ # Convert to a vagrant error descendant so that the box is not cleaned up
83
+ raise Vagrant::Mirror::Errors::Error.new("Vagrant-mirror caught a #{e.class.name} - #{e.message}")
84
+ end
85
+
86
+ ui.success("Completed directory synchronisation at " + DateTime.now.iso8601)
87
+ end
88
+
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,91 @@
1
+ # Executes rsync on the guest to update files between the shared folder and the local virtual disk.
2
+ # Propogates deletes if configured to do so, but not otherwise.
3
+ #
4
+ # @author Andrew Coulton < andrew@ingerator.com >
5
+ module Vagrant
6
+ module Mirror
7
+ class Rsync
8
+ # @return [Vagrant::VM] The VM to mirror on
9
+ attr_reader :vm
10
+
11
+ # @return [String] The path to the virtualbox shared folder on the guest
12
+ attr_reader :guest_sf_path
13
+
14
+ # @return [String] The path to the virtualbox shared folder on the host
15
+ attr_reader :host_path
16
+
17
+ # @return [Bool] Whether Rsync should delete unexpected files
18
+ attr_reader :delete
19
+
20
+ # @return [Array] The array of paths to exclude
21
+ attr_reader :excludes
22
+
23
+ # @return [String] The exclude paths formatted as an array of rsync exclude arguments
24
+ attr_reader :exclude_args
25
+
26
+ # @return [String] The path to the mirror folder on the guest
27
+ attr_reader :guest_path
28
+
29
+ # Creates an instance
30
+ #
31
+ # @param [Vagrant::VM] The VM to mirror on
32
+ # @param [String] The path of the shared folder on the guest to use as the rsync source
33
+ # @param [String] The path of the shared folder on the host - used to check whether a path is a directory
34
+ # @param [Hash] The config.mirror options hash with the guest mirror destination, delete, etc
35
+ def initialize(vm, guest_sf_path, host_path, mirror_config)
36
+ @vm = vm
37
+ @guest_sf_path = guest_sf_path
38
+ @host_path = host_path
39
+ @delete = mirror_config[:delete]
40
+ @excludes = mirror_config[:exclude]
41
+ @guest_path = mirror_config[:guest_path]
42
+
43
+ # Build the exclude argument array
44
+ @exclude_args = []
45
+ if (@excludes.count > 0)
46
+ @excludes.each do | exclude |
47
+ exclude_args << "--exclude '#{exclude}'"
48
+ end
49
+ end
50
+ end
51
+
52
+ # Run rsync on the guest to update a path - either the whole mirror directory or individual
53
+ # files and folders within it.
54
+ #
55
+ # @param [String] The path to run in
56
+ def run(path)
57
+ # Strip a leading / off the path to avoid any problems
58
+ path.sub!(/^\//, '')
59
+
60
+ # Build the source and destination paths
61
+ source = "#{guest_sf_path}/#{path}"
62
+ dest = "#{guest_path}/#{path}"
63
+
64
+ # Check if the source is a directory on the host - if so, add a / for rsync
65
+ if ((path != '') && (File.directory?(File.join(host_path, path))))
66
+ source << '/'
67
+ dest << '/'
68
+ end
69
+
70
+
71
+ # Build the rsync command
72
+ args = ['rsync -av']
73
+
74
+ if (delete)
75
+ args << '--del'
76
+ end
77
+
78
+ args = args + exclude_args
79
+
80
+ args << source
81
+ args << dest
82
+
83
+ cmd = args.join(' ')
84
+
85
+ # Run rsync
86
+ vm.channel.sudo(cmd)
87
+ end
88
+
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,60 @@
1
+ # Compares the contents of the host and guest paths and transfers any
2
+ # missing or modified paths to the other side of the mirror. If a whole
3
+ # directory is missing, uses recursive upload/download from the SFTP class,
4
+ # otherwise it iterates over and compares the directory contents.
5
+ #
6
+ # This class does not detect deletions - if a file is missing on one side
7
+ # of the mirror it will simply be replaced.
8
+ #
9
+ # @author Andrew Coulton < andrew@ingenerator.com >
10
+ module Vagrant
11
+ module Mirror
12
+ module Sync
13
+ class All < Base
14
+
15
+ # Compares a folder between guest and host, transferring any new or
16
+ # modified files in the right direction.
17
+ #
18
+ # @param [string] The base path to compare
19
+ def execute(path)
20
+ path = path.chomp('/')
21
+ host_dir = host_path(path).chomp('/')
22
+ guest_dir = guest_path(path).chomp('/')
23
+
24
+ if !@connection.exists?(guest_dir)
25
+ # Create the guest directory
26
+ @connection.mkdir(guest_dir)
27
+ end
28
+
29
+ if !File.exists?(host_dir)
30
+ # Create the host directory
31
+ FileUtils.mkdir_p(host_dir)
32
+ end
33
+
34
+ # If the guest path already exists, have to sync manually
35
+ # First get a combined listing of the two paths
36
+ all_files = @connection.dir_entries(guest_dir) | Dir.entries(host_dir)
37
+ all_files.each do | file |
38
+ # Ignore . and ..
39
+ if (file == '.') or (file == '..')
40
+ next
41
+ end
42
+
43
+ # Get local paths
44
+ host_file = File.join(host_dir, file)
45
+ guest_file = File.join(guest_dir, file)
46
+ # Recurse for directories
47
+ if File.directory?(host_file) \
48
+ or ( !File.exists?(host_file) and @connection.directory?(guest_file))
49
+ execute("#{path}/#{file}")
50
+ else
51
+ # Transfer new/modified files between host and guest
52
+ compare_and_transfer(host_file, guest_file)
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,98 @@
1
+ # Base class for file sync actions, providing common required functionality
2
+ #
3
+ # @author Andrew Coulton < andrew@ingerator.com >
4
+ module Vagrant
5
+ module Mirror
6
+ module Sync
7
+ class Base
8
+
9
+ # Initialises the synchroniser
10
+ #
11
+ # @param [Vagrant::Mirror::Connection::SFTP] The sftp connection instance
12
+ # @param [String] The root path on the host to mirror
13
+ # @param [String] The root path on the guest to mirror
14
+ # @param [Vagrant::UI::Interface] The Vagrant UI class
15
+ def initialize(connection, host_path, guest_path, ui)
16
+ @connection = connection
17
+ @host_root = host_path
18
+ @guest_root = guest_path
19
+ @ui = ui
20
+ end
21
+
22
+ # ==================================================================
23
+ # Begin protected internal methods
24
+ # ==================================================================
25
+ protected
26
+
27
+ # Gets the absolute host path for a file
28
+ #
29
+ # @param [String] Relative path to the file
30
+ # @return [String] Absolute path to the file on the host
31
+ def host_path(relative_path)
32
+ File.join(@host_root, relative_path)
33
+ end
34
+
35
+ # Gets the absolute guest path for a file
36
+ #
37
+ # @param [String] Relative path to the file
38
+ # @return [String] Absolute path to the file on the guest
39
+ def guest_path(relative_path)
40
+ File.join(@guest_root, relative_path)
41
+ end
42
+
43
+ # Gets the mtime of a file on the host from an absolute path
44
+ #
45
+ # @param [String] Absolute host path
46
+ # @return [Time] mtime, or nil if the file does not exist
47
+ def host_mtime(path)
48
+ if !File.exists?(path)
49
+ return nil
50
+ end
51
+ return File.mtime(path)
52
+ end
53
+
54
+ # Gets the mtime of a file on the guest from an absolute path
55
+ #
56
+ # @param [String] Absolute guest path
57
+ # @return [Time] mtime, or nil if the file does not exist
58
+ def guest_mtime(path)
59
+ @connection.mtime(path)
60
+ end
61
+
62
+ # Compares files on the host and the guest and transfers the newest
63
+ # to the other side.
64
+ #
65
+ # @param [String] Absolute host path
66
+ # @param [String] Absolute guest path
67
+ def compare_and_transfer(host_file, guest_file)
68
+
69
+ # Get the mtimes
70
+ host_time = host_mtime(host_file)
71
+ guest_time = guest_mtime(guest_file)
72
+
73
+ # Check what to do
74
+ if (host_time.nil? and guest_time.nil?) then
75
+ # Report an error
76
+ @ui.error("#{host_file} was not found on either the host or guest filesystem - cannot sync")
77
+ elsif (host_time == guest_time)
78
+ # Do nothing
79
+ return
80
+ elsif (guest_time.nil?)
81
+ # Transfer to guest
82
+ @connection.upload(host_file, guest_file, host_time)
83
+ elsif (host_time.nil?)
84
+ # Transfer to host
85
+ @connection.download(guest_file, host_file, guest_time)
86
+ elsif (host_time > guest_time)
87
+ # Transfer to guest
88
+ @connection.upload(host_file, guest_file, host_time)
89
+ elsif (host_time < guest_time)
90
+ # Transfer to guest
91
+ @connection.download(guest_file, host_file, guest_time)
92
+ end
93
+ end
94
+
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,61 @@
1
+ # Processes notified file additions, modifications and deletions notified by
2
+ # guard/listen, and replays them on the other side of the mirror.
3
+ # The execute method will be called for each changeset notified whether
4
+ # by guard or over the TCP socket (from guard on the guest). This could
5
+ # cause a race condition as changes on the guest trigger changes on the host
6
+ # and vice versa.
7
+ #
8
+ # Therefore, on notification of added or modified file, the class just
9
+ # syncs whichever is the newest file between the two machines. On deletion
10
+ # it will quietly fail if the file it is notified of has already been
11
+ # deleted.
12
+ #
13
+ # @author Andrew Coulton < andrew@ingenerator.com >
14
+ module Vagrant
15
+ module Mirror
16
+ module Sync
17
+ class Changes < Base
18
+
19
+ # Compares a single notified changeset and transfers any changed,
20
+ # modified or removed files in the right direction.
21
+ #
22
+ # @param [Symbol] Which side of the mirror the change was detected
23
+ # @param [Array] Array of added paths
24
+ # @param [Array] Array of changed paths
25
+ # @param [Array] Array of removed paths
26
+ def execute(source, added, modified, removed)
27
+
28
+ # Combine added and modified, they're the same for our purposes
29
+ changed = added + modified
30
+ changed.each do |file|
31
+ # Transfer the newest file to the other side, or do nothing
32
+ compare_and_transfer(host_path(file), guest_path(file))
33
+ end
34
+
35
+ # Process deleted files
36
+ removed.each do |file|
37
+ # Expect a cascade - only delete on opposite side if exists
38
+ if source == :host
39
+ guest_file = guest_path(file)
40
+ if !guest_mtime(guest_file).nil?
41
+ @connection.delete(guest_file)
42
+ else
43
+ @ui.info("#{file} was not found on guest - nothing to delete")
44
+ end
45
+ elsif source == :guest
46
+ host_file = host_path(file)
47
+ if File.exists?(host_file)
48
+ File.delete(host_file)
49
+ else
50
+ @ui.info("#{file} was not found on host - nothing to delete")
51
+ end
52
+ end
53
+ end
54
+
55
+ # Complete all transfers
56
+ @connection.finish_transfers
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ module Vagrant
2
+ module Mirror
3
+ VERSION = "0.1.0.alpha"
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+ require 'vagrant'
2
+
3
+ require 'vagrant-mirror/errors'
4
+ require 'vagrant-mirror/version'
5
+ require 'vagrant-mirror/config'
6
+
7
+ # Require the host listener
8
+ require 'vagrant-mirror/listener/host'
9
+
10
+ # Require the rsync wrapper
11
+ require 'vagrant-mirror/rsync'
12
+
13
+ # Require the middlewares
14
+ require 'vagrant-mirror/middleware/base'
15
+ require 'vagrant-mirror/middleware/sync'
16
+ require 'vagrant-mirror/middleware/mirror'
17
+
18
+ # Require the command
19
+ require 'vagrant-mirror/command'
20
+
21
+ # Register the config
22
+ Vagrant.config_keys.register(:mirror) { Vagrant::Mirror::Config }
23
+
24
+ # Register the command
25
+ Vagrant.commands.register(:mirror) { Vagrant::Mirror::Command }
26
+
27
+ # Add the sync middleware to the start stack
28
+ Vagrant.actions[:start].insert Vagrant::Action::VM::ShareFolders, Vagrant::Mirror::Middleware::Sync
29
+
30
+ # Add the mirror middleware to the standard stacks
31
+ Vagrant.actions[:start].insert Vagrant::Action::VM::Provision, Vagrant::Mirror::Middleware::Mirror
32
+
33
+ # Abort on unhandled exceptions in any thread
34
+ Thread.abort_on_exception = true
@@ -0,0 +1,4 @@
1
+ # This file is automatically loaded by Vagrant to load any
2
+ # plugins. This file kicks off this plugin.
3
+
4
+ require 'vagrant-mirror'
@@ -0,0 +1,15 @@
1
+ require 'vagrant-mirror'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+
8
+ # Run specs in random order to surface order dependencies. If you find an
9
+ # order dependency and want to debug it, you can fix the order by providing
10
+ # the seed, which is printed after each run.
11
+ # --seed 1234
12
+ config.order = 'random'
13
+
14
+ config.include Vagrant::TestHelpers
15
+ end
@@ -0,0 +1,91 @@
1
+ describe Vagrant::Mirror::Command do
2
+ let (:argv) { [] }
3
+ let (:env) { double("Vagrant::Environment").as_null_object }
4
+ let (:vm) { double("Vagrant::VM").as_null_object }
5
+ let (:ui) { double("Vagrant::UI::Interface").as_null_object }
6
+
7
+ before (:each) do
8
+ env.stub(:primary_vm).and_return vm
9
+ env.stub(:multivm?).and_return false
10
+ env.stub(:ui).and_return ui
11
+ end
12
+
13
+ subject { Vagrant::Mirror::Command.new(argv, env) }
14
+
15
+ describe "#execute" do
16
+ shared_examples "cannot run in multivm" do
17
+ before (:each) do
18
+ env.stub(:multivm?).and_return(true)
19
+ end
20
+
21
+ it "throws a SingleVMEnvironmentRequired error" do
22
+ expect { subject.execute }.to raise_error(Vagrant::Mirror::Errors::SingleVMEnvironmentRequired)
23
+ end
24
+ end
25
+
26
+ context "with vagrant mirror sync" do
27
+ let (:argv) { ['sync' ] }
28
+
29
+ it_behaves_like "cannot run in multivm"
30
+
31
+ it "runs the sync middleware with primary vm" do
32
+ vm.should_receive(:run_action).with(Vagrant::Mirror::Middleware::Sync)
33
+
34
+ subject.execute
35
+ end
36
+
37
+ end
38
+
39
+ shared_examples "the monitor command" do
40
+
41
+ it "runs the monitor middleware with primary vm" do
42
+ vm.should_receive(:run_action).with(Vagrant::Mirror::Middleware::Mirror)
43
+
44
+ subject.execute
45
+ end
46
+ end
47
+
48
+ context "with vagrant mirror monitor" do
49
+ let (:argv) { ['monitor' ] }
50
+
51
+ it_behaves_like "cannot run in multivm"
52
+ it_behaves_like "the monitor command"
53
+ end
54
+
55
+ context "with an empty command" do
56
+ let (:argv) { [] }
57
+
58
+ it_behaves_like "cannot run in multivm"
59
+ it_behaves_like "the monitor command"
60
+ end
61
+
62
+ shared_examples "the help command" do
63
+ it "prints valid usage" do
64
+ ui.should_receive(:info).with(/monitor/, anything())
65
+
66
+ subject.execute
67
+ end
68
+
69
+ it "does not run anything" do
70
+ ui.stub(:info)
71
+ vm.should_not_receive(:run_action)
72
+
73
+ subject.execute
74
+ end
75
+ end
76
+
77
+ context "with unknown command" do
78
+ let (:argv) { ['nothing'] }
79
+
80
+ it_behaves_like "cannot run in multivm"
81
+ it_behaves_like "the help command"
82
+ end
83
+
84
+ context "with help command" do
85
+ let (:argv) { ['-h', 'monitor'] }
86
+
87
+ it_behaves_like "the help command"
88
+ end
89
+
90
+ end
91
+ end