doable 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8049e12988f84644e8ad77af4aa81c69586cd584
4
- data.tar.gz: cdde8ea4ff303b1cdd1407ce2a45f4155fad67f4
3
+ metadata.gz: 12cd4bee334b582b69a835051c8cfeb0314aa3e3
4
+ data.tar.gz: 84db114358f2ea2f7511900421206ed8ddcbe9d5
5
5
  SHA512:
6
- metadata.gz: 8e5e57273ae3eb3a16aafa7992a2513bc7eefada445a1feac54d53e55a4633001eeed83b7a7967a3dbced96d7868a7fb041db469c5ed968763c4023a2a2f8af4
7
- data.tar.gz: e13885ee5299ae731b16a9d1c83ec4868994a66cd2ed57e5bd0a8372b7758a3645e12c95e5ad4d0eab946e84d42c71af7da200b6d41caef7ccf20bebc9f56074
6
+ metadata.gz: 380841e88c0f7dcf73a6d9a85bf6d3bdb0941a519f8072e0957515c42a38b224fc3e8be81ed6be0b4c338a7ae071f3c55228f0877fb634bfb8322cf7c69f4745
7
+ data.tar.gz: 56aea29e8c7bfd238d806d8953ab456ef13239b9ec019d2316b22be8e2a747bf9620106a3f686d7d429c4446d79ef41e2b7adfec0e98ba8de829596deabdfccd
@@ -1,21 +1,21 @@
1
1
  module Doable
2
2
  module Exceptions
3
3
  module Framework
4
+ # Exception to be thrown when nothing else applies. Inherited by other exceptions.
4
5
  class GenericFrameworkError < StandardError
5
6
  end
6
7
 
8
+ # When the provided parameters or other input is not valid.
7
9
  class InvalidInput < GenericFrameworkError
8
10
  end
9
11
 
12
+ # Correct number of parameters is passed but it is not of the correct type.
13
+ # i.e., type error in strongly typed languages
10
14
  class NotApplicable < InvalidInput
11
15
  end
12
16
 
13
- class InvalidAction < GenericFrameworkError
14
- end
15
-
16
- class MissingDefinitionsDirectory < GenericFrameworkError
17
- end
18
-
17
+ # This can be called when a helper method is expecting a parameter that wasn't passed.
18
+ # i.e., an options hash doesn't contain the requisite elements.
19
19
  class MissingParameter < InvalidInput
20
20
  end
21
21
 
@@ -24,11 +24,13 @@ module Doable
24
24
  end
25
25
 
26
26
  # !These exceptions should never be caught by anything!
27
+ # This is an internal exception used primarily for logging purposes. Provides a clean way to log
27
28
  class SkipStep < StandardError
28
29
  end
29
30
 
31
+ # Allows the the application to notify the completion of a Rollback and that the job object was not successfully executed.
30
32
  class RolledBack < StandardError
31
33
  end
32
34
  end
33
35
  end
34
- end
36
+ end
@@ -0,0 +1,11 @@
1
+ module Doable
2
+ module Exceptions
3
+ module Linux
4
+ class MissingRPMs < StandardError
5
+ end
6
+
7
+ class InvalidRPMList < StandardError
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,6 +1,8 @@
1
1
  module Doable
2
2
  module Exceptions
3
+ # Exceptions which are agnostic to the underlying OS.
3
4
  module OS
5
+ # Generic exception to be inherited by all other OS-level exceptions.
4
6
  class OSException < GenericFrameworkError
5
7
  end
6
8
 
@@ -16,8 +18,50 @@ module Doable
16
18
  class InvalidHostname < OSException
17
19
  end
18
20
 
21
+ class FailedPasswordChange < OSException
22
+ end
23
+
19
24
  class MissingFile < OSException
20
25
  end
26
+
27
+ class MissingExecutable < OSException
28
+ end
29
+
30
+ class FailedCopy < OSException
31
+ end
32
+
33
+ class NotApplicable < OSException
34
+ end
35
+
36
+ class DuplicateUsers < OSException
37
+ end
38
+
39
+ class DuplicateGroups < OSException
40
+ end
41
+
42
+ class WrongHomedir < OSException
43
+ end
44
+
45
+ class InvalidArchitecture < OSException
46
+ end
47
+
48
+ class MissingUser < OSException
49
+ end
50
+
51
+ class WrongUser < OSException
52
+ end
53
+
54
+ class FailedGroupAdd < OSException
55
+ end
56
+
57
+ class FailedUserModification < OSException
58
+ end
59
+
60
+ class FailedUncompress < OSException
61
+ end
62
+
63
+ class AmbiguousProcess < OSException
64
+ end
21
65
  end
22
66
  end
23
- end
67
+ end
@@ -0,0 +1,296 @@
1
+ require 'doable/helpers/os'
2
+ require 'doable/exceptions/linux'
3
+
4
+ module Doable
5
+ module Helpers
6
+ module Linux
7
+ include OS
8
+ include Exceptions::Linux
9
+ # Used like Unix chown
10
+ # @param user [String] User to set as the owner
11
+ # @param group [String] Group to set as the owner
12
+ # @param file_list [Array<String>,String] List of files to change the owner of
13
+ def chown(user, group, file_list, options = {recursive: false})
14
+ if options[:recursive]
15
+ options.delete :recursive
16
+ FileUtils.chown_R(user, group, file_list, options)
17
+ else
18
+ options.delete(:recursive) if options.has_key?(:recursive)
19
+ FileUtils.chown(user, group, file_list, options)
20
+ end
21
+ end
22
+
23
+ # Used like Unix chmod
24
+ # @param permission [String, Fixnum] Permissions to set
25
+ # @param file_list [Array<String>,String] List of files to change the permissions of
26
+ def chmod(permission, file_list, options = {recursive: false})
27
+ if options[:recursive]
28
+ options.delete :recursive
29
+ FileUtils.chmod_R(permission, file_list, options)
30
+ else
31
+ options.delete(:recursive) if options.has_key?(:recursive)
32
+ FileUtils.chmod(permission, file_list, options)
33
+ end
34
+ end
35
+
36
+ # Apply a patch file
37
+ # @param options [String] Options to be added to the `patch` system command
38
+ # @param patch_file [String] Path to the patch file to apply
39
+ # @param path [String] Optionally cd to this directory before patching
40
+ # @param user [String] Optionally apply patch as this user
41
+ def apply_patch(options, patch_file, path = nil, user = nil)
42
+ cmd = "patch #{options} < #{patch_file}"
43
+
44
+ olddir = `pwd`.chomp
45
+ Dir.chdir(path) if path
46
+ tee 'pwd'
47
+ if user
48
+ run_as_user user, cmd
49
+ else
50
+ tee cmd
51
+ end
52
+ Dir.chdir(olddir) if path
53
+ end
54
+
55
+ # Add an entry to a user's ~/.bashrc
56
+ # @param user [String] User to add bashrc entries to
57
+ # @param entry [String] Entry to add to user's .bashrc
58
+ def add_bashrc_entry(user, entry)
59
+ File.open(File.expand_path("~#{user}/.bashrc"), "a") {|f| f.puts entry }
60
+ end
61
+
62
+ # Get a list of process matching a Regexp
63
+ # @param regex [Regexp] Regular expression to use for finding processes
64
+ # @return [Array<String>] Array of PIDs that match the regex
65
+ def find_processes(regex)
66
+ `ps aux`.chomp.split("\n").map {|l| l.split(nil, 11) }.collect {|p| p if p[10].match(regex)}.compact
67
+ end
68
+
69
+ # Kill a process
70
+ # @param pid [Fixnum,String] PID to kill
71
+ # @param signal [String] Signal to kill PID with
72
+ def kill(pid, signal = "TERM")
73
+ Process.kill(signal, pid)
74
+ end
75
+
76
+ # Does the system have an executable in $PATH? If not, return false.
77
+ # @param name [String] Executable to verify
78
+ # @return [String, false]
79
+ def have_executable?(name)
80
+ app_path = `which #{name}`.chomp
81
+ if app_path.match(name)
82
+ return app_path
83
+ else
84
+ return false
85
+ end
86
+ end
87
+
88
+ # Require an executable to be in $PATH
89
+ # @raise [MissingExecutable]
90
+ # @param name [String] Required executable
91
+ def find_executable(name)
92
+ have_executable?(name) ? true : raise(MissingExecutable, name)
93
+ end
94
+
95
+ # Check for RPMs (kind of RHEL / SuSE specific)
96
+ # @raise [InvalidRPMList]
97
+ # @raise [NotApplicable]
98
+ # @raise [MissingRPMs]
99
+ # @param rpms [Array<String>] RPMs to verify
100
+ # #return [Boolean]
101
+ def check_for_rpms(rpms)
102
+ raise InvalidRPMList unless rpms.kind_of?(Array)
103
+ # Split the list of all installed RPMs into an Array containing only the RPM's base name
104
+ system_rpms = `rpm -qa`.chomp.split("\n").collect {|rpm| rpm.split(/-[0-9]/)[0]}
105
+ i686_rpms = `rpm -qa`.chomp.split("\n").collect {|rpm| rpm.split(/-[0-9]/)[0] if rpm.match(/\.i686$/)}.compact
106
+ raise NotApplicable unless $?.success?
107
+ missing_rpms = rpms.collect {|rpm| rpm unless rpm.match(/\.i686$/) }.compact - system_rpms # calculate what RPMs are not installed
108
+ missing_rpms = (missing_rpms + (rpms.collect {|rpm| rpm.split('.')[0] if rpm.match(/\.i686$/) }.compact - i686_rpms).compact.map {|r| "#{r}.i686" } ).compact
109
+ if missing_rpms.size > 0
110
+ log "Missing RPMs: #{missing_rpms.join(', ')}", :error
111
+ raise MissingRPMs
112
+ else
113
+ return true
114
+ end
115
+ end
116
+
117
+ # Ensure a user exists on the system
118
+ # @raise [DuplicateUsers]
119
+ # @raise [WrongHomedir]
120
+ # @param user [String] User to add or verify
121
+ # @param homedir [String] User home directory
122
+ # @return [Boolean]
123
+ def find_or_create_user(user, homedir, options = {})
124
+ # Create an Array of Arrays storing all known system users and their attributes
125
+ all_users = `getent passwd`.chomp.split("\n").map {|line| line.split(':')}
126
+ # Determine if the user we want exists
127
+ this_user = all_users.collect {|u| u if u[0] == user}.compact
128
+ if this_user.size > 1
129
+ log "Multiple instances of user #{user} found. This is known to cause problems.", :error
130
+ raise DuplicateUsers
131
+ elsif this_user.size == 1
132
+ log "User '#{user}' already exists with uid: #{this_user.flatten[2]}. Skipping creation of user...", :warn
133
+ full_home = File.expand_path(homedir)
134
+ current_home = File.expand_path(this_user.flatten[5])
135
+ if (full_home != current_home) and (homedir != current_home)
136
+ log "User's home directory is set to #{current_home}, not #{homedir}!", :error
137
+ raise WrongHomedir
138
+ end
139
+ else
140
+ log "Creating #{user} user..."
141
+ shell = options[:shell] ? options[:shell] : '/bin/bash'
142
+ `useradd -d "#{homedir}" -s "#{shell}" -mr "#{user}"`
143
+ end
144
+
145
+ return true
146
+ end
147
+
148
+ # Sets a user's password
149
+ # @note This _may_ only work on RHEL (and the like), as chpasswd may not be available on other OS's
150
+ # @raise [FailedPasswordChange]
151
+ # @param user [String] User to set password for
152
+ # @param new_pass [String] New password for user
153
+ # @return [Boolean]
154
+ def set_user_password(user, new_pass)
155
+ log "Setting password for #{user}..."
156
+ `echo "#{user}:#{new_pass}" | chpasswd`
157
+ raise FailedPasswordChange unless $?.success?
158
+ return $?.success?
159
+ end
160
+
161
+ # Die if a user doesn't exist
162
+ # @raise [DuplicateUsers]
163
+ # @raise [MissingUser]
164
+ # @param user [String] User to verify
165
+ # @param noisy [Boolean] Should extra info be logged?
166
+ # @return [Array]
167
+ def find_user(user, noisy = false)
168
+ # Create an Array of Arrays storing all known system users and their attributes
169
+ all_users = `getent passwd`.chomp.split("\n").map {|line| line.split(':')}
170
+ # Determine if the user we want exists
171
+ this_user = all_users.collect {|u| u if u[0] == user}.compact
172
+ if this_user.size > 1
173
+ log "Multiple instances of user #{user} found. This is known to cause problems.", :error
174
+ raise DuplicateUsers
175
+ elsif this_user.size < 1
176
+ log "Missing Critical User '#{user}'!", :error
177
+ raise MissingUser
178
+ else
179
+ log "Found required user '#{user}' with uid #{this_user.flatten[2]}..." if noisy
180
+ end
181
+ return this_user.flatten
182
+ end
183
+
184
+ # Ensure a group exists
185
+ # @raise [DuplicateGroups]
186
+ # @raise [FailedGroupAdd]
187
+ # @param group [String] Group to create or verify
188
+ # @return [Boolean]
189
+ def find_or_create_group(group)
190
+ all_groups = `getent group`.chomp.split("\n").map {|line| line.split(':')}
191
+ this_group = all_groups.collect {|g| g if g[0] == group}.compact
192
+ if this_group.size > 1
193
+ log "Multiple instances of group #{group} found. This is known to cause problems.", :error
194
+ raise DuplicateGroups
195
+ elsif this_group.size == 1
196
+ log "Group '#{group}' already exists with gid: #{this_group.flatten[2]}. Skipping creation of group...", :warn
197
+ else
198
+ log "Creating group '#{group}'..."
199
+ `groupadd -r "#{group}"`
200
+ raise FailedGroupAdd unless $?.success?
201
+ return $?.success?
202
+ end
203
+ end
204
+
205
+ # Set a user's gid (default group)
206
+ # @raise [FailedUserModification]
207
+ # @param user [String] User to update
208
+ # @param gid [String,Fixnum] Group to set as default for user
209
+ def update_user_gid(user, gid)
210
+ user_details = find_user(user)
211
+ if user_details[3] != gid
212
+ log "Setting default group for #{user} to #{gid}..."
213
+ `usermod -g #{gid} "#{user}"`
214
+ raise FailedUserModification unless $?.success?
215
+ log "Fixing permissions..."
216
+ `chown -R #{user}:#{gid} "#{user_details[5]}"`
217
+ end
218
+ return gid
219
+ end
220
+
221
+ # Finds the mount point for a directory, then checks it for a minimum number of kilobytes
222
+ # @param install_dir [String] Directory from which check starts
223
+ # @param min_space [Fixnum] Minumum amount of space required on mount-point discovered from install_dir
224
+ def check_disk_space(install_dir, min_space)
225
+ all_filesystems = `df -PB 1024`.chomp.split("\n").collect {|fs| fs.split if fs.match(/^\/dev/) }.compact
226
+ best_fit = nil
227
+ current_test = install_dir
228
+ until best_fit
229
+ matching_fs = all_filesystems.collect {|fs| fs if File.expand_path(fs[5]) == File.expand_path(current_test) }.compact
230
+ if matching_fs.size == 1
231
+ best_fit = matching_fs.flatten
232
+ else
233
+ current_test = File.dirname(current_test)
234
+ end
235
+ end
236
+
237
+ log "Closest matching filesystem is '#{best_fit[5]}'. Testing for free space..."
238
+ if Integer(best_fit[3]) >= min_space
239
+ log "Found sufficient space #{best_fit[3]} for install on #{best_fit[5]} with an install directory of #{install_dir}."
240
+ else
241
+ log "Insufficient space #{best_fit[3]} for install on #{best_fit[5]} with an install directory of #{install_dir}!", :error
242
+ end
243
+ end
244
+
245
+ # Linux specific way to run something as another user. Command must be passed as a String.
246
+ # @param user [String] User that will run command
247
+ # @param command [String] Command to be run by user
248
+ def run_as_user(user, command)
249
+ log "Running '#{command}' as '#{user}'..."
250
+ tee("su -l #{user} -c \"#{command}\"")
251
+ end
252
+
253
+ # Set the max number of open files for a user (nofiles)
254
+ # (sets both hard and soft, errors out if user is already listed in limits.conf)
255
+ # Currently does NOT verify the existence of the specified user on the system
256
+ # @param user [String] User to set 'nofile' ulimit security setting for
257
+ # @param limit [Fixnum] Limit to set for user
258
+ # @return [Boolean]
259
+ def set_nofile(user, limit)
260
+ # See if the user is already in the limits.conf file
261
+ user_entries = `grep -E '^#{user}(\s|\t)+' /etc/security/limits.conf`.chomp.split("\n")
262
+ if user_entries.empty?
263
+ # Doing this the lazy way for now... this is probably dangerous for very large files
264
+
265
+ # Open and copy the current content of the limits file plus a few extra lines towards the end
266
+ current_file = File.readlines("/etc/security/limits.conf")
267
+ temporary = Tempfile.new("new_limits")
268
+ current_file[0..(current_file.size - 2)].each {|l| temporary.write l }
269
+
270
+ temporary.write "#{user}\t\tsoft\tnofile\t\t#{limit}\n"
271
+ temporary.write "#{user}\t\thard\tnofile\t\t#{limit}\n"
272
+ temporary.write "\n"
273
+ temporary.write current_file[current_file.size - 1]
274
+ temporary.rewind
275
+
276
+ # Create a backup of the current file
277
+ FileUtils.cp "/etc/security/limits.conf", "/etc/security/limits.conf.#{Time.now.to_i}"
278
+ # Clear out the current file
279
+ File.truncate("/etc/security/limits.conf", 0)
280
+ # Copy the temporary (new) file to the existing
281
+ current_file = File.new("/etc/security/limits.conf", "w")
282
+ temporary.each {|l| current_file.puts l }
283
+ # Close everything up
284
+ temporary.close
285
+ temporary.unlink
286
+ current_file.close
287
+ return true
288
+ else
289
+ log "User '#{user}' already has a specific entry in /etc/security/limits.conf. Ensure 'nofile' is at least #{limit}.", :warn
290
+ return false
291
+ end
292
+ end # set_nofile()
293
+
294
+ end
295
+ end
296
+ end
@@ -10,6 +10,19 @@ module Doable
10
10
 
11
11
  (1..length).collect{|a| options[:characters][rand(options[:characters].size)] }.join
12
12
  end # generate_password()
13
+
14
+ def sha1_hash(password, add_salt = true)
15
+ salt = if add_salt?
16
+ if add_salt.kind_of?(String) and add_salt.downcase.match(/^[0-9a-f]+$/)
17
+ add_salt.downcase
18
+ else
19
+ generate_password(16, characters: [*(0..9), *('a'..'f')])
20
+ end
21
+ else
22
+ ''
23
+ end
24
+ Base64.encode64(Digest::SHA1.digest(password + salt) + salt).chomp
25
+ end
13
26
  end
14
27
  end
15
28
  end
data/lib/doable/job.rb CHANGED
@@ -8,10 +8,19 @@ module Doable
8
8
  include Helpers::Logging
9
9
  attr_reader :steps, :hooks, :handlers, :threads
10
10
 
11
+ # Allows sequential definition of job steps.
12
+ # @example usage
13
+ # job = Doable::Job.plan do |j|
14
+ # j.before { log "Starting my awesome job" }
15
+ # j.step { # do some stuff here }
16
+ # j.attempt { # try to do some other stuff here }
17
+ # j.after { log "Looks like we're all set" }
18
+ # end
11
19
  def self.plan(&block)
12
20
  self.new(&block)
13
21
  end
14
-
22
+
23
+ # Yields itself to allow the syntax seen in the plan class method.
15
24
  def initialize
16
25
  @hooks = {}
17
26
  @steps = []
@@ -58,7 +67,7 @@ module Doable
58
67
  # WARNING! Exception handlers are __not__ used with these steps, as they never actually raise exceptions
59
68
  # @param options [Hash]
60
69
  # @param block [Proc]
61
- # @return [Boolean]
70
+ # @return [Boolean] this will __always__ be true by the nature of "attempt"
62
71
  def attempt(options = {}, &block)
63
72
  @steps << Step.new(self, options) do
64
73
  begin
@@ -113,6 +122,7 @@ module Doable
113
122
 
114
123
  # This triggers a block associated with a hook
115
124
  # @param hook [Symbol] Hook to trigger
125
+ # @return [Boolean] returns true no exceptions are encounter during enumeration of hook steps.
116
126
  def trigger(hook)
117
127
  @hooks[hook].each_with_index do |step, index|
118
128
  begin
@@ -151,6 +161,7 @@ module Doable
151
161
  end
152
162
  end # begin()
153
163
  end if @hooks[hook] # each_with_index()
164
+ return true
154
165
  end # trigger()
155
166
 
156
167
  # Here we actually trigger the execution of a Job
@@ -221,4 +232,4 @@ module Doable
221
232
  log "All Job steps completed successfully!", :success # This should only happen if everything goes well
222
233
  end # run()
223
234
  end
224
- end
235
+ end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Gnagy
8
+ - Tim Earle
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2015-01-29 00:00:00.000000000 Z
12
+ date: 2015-02-02 00:00:00.000000000 Z
12
13
  dependencies: []
13
14
  description: A framework for automating tasks with ease
14
15
  email: jonathan.gnagy@gmail.com
@@ -20,8 +21,10 @@ files:
20
21
  - README.md
21
22
  - lib/doable.rb
22
23
  - lib/doable/exceptions/framework.rb
24
+ - lib/doable/exceptions/linux.rb
23
25
  - lib/doable/exceptions/os.rb
24
26
  - lib/doable/helpers/framework.rb
27
+ - lib/doable/helpers/linux.rb
25
28
  - lib/doable/helpers/logging.rb
26
29
  - lib/doable/helpers/os.rb
27
30
  - lib/doable/helpers/password.rb