souffle 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/Gemfile +9 -3
  2. data/README.md +6 -0
  3. data/bin/{souffle-server → souffle} +0 -0
  4. data/lib/souffle.rb +8 -8
  5. data/lib/souffle/application.rb +15 -10
  6. data/lib/souffle/application/souffle-server.rb +90 -5
  7. data/lib/souffle/config.rb +88 -59
  8. data/lib/souffle/daemon.rb +156 -0
  9. data/lib/souffle/exceptions.rb +29 -17
  10. data/lib/souffle/http.rb +43 -0
  11. data/lib/souffle/log.rb +11 -14
  12. data/lib/souffle/node.rb +91 -53
  13. data/lib/souffle/node/runlist.rb +16 -18
  14. data/lib/souffle/node/runlist_item.rb +43 -36
  15. data/lib/souffle/node/runlist_parser.rb +60 -62
  16. data/lib/souffle/polling_event.rb +110 -0
  17. data/lib/souffle/provider.rb +231 -23
  18. data/lib/souffle/provider/aws.rb +654 -7
  19. data/lib/souffle/provider/vagrant.rb +42 -5
  20. data/lib/souffle/provisioner.rb +55 -0
  21. data/lib/souffle/provisioner/node.rb +157 -0
  22. data/lib/souffle/provisioner/system.rb +195 -0
  23. data/lib/souffle/redis_client.rb +8 -0
  24. data/lib/souffle/redis_mixin.rb +40 -0
  25. data/lib/souffle/server.rb +42 -8
  26. data/lib/souffle/ssh_monkey.rb +8 -0
  27. data/lib/souffle/state.rb +16 -0
  28. data/lib/souffle/system.rb +139 -37
  29. data/lib/souffle/template.rb +30 -0
  30. data/lib/souffle/templates/Vagrantfile.erb +41 -0
  31. data/lib/souffle/version.rb +6 -0
  32. data/spec/config_spec.rb +20 -0
  33. data/spec/log_spec.rb +24 -0
  34. data/spec/{runlist_parser_spec.rb → node/runlist_parser_spec.rb} +1 -1
  35. data/spec/{runlist_spec.rb → node/runlist_spec.rb} +1 -1
  36. data/spec/node_spec.rb +43 -8
  37. data/spec/provider_spec.rb +56 -0
  38. data/spec/providers/aws_provider_spec.rb +114 -0
  39. data/spec/providers/vagrant_provider_spec.rb +22 -0
  40. data/spec/provisioner_spec.rb +47 -0
  41. data/spec/spec_helper.rb +8 -0
  42. data/spec/system_spec.rb +242 -13
  43. data/spec/template_spec.rb +20 -0
  44. data/spec/templates/example_template.erb +1 -0
  45. metadata +125 -30
  46. data/bin/souffle-worker +0 -7
  47. data/lib/souffle/application/souffle-worker.rb +0 -46
  48. data/lib/souffle/providers.rb +0 -2
  49. data/lib/souffle/worker.rb +0 -14
data/Gemfile CHANGED
@@ -7,21 +7,27 @@ gem "yajl-ruby", "~> 1.1.0"
7
7
  gem "eventmachine", "~> 0.12.10"
8
8
  gem "amqp", "~> 0.9.7"
9
9
  gem "state_machine", "~> 1.1.2"
10
+ gem "em-ssh", "~> 0.4.0"
11
+ gem "em-synchrony", "~> 0.2.0"
10
12
 
11
13
  gem "right_aws", "~> 3.0.4"
14
+ gem "tilt", "~> 1.3.3"
15
+ gem "redis", "~> 3.0.1"
12
16
 
13
- gem "mixlib-cli"
17
+ gem "mixlib-cli", ">= 1.2.2"
14
18
  gem "mixlib-config", ">= 1.1.0"
15
19
  gem "mixlib-log", ">= 1.3.0"
16
20
 
21
+ gem "thin", "~> 1.4.1"
22
+ gem "rack", "~> 1.4.1"
23
+ gem "sinatra", "~> 1.3.2"
24
+
17
25
  # Add dependencies to develop your gem here.
18
26
  # Include everything needed to run rake, tests, features, etc.
19
27
  group :development do
20
28
  gem "rspec", "~> 2.10.0"
21
- gem "fakefs", "~> 0.4.0"
22
29
  gem "yard", "~> 0.8"
23
30
  gem "redcarpet", "~> 2.1.1"
24
- gem "rdoc", "~> 3.12"
25
31
  gem "cucumber", ">= 0"
26
32
  gem "bundler", "~> 1.1.0"
27
33
  gem "jeweler", "~> 1.8.3"
data/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  An orchestrator for setting up isolated chef-managed systems.
4
4
 
5
+ ## A note on tests
6
+
7
+ In order to avoid painfully silly charges and costs, all of the AWS tests
8
+ that require you to pay (spinning up machines, etc), will only run if you
9
+ have the environment variable `AWS_LIVE` set to `true`.
10
+
5
11
  ## Contributing to souffle
6
12
 
7
13
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
File without changes
data/lib/souffle.rb CHANGED
@@ -1,17 +1,17 @@
1
1
  $:.unshift File.dirname(__FILE__)
2
2
  require 'yajl'
3
+ require 'yajl/json_gem'
3
4
  require 'eventmachine'
4
- require 'state_machine'
5
- require 'right_aws'
6
-
7
- # An orchestrator for setting up isolated chef-managed systems.
8
- module Souffle
9
- VERSION = "0.0.1"
10
- end
5
+ require 'em-ssh'
6
+ require 'em-synchrony'
11
7
 
8
+ require 'souffle/version'
9
+ require 'souffle/ssh_monkey'
12
10
  require 'souffle/log'
13
11
  require 'souffle/exceptions'
14
12
  require 'souffle/config'
15
- require 'souffle/providers'
13
+ require 'souffle/daemon'
16
14
  require 'souffle/node'
17
15
  require 'souffle/system'
16
+ require 'souffle/provider'
17
+ require 'souffle/provisioner'
@@ -4,11 +4,15 @@ require 'mixlib/cli'
4
4
  class Souffle::Application
5
5
  include Mixlib::CLI
6
6
 
7
+ # The commands that were left unparsed from parse_options.
8
+ attr_accessor :commands
9
+
7
10
  # Added a Wakeup exception.
8
11
  class Wakeup < Exception; end
9
12
 
10
13
  # Initialize the application, setting up default handlers.
11
14
  def initialize
15
+ @commands = []
12
16
  super
13
17
 
14
18
  trap("TERM") do
@@ -42,10 +46,11 @@ class Souffle::Application
42
46
  begin
43
47
  ::File.open(config[:config_file]) { |f| apply_config(f.path) }
44
48
  rescue Errno::ENOENT => error
45
- noconfig = "Did not find config file: #{config[:config_file]}"
46
- Souffle::Log.warn("*****************************************")
47
- Souffle::Log.warn("#{noconfig}, using command line options.")
48
- Souffle::Log.warn("*****************************************")
49
+ msg = "Did not find the config file: #{config[:config_file]}"
50
+ msg << ", Using command line options."
51
+ Souffle::Log.warn "*****************************************"
52
+ Souffle::Log.warn msg
53
+ Souffle::Log.warn "*****************************************"
49
54
  end
50
55
  end
51
56
 
@@ -56,9 +61,9 @@ class Souffle::Application
56
61
  Souffle::Log.init(Souffle::Config[:log_location])
57
62
  if ( Souffle::Config[:log_location] != STDOUT ) && STDOUT.tty? &&
58
63
  ( !Souffle::Config[:daemonize] )
59
- stdout_loger = Logger.new(STDOUT)
64
+ stdout_logger = Logger.new(STDOUT)
60
65
  STDOUT.sync = true
61
- stdout_logger = Souffle::Log.logger.formatter
66
+ stdout_logger.formatter = Souffle::Log.logger.formatter
62
67
  Souffle::Log.loggers << stdout_logger
63
68
  end
64
69
  Souffle::Log.level = Souffle::Config[:log_level]
@@ -113,8 +118,8 @@ class Souffle::Application
113
118
  # Log a fatal error message to both STDERR and the Logger,
114
119
  # exit the application with a fatal message.
115
120
  #
116
- # @param [ msg ] String The message to log.
117
- # @param [ err ] Integer The exit level.
121
+ # @param [ String ] msg The message to log.
122
+ # @param [ Fixnum ] err The exit level.
118
123
  def fatal!(msg, err = -1)
119
124
  Souffle::Log.fatal(msg)
120
125
  Process.exit err
@@ -123,8 +128,8 @@ class Souffle::Application
123
128
  # Log a fatal error message to both STDERR and the Logger,
124
129
  # exit the application with a debug message.
125
130
  #
126
- # @param [ msg ] String The message to log.
127
- # @param [ err ] Integer The exit level.
131
+ # @param [ String ] msg The message to log.
132
+ # @param [ Fixnum ] err The exit level.
128
133
  def exit!(msg, err = -1)
129
134
  Souffle::Log.debug(msg)
130
135
  Process.exit err
@@ -7,7 +7,7 @@ class Souffle::Application::Server < Souffle::Application
7
7
  option :config_file,
8
8
  :short => "-c CONFIG",
9
9
  :long => "--config CONFIG",
10
- :default => "/etc/souffle/worker.rb",
10
+ :default => "/etc/souffle/souffle.rb",
11
11
  :description => "The configuration file to use"
12
12
 
13
13
  option :log_level,
@@ -17,10 +17,78 @@ class Souffle::Application::Server < Souffle::Application
17
17
  :proc => lambda { |l| l.to_sym }
18
18
 
19
19
  option :log_location,
20
- :short => "-L LOGLOCATION",
21
- :long => "--logfile LOGLOCATION",
22
- :description => "Set the log file location, defaults to STDOUT",
23
- :proc => nil
20
+ :short => "-L LOG_LOCATION",
21
+ :long => "--logfile LOG_LOCATION",
22
+ :description => "Set the log file location, defaults to STDOUT",
23
+ :proc => nil
24
+
25
+ option :user,
26
+ :short => "-u USER",
27
+ :long => "--user USER",
28
+ :description => "User to set privilege to",
29
+ :proc => nil
30
+
31
+ option :group,
32
+ :short => "-g GROUP",
33
+ :long => "--group GROUP",
34
+ :description => "Group to set privilege to",
35
+ :proc => nil
36
+
37
+ option :daemonize,
38
+ :short => "-d",
39
+ :long => "--daemonize",
40
+ :default => false,
41
+ :description => "Run the application as a daemon (forces `-s`)",
42
+ :proc => lambda { |p| true }
43
+
44
+ option :environment,
45
+ :short => "-E",
46
+ :long => "--environment",
47
+ :description => "The environment profile to use",
48
+ :proc => nil
49
+
50
+ option :rack_host,
51
+ :short => "-H HOSTNAME",
52
+ :long => "--hostname HOSTNAME",
53
+ :description => "Hostname to listen on (default: 0.0.0.0)",
54
+ :proc => nil
55
+
56
+ option :rack_port,
57
+ :short => "-P PORT",
58
+ :long => "--port PORT",
59
+ :description => "Port to listen on (default: 8080)",
60
+ :proc => lambda { |p| p.to_i }
61
+
62
+ option :vagrant_dir,
63
+ :short => "-V VAGRANT_DIR",
64
+ :long => "--vagrant_dir VAGRANT_DIR",
65
+ :description => "The path to the base vagrant vm directory",
66
+ :proc => nil
67
+
68
+ option :pid_file,
69
+ :short => "-f PID_FILE",
70
+ :long => "--pid PID_FILE",
71
+ :description => "Set the PID file location, defaults to /tmp/souffle.pid",
72
+ :proc => nil
73
+
74
+ option :provider,
75
+ :short => "-p PROVIDER",
76
+ :long => "--provider PROVIDER",
77
+ :description => "The provider to use (overrides config)",
78
+ :proc => nil
79
+
80
+ option :json,
81
+ :short => "-j JSON",
82
+ :long => "--json JSON",
83
+ :description => "The json for a single provision (negates `-s`)",
84
+ :proc => nil
85
+
86
+ option :server,
87
+ :short => "-s",
88
+ :long => "--server",
89
+ :default => false,
90
+ :description => "Start the application as a server",
91
+ :proc => nil
24
92
 
25
93
  option :help,
26
94
  :short => "-h",
@@ -39,8 +107,25 @@ class Souffle::Application::Server < Souffle::Application
39
107
  :proc => lambda { |v| puts "Souffle: #{::Souffle::VERSION}"},
40
108
  :exit => 0
41
109
 
110
+ # Grabs all of the cli parameters and generates the mixlib config object.
42
111
  def initialize
43
112
  super
113
+ Souffle::Config.merge!(config)
114
+ end
115
+
116
+ # Configures the souffle server based on the cli parameters.
117
+ def setup_application
118
+ Souffle::Daemon.change_privilege
119
+ Souffle::Config[:server] = true if Souffle::Config[:daemonize]
120
+ @app = Souffle::Server.new
121
+ end
44
122
 
123
+ # Runs the Souffle Server.
124
+ def run_application
125
+ if Souffle::Config[:daemonize]
126
+ Souffle::Config[:server] = true
127
+ Souffle::Daemon.daemonize("souffle")
128
+ end
129
+ @app.run
45
130
  end
46
131
  end
@@ -2,74 +2,103 @@ require 'souffle/log'
2
2
  require 'mixlib/config'
3
3
  require 'yajl'
4
4
 
5
- module Souffle
6
- # The configuration object for the souffle server.
7
- class Config
5
+ # The configuration object for the souffle server.
6
+ class Souffle::Config
7
+ extend Mixlib::Config
8
8
 
9
- extend Mixlib::Config
9
+ # Return the configuration itself upon inspection.
10
+ def self.inspect
11
+ configuration.inspect
12
+ end
10
13
 
11
- # Return the configuration itself upon inspection.
12
- def self.inspect
13
- configuration.inspect
14
- end
14
+ # Loads a given file and passes it to the appropriate parser.
15
+ #
16
+ # @raise [ IOError ] Any IO Exceptions that occur.
17
+ #
18
+ # @param [ String ] filename The filename to read.
19
+ def self.from_file(filename, parser="ruby")
20
+ send("from_file_#{parser}".to_sym, filename)
21
+ end
15
22
 
16
- # Loads a given file and passes it to the appropriate parser.
17
- #
18
- # @raise [ IOError ] Any IO Exceptions that occur.
19
- #
20
- # @param [ String ] filename The filename to read.
21
- def self.from_file(filename, parser="ruby")
22
- send("from_file_#{parser}".to_sym, filename)
23
- end
23
+ # Loads a given ruby file and runs instance_eval against it
24
+ # in the context of the current object.
25
+ #
26
+ # @raise [ IOError ] Any IO Exceptions that occur.
27
+ #
28
+ # @param [ String ] filename The file to read.
29
+ def self.from_file_ruby(filename)
30
+ self.instance_eval(IO.read(filename), filename, 1)
31
+ end
24
32
 
25
- # Loads a given ruby file and runs instance_eval against it
26
- # in the context of the current object.
27
- #
28
- # @raise [ IOError ] Any IO Exceptions that occur.
29
- #
30
- # @param [ String ] filename The file to read.
31
- def self.from_file_ruby(filename)
32
- self.instance_eval(IO.read(filename), filename, 1)
33
- end
33
+ # Loads a given json file and merges the current context
34
+ # configuration with the updated hash.
35
+ #
36
+ # @raise [ IOError ] Any IO Exceptions that occur.
37
+ # @raise [ Yajl::ParseError ] Raises Yajl Parsing error on improper json.
38
+ #
39
+ # @param [ String ] filename The file to read.
40
+ def self.from_file_json(filename)
41
+ self.from_stream_json(IO.read(filename))
42
+ end
34
43
 
35
- # Loads a given json file and merges the current context
36
- # configuration with the updated hash.
37
- #
38
- # @raise [ IOError ] Any IO Exceptions that occur.
39
- # @raise [ Yajl::ParseError ] Raises Yajl Parsing error on improper json.
40
- #
41
- # @param [ String ] filename The file to read.
42
- def self.from_file_json(filename)
43
- self.from_input_json(IO.read(filename))
44
- end
44
+ # Loads a given json input and merges the current context
45
+ # configuration with the updated hash.
46
+ #
47
+ # @raise [ IOError ] Any IO Exceptions that occur.
48
+ # @raise [ Yajl::ParseError ] Raises Yajl Parsing error on improper json.
49
+ #
50
+ # @param [ String ] input The json configuration input.
51
+ def self.from_stream_json(input)
52
+ parser = Yajl::Parser.new(:symbolize_keys => true)
53
+ configuration.merge!(parser.parse(input))
54
+ end
45
55
 
46
- # Loads a given json input and merges the current context
47
- # configuration with the updated hash.
48
- #
49
- # @raise [ IOError ] Any IO Exceptions that occur.
50
- # @raise [ Yajl::ParseError ] Raises Yajl Parsing error on improper json.
51
- #
52
- # @param [ String ] input The json configuration input.
53
- def self.from_stream_json(input)
54
- parser = Yajl::Parser.new
55
- configuration.merge!(parser.parse(input))
56
+ # When you are using ActiveSupport, they monkey-patch 'daemonize' into
57
+ # Kernel. So while this is basically identical to what method_missing
58
+ # would do, we pull it up here and get a real method written so that
59
+ # things get dispatched properly.
60
+ config_attr_writer :daemonize do |v|
61
+ configure do |c|
62
+ c[:daemonize] = v
56
63
  end
64
+ end
57
65
 
58
- # When you are using ActiveSupport, they monkey-patch 'daemonize' into
59
- # Kernel. So while this is basically identical to what method_missing
60
- # would do, we pull it up here and get a real method written so that
61
- # things get dispatched properly.
62
- config_attr_writer :daemonize do |v|
63
- configure do |c|
64
- c[:daemonize] = v
65
- end
66
- end
66
+ # Configuration Settings
67
+ config_file "/etc/souffle/souffle.rb"
67
68
 
68
- log_level :info
69
- log_location STDOUT
69
+ # Enable debug
70
+ debug false
70
71
 
71
- aws_access_key = ""
72
- aws_access_secret = ""
72
+ # Logging Settings
73
+ log_level :info
74
+ log_location STDOUT
73
75
 
74
- end
76
+ # Chef Settings
77
+ chef_cookbook_path []
78
+ chef_provisioner :solo
79
+ chef_domain "souffle"
80
+
81
+ # Provider Settings
82
+ provider "Vagrant"
83
+
84
+ # Daemonization Settings
85
+ user nil
86
+ group nil
87
+ umask 0022
88
+
89
+ pid_file nil
90
+
91
+ # AWS Settings
92
+ aws_access_key ""
93
+ aws_access_secret ""
94
+ aws_region "us-west-1"
95
+ delete_on_termination true
96
+
97
+ # Rack Settings
98
+ rack_host "0.0.0.0"
99
+ rack_port 8080
100
+ rack_environment "development"
101
+
102
+ # Vagrant Settings
103
+ vagrant_dir "#{ENV['HOME']}/vagrant/vms"
75
104
  end
@@ -0,0 +1,156 @@
1
+ require 'etc'
2
+
3
+ # Daemon helper routines.
4
+ class Souffle::Daemon
5
+ class << self
6
+ attr_accessor :name
7
+
8
+ # Daemonize the current process, managing pidfiles and process uid/gid.
9
+ #
10
+ # @param [ String ] name The name to be used for the pid file
11
+ def daemonize(name)
12
+ @name = name
13
+ pid = pid_from_file
14
+ unless running?
15
+ remove_pid_file()
16
+ Souffle::Log.info("Daemonizing...")
17
+ begin
18
+ exit if fork; Process.setsid; exit if fork
19
+ msg = "Forked, in #{Process.pid}. "
20
+ msg << "Privileges: #{Process.euid} #{Process.egid}"
21
+ Souffle::Log.info(msg)
22
+ File.umask Souffle::Config[:umask]
23
+ $stdin.reopen("/dev/null")
24
+ $stdout.reopen("/dev/null", "a")
25
+ $stderr.reopen($stdout)
26
+ save_pid_file;
27
+ at_exit { remove_pid_file }
28
+ rescue NotImplementedError => e
29
+ Souffle::Application.fatal!("There is no fork: #{e.message}")
30
+ end
31
+ else
32
+ Souffle::Application.fatal!("Souffle is already running pid #{pid}")
33
+ end
34
+ end
35
+
36
+ # Checks if Souffle is running based on the pid_file.
37
+ #
38
+ # @return [ Boolean ] Whether or not Souffle is running.
39
+ def running?
40
+ if pid_from_file.nil?
41
+ false
42
+ else
43
+ Process.kill(0, pid_from_file)
44
+ true
45
+ end
46
+ rescue Errno::ESRCH, Errno::ENOENT
47
+ false
48
+ rescue Errno::EACCES => e
49
+ msg = "You don't have access to the PID "
50
+ msg << "file at #{pid_file}: #{e.message}"
51
+ Souffle::Application.fatal!(msg)
52
+ end
53
+
54
+ # Gets the pid file for @name.
55
+ #
56
+ # @return [ String ] Location of the pid file for @name.
57
+ def pid_file
58
+ Souffle::Config[:pid_file] or "/tmp/#{@name}.pid"
59
+ end
60
+
61
+ # Sucks the pid out of pid_file.
62
+ #
63
+ # @return [ Fixnum,NilClass ] The PID from pid_file or nil if it doesn't
64
+ # exist.
65
+ def pid_from_file
66
+ File.read(pid_file).chomp.to_i
67
+ rescue Errno::ENOENT, Errno::EACCES
68
+ nil
69
+ end
70
+
71
+ # Store the PID on the filesystem.
72
+ #
73
+ # @note
74
+ # This uses the Souffle::Config[:pid_file] option or "/tmp/name.pid"
75
+ # by default.
76
+ def save_pid_file
77
+ file = pid_file
78
+ begin
79
+ FileUtils.mkdir_p(File.dirname(file))
80
+ rescue Errno::EACCES => e
81
+ msg = "Failed store pid in #{File.dirname(file)}, "
82
+ msg << "permission denied: #{e.message}"
83
+ Souffle::Application.fatal!(msg)
84
+ end
85
+
86
+ begin
87
+ File.open(file, "w") { |f| f.write(Process.pid.to_s) }
88
+ rescue Errno::EACCES => e
89
+ msg = "Couldn't write to pidfile #{file}, "
90
+ msg << "permission denied: #{e.message}"
91
+ Souffle::Application.fatal!(msg)
92
+ end
93
+ end
94
+
95
+ # Delete the PID from the filesystem
96
+ def remove_pid_file
97
+ FileUtils.rm(pid_file) if File.exists?(pid_file)
98
+ end
99
+
100
+ # Change process user/group to those specified in Souffle::Config
101
+ def change_privilege
102
+ Dir.chdir("/")
103
+
104
+ msg = "About to change privilege to "
105
+ if Souffle::Config[:user] and Souffle::Config[:group]
106
+ msg << "#{Souffle::Config[:user]}:#{Souffle::Config[:group]}"
107
+ Souffle::Log.info(msg)
108
+ _change_privilege(Souffle::Config[:user], Souffle::Config[:group])
109
+ elsif Souffle::Config[:user]
110
+ msg << "#{Souffle::Config[:user]}"
111
+ Souffle::Log.info(msg)
112
+ _change_privilege(Souffle::Config[:user])
113
+ end
114
+ end
115
+
116
+ # Change privileges of the process to be the specified user and group
117
+ #
118
+ # @param [ String ] user The user to change the process to.
119
+ # @param [ String ] group The group to change the process to.
120
+ #
121
+ # @note
122
+ # The group parameter defaults to user unless specified.
123
+ def _change_privilege(user, group=user)
124
+ uid, gid = Process.euid, Process.egid
125
+
126
+ begin
127
+ target_uid = Etc.getpwnam(user).uid
128
+ rescue ArgumentError => e
129
+ msg = "Failed to get UID for user #{user}, does it exist? "
130
+ msg << e.message
131
+ Souffle::Application.fatal!(msg)
132
+ return false
133
+ end
134
+
135
+ begin
136
+ target_gid = Etc.getgrnam(group).gid
137
+ rescue ArgumentError => e
138
+ msg = "Failed to get GID for group #{group}, does it exist? "
139
+ msg << e.message
140
+ Souffle::Application.fatal!(msg)
141
+ return false
142
+ end
143
+
144
+ if (uid != target_uid) or (gid != target_gid)
145
+ Process.initgroups(user, target_gid)
146
+ Process::GID.change_privilege(target_gid)
147
+ Process::UID.change_privilege(target_uid)
148
+ end
149
+ true
150
+ rescue Errno::EPERM => e
151
+ msg = "Permission denied when trying to change #{uid}:#{gid} "
152
+ msg << "to #{target_uid}:#{target_gid}. #{e.message}"
153
+ Souffle::Application.fatal!(msg)
154
+ end
155
+ end
156
+ end