Class: Salticid::Host
Direct Known Subclasses
Defined Under Namespace
Modules: IP
Instance Attribute Summary collapse
-
#env ⇒ Object
Returns the value of attribute env.
-
#groups ⇒ Object
Returns the value of attribute groups.
-
#name(name = nil) ⇒ Object
Sets or gets the name of this host.
-
#password ⇒ Object
Returns the value of attribute password.
-
#roles ⇒ Object
Returns the value of attribute roles.
-
#salticid ⇒ Object
Returns the value of attribute salticid.
-
#tasks ⇒ Object
Returns the value of attribute tasks.
-
#user(user = nil) ⇒ Object
Returns the value of attribute user.
Instance Method Summary collapse
- #==(other) ⇒ Object
-
#append(str, file, opts = {}) ⇒ Object
Appends the given string to a file.
-
#as(user = nil) ⇒ Object
All calls to exec! within this block are prefixed by sudoing to the user.
-
#cd(dir = nil) ⇒ Object
Changes our working directory.
-
#chmod(mode, path) ⇒ Object
Changes the mode of a file.
-
#chmod_r(mode, path) ⇒ Object
Changes the mode of a file, recursively.
-
#cwd ⇒ Object
Returns current working directory.
-
#dir?(path) ⇒ Boolean
Returns true if a directory exists.
-
#download(remote, local = nil, opts = {}) ⇒ Object
Downloads a file from the remote server.
-
#escape(string) ⇒ Object
Quotes a string for inclusion in a bash command line.
-
#exec!(command, opts = {}, &block) ⇒ Object
Runs a remote command.
-
#exists?(path) ⇒ Boolean
Returns true when a file exists, otherwise false.
-
#expand_path(path) ⇒ Object
Generates a full path for the given remote path.
-
#file?(path) ⇒ Boolean
Returns true if a regular file exists.
-
#ftype(path) ⇒ Object
Returns the filetype, as string.
-
#group(name) ⇒ Object
Adds this host to a group.
-
#group?(name) ⇒ Boolean
Abusing convention slightly…
-
#gw(gw = nil) ⇒ Object
Returns the gateway for this host.
-
#homedir(user = (@sudo||@user)) ⇒ Object
Returns the home directory of the given user, or the current user if none specified.
-
#initialize(name, opts = {}) ⇒ Host
constructor
A new instance of Host.
- #inspect ⇒ Object
-
#log(*args) ⇒ Object
Issues a logging statement to this host’s log.
-
#method_missing(meth, *args, &block) ⇒ Object
Missing methods are resolved as follows: 0.
-
#mode(path) ⇒ Object
Returns the file mode of a remote file.
- #on_log(&block) ⇒ Object
-
#resolve_task(name) ⇒ Object
Finds a task for this host, by name.
-
#role(role) ⇒ Object
Assigns roles to a host from the Salticid.
-
#role?(role) ⇒ Boolean
Does this host have the given role?.
-
#role_proxy(name) ⇒ Object
Returns a role proxy for role on this host, if we have the role.
-
#run(role, task, *args) ⇒ Object
Runs the specified task on the given role.
-
#ssh ⇒ Object
Opens an SSH connection and stores the connection in @ssh.
-
#sudo(*args, &block) ⇒ Object
If a block is given, works like #as.
-
#sudo_upload(local, remote, opts = {}) ⇒ Object
Uploads a file and places it in the final destination as root.
-
#task(name, &block) ⇒ Object
Finds (and optionally defines) a task.
- #to_s ⇒ Object
- #to_string ⇒ Object
-
#tunnel ⇒ Object
Returns an SSH::Gateway object for connecting to this host, or nil if no gateway is needed.
-
#upload(local, remote = nil, opts = {}) ⇒ Object
Upload a file to the server.
Constructor Details
#initialize(name, opts = {}) ⇒ Host
Returns a new instance of Host.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# File 'lib/salticid/host.rb', line 4 def initialize(name, opts = {}) @name = name.to_s @user = opts[:user].to_s @groups = opts[:groups] || [] @roles = opts[:roles] || [] @tasks = opts[:tasks] || [] @salticid = opts[:salticid] @sudo = nil @on_log = proc { || } @ssh_lock = Mutex.new @env = {} @cwd = nil @role_proxies = {} end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(meth, *args, &block) ⇒ Object
Missing methods are resolved as follows:
-
Create a RoleProxy from a Role on this host
-
From task_resolve
-
Converted to a command string and exec!‘ed
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 |
# File 'lib/salticid/host.rb', line 386 def method_missing(meth, *args, &block) if meth.to_s == "to_ary" raise NoMethodError end if args.empty? and rp = role_proxy(meth) rp elsif task = resolve_task(meth) task.run(self, *args, &block) else if args.last.kind_of? Hash opts = args.pop else opts = {} end str = ([meth] + args.map{|a| escape(a)}).join(' ') exec! str, opts, &block end end |
Instance Attribute Details
#env ⇒ Object
Returns the value of attribute env.
2 3 4 |
# File 'lib/salticid/host.rb', line 2 def env @env end |
#groups ⇒ Object
Returns the value of attribute groups.
2 3 4 |
# File 'lib/salticid/host.rb', line 2 def groups @groups end |
#name(name = nil) ⇒ Object
Sets or gets the name of this host.
412 413 414 |
# File 'lib/salticid/host.rb', line 412 def name @name end |
#password ⇒ Object
Returns the value of attribute password.
2 3 4 |
# File 'lib/salticid/host.rb', line 2 def password @password end |
#roles ⇒ Object
Returns the value of attribute roles.
2 3 4 |
# File 'lib/salticid/host.rb', line 2 def roles @roles end |
#salticid ⇒ Object
Returns the value of attribute salticid.
2 3 4 |
# File 'lib/salticid/host.rb', line 2 def salticid @salticid end |
#tasks ⇒ Object
Returns the value of attribute tasks.
2 3 4 |
# File 'lib/salticid/host.rb', line 2 def tasks @tasks end |
#user(user = nil) ⇒ Object
Returns the value of attribute user.
2 3 4 |
# File 'lib/salticid/host.rb', line 2 def user @user end |
Instance Method Details
#==(other) ⇒ Object
22 23 24 |
# File 'lib/salticid/host.rb', line 22 def ==(other) self.name == other.name end |
#append(str, file, opts = {}) ⇒ Object
Appends the given string to a file. Pass :uniq => true to only append if the string is not already present in the file.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/salticid/host.rb', line 29 def append(str, file, opts = {}) file = (file) if opts[:uniq] and exists? file # Check to ensure the file does not contain the line already. begin grep(str, file) or raise rescue # We're clear, go ahead. tee '-a', file, :stdin => str end else # No need to check, just append. tee '-a', file, :stdin => str end end |
#as(user = nil) ⇒ Object
All calls to exec! within this block are prefixed by sudoing to the user.
46 47 48 49 50 |
# File 'lib/salticid/host.rb', line 46 def as(user = nil) old_sudo, @sudo = @sudo, (user || 'root') yield @sudo = old_sudo end |
#cd(dir = nil) ⇒ Object
Changes our working directory.
53 54 55 56 57 |
# File 'lib/salticid/host.rb', line 53 def cd(dir = nil) dir ||= homedir dir = (dir) @cwd = dir end |
#chmod(mode, path) ⇒ Object
Changes the mode of a file. Mode is numeric.
60 61 62 |
# File 'lib/salticid/host.rb', line 60 def chmod(mode, path) exec! "chmod #{mode.to_s(8)} #{escape(expand_path(path))}" end |
#chmod_r(mode, path) ⇒ Object
Changes the mode of a file, recursively. Mode is numeric.
65 66 67 |
# File 'lib/salticid/host.rb', line 65 def chmod_r(mode, path) exec! "chmod -R #{mode.to_s(8)} #{escape(expand_path(path))}" end |
#cwd ⇒ Object
Returns current working directory. Tries to obtain it from exec ‘pwd’, but falls back to /.
71 72 73 74 75 76 77 78 |
# File 'lib/salticid/host.rb', line 71 def cwd @cwd ||= begin exec! 'pwd' rescue => e raise e '/' end end |
#dir?(path) ⇒ Boolean
Returns true if a directory exists
81 82 83 84 85 86 87 |
# File 'lib/salticid/host.rb', line 81 def dir?(path) begin ftype(path) == 'directory' rescue false end end |
#download(remote, local = nil, opts = {}) ⇒ Object
Downloads a file from the remote server. Local defaults to remote filename (in current path) if not specified.
91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/salticid/host.rb', line 91 def download(remote, local = nil, opts = {}) remote_filename ||= File.split(remote).last if File.directory? local local = File.join(local, remote_filename) else local = remote_filename end remote = remote log "downloading from #{remote.inspect} to #{local.inspect}" ssh.scp.download!(remote, local, opts) end |
#escape(string) ⇒ Object
Quotes a string for inclusion in a bash command line
105 106 107 108 109 |
# File 'lib/salticid/host.rb', line 105 def escape(string) return '' if string.nil? return string unless string.to_s =~ /[\\\$`" \(\)\{\}\[\]]/ '"' + string.to_s.gsub(/[\\\$`"]/) { |match| '\\' + match } + '"' end |
#exec!(command, opts = {}, &block) ⇒ Object
Runs a remote command. If a block is given, it is run in a new thread after stdin is sent. Its sole argument is the SSH channel for this command: you may use send_data to write to the processes stdin, and use ch.eof! to close stdin. ch.close will stop the remote process.
Options:
:stdin => Data piped to the process' stdin.
:stdout => A callback invoked when stdout is received from the process.
The argument is the data received.
:stderr => Like stdout, but for stderr.
:echo => Prints stdout and stderr using print, if true.
:to => Shell output redirection to file. (like cmd >/foo)
:from => Shell input redirection from file. (like cmd </foo)
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/salticid/host.rb', line 124 def exec!(command, opts = {}, &block) # Options stdout = '' stderr = '' defaults = { :check_exit_status => true } opts = defaults.merge opts # First, set up the environment... if @env.size > 0 command = ( @env.map { |k,v| k.to_s.upcase + '=' + v } << command ).join(' ') end # Before execution, cd to cwd command = "cd #{escape(@cwd)}; " + command # Input redirection if opts[:from] command += " <#{escape(opts[:from])}" end # Output redirection if opts[:to] command += " >#{escape(opts[:to])}" end # After command, add a semicolon... unless command =~ /;\s*\z/ command += ';' end # Then echo the exit status. command += ' echo $?; ' # If applicable, wrap the command in a sudo subshell... if @sudo command = "sudo -S -u #{@sudo} bash -c #{escape(command)}" if @password opts[:stdin] = @password + "\n" + opts[:stdin].to_s end end buffer = '' echoed = 0 status = nil written = false # Run ze command with callbacks. # Return status. channel = ssh.open_channel do |ch| ch.exec command do |ch, success| raise "could not execute command" unless success # Handle STDOUT ch.on_data do |c, data| # Could this data be the status code? if pos = (data =~ /(\d{1,3})\n\z/) # Set status status = $1 # Flush old buffer opts[:stdout].call(buffer) if opts[:stdout] stdout << buffer # Save candidate status code buffer = data[pos .. -1] # Write the other part of the string to the callback opts[:stdout].call(data[0...pos]) if opts[:stdout] stdout << data[0...pos] else # Write buffer + data to callback opts[:stdout].call(buffer + data) if opts[:stdout] stdout << buffer + data buffer = '' end if opts[:echo] and echoed < stdout.length stdout[echoed..-1].split("\n")[0..-2].each do |fragment| echoed += fragment.length + 1 log fragment end end end # Handle STDERR ch.on_extended_data do |c, type, data| if type == 1 # STDERR opts[:stderr].call(data) if opts[:stderr] stderr << data log :stderr, stderr if opts[:echo] end end # Write stdin if opts[:stdin] ch.on_process do unless written ch.send_data opts[:stdin] written = true else # Okay, we wrote stdin unless block or ch.eof? ch.eof! end end end end # Handle close ch.on_close do if opts[:echo] # Echo last of input data stdout[echoed..-1].split("\n").each do |fragment| echoed += fragment.length + 1 log fragment end end end end end if block # Run the callback callback_thread = Thread.new do if opts[:stdin] # Wait for stdin to be written before calling... until written sleep 0.1 end end block.call(channel) end end # Wait for the command to complete. channel.wait # Let the callback thread finish as well callback_thread.join if callback_thread if opts[:check_exit_status] # Make sure we have our status. if status.nil? or status.empty? raise "empty status in host#exec() for #{command}, hmmm" end # Check status. status = status.to_i if status != 0 raise "#{command} exited with non-zero status #{status}!\nSTDERR:\n#{stderr}\nSTDOUT:\n#{stdout}" end end stdout.chomp end |
#exists?(path) ⇒ Boolean
Returns true when a file exists, otherwise false
289 290 291 |
# File 'lib/salticid/host.rb', line 289 def exists?(path) true if ftype(path) rescue false end |
#expand_path(path) ⇒ Object
Generates a full path for the given remote path.
294 295 296 297 |
# File 'lib/salticid/host.rb', line 294 def (path) path = path.to_s.gsub(/~(\w+)?/) { |m| homedir($1) } File.(path, cwd.to_s) end |
#file?(path) ⇒ Boolean
Returns true if a regular file exists.
300 301 302 |
# File 'lib/salticid/host.rb', line 300 def file?(path) ftype(path) == 'file' rescue false end |
#ftype(path) ⇒ Object
Returns the filetype, as string. Raises exceptions on failed stat.
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# File 'lib/salticid/host.rb', line 305 def ftype(path) path = (path) begin str = self.stat('-c', '%F', path).strip case str when /no such file or directory/i raise Errno::ENOENT, "#{self}:#{path} does not exist" when 'regular file' 'file' when 'regular empty file' 'file' when 'directory' 'directory' when 'character special file' 'characterSpecial' when 'block special file' 'blockSpecial' when /link/ 'link' when /socket/ 'socket' when /fifo|pipe/ 'fifo' else raise RuntimeError, "unknown filetype #{str}" end rescue raise RuntimeError, "stat #{self}:#{path} failed - #{str}" end end |
#group(name) ⇒ Object
Adds this host to a group.
344 345 346 347 348 349 350 |
# File 'lib/salticid/host.rb', line 344 def group(name) group = name if name.kind_of? Salticid::Group group ||= @salticid.group name group.hosts |= [self] @groups |= [group] group end |
#group?(name) ⇒ Boolean
Abusing convention slightly… Returns the group by name if this host belongs to it, otherwise false.
338 339 340 341 |
# File 'lib/salticid/host.rb', line 338 def group?(name) name = name.to_s @groups.find{ |g| g.name == name } || false end |
#gw(gw = nil) ⇒ Object
Returns the gateway for this host.
353 354 355 356 357 358 359 |
# File 'lib/salticid/host.rb', line 353 def gw(gw = nil) if gw @gw = @salticid.host(gw) else @gw end end |
#homedir(user = (@sudo||@user)) ⇒ Object
Returns the home directory of the given user, or the current user if none specified.
363 364 365 |
# File 'lib/salticid/host.rb', line 363 def homedir(user = (@sudo||@user)) exec! "awk -F: -v v=#{escape(user)} '{if ($1==v) print $6}' /etc/passwd" end |
#inspect ⇒ Object
367 368 369 |
# File 'lib/salticid/host.rb', line 367 def inspect "#<#{@user}@#{@name} roles=#{@roles.inspect} tasks=#{@tasks.inspect}>" end |
#log(*args) ⇒ Object
Issues a logging statement to this host’s log. log :error, “message” log “message” is the same as log “info”, “message”
374 375 376 377 378 379 380 |
# File 'lib/salticid/host.rb', line 374 def log(*args) begin @on_log.call Message.new(*args) rescue # If the log handler is broken, keep going. end end |
#mode(path) ⇒ Object
Returns the file mode of a remote file.
407 408 409 |
# File 'lib/salticid/host.rb', line 407 def mode(path) stat('-c', '%a', path).oct end |
#on_log(&block) ⇒ Object
420 421 422 |
# File 'lib/salticid/host.rb', line 420 def on_log(&block) @on_log = block end |
#resolve_task(name) ⇒ Object
Finds a task for this host, by name.
425 426 427 428 429 430 431 432 433 434 |
# File 'lib/salticid/host.rb', line 425 def resolve_task(name) name = name.to_s @tasks.each do |task| return task if task.name == name end @salticid.tasks.each do |task| return task if task.name == name end nil end |
#role(role) ⇒ Object
Assigns roles to a host from the Salticid. Roles are unique in hosts; repeat assignments will not result in more than one copy of the role.
438 439 440 |
# File 'lib/salticid/host.rb', line 438 def role(role) @roles = @roles | [@salticid.role(role)] end |
#role?(role) ⇒ Boolean
Does this host have the given role?
443 444 445 |
# File 'lib/salticid/host.rb', line 443 def role?(role) @roles.any? { |r| r.name == role.to_s } end |
#role_proxy(name) ⇒ Object
Returns a role proxy for role on this host, if we have the role.
448 449 450 451 452 |
# File 'lib/salticid/host.rb', line 448 def role_proxy(name) if role = roles.find { |r| r.name == name.to_s } @role_proxies[name.to_s] ||= RoleProxy.new(self, role) end end |
#run(role, task, *args) ⇒ Object
Runs the specified task on the given role. Raises NoMethodError if either the role or task do not exist.
456 457 458 459 460 461 462 |
# File 'lib/salticid/host.rb', line 456 def run(role, task, *args) if rp = role_proxy(role) rp.__send__(task, *args) else raise NoMethodError, "No such role #{role.inspect} on #{self}" end end |
#ssh ⇒ Object
Opens an SSH connection and stores the connection in @ssh.
465 466 467 468 469 470 471 472 473 474 475 476 477 |
# File 'lib/salticid/host.rb', line 465 def ssh @ssh_lock.synchronize do if @ssh and not @ssh.closed? return @ssh end if tunnel @ssh = tunnel.ssh(name, user) else @ssh = Net::SSH.start(name, user) end end end |
#sudo(*args, &block) ⇒ Object
If a block is given, works like #as. Otherwise, just execs sudo with the given arguments.
481 482 483 484 485 486 487 |
# File 'lib/salticid/host.rb', line 481 def sudo(*args, &block) if block_given? as *args, &block else method_missing(:sudo, *args) end end |
#sudo_upload(local, remote, opts = {}) ⇒ Object
Uploads a file and places it in the final destination as root. If the file already exists, its ownership and mode are used for the replacement. Otherwise it inherits ownership from the parent directory.
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 |
# File 'lib/salticid/host.rb', line 492 def sudo_upload(local, remote, opts={}) remote = remote # TODO: umask this? local_mode = File.stat(local).mode & 07777 File.chmod 0600, local # Get temporary filename tmpfile = '/' while exists? tmpfile tmpfile = '/tmp/sudo_upload_' + Time.now.to_f.to_s end # Upload upload local, tmpfile, opts # Get remote mode/user/group sudo do if exists? remote mode = self.mode remote user = stat('-c', '%U', remote).strip group = stat('-c', '%G', remote).strip else user = stat('-c', '%U', File.dirname(remote)).strip group = stat('-c', '%G', File.dirname(remote)).strip mode = local_mode end # Move and chmod mv tmpfile, remote chmod mode, remote chown "#{user}:#{group}", remote end end |
#task(name, &block) ⇒ Object
Finds (and optionally defines) a task.
Tasks are first resolved in the host’s task list, then in the Salticid’s task list. Finally, tasks are created from scratch. Any invocation of task adds that task to this host.
If a block is given, the block is assigned to the local (host) task. The task is dup’ed to prevent modifying a possible global task.
The task is returned at the end of the method.
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 |
# File 'lib/salticid/host.rb', line 538 def task(name, &block) name = name.to_s if task = @tasks.find{|t| t.name == name} # Found in self elsif (task = @salticid.tasks.find{|t| t.name == name}) and not block_given? # Found in salticid @tasks << task else # Create new task in self task = Salticid::Task.new(name, :salticid => @salticid) @tasks << task end if block_given? # Remove the task from our list, and replace it with a copy. # This is to prevent local declarations from clobbering global tasks. i = @tasks.index(task) || @task.size task = task.dup task.block = block @tasks[i] = task end task end |
#to_s ⇒ Object
564 565 566 |
# File 'lib/salticid/host.rb', line 564 def to_s @name.to_s end |
#to_string ⇒ Object
568 569 570 571 572 573 574 575 576 |
# File 'lib/salticid/host.rb', line 568 def to_string h = "Host #{@name}:\n" h << " Groups: #{groups.map(&:to_s).sort.join(', ')}\n" h << " Roles: #{roles.map(&:to_s).sort.join(', ')}\n" h << " Tasks:\n" tasks = self.tasks.map(&:to_s) tasks += roles.map { |r| r.tasks.map { |t| " #{r}.#{t}" }} h << tasks.flatten!.sort!.join("\n") end |
#tunnel ⇒ Object
Returns an SSH::Gateway object for connecting to this host, or nil if no gateway is needed.
580 581 582 583 584 585 |
# File 'lib/salticid/host.rb', line 580 def tunnel if gw # We have a gateway host. @tunnel ||= gw.gateway_tunnel end end |
#upload(local, remote = nil, opts = {}) ⇒ Object
Upload a file to the server. Remote defaults to local’s filename (without path) if not specified.
589 590 591 592 593 |
# File 'lib/salticid/host.rb', line 589 def upload(local, remote = nil, opts={}) remote ||= File.split(local).last remote = remote ssh.scp.upload!(local, remote, opts) end |