pax_global_header00006660000000000000000000000064126631255660014526gustar00rootroot0000000000000052 comment=71bc4427ad58eae90d7d54f66bee588c00b49b8b sshkit-1.9.0.rc1/000077500000000000000000000000001266312556600135265ustar00rootroot00000000000000sshkit-1.9.0.rc1/.gitignore000066400000000000000000000001051266312556600155120ustar00rootroot00000000000000*.gem *.swp bin/rake .bundle .yardoc .vagrant* test/tmp Gemfile.lock sshkit-1.9.0.rc1/.travis.yml000066400000000000000000000001331266312556600156340ustar00rootroot00000000000000language: ruby rvm: - 2.3.0 - 2.2.4 - 2.1.8 - 2.0.0 script: "rake test:units lint" sshkit-1.9.0.rc1/.yardopts000066400000000000000000000001411266312556600153700ustar00rootroot00000000000000--no-private - README.md CHANGELOG.md FAQ.md LICENSE.md EXAMPLES.md CONTRIBUTING.md RELEASING.md sshkit-1.9.0.rc1/BREAKING_API_WISHLIST.md000066400000000000000000000012051266312556600172470ustar00rootroot00000000000000# Breaking API Wishlist SSHKit respects semantic versioning. This file is a place to record breaking API improvements which could be considered at the next major release. * Consider no longer stripping by default on `capture` [#249](https://github.com/capistrano/sshkit/pull/249) ## Deprecated code which could be deleted in a future major release * [Abstract.background method](lib/sshkit/backends/abstract.rb#L43) * [`@stderr`, `@stdout` attrs on `Command`](lib/sshkit/command.rb#L28) ## Cleanup when Ruby 1.9 support is dropped * `to_a` can probably be removed from `"str".lines.to_a`, since `"str".lines` returns an `Array` under Ruby 2sshkit-1.9.0.rc1/CHANGELOG.md000066400000000000000000000626231266312556600153500ustar00rootroot00000000000000## Changelog This file is written in reverse chronological order, newer releases will appear at the top. ## `master` (Unreleased) * Add your entries below here, remember to credit yourself however you want to be credited! ## 1.9.0.rc1 ### Potentially breaking changes * The SSHKit DSL is no longer automatically included when you `require` it. **This means you must now explicitly `include SSHKit::DSL`.** See [PR #219](https://github.com/capistrano/sshkit/pull/219) for details. @beatrichartz * `SSHKit::Backend::Printer#test` now always returns true [PR #312](https://github.com/capistrano/sshkit/pull/312) @mikz ### New features * `SSHKit::Formatter::Abstract` now accepts an optional Hash of options [PR #308](https://github.com/capistrano/sshkit/pull/308) @mattbrictson * Add `SSHKit::Backend.current` so that Capistrano plugin authors can refactor helper methods and still have easy access to the currently-executing Backend without having to use global variables. * Add `SSHKit.config.default_runner` options that allows to override default command runner. This option also accepts a name of the custom runner class. * The ConnectionPool has been rewritten in this release to be more efficient and have a cleaner internal API. You can still completely disable the pool by setting `SSHKit::Backend::Netssh.pool.idle_timeout = 0`. @mattbrictson @byroot [PR #328](https://github.com/capistrano/sshkit/pull/328) ### Bug fixes * make sure working directory for commands is properly cleared after `within` blocks [PR #307](https://github.com/capistrano/sshkit/pull/307) @steved * display more accurate string for commands with spaces being output in `Formatter::Pretty` [PR #304](https://github.com/capistrano/sshkit/pull/304) @steved [PR #319](https://github.com/capistrano/sshkit/pull/319) @mattbrictson * Fix a race condition experienced in JRuby that could cause multi-server deploys to fail. [PR #322](https://github.com/capistrano/sshkit/pull/322) @mattbrictson ## 1.8.1 * Change license to MIT, thanks to all the patient contributors who gave their permissions. ## 1.8.0 * add SSHKit::Backend::ConnectionPool#close_connections [PR #285](https://github.com/capistrano/sshkit/pull/285) @akm * Clean up rubocop lint warnings [PR #275](https://github.com/capistrano/sshkit/pull/275) @cshaffer * Prepend unused parameter names with an underscore * Prefer “safe assignment in condition” * Disambiguate regexp literals with parens * Prefer `sprintf` over `String#%` * No longer shadow `caller_line` variable in `DeprecationLogger` * Rescue `StandardError` instead of `Exception` * Remove useless `private` access modifier in `TestAbstract` * Disambiguate block operator with parens * Disambiguate between grouped expression and method params * Remove assertion in `TestHost#test_assert_hosts_compare_equal` that compares something with itself * Export environment variables and execute command in a subshell. [PR #273](https://github.com/capistrano/sshkit/pull/273) @kuon * Introduce `log_command_start`, `log_command_data`, `log_command_exit` methods on `Formatter` [PR #257](https://github.com/capistrano/sshkit/pull/257) @robd * Deprecate `@stdout` and `@stderr` accessors on `Command` * Add support for deprecation logging options. [README](README.md#deprecation-warnings), [PR #258](https://github.com/capistrano/sshkit/pull/258) @robd * Quote environment variable values. [PR #250](https://github.com/capistrano/sshkit/pull/250) @Sinjo - Chris Sinjakli * Simplified formatter hierarchy. [PR #248](https://github.com/capistrano/sshkit/pull/248) @robd * `SimpleText` formatter now extends `Pretty`, rather than duplicating. * Hide ANSI color escape sequences when outputting to a file. [README](README.md#output-colors), [Issue #245](https://github.com/capistrano/sshkit/issues/245), [PR #246](https://github.com/capistrano/sshkit/pull/246) @robd * Now only color the output if it is associated with a tty, or the `SSHKIT_COLOR` environment variable is set. * Removed broken support for assigning an `IO` to the `output` config option. [Issue #243](https://github.com/capistrano/sshkit/issues/243), [PR #244](https://github.com/capistrano/sshkit/pull/244) @robd * Use `SSHKit.config.output = SSHKit::Formatter::SimpleText.new($stdin)` instead * Added support for `:interaction_handler` option on commands. [PR #234](https://github.com/capistrano/sshkit/pull/234), [PR #242](https://github.com/capistrano/sshkit/pull/242) @robd * Removed partially supported `TRACE` log level. [2aa7890](https://github.com/capistrano/sshkit/commit/2aa78905f0c521ad9f697e7a4ed04ba438d5ee78) @robd * Add support for the `:strip` option to the `capture` method and strip by default on the `Local` backend. [PR #239](https://github.com/capistrano/sshkit/pull/239), [PR #249](https://github.com/capistrano/sshkit/pull/249) @robd * The `Local` backend now strips by default to be consistent with the `Netssh` one. * This reverses change [7d15a9a](https://github.com/capistrano/sshkit/commit/7d15a9aebfcc43807c8151bf6f3a4bc038ce6218) to the `Local` capture API to remove stripping by default. * If you require the raw, unstripped output, pass the `strip: false` option: `capture(:ls, strip: false)` * Simplified backend hierarchy. [PR #235](https://github.com/capistrano/sshkit/pull/235), [PR #237](https://github.com/capistrano/sshkit/pull/237) @robd * Moved duplicate implementations of `make`, `rake`, `test`, `capture`, `background` on to `Abstract` backend. * Backend implementations now only need to implement `execute_command`, `upload!` and `download!` * Removed `Printer` from backend hierarchy for `Local` and `Netssh` backends (they now just extend `Abstract`) * Removed unused `Net::SSH:LogLevelShim` * Removed dependency on the `colorize` gem. SSHKit now implements its own ANSI color logic, with no external dependencies. Note that SSHKit now only supports the `:bold` or plain modes. Other modes will be gracefully ignored. [#263](https://github.com/capistrano/sshkit/issues/263) * New API for setting the formatter: `use_format`. This differs from `format=` in that it accepts options or arguments that will be passed to the formatter's constructor. The `format=` syntax will be deprecated in a future release. [#295](https://github.com/capistrano/sshkit/issues/295) * SSHKit now immediately raises a `NameError` if you try to set a formatter that does not exist. [#295](https://github.com/capistrano/sshkit/issues/295) * Fix error message when the formatter does not exist. [#301](https://github.com/capistrano/sshkit/pull/301) ## 1.7.1 * Fix a regression in 1.7.0 that caused command completion messages to be removed from log output. @mattbrictson ## 1.7.0 * Update Vagrantfile to use multi-provider Hashicorp precise64 box - remove URLs. @townsen * Merge host ssh_options and Netssh defaults @townsen Previously if host-level ssh_options were defined the Netssh defaults were ignored. * Merge host ssh_options and Netssh defaults * Fixed race condition where output of failed command would be empty. @townsen Caused random failures of `test_execute_raises_on_non_zero_exit_status_and_captures_stdout_and_stderr` Also fixes output handling in failed commands, and generally buggy output. * Remove override of backtrace() and backtrace_locations() from ExecuteError. @townsen This interferes with rake default behaviour and creates duplicate stacktraces. * Allow running local commands using `on(:local)` * Implement the upload! and download! methods for the local backend ## 1.6.0 * Fix colorize to use the correct API (@fazibear) * Lock colorize (sorry guys) version at >= 0.7.0 ## 1.6.0 (Yanked, because of colorize.) * Force dependency on colorize v0.6.0 * Add your entries here, remember to credit yourself however you want to be credited! * Remove strip from capture to preserve whitespace. Nick Townsend * Add vmware_fusion Vagrant provider. Nick Townsend * Add some padding to the pretty log formatter ## 1.5.1 * Use `sudo -u` rather than `sudo su` to switch users. Mat Trudel ## 1.5.0 * Deprecate background helper - too many badly behaved pseudo-daemons. Lee Hambley * Don't colourize unless $stdout is a tty. Lee Hambley * Remove out of date "Known Issues" section from README. Lee Hambley * Dealy variable interpolation inside `as()` block. Nick Townsend * Fixes for functional tests under modern Vagrant. Lewis Marshal * Fixes for connection pooling. Chris Heald * Add `localhost` hostname to local backend. Adam Mckaig * Wrap execptions to include hostname. Brecht Hoflack * Remove `shellwords` stdlib dependency Bruno Sutic * Remove unused `cooldown` accessor. Bruno Sutic * Replace Term::ANSIColor with a lighter solution. Tom Clements * Documentation fixes. Matt Brictson ## 1.4.0 https://github.com/capistrano/sshkit/compare/v1.3.0...v1.4.0 * Removed `invoke` alias for [`SSHKit::Backend::Printer.execute`](https://github.com/capistrano/sshkit/blob/master/lib/sshkit/backends/printer.rb#L20). This is to prevent collisions with methods in capistrano with similar names, and to provide a cleaner API. See [capistrano issue 912](https://github.com/capistrano/capistrano/issues/912) and [issue 107](https://github.com/capistrano/sshkit/issues/107) for more details. * Connection pooling now uses a thread local to store connection pool, giving each thread its own connection pool. Thank you @mbrictson see [#101](https://github.com/capistrano/sshkit/pull/101) for more. * Command map indifferent towards strings and symbols thanks to @thomasfedb see [#91](https://github.com/capistrano/sshkit/pull/91) * Moved vagrant wrapper to `support` directory, added ability to run tests with vagrant using ssh. @miry see [#64](https://github.com/capistrano/sshkit/pull/64) * Removed unnecessary require `require_relative '../sshkit'` in `lib/sshkit/dsl.rb` prevents warnings thanks @brabic. * Doc fixes thanks @seanhandley @vojto ## 1.3.0 https://github.com/capistrano/sshkit/compare/v1.2.0...v1.3.0 * Connection pooling. SSH connections are reused across multiple invocations of `on()`, which can result in significant performance gains. See: https://github.com/capistrano/sshkit/pull/70. Matt @mbrictson Brictson. * Fixes to the Formatter::Dot and to the formatter class name resolver. @hab287:w * Added the license to the Gemspec. @anatol. * Fix :limit handling for the `in: :groups` run mode. Phil @phs Smith * Doc fixes @seanhandley, @sergey-alekseev. ## 1.2.0 https://github.com/capistrano/sshkit/compare/v1.1.0...v1.2.0 * Support picking up a project local SSH config file, if a SSH config file exists at ./.ssh/config it will be merged with the ~/.ssh/config. This is ideal for defining project-local proxies/gateways, etc. Thanks to Alex @0rca Vzorov. * Tests and general improvements to the Printer backends (mostly used internally). Thanks to Michael @miry Nikitochkin. * Update the net-scp dependency version. Thanks again to Michael @miry Nikitochkin. * Improved command map. This feature allows mapped variables to be pushed and unshifted onto the mapping so that the Capistrano extensions for rbenv and bundler, etc can work together. For discussion about the reasoning see https://github.com/capistrano/capistrano/issues/639 and https://github.com/capistrano/sshkit/pull/45. A big thanks to Kir @kirs Shatrov. * `test()` and `capture()` now behave as expected inside a `run_locally` block meaning that they now run on your local machine, rather than erring out. Thanks to Kentaro @kentaroi Imai. * The `:wait` option is now successfully passed to the runner now. Previously the `:wait` option was ignored. Thanks to Jordan @jhollinger Hollinger for catching the mistake in our test coverage. * Fixes and general improvements to the `download()` method which until now was quite naïve. Thanks to @chqr. ## 1.1.0 https://github.com/capistrano/sshkit/compare/v1.0.0...v1.1.0 * Please see the Git history. `git rebase` ate our changelog (we should have been more careful) ## 1.0.0 * The gem now supports a run_locally, although it's nothing to do with SSH, it makes a nice API. There are examples in the EXAMPLES.md. ## 0.0.34 * Allow the setting of global SSH options on the `backend.ssh` as a hash, these options are the same as Net::SSH configure expects. Thanks to Rafał @lisukorin Lisowski ## 0.0.32 * Lots of small changes since 0.0.27. * Particularly working around a possible NaN issue when uploading comparatively large files. ## 0.0.27 * Don't clobber SSH options with empty values. This allows Net::SSH to do the right thing most of the time, and look into the SSH configuration files. ## 0.0.26 * Pretty output no longer prints white text. ("Command.....") * Fixed a double-output bug, where upon receiving the exit status from a remote command, the last data packet that it sent would be re-printed by the pretty formatter. * Integration tests now use an Ubuntu Precise 64 Vagrant base box. * Additional host declaration syntax, `SSHKit::Host` can now take a hash of host properties in addition to a number of new (common sense) DSN type syntaxes. * Changes to the constants used for logging, we no longer re-define a `Logger::TRACE` constant on the global `Logger` class, rather everyhing now uses `SSHKit::Logger` (Thanks to Rafa Garcia) * Various syntax and documentation fixes. ## 0.0.25 * `upload!` and `download!` now log to different levels depending on completion percentage. When the upload is 0 percent complete or a number indivisible by 10, the message is logged to `Logger::DEBUG` otherwise the message is logged to `Logger::INFO`, this should mean that normal users at a sane log level should see upload progress jump to `100%` for small files, and otherwise for larger files they'll see output every `10%`. ## 0.0.24 * Pretty output now streams stdout and stderr. Previous versions would append (`+=`) chunks of data written by the remote host to the `Command` instance, and the `Pretty` formatter would only print stdout/stderr if the command was `#complete?`. Historically this lead to issues where the remote process was blocking for input, had written the prompt to stdout, but it was not visible on the client side. Now each time the command is passed to the output stream, the stdout/stderr are replaced with the lines returned from the remote server in this chunk. (i.e were yielded to the callback block). Commands now have attribute accessors for `#full_stdout` and `#full_stderr` which are appended in the way that `#stdout` and `#stderr` were previously. This should be considered a private API, and one should beware of relying on `#full_stdout` or `#full_stderr`, they will likely be replaced with a cleaner soltion eventually. * `upload!` and `download!` now print progress reports at the `Logger::INFO` verbosity level. ## 0.0.23 * Explicitly rely on `net-scp` gem. ## 0.0.22 * Added naïve implementations of `upload!()` and `download!()` (syncoronous) to the Net::SSH backend. See `EXAMPLES.md` for more extensive usage examples. The `upload!()` method can take a filename, or an `IO`, this reflects the way the underlying Net::SCP implementation works. The same is true of `download!()`, when called with a single argument it captures the file's contents, otherwise it downloads the file to the local disk. on hosts do |host| upload!(StringIO.new('some-data-here'), '~/.ssh/authorized_keys') upload!('~/.ssh/id_rsa.pub', '~/.ssh/authorized_keys') puts download!('/etc/monit/monitrc') download!('/etc/monit/monitrc', '~/monitrc') end ## 0.0.21 * Fixed an issue with default formatter * Modified `SSHKit.config.output_verbosity=` to accept different objects: SSHKit.config.output_verbosity = Logger::INFO SSHKit.config.output_verbosity = :info SSHKit.config.output_verbosity = 1 ## 0.0.20 * Fixed a bug where the log level would be assigned, not compared in the pretty formatter, breaking the remainder of the output verbosity. ## 0.0.19 * Modified the `Pretty` formatter to include the log level in front of executed commands. * Modified the `Pretty` formatter not to print stdout and stderr by default, the log level must be raised to Logger::DEBUG to see the command outputs. * Modified the `Pretty` formatter to use `Command#to_s` when printing the command, this prints the short form (without modifications/wrappers applied to the command for users, groups, directories, umasks, etc). ## 0.0.18 * Enable `as()` to take either a string/symbol as previously, but also now accepts a hash of `{user: ..., group: ...}`. In case that your host system supports the command `sg` (`man 1 sg`) to switch your effective group ID then one can work on files as a team group user. on host do |host| as user: :peter, group: griffin do execute :touch, 'somefile' end end will result in a file with the following permissions: -rw-r--r-- 1 peter griffin 0 Jan 27 08:12 somefile This should make it much easier to share deploy scripts between team members. **Note:** `sg` has some very strict user and group password requirements (the user may not have a password (`passwd username -l` to lock an account that already has a password), and the group may not have a password.) Additionally, and unsurprisingly *the user must also be a member of the group.* `sg` was chosen over `newgrp` as it's easier to embed in a one-liner command, `newgrp` could be used with a heredoc, but my research suggested that it might be better to use sg, as it better represents my intention, a temporary switch to a different effective group. * Fixed a bug with environmental variables and umasking introduced in 0.0.14. Since that version the environmental variables were being presented to the umask command's subshell, and not to intended command's subshell. incorrect: `ENV=var umask 002 && env` correct: `umask 002 && ENV=var env` * Changed the exception handler, if a command returns with a non-zero exit status then the output will be prefixed with the command name and which channel any output was written to, for example: Command.new("echo ping; false") => echo stdout: ping echo stderr: Nothing written In this contrived example that's more or less useless, however with badly behaved commands that write errors to stdout, and don't include their name in the program output, it can help a lot with debugging. ## 0.0.17 * Fixed a bug introduced in 0.0.16 where the capture() helper returned the name of the command that had been run, not it's output. * Classify the pre-directory switch, and pre-user switch command guards as having a DEBUG log level to exclude them from the logs. ## 0.0.16 * Fixed a bug introduced in 0.0.15 where the capture() helper returned boolean, discarding any output from the server. ## 0.0.15 * `Command` now takes a `verbosity` option. This defaults to `Logger::INFO` and can be set to any of the Ruby logger level constants. You can also set it to the symbol `:debug` (and friends) which will be expanded into the correct constants. The log verbosity level is set to Logger::INFO by default, and can be overridden by setting `SSHKit.config.output_verbosity = Logger::{...}`, pick a level that works for you. By default `test()` and `capture()` calls are surpressed, and not printed by the pretty logger as of this version. ## 0.0.14 * Umasks can now be set on `Command` instances. It can be set globally with `SSHKit.config.umask` (default, nil; meaning take the system default). This can be used to set, for example a umask of `007` for allowing users with the same primary group to share code without stepping on eachother's toes. ## 0.0.13 * Correctly quote `as(user)` commands, previously it would expand to: `sudo su user -c /usr/bin/env echo "Hello World"`, in which the command to run was taken as simply `/usr/bin/env`. By quoting all arguments it should now work as expected. `sudo su user -c "/usr/bin/env echo \""Hello World\""` ## 0.0.12 * Also print anything the program wrote to stdout when the exit status is non-zero and the command raises an error. (assits debugging badly behaved programs that fail, and write their error output to stdout.) ## 0.0.11 * Implementing confuguration objects on the backends (WIP, undocumented) * Implement `SSHKit.config.default_env`, a hash which can be modified and will act as a global `with`. * Fixed #9 (with(a: 'b', c: 'c') being parsed as `A=bC=d`. Now properly space separated. * Fixed #10 (overly aggressive shell escaping), one can now do: `with(path: 'foo:$PATH') without the $ being escaped too early. ## 0.0.10 * Include more attributes in `Command#to_hash`. ## 0.0.9 * Include more attributes in `Command#to_hash`. ## 0.0.8 * Added DSL method `background()` this sends a task to the background using `nohup` and redirects it's output to `/dev/null` so as to avoid littering the filesystem with `nohup.out` files. **Note:** Backgrounding a task won't work as you expect if you give it a string, that is you must do `background(:sleep, 5)` and not `background("sleep 5")` according to the rules by which a command is not processed in any way **if it contains a spaca character in it's first argument**. Usage Example: on hosts do background :rake, "assets:precompile" # typically takes 5 minutes! end **Further:** Many programs are badly behaved and no not work well with `nohup` it has to do with the way nohup works, reopening the processe's file descriptors and redirecting them. Programs that re-open, or otherwise manipulate their own file descriptors may lock up when the SSH session is disconnected, often they block writing to, or reading from stdin/out. ## 0.0.7 * DSL method `execute()` will now raise `SSHKit::Command::Failed` when the exit status is non-zero. The message of the exception will be whatever the process had written to stdout. * New DSL method `test()` behaves as `execute()` used to until this version. * `Command` now raises an error in `#exit_status=()` if the exit status given is not zero. (see below) * All errors raised by error conditions of SSHKit are defined as subclasses of `SSHKit::StandardError` which is itself a subclass of `StandardError`. The `Command` objects can be set to not raise, by passing `raise_on_non_zero_exit: false` when instantiating them, this is exactly what `test()` does internally. Example: on hosts do |host if test "[ -d /opt/sites ]" do within "/opt/sites" do execute :git, :pull end else execute :git, :clone, 'some-repository', '/opt/sites' end end ## 0.0.6 * Support arbitrary properties on Host objects. (see below) Starting with this version, the `Host` class supports arbitrary properties, here's a proposed use-case: servers = %w{one.example.com two.example.com three.example.com four.example.com}.collect do |s| h = SSHKit::Host.new(s) if s.match /(one|two)/ h.properties.roles = [:web] else h.properties.roles = [:app] end end on servers do |host| if host.properties.roles.include?(:web) # Do something pertinent to web servers elsif host.properties.roles.include?(:app) # Do something pertinent to application servers end end Naturally, this is a contrived example, the `#properties` attribute on the Host instance is implemented as an [`OpenStruct`](http://ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html) and will behave exactly as such. ## 0.0.5 * Removed configuration option `SSHKit.config.format` (see below) * Removed configuration option `SSHKit.config.runner` (see below) The format should now be set by doing: SSHKit.config.output = File.open('/dev/null') SSHKit.config.output = MyFormatterClass.new($stdout) The library ships with three formatters, `BlackHole`, `Dot` and `Pretty`. The default is `Pretty`, but can easily be changed: SSHKit.config.output = SSHKit::Formatter::Pretty.new($stdout) SSHKit.config.output = SSHKit::Formatter::Dot.new($stdout) SSHKit.config.output = SSHKit::Formatter::BlackHole.new($stdout) The one and only argument to the formatter is the *String/StringIO*ish object to which the output should be sent. (It should be possible to stack formatters, or build a multi-formatter to log, and stream to the screen, for example) The *runner* is now set by `default_options` on the Coordinator class. The default is still *:parallel*, and can be overridden on the `on()` (or `Coordinator#each`) calls directly. There is no global way to change the runner style for all `on()` calls as of version `0.0.5`. ## 0.0.4 * Rename the ConnectionManager class to Coordinator, connections are handled in the backend, if it needs to create some connections. ## 0.0.3 * Refactor the runner classes into an abstract heirarchy. ## 0.0.2 * Include a *Pretty* formatter * Modify example to use Pretty formatter. * Move common behaviour to an abstract formatter. * Formatters no longer inherit StringIO ## 0.0.1 First release. sshkit-1.9.0.rc1/CONTRIBUTING.md000066400000000000000000000054371266312556600157700ustar00rootroot00000000000000# Contributing to SSHKit * [**Don't** push your pull request](http://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) * [**Do** write a good commit message](http://365git.tumblr.com/post/3308646748/writing-git-commit-messages) ## Ruby versions You can see the current ruby versions we support in [.travis.yml](.travis.yml). Obviously, any language features you use must be available in the oldest version we support. Therefore, it's often helpful to develop / test against the oldest version to avoid accidentally using unsupported features. ## Tests SSHKit has a unit test suite and a functional test suite. Some functional tests run against [Vagrant](https://www.vagrantup.com/) VMs. If possible, you should make sure that the tests pass for each commit by running `rake` in the sshkit directory. This is in case we need to cherry pick commits or rebase. You should ensure the tests pass, (preferably on the minimum and maximum ruby version), before creating a PR. Pull requests are much more likely to be accepted if you write a tests for the new functionality you are adding. If you are fixing a bug, it would be great if you could add a test to expose the bug you are fixing to show that the behaviour is fixed by your changes. We use [Travis CI](https://travis-ci.org/capistrano/sshkit) to run the tests on different ruby versions. **The Travis build does not run the functional tests, so make sure all the tests pass locally before creating your PR.** ## Coding guidelines This project uses [RuboCop](https://github.com/bbatsov/rubocop) to enforce standard Ruby coding guidelines. Currently we run RuboCop's lint rules only, which check for readability issues like indentation, ambiguity, and useless/unreachable code. * Test that your contributions pass with `rake lint` * The linter is also run as part of the full test suite with `rake` * Note the Travis build will fail and your PR cannot be merged if the linter finds errors ## Changelog Most changes should have an accompanying entry in the [CHANGELOG](CHANGELOG.md), unless they are minor documentation / config changes. This is incredibly important so that our users can see the affect of new versions without having to trawl through the commits. ## Breaking changes We adhere to [semver](http://semver.org/) so breaking changes will require a major release. For breaking changes, it would normally be helpful to discuss them by raising a 'Proposal' issue or PR with examples of the new API you're proposing [before doing a lot of work](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/). Bear in mind that breaking changes may require many hundreds / thousands of users to update their code. You can use the [BREAKING_API_WISHLIST](BREAKING_API_WISHLIST.md) to record issues / PRs where API changes have been discussed, and not implemented. sshkit-1.9.0.rc1/EXAMPLES.md000066400000000000000000000243161266312556600152740ustar00rootroot00000000000000# Usage Examples ## Run a command as a different user ```ruby on hosts do |host| as 'www-data' do puts capture(:whoami) end end ``` ## Run with default environmental variables ```ruby SSHKit.config.default_env = { path: '/usr/local/libexec/bin:$PATH' } on hosts do |host| puts capture(:env) end ``` ## Run a command in a different directory ```ruby on hosts do |host| within '/var/log' do puts capture(:head, '-n5', 'messages') end end ``` ## Run a command with specific environmental variables ```ruby on hosts do |host| with rack_env: :test do puts capture("env | grep RACK_ENV") end end ``` ## Print some arbitrary output with the logging methods ```ruby on hosts do |host| f = '/some/file' if test("[ -d #{f} ]") execute :touch, f else info "#{f} already exists on #{host}!" end end ``` The `debug()`, `info()`, `warn()`, `error()` and `fatal()` honor the current log level of `SSHKit.config.output_verbosity` ## Run a command in a different directory as a different user ```ruby on hosts do |host| as 'www-data' do within '/var/log' do puts capture(:whoami) puts capture(:pwd) end end end ``` This will output: www-data /var/log **Note:** This example is a bit misleading, as the `www-data` user doesn't have a shell defined, one cannot switch to that user. ## Run a command which requires interaction between the client and the server ```ruby on hosts do |host| execute(:passwd, interaction_handler: { '(current) UNIX password: ' => "old_pw\n", 'Enter new UNIX password: ' => "new_pw\n", 'Retype new UNIX password: ' => "new_pw\n", 'passwd: password updated successfully' => nil # For stdout/stderr which can be ignored, map a nil input }) end ``` ## Download a file from disk ```ruby on roles(:all) do puts 'Downloading DB Backup File' date_path = Date.today.strftime("%Y/%m/%d") download! "/var/mysql-backup/#{date_path}/my-awesome-db.sql.gz", "my-awesome-db.sql.gz" end ``` ## Upload a file from disk ```ruby on hosts do |host| upload! '/config/database.yml', '/opt/my_project/shared/database.yml' end ``` **Note:** The `upload!()` method doesn't honor the values of `within()`, `as()` etc, this will be improved as the library matures, but we're not there yet. ## Upload a file from a stream ```ruby on hosts do |host| file = File.open('/config/database.yml') io = StringIO.new(....) upload! file, '/opt/my_project/shared/database.yml' upload! io, '/opt/my_project/shared/io.io.io' end ``` The IO streaming is useful for uploading something rather than "cat"ing it, for example ```ruby on hosts do |host| contents = StringIO.new('ALL ALL = (ALL) NOPASSWD: ALL') upload! contents, '/etc/sudoers.d/yolo' end ``` This spares one from having to figure out the correct escaping sequences for something like "echo(:cat, '...?...', '> /etc/sudoers.d/yolo')". **Note:** The `upload!()` method doesn't honor the values of `within()`, `as()` etc, this will be improved as the library matures, but we're not there yet. ## Upload a directory of files ```ruby on hosts do |host| upload! '.', '/tmp/mypwd', recursive: true end ``` In this case the `recursive: true` option mirrors the same options which are available to [`Net::{SCP,SFTP}`](http://net-ssh.github.io/net-scp/). ## Setting global SSH options Setting global SSH options, these will be overwritten by options set on the individual hosts: ```ruby SSHKit::Backend::Netssh.configure do |ssh| ssh.connection_timeout = 30 ssh.ssh_options = { keys: %w(/home/user/.ssh/id_rsa), forward_agent: false, auth_methods: %w(publickey password) } end ``` ## Run a command with a different effective group ID ```ruby on hosts do |host| as user: 'www-data', group: 'project-group' do within '/var/log' do execute :touch, 'somefile' execute :ls, '-l' end end end ``` One will see that the created file is owned by the user `www-data` and the group `project-group`. When combined with the `umask` configuration option, it is easy to share scripts for deployment between team members without sharing logins. ## Stack directory nestings ```ruby on hosts do within "/var" do puts capture(:pwd) within :log do puts capture(:pwd) end end end ``` This will output: /var/ /var/log The directory paths are joined using `File.join()`, which should correctly join parts without forcing the user of the code to care about trailing or leading slashes. It may be misleading as the `File.join()` is performed on the machine running the code, if that's a Windows box, the paths may be incorrectly joined according to the expectations of the machine receiving the commands. ## Do not care about the host block ```ruby on hosts do # The |host| argument is optional, it will # be nil in the block if not passed end ``` ## Change the output formatter ```ruby # The default format is pretty, which outputs colored text SSHKit.config.format = :pretty # Text with no coloring SSHKit.config.format = :simpletext # Red / Green dots for each completed step SSHKit.config.format = :dot # No output SSHKit.config.format = :blackhole ``` ## Implement a dirt-simple formatter class ```ruby module SSHKit module Formatter class MyFormatter < SSHKit::Formatter::Abstract def write(obj) case obj.is_a? SSHKit::Command # Do something here, see the SSHKit::Command documentation end end end end end # If your formatter is defined in the SSHKit::Formatter module configure with the format option: SSHKit.config.format = :myformatter # Or configure the output directly SSHKit.config.output = MyFormatter.new($stdout) SSHKit.config.output = MyFormatter.new(SSHKit.config.output) SSHKit.config.output = MyFormatter.new(File.open('log/deploy.log', 'wb')) ``` ## Set a password for a host. ```ruby host = SSHKit::Host.new('user@example.com') host.password = "hackme" on host do |host| puts capture(:echo, "I don't care about security!") end ``` ## Execute and raise an error if something goes wrong ```ruby on hosts do |host| execute(:echo, '"Example Message!" 1>&2; false') end ``` This will raise `SSHKit::Command::Failed` with the `#message` "Example Message!" which will cause the command to abort. ## Make a test, or run a command which may fail without raising an error: ```ruby on hosts do |host| if test "[ -d /opt/sites ]" within "/opt/sites" do execute :git, :pull end else execute :git, :clone, 'some-repository', '/opt/sites' end end ``` The `test()` command behaves exactly the same as execute however will return false if the command exits with a non-zero exit (as `man 1 test` does). As it returns boolean it can be used to direct the control flow within the block. ## Do something different on one host, or another depending on a host property ```ruby host1 = SSHKit::Host.new 'user@example.com' host2 = SSHKit::Host.new 'user@example.org' on hosts do |host| target = "/var/www/sites/" if host.hostname =~ /org/ target += "dotorg" else target += "dotcom" end execute! :git, :clone, "git@git.#{host.hostname}", target end ``` ## Connect to a host in the easiest possible way ```ruby on 'example.com' do |host| execute :uptime end ``` This will resolve the `example.com` hostname into a `SSHKit::Host` object, and try to pull up the correct configuration for it. ## Run a command without it being command-mapped If the command you attempt to call contains a space character it won't be mapped: ```ruby Command.new(:git, :push, :origin, :master).to_s # => /usr/bin/env git push origin master # (also: execute(:git, :push, :origin, :master) Command.new("git push origin master").to_s # => git push origin master # (also: execute("git push origin master")) ``` This can be used to access shell builtins (such as `if` and `test`) ## Run a command with a heredoc An extension of the behaviour above, if you write a command like this: ```ruby c = Command.new <<-EOCOMMAND if test -d /var/log then echo "Directory Exists" fi EOCOMMAND c.to_s # => if test -d /var/log; then echo "Directory Exists; fi # (also: execute <<- EOCOMMAND........)) ``` **Note:** The logic which reformats the script into a oneliner may be naïve, but in all known test cases, it works. The key thing is that `if` is not mapped to `/usr/bin/env if`, which would break with a syntax error. ## Using with Rake Into the `Rakefile` simply put something like: ```ruby require 'sshkit' SSHKit.config.command_map[:rake] = "./bin/rake" desc "Deploy the site, pulls from Git, migrate the db and precompile assets, then restart Passenger." task :deploy do include SSHKit::DSL on "example.com" do |host| within "/opt/sites/example.com" do execute :git, :pull execute :bundle, :install, '--deployment' execute :rake, 'db:migrate' execute :rake, 'assets:precompile' execute :touch, 'tmp/restart.txt' end end end ``` ## Using without the DSL The *Coordinator* will resolve all hosts into *Host* objects, you can mix and match. ```ruby Coordinator.new("one.example.com", SSHKit::Host.new('two.example.com')).each in: :sequence do puts capture :uptime end ``` You might also look at `./lib/sshkit/dsl.rb` where you can see almost the exact code as above, which implements the `on()` method. ## Use the Host properties attribute Implemented since `v0.0.6` ```ruby servers = %w{one.example.com two.example.com three.example.com four.example.com}.collect do |s| h = SSHKit::Host.new(s) if s.match /(one|two)/ h.properties.roles = [:web] else h.properties.roles = [:app] end end on servers do |host| if host.properties.roles.include?(:web) # Do something pertinent to web servers elsif host.properties.roles.include?(:app) # Do something pertinent to application servers end end ``` The `SSHKit::Host#properties` is an [`OpenStruct`](http://ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html) which is not verified or validated in any way, it is up to you, or your library to attach meanings or conventions to this mechanism. ## Running local commands Replace `on` with `run_locally` ```ruby run_locally do within '/tmp' do execute :whoami end end ``` You can achieve the same thing with `on(:local)` ```ruby on(:local) do within '/tmp' do execute :whoami end end ``` sshkit-1.9.0.rc1/FAQ.md000066400000000000000000000032321266312556600144570ustar00rootroot00000000000000## Is it better than Capistrano? *SSHKit* is designed to solve a different problem than Capistrano. *SSHKit* is a toolkit for performing structured commands on groups of servers in a repeatable way. It provides concurrency handling, sane error checking and control flow that would otherwise be difficult to achieve with pure *Net::SSH*. Since *Capistrano v3.0*, *SSHKit* is used by *Capistrano* to communicate with backend servers. Whilst Capistrano provides the structure for repeatable deployments. ## Why does stop responding after I started it with `background()`? The answer is complicated, but it can be summed up by saying that under certain circumstances processes can find themselves connected to file descriptors which no longer exist. The following resources are worth a read to better understand what a process must do in order to daemonize reliably, not all processes perform all of the steps necessary: * [http://stackoverflow.com/questions/881388/what-is-the-reason-for-performing-a-double-fork-when-creating-a-daemon] This can be summarized as: > On some flavors of Unix, you are forced to do a double-fork on startup, in order to go into daemon mode. This is because single forking isn’t guaranteed to detach from the controlling terminal. If you experience consistent problems, please report it as an issue, I'll be in a position to give a better answer once I can examine the problem in more detail. ## My daemon doesn't work properly when run from SSHKit You should probably read: * http://www.enderunix.org/docs/eng/daemon.php If any of those things aren't being done by your daemon, then you ought to adopt some or all of those techniques. sshkit-1.9.0.rc1/Gemfile000066400000000000000000000007021266312556600150200ustar00rootroot00000000000000source 'https://rubygems.org' gemspec platforms :rbx do gem 'rubysl', '~> 2.0' gem 'json' end # Chandler requires Ruby >= 2.1.0, but depending on the Travis environment, # we may not meet that requirement. Only include the chandler gem if the Ruby # requirement is met. (Chandler is used only for `rake release`; see Rakefile.) if Gem::Requirement.new('>= 2.1.0').satisfied_by?(Gem::Version.new(RUBY_VERSION)) gem 'chandler', '>= 0.1.1' end sshkit-1.9.0.rc1/LICENSE.md000066400000000000000000000020601266312556600151300ustar00rootroot00000000000000Copyright (c) 2008- Lee Hambley & Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sshkit-1.9.0.rc1/README.md000066400000000000000000000511511266312556600150100ustar00rootroot00000000000000![SSHKit Logo](https://raw.github.com/leehambley/sshkit/master/examples/images/logo.png) **SSHKit** is a toolkit for running commands in a structured way on one or more servers. [![Gem Version](https://badge.fury.io/rb/sshkit.svg)](https://rubygems.org/gems/sshkit) [![Build Status](https://travis-ci.org/capistrano/sshkit.svg?branch=master)](https://travis-ci.org/capistrano/sshkit) [![Dependency Status](https://gemnasium.com/capistrano/sshkit.svg)](https://gemnasium.com/capistrano/sshkit) ## How might it work? The typical use-case looks something like this: ```ruby require 'sshkit' include SSHKit::DSL on %w{1.example.com 2.example.com}, in: :sequence, wait: 5 do |host| within "/opt/sites/example.com" do as :deploy do with rails_env: :production do rake "assets:precompile" runner "S3::Sync.notify" execute :node, "socket_server.js" end end end end ``` You can find many other examples of how to use SSHKit over in [EXAMPLES.md](EXAMPLES.md). ## Basic usage The `on()` method is used to specify the backends on which you'd like to run the commands. You can pass one or more hosts as parameters; this runs commands via SSH. Alternatively you can pass `:local` to run commands locally. By default SSKit will run the commands on all hosts in parallel. #### Running commands All backends support the `execute(*args)`, `test(*args)` & `capture(*args)` methods for executing a command. You can call any of these methods in the context of an `on()` block. **Note: In SSHKit, the first parameter of the `execute` / `test` / `capture` methods has a special significance. If the first parameter isn't a Symbol, SSHKit assumes that you want to execute the raw command and the `as` / `within` / `with` methods, `SSHKit.config.umask` and [the comand map](#the-command-map) have no effect.** Typically, you would pass a Symbol for the command name and it's args as follows: ```ruby on '1.example.com' if test("[ -f somefile.txt ]") execute(:cp, 'somefile.txt', 'somewhere_else.txt') end ls_output = capture(:ls, '-l') end ``` By default the `capture` methods strips whitespace. If you need to preserve whitespace you can pass the `strip: false` option: `capture(:ls, '-l', strip: false)` #### Transferring files All backends also support the `upload!` and `download!` methods for transferring files. For the remote backend, the file is tranferred with scp. ```ruby on '1.example.com' do upload! 'some_local_file.txt', '/home/some_user/somewhere' download! '/home/some_user/some_remote_file.txt', 'somewhere_local' end ``` #### Users, working directories, environment variables and umask When running commands, you can tell SSHKit to set up the context for those commands using the following methods: ```ruby as(user: 'un', group: 'grp') { execute('cmd') } # Executes sudo -u un -- sh -c 'sg grp cmd' within('/somedir') { execute('cmd') } # Executes cd /somedir && cmd with(env_var: 'value') { execute('cmd') } # Executes ENV_VAR=value cmd SSHKit.config.umask = '077' # All commands are executed with umask 077 && cmd ``` The `as()` / `within()` / `with()` are nestable in any order, repeatable, and stackable. When used inside a block in this way, `as()` and `within()` will guard the block they are given with a check. In the case of `within()`, an error-raising check will be made that the directory exists; for `as()` a simple call to `sudo su - whoami` wrapped in a check for success, raising an error if unsuccessful. The directory check is implemented like this: if test ! -d ; then echo "Directory doesn't exist" 2>&1; false; fi And the user switching test implemented like this: if ! sudo su -c whoami > /dev/null; then echo "Can't switch user" 2>&1; false; fi According to the defaults, any command that exits with a status other than 0 raises an error (this can be changed). The body of the message is whatever was written to *stdout* by the process. The `1>&2` redirects the standard output of echo to the standard error channel, so that it's available as the body of the raised error. Helpers such as `runner()` and `rake()` which expand to `execute(:rails, "runner", ...)` and `execute(:rake, ...)` are convenience helpers for Ruby, and Rails based apps. ## Parallel Notice on the `on()` call the `in: :sequence` option, the following will do what you might expect: ```ruby on(in: :parallel) { ... } on(in: :sequence, wait: 5) { ... } on(in: :groups, limit: 2, wait: 5) { ... } ``` The default is to run `in: :parallel` which has no limit. If you have 400 servers, this might be a problem and you might better look at changing that to run in `groups`, or `sequence`. Groups were designed in this case to relieve problems (mass Git checkouts) where you rely on a contested resource that you don't want to DDOS by hitting it too hard. Sequential runs were intended to be used for rolling restarts, amongst other similar use-cases. ## Synchronisation The `on()` block is the unit of synchronisation, one `on()` block will wait for all servers to complete before it returns. For example: ```ruby all_servers = %w{one.example.com two.example.com three.example.com} site_dir = '/opt/sites/example.com' # Let's simulate a backup task, assuming that some servers take longer # then others to complete on all_servers do |host| within site_dir do execute :tar, '-czf', "backup-#{host.hostname}.tar.gz", 'current' # Will run: "/usr/bin/env tar -czf backup-one.example.com.tar.gz current" end end # Now we can do something with those backups, safe in the knowledge that # they will all exist (all tar commands exited with a success status, or # that we will have raised an exception if one of them failed. on all_servers do |host| within site_dir do backup_filename = "backup-#{host.hostname}.tar.gz" target_filename = "backups/#{Time.now.utc.iso8601}/#{host.hostname}.tar.gz" puts capture(:s3cmd, 'put', backup_filename, target_filename) end end ``` ## The Command Map It's often a problem that programmatic SSH sessions don't have the same environment variables as interactive sessions. A problem often arises when calling out to executables expected to be on the `$PATH`. Under conditions without dotfiles or other environmental configuration, `$PATH` may not be set as expected, and thus executables are not found where expected. To try and solve this there is the `with()` helper which takes a hash of variables and makes them available to the environment. ```ruby with path: '/usr/local/bin/rbenv/shims:$PATH' do execute :ruby, '--version' end ``` Will execute: ( PATH=/usr/local/bin/rbenv/shims:$PATH /usr/bin/env ruby --version ) By contrast, the following won't modify the command at all: ```ruby with path: '/usr/local/bin/rbenv/shims:$PATH' do execute 'ruby --version' end ``` Will execute, without mapping the environmental variables, or querying the command map: ruby --version (This behaviour is sometimes considered confusing, but it has mostly to do with shell escaping: in the case of whitespace in your command, or newlines, we have no way of reliably composing a correct shell command from the input given.) **Often more preferable is to use the *command map*.** The *command map* is used by default when instantiating a *Command* object The *command map* exists on the configuration object, and in principle is quite simple, it's a *Hash* structure with a default key factory block specified, for example: ```ruby puts SSHKit.config.command_map[:ruby] # => /usr/bin/env ruby ``` To make clear the environment is being deferred to, the `/usr/bin/env` prefix is applied to all commands. Although this is what happens anyway when one would simply attempt to execute `ruby`, making it explicit hopefully leads people to explore the documentation. One can override the hash map for individual commands: ```ruby SSHKit.config.command_map[:rake] = "/usr/local/rbenv/shims/rake" puts SSHKit.config.command_map[:rake] # => /usr/local/rbenv/shims/rake ``` Another opportunity is to add command prefixes: ```ruby SSHKit.config.command_map.prefix[:rake].push("bundle exec") puts SSHKit.config.command_map[:rake] # => bundle exec rake SSHKit.config.command_map.prefix[:rake].unshift("/usr/local/rbenv/bin exec") puts SSHKit.config.command_map[:rake] # => /usr/local/rbenv/bin exec bundle exec rake ``` One can also override the command map completely, this may not be wise, but it would be possible, for example: ```ruby SSHKit.config.command_map = Hash.new do |hash, command| hash[command] = "/usr/local/rbenv/shims/#{command}" end ``` This would effectively make it impossible to call any commands which didn't provide an executable in that directory, but in some cases that might be desirable. *Note:* All keys should be symbolised, as the *Command* object will symbolize it's first argument before attempting to find it in the *command map*. ## Interactive commands > (BETA) (Added in version #.##) By default, commands against remote servers are run in a *non-login, non-interactive* ssh session. This is by design, to try and isolate the environment and make sure that things work as expected, regardless of any changes that might happen on the server side. This means that, although the server may have prompted you, and be waiting for it, **you cannot send data to the server by typing into your terminal window**. Wherever possible, you should call commands in a way that doesn't require interaction (eg by specifying all options as command arguments). However in some cases, you may want to programmatically drive interaction with a command and this can be achieved by specifying an `:interaction_handler` option when you `execute`, `capture` or `test` a command. **It is not necessary, or desirable to enable `Netssh.config.pty` to use the `interaction_handler` option. Only enable `Netssh.config.pty` if the command you are calling won't work without a pty.** An `interaction_handler` is an object which responds to `on_data(command, stream_name, data, channel)`. The `interaction_handler`'s `on_data` method will be called each time `stdout` or `stderr` data is available from the server. Data can be sent back to the server using the `channel` parameter. This allows scripting of command interaction by responding to `stdout` or `stderr` lines with any input required. For example, an interaction handler to change the password of your linux user using the `passwd` command could look like this: ```ruby class PasswdInteractionHandler def on_data(command, stream_name, data, channel) puts data case data when '(current) UNIX password: ' channel.send_data("old_pw\n") when 'Enter new UNIX password: ', 'Retype new UNIX password: ' channel.send_data("new_pw\n") when 'passwd: password updated successfully' else raise "Unexpected stderr #{stderr}" end end end # ... execute(:passwd, interaction_handler: PasswdInteractionHandler.new) ``` #### Using the `SSHKit::MappingInteractionHandler` Often, you want to map directly from a short output string returned by the server (either stdout or stderr) to a corresponding input string (as in the case above). For this case you can specify the `interaction_handler` option as a hash. This is used to create a `SSHKit::MappingInteractionHandler` which provides similar functionality to the linux [expect](http://expect.sourceforge.net/) library: ```ruby execute(:passwd, interaction_handler: { '(current) UNIX password: ' => "old_pw\n", /(Enter|Retype) new UNIX password: / => "new_pw\n" }) ``` Note: the key to the hash keys are matched against the server output `data` using the case equals `===` method. This means that regexes and any objects which define `===` can be used as hash keys. Hash keys are matched in order, which allows for default wildcard matches: ```ruby execute(:my_command, interaction_handler: { "some specific line\n" => "specific input\n", /.*/ => "default input\n" }) ``` You can also pass a Proc object to map the output line from the server: ```ruby execute(:passwd, interaction_handler: lambda { |server_data| case server_data when '(current) UNIX password: ' "old_pw\n", when /(Enter|Retype) new UNIX password: / "new_pw\n" end }) ``` `MappingInteractionHandler`s are stateless, so you can assign one to a constant and reuse it: ```ruby ENTER_PASSWORD = SSHKit::MappingInteractionHandler.new( "Please Enter Password\n" => "some_password\n" ) execute(:first_command, interaction_handler: ENTER_PASSWORD) execute(:second_command, interaction_handler: ENTER_PASSWORD) ``` #### Exploratory logging By default, the `MappingInteractionHandler` does not log, in case the server output or input contains sensitive information. However, if you pass a second `log_level` parameter to the constructor, the `MappingInteractionHandler` will log information about what output is being returned by the server, and what input is being sent in response. This can be helpful if you don't know exactly what the server is sending back (whitespace, newlines etc). ```ruby # Start with this and run your script execute(:unfamiliar_command, MappingInteractionHandler.new({}, :debug)) # DEBUG log => Unable to find interaction handler mapping for stdout: # "Please type your input:\r\n" so no response was sent" # Update mapping: execute(:unfamiliar_command, MappingInteractionHandler.new( {"Please type your input:\r\n" => "Some input\n"} :debug )) ``` #### The `data` parameter The `data` parameter of `on_data(command, stream_name, data, channel)` is a string containing the latest data delivered from the backend. When using the `Netssh` backend for commands where a small amount of data is returned (eg prompting for sudo passwords), `on_data` will normally be called once per line and `data` will be terminated by a newline. For commands with larger amounts of output, `data` is delivered as it arrives from the underlying network stack, which depends on network conditions, buffer sizes, etc. In this case, you may need to implement a more complex `interaction_handler` to concatenate `data` from multiple calls to `on_data` before matching the required output. When using the `Local` backend, `on_data` is always called once per line. #### The `channel` parameter When using the `Netssh` backend, the `channel` parameter of `on_data(command, stream_name, data, channel)` is a [Net::SSH Channel](http://net-ssh.github.io/ssh/v2/api/classes/Net/SSH/Connection/Channel.html). When using the `Local` backend, it is a [ruby IO](http://ruby-doc.org/core/IO.html) object. If you need to support both sorts of backends with the same interaction handler, you need to call methods on the appropriate API depending on the channel type. One approach is to detect the presence of the API methods you need - eg `channel.respond_to?(:send_data) # Net::SSH channel` and `channel.respond_to?(:write) # IO`. See the `SSHKit::MappingInteractionHandler` for an example of this. ## Output Handling ![Example Output](https://raw.github.com/leehambley/sshkit/master/examples/images/example_output.png) By default, the output format is set to `:pretty`: ```ruby SSHKit.config.use_format :pretty ``` However, if you prefer non colored text you can use the `:simpletext` formatter. If you want minimal output, there is also a `:dot` formatter which will simply output red or green dots based on the success or failure of commands. There is also a `:blackhole` formatter which does not output anything. By default, formatters log to `$stdout`, but they can be constructed with any object which implements `<<` for example any `IO` subclass, `String`, `Logger` etc: ```ruby # Output to a String: output = String.new SSHKit.config.output = SSHKit::Formatter::Pretty.new(output) # Do something with output # Or output to a file: SSHKit.config.output = SSHKit::Formatter::SimpleText.new(File.open('log/deploy.log', 'wb')) ``` #### Output Colors By default, SSHKit will color the output using ANSI color escape sequences if the output you are using is associated with a terminal device (tty). This means that you should see colors if you are writing output to the terminal (the default), but you shouldn't see ANSI color escape sequences if you are writing to a file. Colors are supported for the `Pretty` and `Dot` formatters, but for historical reasons the `SimpleText` formatter never shows colors. If you want to force SSHKit to show colors, you can set the `SSHKIT_COLOR` environment variable: ```ruby ENV['SSHKIT_COLOR'] = 'TRUE' ``` #### Custom formatters Want custom output formatting? Here's what you have to do: 1. Write a new formatter class in the `SSHKit::Formatter` module. Your class should subclass `SSHKit::Formatter::Abstract` to inherit conveniences and common behavior. For a basic an example, check out the [Pretty](https://github.com/capistrano/sshkit/blob/master/lib/sshkit/formatters/pretty.rb) formatter. 1. Set the output format as described above. E.g. if your new formatter is called `FooBar`: ```ruby SSHKit.config.use_format :foobar ``` All formatters that extend from `SSHKit::Formatter::Abstract` accept an options Hash as a constructor argument. You can pass options to your formatter like this: ```ruby SSHKit.config.use_format :foobar, :my_option => "value" ``` You can then access these options using the `options` accessor within your formatter code. For a much more full-featured formatter example that makes use of options, check out the [Airbrussh repository](https://github.com/mattbrictson/airbrussh/). ## Output Verbosity By default calls to `capture()` and `test()` are not logged, they are used *so* frequently by backend tasks to check environmental settings that it produces a large amount of noise. They are tagged with a verbosity option on the `Command` instances of `Logger::DEBUG`. The default configuration for output verbosity is available to override with `SSHKit.config.output_verbosity=`, and defaults to `Logger::INFO`. At present the `Logger::WARN`, `ERROR` and `FATAL` are not used. ## Deprecation warnings Deprecation warnings are logged directly to `stderr` by default. This behaviour can be changed by setting the `SSHKit.config.deprecation_output` option: ```ruby # Disable deprecation warnings SSHKit.config.deprecation_output = nil # Log deprecation warnings to a file SSHKit.config.deprecation_output = File.open('log/deprecation_warnings.log', 'wb') ``` ## Connection Pooling SSHKit uses a simple connection pool (enabled by default) to reduce the cost of negotiating a new SSH connection for every `on()` block. Depending on usage and network conditions, this can add up to a significant time savings. In one test, a basic `cap deploy` ran 15-20 seconds faster thanks to the connection pooling added in recent versions of SSHKit. To prevent connections from "going stale", an existing pooled connection will be replaced with a new connection if it hasn't been used for more than 30 seconds. This timeout can be changed as follows: ```ruby SSHKit::Backend::Netssh.pool.idle_timeout = 60 # seconds ``` If you suspect the connection pooling is causing problems, you can disable the pooling behaviour entirely by setting the idle_timeout to zero: ```ruby SSHKit::Backend::Netssh.pool.idle_timeout = 0 # disabled ``` ## Tunneling and other related SSH themes In order to do special gymnasitcs with SSH, tunneling, aliasing, complex options, etc with SSHKit it is possible to use [the underlying Net::SSH API](https://github.com/capistrano/sshkit/blob/master/EXAMPLES.md#setting-global-ssh-options) however in many cases it is preferred to use the system SSH configuration file at [`~/.ssh/config`](http://man.cx/ssh_config). This allows you to have personal configuration tied to your machine that does not have to be committed with the repository. If this is not suitable (everyone on the team needs a proxy command, or some special aliasing) a file in the same format can be placed in the project directory at `~/yourproject/.ssh/config`, this will be merged with the system settings in `~/.ssh/config`, and with any configuration specified in [`SSHKit::Backend::Netssh.config.ssh_options`](https://github.com/capistrano/sshkit/blob/master/lib/sshkit/backends/netssh.rb#L133). These system level files are the preferred way of setting up tunneling and proxies because the system implementations of these things are faster and better than the Ruby implementations you would get if you were to configure them through Net::SSH. In cases where it's not possible (Windows?), it should be possible to make use of the Net::SSH APIs to setup tunnels and proxy commands before deferring control to Capistrano/SSHKit.. ## SSHKit Related Blog Posts [SSHKit Gem Basics](http://www.rubyplus.com/articles/591) [SSHKit Gem Part 2](http://www.rubyplus.com/articles/601) [Embedded Capistrano with SSHKit](http://ryandoyle.net/posts/embedded-capistrano-with-sshkit/) sshkit-1.9.0.rc1/RELEASING.md000066400000000000000000000021361266312556600153630ustar00rootroot00000000000000# Releasing ## Prerequisites * You must have commit rights to the SSHKit repository. * You must have push rights for the sshkit gem on rubygems.org. * You must be using Ruby >= 2.1.0. * Your `~/.netrc` must be configured with your GitHub credentials, [as explained here](https://github.com/mattbrictson/chandler#2-configure-netrc). ## How to release 1. Run `bundle install` to make sure that you have all the gems necessary for testing and releasing. 2. **Ensure the tests are passing by running `rake test`.** If functional tests fail, ensure you have [Vagrant](https://www.vagrantup.com) installed and have started it with `vagrant up`. 3. Determine which would be the correct next version number according to [semver](http://semver.org/). 4. Update the version in `./lib/sshkit/version.rb`. 5. Update the `CHANGELOG`. 6. Commit the changelog and version in a single commit, the message should be "Preparing vX.Y.Z" 7. Run `rake release`; this will tag, push to GitHub, publish to rubygems.org, and upload the latest changelog entry to the [GitHub releases page](https://github.com/capistrano/sshkit/releases). sshkit-1.9.0.rc1/Rakefile000066400000000000000000000020421266312556600151710ustar00rootroot00000000000000#!/usr/bin/env rake require 'bundler/gem_tasks' require 'rake/testtask' require 'rubocop/rake_task' task :default => [:test, :lint] desc "Run all tests" task :test => ['test:units', 'test:functional'] namespace :test do Rake::TestTask.new(:units) do |t| t.libs << "test" t.test_files = FileList['test/unit/**/test*.rb'] end Rake::TestTask.new(:functional) do |t| t.libs << "test" t.test_files = FileList['test/functional/**/test*.rb'] end end Rake::Task["test:functional"].enhance do warn "Remember there are still some VMs running, kill them with `vagrant halt` if you are finished using them." end desc 'Run RuboCop lint checks' RuboCop::RakeTask.new(:lint) do |task| task.options = ['--lint'] end task "release:rubygem_push" do # Delay loading Chandler until this point, since it requires Ruby >= 2.1, # which may not be available in all environments (e.g. Travis). # We assume that the person doing `rake release` has Ruby >= 2.1. require "chandler/tasks" Rake.application.invoke_task("chandler:push") end sshkit-1.9.0.rc1/Vagrantfile000066400000000000000000000006131266312556600157130ustar00rootroot00000000000000VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = 'hashicorp/precise64' json_config_path = File.join("test", "boxes.json") list = File.open(json_config_path).read list = JSON.parse(list) list.each do |vm| config.vm.define vm["name"] do |web| web.vm.network "forwarded_port", guest: 22, host: vm["port"] end end end sshkit-1.9.0.rc1/examples/000077500000000000000000000000001266312556600153445ustar00rootroot00000000000000sshkit-1.9.0.rc1/examples/images/000077500000000000000000000000001266312556600166115ustar00rootroot00000000000000sshkit-1.9.0.rc1/examples/images/example_output.png000066400000000000000000002434301266312556600224000ustar00rootroot00000000000000PNG  IHDRb60 iCCPICC ProfileH wThH #E U@B !$ؕX *"`CWD\ kA, \,Pyޜ33ߝ̝s.+d*Yb4:؏=;1Mz@-pr%QQmL6 _(|A.†S,auO"b1BDKb\qޥqbhLs LriKcl'tcxB.cuVV7`l7?ic.7e'6{fb?J2>MVXZznFL֛b1qc&X((l.EOHƉ`<$nq6̌r؏,&L0_8I}n^̤@?sB UڸRÂJdQgΜK4hR#k2alȄv&8Uę`4d.ckʣ' MƐ -Ă >@ ) 6rA}q;n cْERQP^lvxc ;AXWc穸l kp)_6T7O. #0pgPv' BX+J`lJ {`?# ',\N[za^|AHa"Z>bX!+"H4$#i#KH RT":r >dy|Aq(UGuQStah,:MCs]V5A =v^%: lp8\$. q\ׁŽ}L<o<|~_ߏoŸ Nf Er>q-Hd͈.b"1XJNl${aE"y"I\TDF:H:CN }"dr9,&"OG(*;%§,쥴RQ(#TUՓKMVPh4!͍6&U.hjtK?}.]N_G1 S#!cc113>)1l8J|JUJMJו^+SM}+(+UJbUYRrBʰ*S^5R5KTej$5S@5ZsjLӈ1W32/0ԉftC]Cj5554Nip,SZ:º2EwS\Qs@XQ-VVFfGxmKY wh_~5U}TGAu,uuѹ3+ݦ{NKG/]oiA}Hl /;]>211621433\ehȈjjj٨hX8xq}dIGS35ͦ48fff957---QK'Ke5+Jdݪǚ`f-cCɳeنۮm}=xZҴ:}s˴k^>~}[KCMGcr7ӭ ~׉Ʃ雳Թy%٥厫kk%7rnݝeG8| 3= =={^^z 5O||>|Z}g';[. 88+P-0.2qaPZP}PSBHXƐ;]S u ]z>V$2\FFlx8dxfs$Dr"7E>2ʉuqVԬYODw0c>Ay<=^9~n|]DŽf/ݙ(JlI"%'K8g˜Nsޞg6/3Zh2!9!@Wn$;IN^}OAYgjY4ϴMiBoa_T)z3cFdFmhfBfc9+9XM!>#Izss Iäry-2u,*7 ʫ0~||qE.zVTbb%KV.[t2dYʲF ^%ueV٭*[~uB?P_T$-c?~Zv+%v%%_KyW~u;߱AFT 7Elj\[.O/߹U"e ۾V +oUU5VT ;uwKM55{{<gןi+V\ۻ?z::ףsv 8`ӰXr%GŽu=pXq&iQP%DVZ{d)SOSO=SpfMl>w].\t\oǙKN^v|NΦNW.箦k.Zݺ[{f}썀ornvޚyvw˿^7:m/hϽj @Iq^16oͿG7-\iTXtXML:com.adobe.xmp 1122 239 @IDATx]\ ID+ETbCg.v{G"*XA& I(!dCH`,3|6o^n6.F#`0F#`0F#`0D華5N1̎`0F#`Gw D \]mk~^K)o"};ֵq~_$V`0F#'!F̄-?=O˟ ?L{lUۺM K͹&A܍~In~X F#`0F Nڌrchٟd|-,|@ ,奊SN;:M;Yvk\+bh{Q%.b%F#`0I& %|y]k= 7cݎYOaê˺؀H73?ާƤN;KN-d)a]M ^y2)M4gMiB_~L>Ni Dg>lrYnf|c3y9$H3)PV\)q1)|~ %z=437sNeoue-:Jbx#`0F#P _~El:αߴV&;Ƶ;,%&Y,.<}=#A)%t޸|+p#N\>ǰN{J֌I5gm{kkW|gʅ$f3j~Cf:[Hd\ i3d;kc$]uqsiPeі mlnAH)6ٱ$׶gT 9˱sHt֑WtE:RGN0F#`0ߎ(!輊 ;uluLjN >|}և~0PĨx% yya'Ϫ# awn`ܷm>$W5hmm/_S)__)w/&.툢w?l݀U]M֩V--t(H]F#`0F7 @Y]|S9GUɄ9G_ҙ 8DQP` %S|cOv^.-Xݺt_T&Vݑ]7ޣZ[ԫesssgZ,{w4n|1r-2wqb6m§O~5uI&ˀ2`X-gdK ~&#i.ףTN%%`0F#V>jܨaض].mwrϴllq T QH'` *{O=gWe}'~W3V'ً92C1IqzȽL1H;ds)擎ZE,,βAl"TQJ]: 4UtIw5AG*D'iF#`0߈Z6ɼ׮j1ėc>JI"HHNѣT]ѱ"M"-DI`Mª̌ρPNҙ#:S wL }FQԡKjr hҰLCթSPl2I 8н4S < HDAiN<B F#`0F#P :M;dDI hC"Ȩ^Al&)%9~ŪYkoB }gNCK/]F*^-rzMa~UO] =) Zxyf˔Fhmmu& &{>q]ׇ; Ԡ2UJ!8jw,ۀI7^t~0J~:(q̌7=J*Lx#`0F#l:@uc Kf.7c(1ɂ%?dHWEv~B_1(-;xw ŀO?t,ʼ ) Fg8[/ ``4MKFK8l%׀<y~e@חY([B:rOyCdHR F#`0F#"`=hofcnwiSձ|i|~BruN o'޷! 2бAkjoGy:fD0XQ%M0#`0F#{/_joC ZI=Lֱ8F#`0F#аvAQ[qKZ2JH(.TX!ރ7g #`0F#Ыߧ! F@m\5^ `0F# C@iI7{dN]qI뉇\mm8)gwc?X k -RcSmsVctݶ}Zxu2:(};--LJlD yUw`WrecΌ9spBfUY n]+++Wt1]Ъ]#PPZJ'Ӥ; [My>ϳ9h E.#+\3QTs0DKUם- C/$TAyv"geYkZk]pۜ~PQX^^!T ┱ς"ߘE7cH]}Q~ܛh~Nu TGԛ~-.so}[M y9jkʠ;o=(<'6WQq96~r}/Rxyáאm}R9/R&?y1,;a{C}ϥ|l?E%}i|M?"w.;Ѣ}7~E^WǟD!Nu#.'g{~BWVn]Yq@'VK4{ H\`0F0I3)(cjcVko oZSS(5ttneoYKqwQ{̬z;p$ش6긢*y-q5 =#$ʣ,:V뽴s3g,#Bt:;:<[+^*>k5 ma[,Ie͐gn23o[i)3hB]=cO6鋞C=9{\HQehMG1<.nά]9!,**3yi2nb7i-s@ԧ4[4{c 15GP|T0͎C3C^:]3 umpx<3hnڽB TщTq ) CL?}ήH!g,.`n^%~Mmu(⢢jFXҗOE@ bZhA=rdk"O|.ih p XYP)66a.Iz9ivZj_0A0tuY<]] tyRNƎ4M x"UhrN >QDDl=5:~!gmӳ.~TdYy?ͯQ$ВæԉOYx *&`8ۈK1+&OtmQx~r^_b0v +о/M ovo`zT&8z3 HP}~Om]g,dը;hu z,ڹ0dA@+/=mQ XTaJ/yz~{-pzYC:zNhd6ZYE^'!,N 5;w1$%/nV܆A^YڳoXbo_NoӠ>QfS;v?0/0&ܳ10͍g_FLkȢu:iWlidbZwQ#;@ܸz3zaQLxF5C͙uB6Gwub3 ~H$/208 T|䄵|OFN[zvV/!) _5*S]xnVsMGQ$vDA AG CO ˘ÿrYH.X8y:?R_UpSY D=a]%B"LN. a% [wЋ̎9Wbul}NX4 @nupUoCKB!ktb{BZh/ɸ}PD3$s䰏|K[֜Գ[wްM@S6EOR P&*:лP/,@lM4YiQbTllV\ _ +W\VF {(ѩB%$ F#`0 ̀Z\:U5 $Wgڹˠmcp%/˱n̥Pl"U:(2X[U6=ݤ,=_=~=΋|33j'/Wԉl[IJgiA椚ݰDݾ_|x9Y/$)=^F8 S-QA)̆XbAnZZF [ TXrP =YEOsqAucnV鎇9nȨPR6^%$ʔ 0CqVRXX"!GSuEYM:E"ju;0ivy3UQ&!&MܻYq2!ClNO*ȥq?y2%Uqe!CuF\~ؤ8h2.o!*%|CU|.m4OgSWת6s/;݌B ,,"K^))V=+ϽWC\FJtw':fYهGs6x՟bGHB2kTM^&EqbZü 姉C͖ulЙFC0 u,iD+IxK!}Ǜ i=ceǷY>o^R+!Cί= 6C1]:M k:Ђ)HQD^Nu䁲ysP^P"-SL6KCU4$Ǿ3;HWc߱iFj vQ=.DXC0p$>yZ+$^˖ =UBåC;GSE31 leW"iUhs=1w(ʀMlDbdmGmZ*I)pV[8'v6856O1-F#`0ߋ=fQW4Sma&Y}la}VR0#KV[BO ZLRd>8oTQ e|*VrCƣGu0տZ{5`A9mkv-M&,Żmʜۛԯc 2~xр!fT-5 \gL V&s|3k6Omjy Gh7}E?fbR2Pxȉ>qM+dO,5\%9W0F#E>S=^ܝM,rbEl v$ls޳Zܬ״0GZ^ ],xORl:Mq +Ɂڷs1(0&,GKUjnI.MZZ")-SQn]#M(=&>zSLacStsΩOƎV)6CEbVfϙӧ <,v0qt:҅mSf a^XnRҠ"bŠ}`3ȮE~-Wѝ=H00?[; 2>KG]\Qys?MBW>'յK„aWR8i/J*BRPQV\V$,yvŅi'G*FWIKa #A6Ṿt>H(깴*##ht Ӟ \ :L&XlTGVܺ" @eʛ3[yh*W|̴m6m`+"}JTd?z[۠~٩9tn oř`~J1{oDSH̝ϘW˒hszȒM>8VܩfURqnpb`ae 9}sԮ~-ubb 5$_$,wE(!/+qZz ڴ*1|\elipxG)r㟢 J-Z [1R""iu#kkg~$ W0F# [KPɈ8 1[٥S jDȧFmGRUjnjEv ;B (||ZɦT3M>r/JnbbuyxNMkK-*Z"߬rI΅e@yVa?vn#RVQӵfHW T5 #OS]vƩ𺪼w[՛)N|}}/ߤTuܸ49fMtTLU"k9jn6thÎJ'Æxf7 [F;h NM==64ٵi7 F#`0?5kJ]\d՝4<`ވo_KfwR%/9lx#q-uW,/Š {OvH^F{ڨc蕯ej+=|λ6`0F#_A@3gufa5٬{ѺM\;xF{b\lW"  N2/iPxʅ$93ME;NOhuwce^q{;L\ӸMXuWZܞuG4A0bMaGżמT7&\4g-6zFg = N-߰6/5<=s;U5d| M=rQv(NlkÁ鵕׵<֏oiRvぷj0h2F#`(.Nzk̫ysΫQ~;D=|VS;w6C e8lOG If思.hUwYpYjk>7DiK8ylrmWp tOKUjp|[7{5{3󿾨x mk8fFX~*? qZO-Y 8w)dΡh?Fv^[RIޚtA~L+9p%tAwMtw .S1F#BfAp@FU[6 ݭVƜs澯ͪ8ܺVVV:b*}U3Fl(OI%Uw -Z.B}gus r.#+\3QTs0DKUם- C/[*/f~YkZk]e6f(/UeQ`_P$))mӛܣMQEOb[o +ՙnSX|3Iɥ|{A7ch]ʒ\|q;eB(&Ϻ-~3E<Kڛ +}ƅwמvy.]*"N NDF#`0ЄIS[I{ᢗƗн- x᪹c@9Tv{CFJFMG֖Swtab&t("½Ov5\fyAd I&N%ȄTXPz&wf ?7++v*!=AKPI?#EWz>PG8Kf-7UOܝNg6!M Cשzd?t1b+.F#`T:1ر77)USOGϋѭ +}r)..j[o.d p^LD'`(nX͜ RsSHBt:;:<[+^r@Է7tܸq;5^޿ 'mY;^>ooPыqhQ>nإ7: i~sI@Wbl.`;=5\KDbhiHrm]g]?'U\mY}e@ں0zubCT߇ r浳!dAޣ5%s&lN;裁 )Ɍ}:5EQ[g/> OWo&Ɔ"reHT҄ f&|ň[f[& =@ﵮ"N|ұC2E"@]Z:Oŕv7a}NɢDQJ\J| |>7? OΕ1L!3(F#`(wU(b)A>*w*⢬)LVh4!S6ВCowp_{ۮ4?+զ%Z4La;[(i(u4bWQ9`̟w p1̩vtg[j[#{uC{Qs!DmRJԭSon##%:M=$~FvbAzd4LbyMfc(%ҧm{Sln>/<xhnRY8o5kBRZQ:L%Z@#s*2 ZS hȟ]m7nCJ?E Oue=PIJv ӫS<";T%$ԙ~)g(QW9*C9dêK=VBڅ$&dsjf\`0F#E6zMݭuiR K$"15[ :Z4\I UsFSKLt/yJ!h 㼸Ƕ붶Q7H9=%&QD%֍r ! = -l-ܒ߆*h.(荻 m]b+|.gd}FfsG'p:?M@*m鈾}űMY]w~=ڽ"gt3NJ>JA>b>=5wCQrm6}0VAa7 +S{h- E%p K`a ~!zZ⩃Gt۴넦n K({ProV܈lWlj#p2y<=4dLªL6ۘDž{_.ڼQ$=MtIۚ.S~|=̀`0F?iYԋ`>֭ҩ5"NNTiS&V5z U{Γq5`W>+"X>a FWrSQʎ6 æ^׈㵴/H)|&㯌OdkbSZ-l| dV&/J\LNDo=L/Y7wSk+749d<͆3x:5'3RG쫖ֵGX liM-}fH w)ȉG 4yb3(05EnɎu}źR$g!GQē~ૈܨU@J*d?#v>Fl߹3uґzr+j5:ڷJ(?4z!h PG~K"i3?QFNtiv&ԩt hKDR[aN),uh"DJ5D݆dʾ~Ł+ӥ:^\~:]֐E:Sd \ҿZGS/)ONmq?WFB jnߋ>es}$WRsO 'sEıa uqvÎև"KÝW4ss Wo47)C|%"wnw<;EnkSG 9{_8=L})}2 0F,*ۨ[WlU7^|ԊuscS}?򴣳]RRw00IŔ!{4VO$ .W5qy'}TާTOGϋK1+VB3DNm-v:̚+?_Ā(  *1_=}x/²H&~gA(ƀIeАg.O@7jlv ´הXD`~yd1vs6_4u''=| 0; NdԶU u# π44FFP"s&j))*&\qIkM-L$ʤ)*:)dN}X,r)u]*6x,w?ƌVU+ǵ2dEE%zv7ςxZq!7^ԬjP:U5ٱ!H( 11@WCtћR5{ྑRB<ʫT:]3Z+mi|̪W6O[ʭb[slKKz}UZM-,:z&&Hϲ!I@T!ٵ_/ųd 2S:EEp3җOfC986Ŋ tq4p`f‘_.kũsh9gӺ?h"@ -t duu!/e t?]ݬtSn3j撤CK̝mMq2 5.K}=]?4`Hs'Rf-lTۀ?jJ^qJp1EDp;|(,[S O“de?Ik[ZQKzM!NHRu_rϏ($cw51w&[TKL`pXV#y]5V+iQ1Bʪ/kVvMUsv͓o?TY2}l{p~ĸwYÁˏKJF# U Yڍ1^i4S5>2-%+CSߧ;~/ѐECńNfv9.~џԉWfW[@xT]eEYkw̗_.# ){D:R& D?@>gmӳ.~Tl`4.FCKjS'^>e7pc.#i9F<] \1y +ow4-Ir4%v9+xk[5F2j&/ںc"NQTߕ -l9?H9(ڊ_w I^>ŧ̦MFʮR}6 Oѻr/ބJI&¯CNHz8De߹ޟ z0ω'TIMTЩ,*0eHE9㚘@q5,I8b *ĭ헸b"Zu?Ŋo"~P"eHP sߞ ^#(jUw?ߣ]X\p@:筃Z}PО`*lC3%|g?P܄A",:NԏUK-x2@јY)(͐y86yEG'h<2FrAS)7k"FH氲*HG UpPd͗tmu:ˠW$js72c d֪ [Y G1vt5|v7E ¯%_{%RQ^[52XgI|aՂZFXFߺ;  `2#W6@,- uIQL]q(&#i"*Q <Uf_2R-w Pu h&l6ab}ӸlP_c5ݘKkGy@cX"tBKԶ-;Xg޼E5+Ec~3> Ԥ*"Dݾ_|x9Y/'*P{@D#FKT*r ѝ 7--#-c疳զ5gIGSl峷J<=Z֡Y;9"aJC4HM9,EuDrAF hԨ޽2]eˡ 5cyJ@g>NJ~6w=ݲMBP*=;_#G|1wՉĺJߩ^Ӝ`pJ7ph@f+ nʌY?V7߁IHLV x_uĽő]_a[*('—"O].髚t=xw1D0\D?gUA"iWUg"?v#aZVlRRH7KJO0)&8U}*P|Ms>k{O]]ν(w3 %EE PlRC'Ǫ;eS~MLzPK6ֈ't?DǛ5 bVS͸ XUtxeo=cڐ0`~5jk 9MBM׺tİ=, }ߗpQXE ty-Q#A:cdhȉ4CH=kccAB _Dϲxu=ZoZ*l$o*kQ0hX׌C楽 RWœ3QcaP}p!gTYkB*s6x՟bGHB2kTEI6Bb#aʓLz%h7Lq: 6 )u%Y]Cϕ=aWIٶ><U,7߷KFޫ!z WAapM.;u)%;#-8ߎ%JGܓy",}tW?~޺# }G4' U֟P9D^m7[jkVWޛ+o۱uIR+!CG ᩨD^YC`IzQN'm*@TE(w ՗7VpPNȚ.\4,OysP,? C9rbvJThQ&ħmHɗJt%ncZ;H,P8\">s;v4HMۃ׎]=7`°gB7^F$>yZ+$^˖ f˪U!us޿ҡ^~#e#-H)$Z 42Y!f+{V$"l4Y9Qc̘;e@&6_"1v6ԤX8:v6856 f=3]r+Oͫw?rE*ۉW֪ gMp1Ek?hhg3I^o \gL 49fsʙ5ip~q6<Grxu+fϐ߸~sywyNa"af˼>T8Io{1 ڌl.9Q,0X/D/h] !*wf83X&mM@ֽ%=\?89\|Njcnzh9a\&_dɨ26y3UjM _ksIB`$ЄW;;=^5#ÓgꠓqJL>&J NB߆ۺ./¯nzW'va{Eόy[D&s~ʲkG܈/aJq]q(jLu<}?;)%mqK\|{.9SA*n$&)i BI?R\]F$`H).pnWp.EMbWr3-2=L.;9"IUEs_"n(őڔT&i8RK@G +]:R8@@۔f[oHtH2֬>f2cְzX ?HTI(;S_o7v LԷ0bႦqbaN~(2t=k %&D:CӬUʹ"ӀX珒Vm֐"~ksZZ•y̡E>{`v&!FP/R.޵$vϘue/,,k8ͩ!a{B CIۈ4L$=Hxnؖϯ>z9^;?Zv[1ugjBĆ2"ٖ쨃0շbtzT6cTnpAQUrE/Ot^9'/J b{__-v+a.A[Gje:( 6Aн7TTV ]!I[r|ر9n."mUjZ@ݫ9 Y\">]FzLC% PlS,Rhٺ?*(iݱ5ctuoݡ >(. jRL8С;C5ld˱-&TNhzWX {^UA; & V1ʓeDWYv:qZ2篌S>ٲD*J l%YwӢ##>QScG׶z#>3_^U K1PD{;%|=^_~X/w.Fv_G6D% o݂ghdSbʽ6H1.pX֧e[z-+?8yNy!jAD'0Zýuxhj#Ry#? I f'QD@~g:6: 6lگ%_yGD@D@@u~TF-l׹o3ī;}whƧaQX i¦_9w` wqo⫶=9mmYNnX^Z9m_x#YNA [c RŪpYmHhS ,Q{G &7.G~MzTK6etCd" " n';fԖ[ճl^v~Uzx0c7oi(-#G$:imwkܥWw3R3:[{w$m=J0C6$nt=طn@RcE?Q&C'_+!j( " 8m$tkoNJ'=>G{oVyl 2~iyҪ@**K.\6r rŶ3٨gDFJU7$uGV ʾ##5L) TxTCԂBewՑ" " #nE6|NW^Rl v, JIv΁'Bp=<6k񮤩S; Čq^.,{s ALskkPe:宰e/ʩh6ѽVAF 34?*/e[>8 9ͷĄAR)nsnrYCkaYɅ~TaD&4Ӷn@߸Z~Ů'IFt1x[@ϖ!v0s c.==. k:22?!.ZG_RivQ'g# #:N^ֺ om^w2Q]|hΫwڝv|PVv@J" " Sp9h쁳J"g؏Yd_fXgNMX/%¹۝fk$>.aT>CC]U"ӄ 3naa[y⃏Jc*̈I~ Fr3%'5sR45\*M ՟^J,S챲 J]&k dDop=ϋWY;Ud^\mrH4]4yKG_Ѝ[)({jh0h!CG3,SpG_{mhBN#AWg]@Fa=g;@" " C~DIlP(K/lЉb_tʋ2tը0[b:ӧZ[,6PθkO հj{}@LENG&}dU $a(U| ⮋Ш'_6+[>ŀ#1NGB"$"A:c[ˈ3)nYj3P(rcc#zPl,_DQVLtx:9icSg{0CQ/\4ůud3V^{iܕnmCb|}PV{U͢rsC]ρTC^'Aȱf8u˥/?I*zED@DSG$ 1cj  g·1mA bQpxk)jfF油B@z;3E0;(K؄MߋHʂ,Wݠso !77tm\6%,-&QT IlDަXH- q>.53i-ԩ4*B1nR`Z +:RuPKCڋ]aN b/|Jy!*51Z\/eg:,z)oN77] MG^#S߲5̯\y3_f«H&nBH fգ%P) eD@D@hLR:"ta Cҍ!)5\0'gqR~8;[9)X<"&*jsMnbNO'&)%MI)KlN܋Lfu4:KSrD2Bb%윚zᔴ8 ||)Di!"rA^~!=5kiKt)'q3²kJq2@@=GٍgucG,KD@D@ I\3aXn9vsGbq\7L<Dtc|H&V͠,U'U3ng7pb#23ʴjmbAk2|"6zU#!+A2"ִ)(WcD/N1$PPIl~3]~'%}Ϸ"QŮ1kRRn<^V=g;BeQV*~ի—>/WyS@ rq̹8|m{Ƣ-_BrH " M߶mZj=6EԠkV-K.63młެskUTRE]3{3>f%J7v{!nEV=%2"Ly@3sP#f(l~ ˀ)vm9kklˮr$ܖUk\p ,3@$`IŐnT)1Qs΀ޙ)¤;13O)l<<[vqRs2=1M'̇w8e ֪ G;Ҋܳ.h)u0ĤHv7 >Nn_7cEƞ-۴_lmf>%.8*ED@D%&aC!oJ1-`O#%z|;n}y/Tg8קq>'Mz[11w9&c۷mR8>WBږ:Qز :y-M]i~ձ4;֊f*%K[0)G{KTcJ_C蟈\qgR!$R`_2kN־7{iX.s`y{Zȥw}b_=4]6^Y&&3#fWs2%KT*3" " _SU{( EǺhװIYmyyC͙5XjѼ[\啾B ,gRQ@/=k&mC|+ߦ ȝ*gUoAҟ^4tr7VVT$OjStܽW)I n9B2H " "FأѬ9;"]eWsWwd 2{93od{BCQ!" "TW˅ɘ&/3)79|R70 Ejo%fdl XSL~+.[O(E @D@D.v>#tWńꚪXί^h>" " "_aM_mOa0L_y Қ:h;q Yˆ!} ώĥj3o)*ikpB\d*9]T<ՎKqw4Bį8"oN57CF Z Cnb *c Atld؂M^<$UA^_ܳKL¥Seʒj-m~goUL]',o#\;yU^qv2~yr'b ʵ͝k}F gxc=Ӌؐ."AqN/gWl(ψ}<O4b?`$F(%ڞ!9" " Amdzת8 lp*ŴmTn ".ͅ=&=5.GJbueQEZ1Ժ.{vcglyUTz^TO}$eiӌV 4wߍ}mD&{YJj,0U³/!*vA̪va:444,2s~! Se\??*=kt3O Be:H " '@!8SV.^d2Mڌ|W2aQo/;op.-l35e\+M_{ai]^=7o0X2s/kΝI,.B)%^?l蹹qR\,:U n8@؇ N6q㎃2:40k5zei7XӈV4y]*A2D`ԇOs%1O ncԈ n&qvyylA_BSѺlHUOerӻ.;6wHP[=JHyr G|@9 MYIyz^]E+/Cs 艎uI`=c#ہ jL|&Mvdbؗvl b^Coޚ1lO']|rRGi0fl镃{H#6z/MlMh*:ȫǪ7K9f[2teݶ7 @D@߇y7I.>-:Q` ,Z6ꔯܑDMtsv*N{+rDaҪgʐU{R_͸9One)q#V!1 gjy#s)4fˮzx@*'ߡi4r„ Z^?*L]SŪX%Й޹Fh`0l^V֕g{h?cj[BtAeuӠm+^(k YNīb.\+S"\E0Z ՛7~ƝŎG.O:,- '*/Y4Q%KFJlX%%{!D*9D=ҹG=L6/c(w5Ҷ$N 0QYRBi^B iBT6;go6w]uI'a;nO=} AB?zDL O:e‡ԆYeǾAKMס7Lv 61axgmLoy=LTqHw.c.yHH D@DiT&eN!vq,犟 ^f%R55}VݖZZ_Ӂ]]}haiafI`O(t܀_ŔiHjiyj^%!fd9 |CTVE2y{^:(:bڢ+ f u1c!."Ȉ4f6M5BTzC6TʕUA 8Gۓ1Tۉ ^IK㔪T-Dv{u|I5WU&?M J>I(ڽW,[ HzT9#ڛG}<%R #mHpL+4tXJVpɵ'w${D.' n`2aDH#6t L,Wq+IU%IghL"6Z΅ ;: tq6c]ѷvl5TJt:͡,:bȄp-1A׮49_.H " oB@lt.>*2oע xV =C׿/3rh/C“Aȳ4ꃵN%X\tLY~t#EaxKxB , h o}D\CԬaOj)aD*:7b׶m /">X$h7.e_L'x,,"& ^wGFk E˟薞^u||=v^CNIr<9NjbhfF*d&m a&sg3QN&Rh'( ӫ.;q !%R -Lt)Xz\^8bۦnka C7%|":+'wj坢FSo]}HB>'LBk?U,^1oxY2ͯs2R\v1O YQ9y5($Jfp_ZxXNbȄYCYXZxdY#7?/D@DM,}Ѻ>H?MMΧTHtiw)b x + %Ud3îVl ?(9 7P _rdp?#޾wZDLY qSmVd+ dzw!]?w^ER^H)ZF$= y)x^霧vl˜v!kCzdW7z:ak RuNͅYU5I%9 :uHbjaLPPʼK{|mU'l3í1c38x$]n'1+4fѓE+v n I$xKָi\T/6+tc(,]JFT1*ڕ_ ?dBA" "NT&5m8ajXTт7w,aJ}Bn$` ,V0C*aBؔ>dtugaSQ_QoqA\Ϊqbc~FcU!.ykl~{@b*︩0蝐? ѧ?WFֻdeaĺK -`W(#f[uhrQYgm^zjzGUWפ|9=2UO?q]l wO%)ٟHusY5]a|r>{C=ݛt^3ݦFR$GJtjv5իS=fw紳kwC`Lx{onz60x- 5k2qϽs+o78 lMğPFkF< lUw;b ]LuOƇaH9YfvbS;kGs:d>d.*RxMdÛĜMxT#q~]6y~T,ŽrnX+Sp~ v߼XFQ,W|W&{9sh uhC&ϙ9Bfk2I'|[ ZZi)i&*<ձ5K<jn[3a{FyI*f,Ɇ'".)%2eOh6 g[ʺ2 f9qH]ؓ+?b9l6gZAX,0`9%2 vXw@IDATHO=[ˌYaY,T"Nʓ;i*{/_e|.D*<}hl>ϩ礙C "a_2NB5Kt3KgK[y]\căsW2C3SB|kV܃->}1gkRaܾn񞞺vU!#2I!a>vÛW(12" "<}劏v쨃0BrIdHr:~M `өqej)eB+=/JZ&~paUIYvM7>gmo_]UF&~x8U$FN TceN<'RɪwRT *l:sOhւ@d4<(]$uD*_yARe̻ʷL-t " "v|f7-(NXߛ[OY8T"^w< " " " " " "HeN3Z/@D@D@Eއ>g6/WԨDegզR-tMsbۊҴHbmK%m_}1]ʝ'Ia& :l^̓QdX!gqbo2 /NZb\Z Т/OC;;ihW$6+uс"%j@D@D@{ ݤWf ݦ5FZLc6ENkeB ]dRՠKKSW0R5i~rx6f4t&;154tZtGpaY B05F`oPG Ai>ẦAV 2']QVjs2Q@;|4F#M!6;go6w]u 34DeI i:&y )Yc mhSS @hr勑ȼQ%EtH*!" " O'8}6> cI]s:6>*.|="f9 Aqع@c/ᕄBNo u"?"ΆcIr޵Il}EDo&Cir~D6pDd.a?"$K<%'*x.Czʼr)B" " " oijf<[hsnԱA .t._& F\!_yx7g>RnյbWKln5Hcza%lGk O atKO/|~ĞSYe?;miBY_4S4|FS7W'2lN *[̻ĎL "%*@D@D@~ /[>cNJƀOJf :dutB}uww-򡓕bU fxb:*BDr" C qCi &fË{9_#_$lߘ ʥŪ@]ΖR:BWYm$CD@D@ha[ѨXƑq@RҕGEc h;CW+SQyE6mqa1S1(xJ(yw@ Ot]ar^25H>ISSAD͘|wp(бlbː6xw M($ wl D.9exIJw"):EJD@D@D'&M?dثNt_N'닸7ZLTzFvN\|rs]b WvaG쳧[/D^a(aTv 54u= Xd1kv3$E}# pds0%}ltkNj'AQ='OZFl@L4%3Ǭذd碁-18x=jiN]0/: In))KD@D@Dνed5x[Na[XQ}\*'d ԏbyu#$UD|).d#g =NŷOPO l"?r\= l4(M#ٹ_#`zGēY.Jŷpx k؄Aؐ"|/ b ٹ;uzo"Qw<V(jH~fz5'm7GJCػoug־)2R()s" " "S X4`#;==#" " 'dnY[Xn&?V;${x뿦cFD@D@Dh7 H?y¦ciV/U" " dtlXWS;WVqj̈́ )'v33HJjap C7'bq:4ipݮ]^c99!x!5nb *c A P[ )rʽUEߓvp锻%q|OCƏjYdwVm/mqʐV7pvpw[ÏϯRV{q~MEZ-}6榬Ɔ(VW]huy`h- N?uynke)׊WƆfCD@Dw"@M2;N w}Jwף|-.m=|miڷu:I׿wg/.ǩ.`íϋ#W'_V]jS4v6oȍGkhbH~U/:.:U5Ph/LݝYDD@ @d p8epLI2eJ.JK"A9O$225ŝ}௳*: zs)$ 8#ؤp'%ɿ"q#R@#A+/!p6ҁ`"Ab 7B^JaZyn@RkYwsU9ڄlRˊUꄛ̯.Ud>Vv^!m*P Q,Vao{sܧ\TDkFUj_r3w6r: ﴟCD@ tE˶ӮͻZ4'7Qpd 7$.mo(.VWLj/X:40{IN<5yr^Oǿ/ewb鋖bKWG8?AݜAHGD@2$E:cC<צG6~z7f<s^ѧДjFZ] !q2;x^oG#f`152B"9_eډx)0444X`٩ES&nC` |ʂ@Ϲj-FNg\: L!"VaS\n!BV{(GL2̀љ:@Q?)yrd@E4>EWMWWɂLT.U/Wn Vh7.e_L'x%< XC~eEM^LB&W )ٰd< *b5cp4Pb'B[60~|.aN: +,pK2>/j)0ܿUJӜ8PD%Wvv;(-nѥ$SѦyAK`YE C q!0BNψSHNN_OJ#X:ޛNM0De%`&MjU~TxϿf{]_?(X[5Wޔ;`mtp[Ё)+,W|v.vmjo: !5Yʪ33[̨k+ԴXȯ-NſN)4:K.>I$57.KKley"Fm\Fcspݲ1ee_&>̉WD @D@\/Qgiagկ6&:CпA_bN*ka&f(<#cjUYcx tb華={ⅱD)Ԏ?H%alVET(U*+YS'XWPUS)J1IXy˻0kDeWkIpbe :F@3fR)^}8QT tMfAt]e6AN!}0ǔLF Q^m{2:ɘG!\tfO6 aDAV1("^聣Z,$Uޅ~Zow>X5ӽ}'TH"^fLDi!$;**,ױi7խ[ fPJ:i$^ Y&O!bM264beª ƕE0ЩZ"PQe(*SŻ`*;.D@D@JIoqE"=sf̓AYLW$5]}j~b.~8j&>.(ͽSTnśMb 5W_䲬ǟD {'{x2>Q`WMHqvA)ʌJ(aYt *dgVg?#2𮚺(G cs܍}>lAk3/6XEVNU?Vc %OT q7i֮&~Đhtԙtq%BIftSf `̲naP2 tضv\ 0/ZМҟQ9RCrAVv3pkec>ٔl-ġ>"]$ONJ(vV562=AZ$OѰl r{S+>\BjXvm _2i mغC1Q[SwUeIÆ0ts~ò]^.TS P4u~>[e+etly5Q'y=oA.X$ҭoA\Z;A&SFM_,-L)H6^D@DSاeV6u-xu3붦+=U`8EysO cz47'o}n~Zf[)*6%k {2}SW:2ܹ*Z j͚ܩL`w紳kw>6π_MYywn$R/.NjcGD cœ(66SBVlKs xU>tSRGB˲uI 攤^s5Ӣ7{ Q$-e~ MJܽ9n|ف`ZU1Mqe28KqBJY2ꮌ -~ X8IFs /E}ćeёBXv@f﯏6;.Ak[7^yE I?8»« jAjTvI/E>]Xj7D K$B}A6uE 1ץ ML(Œű8uZ|cf;40hfIX *JHnna(J Xa&:_gϟq=מ* h|6btWHOikދNFVuae%Z<ܹ76TIڵj1;!{h(Xiq aN 6nxU*q~ra A ilH1 " )ҡ@S=yO S=wtCl׵n<`jsIg+bÿg5pj2|y=߈PDBLDJMġD3ŽrnX+Sp~c٠ĶB gZ[ëxEUأxZSSnӑ_l%"l#b" ^s~,2y-I[ 4jJ-/i޹rt`3Xόy9~W'{#L'ˋFm M5s #*$Es3(V6^}%Gg>ű~ vh~<"TmG.Q̗d$)/ûD:«LĩQ1û^9,[\}$ K~O*k2=L.\vB_#@G f`3 GI1*ɓڔxBئ7zCCyf'43òv/Xx3W3lkh`@^z>EnavF,\Ը<ھ[2,qEνgͷ2^DHo4kr`4`$'m5\)ֳpe?sk81LSP/aR.޵$vϘue/,,k8ͩ!ܡv' Hc?S$=Hxnؖϯ>z9^;?Zv[1ugjBĆ2" ">rCuPN+ߦNwa$Pcկ1>QC"פw&iOt^9'/J2{e:):+a.A[GUovѽ֩9{ TJ0Xir$`8(<\N{ؽKY&wU4Grq5 MTv3P$OjSWḪ%Z?( iݱ5N.;t7azЪIu3\ކC6 1ְu>:p-ǶTXV:i]\c1AzMTQ}R ['o/ܚlZ K +O bAD@o'hud|okҭ, !"GV9eTJz\EI%p,ZSjc~nܿ'o_B7?ނ'GV[e5kG\쀟N" ")~Sv ݣNa+Q,ehiB(ՂoO`އ{/ jѠAծG>;|OeH.x( " "4|F 變5UV[_뽶"}D@D@D"@6_gÚ`'ݙXi?^~ߔjE%m `0i0h_oN bGuhH3]+_I\srCBnk>w&T>Fo]q,5y⭹t=[Ĵ.\:n(hsvD$z~&ٺZu’6µW(_5$a"XbnRAs'ZD#-n: I5+kyjv\{>ӈ/*kEiH)a|L D@DpxKENZ4-)j<߮ݠze%qff_/]RMPxeDyL%q2Xx{<ȯý(ͨ\^?n'LoⲚWU4&˲;E{FW*-%Kop{ejruhr!?5Z"9 퓡YT ϟJkt3O Be:H " '@!8S<06i3VJK"A9caejH;_gUt|Y}s:SpI#pGI51'%ɿ"qHPJ] r8&VpH>xI;D84ݜi`nIY*'+=OFd+p_1a}ښr.2"y"Wz]nwMK;k݈ c"._:HyJ ]et2k9_xX!:9O ak%M矢".(HGoX#ev9^ _ n7oߝA[&(w$QAzox.ł@5%;۰,n=6\E.]棂_M:;IT_NhPW=1%:-4soiIj泻I$%'|J)e j| UjBMv"iyZή,ۦ7L"e~pwZhNC.KR#9O>ԭ R M=bזj޿A &0f3Xi~[7-'Ԛadu#q76[y5{v”WhDPuuE~RAio!.hkc iI 3oW#nj0XXph_$кK+\ ;:Z};Ou*[32&ĥq:uLc\fXs]ZD?+TA?vX?խ.u1XNHSa i8\mϚ}0'ĚFe%7}4;mTo2aǴl5qQ a=j&u[nx$9bWGLDj$֦.uyݹWؼ.Ҷg}6Q~}uDW'\< 4ZtH4":fǜ( 9E## ܓYiJoՠ>6mڷz)rlܕ\cO'|D28:u8K .!L1w[a' ~aBkG&Gy";h tdAuIJj qIq=_iR\X氿>B.{PY5TO~>].]1iڛktGeKwp'κ\V*U`>>*"$I1h~71nù6y<:QK[2}׶}}Z'bQ^v%3?ʙR1h%Y>3CXI^dhVOXPxR $#&lG\NszZzi}J_S9d3d MS<@T.j34_Ωp:ژ Cs$$v˽w"23J*9e*,'~9Aq9"a dmnl<,TExR35|uB[TvnnSQP<hy]N~6n3m5,Щ_D!o\ۄ5Īy-j393|oёA5(,(q:Ia\UJc4444444?Rd͗|L,ovck }6f*/@ u ` ᰲgY,a-J,J. Ẍ7)RXδ73:6vk=iq씻 p6@թQe9N%jOK>7+㬽ҾE=RF:o;S#YԷAC3\Fy02u^+;]|[ΰvKa76\qŒ)o=ϘumNjwRS aaȞ^%/elrHCuNW(c~sC}f^,5ʡ5C= zca|c0OuL~.U_Su"CqU䢻*1վi>s%`O^FŪ[mej˃q], ubP'cEKaGDSuH51:˨֫Wu 8%ONi䍠Z+4444444? jo6al@ o*ey^ #>y擧9-\ xazvcР.pG7~" .e$pf_.gzLiF^ZvQ(:^,cGFz .,]: pd<lKmXGkލk3`epPȅɒ*dIq"~6 7,dtj7qcO?ײr_2OŊ%A(ֺ,+ QGA}~&VY$>3_*@]2A,Ttj<% p(D/R!sV)ey2m?D>3w,x2UEU^*KÊKp09K<{xTNY3xv ŢjN8_Lwk`} ztz拡H!,,AG&y 7{G215tkɮp,13~#ez7.]UAhl^pEz_UzOC`OyLAAòvm\tm,G$;5pÿTZ'^W*&cz.ag4^9TO8-Z;vmT%u7Gy]<xƣ9aeEEx+XǪre"y,3ϪU7ζVv; p:94FAg:j+.UVYUQFFFFFFFJ/ \V.sIN?+LZ{fb&",^vFFFFFFFFFFFFFFFFFFFFFFFF@a[߿w\~sn=:kmOÓ(p~ZD?WEcOkF? ti~_L*/\7& u¸֟JĆlho_98SNukvrZ >hu/3cu^{0?c`6:v3<$tL=͢f콈_WȲ.).aKSN~"4۪4ú"?({C곽JN93F9KBXKvsVϻ?`Q3?xȿ+e8 v-HCX3O/XnUU~ҹKr ˋtՎp=졠 Y]PEeҾ T~cw"';T9}"hLR=<۪U}`+)|u* 2Rx`ށ,Gߪj ׄ )4LVZeH71SS)ߤze_uP!um|FC:-oR;Twe~UhTU|DU/YW&k[ꤖO+fG%leP]_K!TkXаK845bA؀~#uE$83#P@]6NGR"ORt.-i{:'x8)'G?bz)tLTnXC6!MCCru:NQ8;* 璲 EbY"8(LB"7*]gw r\ܹ*-bu[v+Hsu%_,n l֜w7u{X> aBUY?4wM6s\_is m LUY+[[ Z:oIٗvmܫgn*ICFEcTxx]+ RT/pʎtRAX8(ټB&\}N22#ԤKij6*AF嘵[;Mќid9Ee}OCV7ĿzeU4ɐ:K3DeK-8 >ˊPP@IDATMq[nȻeT#onXfT+W PkJij2(AFjH% 9Ӥϼljǫ ɞӹ_,ͺHqh‚Ҽi#Hԫ)Ώ?r&yx jHJؤ t۴2P͝q [dNO+7' zo{y%t慒qO.5[Q:tR97ݑf8 F#Ȅ㓻'ݍ=88ZloǕH$ ZO(/EM!G Az<ݵ(H'"&KA[YtB>] еHORgRǵLNG=y Ƨ'<~I<b@JGŠK]3Heff6?0N[|ICGWXXZ+晎Ā?oI=rrrJaQh ut+7w"q PP$ȸ2\R0,fzlt9W8rB3^ggQt <39֗^ң.hX} + {3P ?;FX$>,"YXIb8ҲR\5D Xg*@VWi!Idݥ%o@9YVB1K߽{7M1X8KvGN'W-)N-{P lhYcAąhUM^ٴG0`ېr PcEG%ŒN@WԁTE0~22v)2Gc DK2$IT?5yO~d?>Um+{nԏ\Gg֭PӒgwӡyɼ ҷlz g\)^`t$auZ;f<-gW^Xpm[OKD :TM- ɗʹ8Y'lm6D-.B\M#[2!sudcR M=9sCZ*:Á>QRCnjjoa@&}r5y KIn˭;`ن]t،قQk٠aILgoSoxvrٗ rmi+:2F4j?d.nVM_rƵގw> _p⒔d- Ln:5 poֈpS1j SdFwμXui=>BEֿ;x*J$KO#p N}!A!nϲYFǶg}6Q~}uDq$д<&{5AJS>n#I}yDDT =2u$x7=-5KdgH$QTJLu0 ;'Y zQB9Jd!SSq'xlܕ#`P"7fTihsf.(K{ڿ*V'B(C<5!<{Sb3dsF9o2Y- (O i rf,LcMT wlKo'aťʿ92*vztuЋCކ_ ';%eঐ4`߿*1d4G)㋡W76(ɑNF]ۺ+g}uZ?ypjq04Pn&:L`[ W_Ǡm^5} z}wuFk_pAUWiuC~Ӿ}=a03, ^g Q)Iϐ' Pe: ~CV 7S0qƱJjNH TFpjd;=(yZ\ͪJTo՝y!E)uY XCX͚T95Jq`NUKOWH7X'4bQ7O×ǥuҥEծmwe=kx?~R@JM1:<ȸ"VluT򋓫!5hX?Ъϒe?ԩY'AE0Ҷ=pPY} Pq߶gK-ףǧwCKo;F`*6^4(J{\–&hфɸf𑽏>[ls;e_H+ ^xٵ:~v#FpBnM N3ó20P Q~h)de93sl3~Q +ۮhZ֩|@1aVt9VprحmYm5*tT,7Z#ASi֎ Ȫ^r>Wu8aCvw3((E%i7{}ä5/Xׇs,6=I_34PUg7Cj Fn&:hPS|H T~7vuj}c* ,$8)FF%--OX"ȯrEƦN<ۿn]=᧧|ɢ0$xJ3\Xl>ؽ58g:#HJ(g##O~lf"z.iс^*Nұl:]YX f K:Wi,/܃ Sq`ag]w"̉{w60j@-S⼇O8h9Jn?WëןUῩI܏1g{GXP Q' VT֢"D>b)( ryYCJSQ^Z!5ET,jޱ .u,#h ,X*O 4TVT.U+uXƦ?Įd_/׶| fo|stC&]Q/;J;]Iʬ6!.MJ*7 3;vt2Y1O8sClz}+3nڷ`1l>ǟ@O,BM^l4Z(˕ll s'g-qFћnj*;i|#Kƹ{m8WYcݽzD\VM5ZRFzB[!$:>ċ"/EY"l\}hnBk\VF2o q%$ 5^| I*STVq;!| N=e=&WClXmS4u`٥6|:, 4F=>Fg:7jƶ#.96^ DQvݤiwg9T$T`W}>/xSH13 J&A  *İDaskɗyD:V sc׎#FKwwP Mck5vŋf/̚OgҨ6( %:uHZ&W> *m$+܈xgWE#uAn*QEX ^39R]xtݓ{9nةX^dM@VOXPkҵq$I7 5 7^eZ:O*iݮ5DlLի $u]Aۺ!2.g^_/eFJ`P/$Jo %9Ffrۂ8߀Y Зng,ӞtkҰbc$Ɋ+q@a^.6̯dG6kƭzsb)O9ҷhڹ&!BOXj9Dиi}AekJOg)͓ldqIfd|GI ) t@VTmUZY锅B߹?CrfyN $6׾ غ}۪ U"k]J 5N((drrJı5DNg3-X:j3EIлIL%?yYgZщV]=in.^3W|ɔQ' 3?rſɪ4Fu5dЌe^I#BXز]+$&C673aRbaWB܈ ?Zx_ Ԏ }韐]rD vp BQ=~a07Vy?hf |RN,%)YPv6dmЩDnHL+ѱ[I6`M#:LXӣ$n;Kʹ!J2M*alM:9js8?˯mpSvuփz%[ћ.S.^4sR&LcNiUBO@&:h~Z834Wƣ8c0GĢkbVelsOش~\'X~r$rV*>jsuumyPɮϪU5X/VKF4ՂSDcoy$z,l:2V3`%.x?@0`jt/E|>j99TPBa)? ?Q,ڧ XZ(g oH!ݹA7yDzlXG;X| X|6E.w| ^,AB\tWa mh"kk,^݇kgFo}W ixρ(w٦! ZtEVjfL.j3ޡ]×\C_]lǜf]ma֣4]I0Z`iVť!g3=th =By"eOn -[BP9&<(#jYz6ƅ}?0/eTaHzK.F7,Br4lE9x pkSevNl8Wkυ=fX,u})?)AIYNZO1ﺁ0FKO}c1E\W?U\9AAfeP{ ȄH)C52J' FU8B$G 5MpaAp)D>݂C) BVTVPdk~Ʃ*CCKahDO.B'Rvq|۪CJ"_1 raAVEN_kj&ߥz$Dj@ Nl/!5 ^8;{>zM8H yH44¶ XCXjEBNZW0Z.umX>Ft&>ڻ[aH*Z86"CWn?\GCi klr/ R&r%ޤ#{4:LG 20Ѕ ?^W>l3i G%yx/YxU O_0jGvJC j^MjG/%>9{n0.o'HΩ54Ip8Kl1& Xq G{`1YsG .]"- Jqqq Iˤ1K_[\,t1a}m5p C^۠d>]ƽ@c$e}r]Ư1R' ^pľ1ԛ[ %.I} BpU2^&- @M2rv]kW Jb@j>IuN=|sBX[;vQ~^Pu'0[7DFo ]uos(+ȳj7pDw3dy!*.*f!g*I49R pm.w} rrl.Ǭj$vZJ{tԢݱfH2(Nd2 *XTمgo#}Z Gu, X%D5DE@8=I5r+*Mؖk<)a aXCIͻ"qjK9uN,Q.J?Yi:"VdA(*bl1QG~J;FM:U_T>0lir8eݽb~ϢڰC_۶ulm޶[:*:,`Fah\:mϦ+'yD.2IV_غg 3c[svBڪpFFJVoDRDe[$aմ*"L얳l"=ߴ|/ ^%2ݲdO `#Ge)߂@ZXQ O}G2&t'߆p/x!@|i&f#ׄ?_ХK|2]Ze2~01:R dɇ_i.2%]8T!ۿP/GB5~R~Zbuԧvuk¯ CP)ӫpcr_l\{9SrCU/kf#(_/lA~/%ϥYq Os,"BҲ+VoVh? V.cUEꜪh)Ͽk444444444!6IcqTgSخA'-~Fr&D;~u<_)Wg~z|J*Vl׈=$"6N#@#@#@#@#@#@## {C곽Jrfes煘R+ȟnFF<ߕ2;}VS 4AT\ez./U=AaHA .~SN`D5@Hfa^#(TNLRV⢮؀\q>s'C4 !'|h??I<W*ZO:\ =e[$xh:Gs xAA)L U8EO['쨘/KJ+,7H( ܨhw ׿G˝RLV'e{8ZWbNu6ux{kλ\H:kN˽egn / Aa|Df2t_pܜklk`*F:INL\NV=JboALJp-}VƅarӻIn9ު/?vk'i~I܇/TqVޑlM8/ܣ[ g#7?H.-@%w !yYϺNs9tMvwDeu0 j#Oz\t7khhW,#P-z'YK.&lЫ|w-DEdT9:o,e:!y]׮G!wYYOsza8&I<Ir9$ $`uGN谻섗ҙ~֘vz-R""val\ѐY)\eжueѰٮ2g$ ho kԲ |[Ͻ WK׷b;d4444444a/ꭧi){z#`%bҪkKutfn 5-I|v7&DUWMጫޫLՂD:jAZ+/,8魧&(mwԂ\3ﰴǎ7}nmHG^'jhxD 6W `E54AukM0J~\LEE}rBy ]OVLY7HwZPaGOZ n'Ly{~GfIպ"?=4ąquy,6!C/DWi8}KeXpĬuG-MWpv% Ϲh_$кK+\ ;:Z};Ouj%Ҹd:&ٱ ic3m,~.s"ݠVyVSK,mi`DX\]m4Y6gG蓳|Fe%7}4;mTo2aǴgK׭Ws6 X w6Ż$⧆MwPidi`aԩX\u]5$("-Yw4krUH&ʯϿnb5Fд<&{Mjƛ|GzTS""aNd:= ND`c%NT[,(e5ϦMC[7w@ B9Jd!SnpjrԨ~6Wb5Ax_!е q}#{tF8Ȫz==:tI0[Uf0@wYt\xc!uK֘V96 7nx8*kl#d^w l F< ,UFɁ@nےlpiKr%o7O[+8(#S{r'("!oHu^pQ[cz.璔NGhhhhhhh~5THV X3Ϯ~uY9JΪI!*_N3xyW76sPߖ{rO4<' ;u#ËRX >JM6}2I&0` $#&lG\szZzi}J_S9d3d MSS7@T.j34_ Ehc&*H͑ -މs*FQG5'% @}`9,@l%q9"a qC۬9uc؄!N yYZǺֱ:-`*lso77|X((r?mk<.'r`?[ZJԯgܐ[7mӚbռ3%jp?8g/}=x 8: %Z}(q:U PbU)TM(1\G KDwѢAo7K[8!CE 9Y"´;ZhAc7ݽNҎve)Iɂ򴳡/$&k;pN%r2)3(sFn'm6nڀr70cFO"C,5ӂc+Wqjޮ.Pk=x];hp]ڷGHmujd\1K!|h(c]\?wsoc#n)ކ+LT(Y)o=ϘumNjwaջ>sCp2~ /δޖ!Nz7p#</$nM'V^͝qxv(  4G{j葎uOCsPqxbSe%YU]䱿D㬙 'EuʎLfnKnS>Wb eT^^;C@\m 4KCX{ђc⑰єmlrEqz2!u甆Xa[5Bfn}ZFPNUOAڛfE8ʌ%X6VrvOycز9ͥ fmd+vAI` H.0@8}71Q{i& ,{wTm/Vږ3#vU],YtX'ird<tq>Znl][/3!B.Mήn ʼn?,ܰZЩp=\}>U\%Xr j>Ruiz?CBH̽5k\ƓM2,CM:swSH{ق jO=ptxN⡅} Kh~nVV5=*]8G*33[86?;& ^=<4Y\y3 ש^RX ?c $E_FN~ 8 to3uQNHzkl sv1h푗xQ}vYAxfrLp^ң.p>~_f*Js,$09$0XDޓAqȥe=Hfkh uxIiJ^16/_"`GDQKڥN@TԂaĝJ蠩nE0's:,yִR G.GavO~swz1[ͫw CKץF:~͌k'#ǸւԞ랝|ӫjyuLG [S|r();<q:oZ"Jzi`*-9 }S \:.IDAT˦VA[INy?E;m ]~))@oҴꚯR?rhPE]yaY Mo=-6tDK51a9ܺjZn:̑[6=~Ɔ3MXq9UxDS%N֨.lm6DZ4u?0 r5ll _ ]Q%s[J54xM_xΎI9xr "JÃVz9_{= (bC]@WbD`{5Ѩ=ذXQTAE齗+7pws᳷7oG7!2^a}û3GSid܋a+'1 3Zem#@  sAj uE쪿 O[$)-⓸&&`YM1kM$#NZ"H }m$K:y҃kۤ3{]artT`9W>i3d:<2utR6GdrE8+n PT!P x}>N}IuSenvLn\J@T)NM_.rKA n p`~n_3ӧf7o?poH~3k;mpā"c74g,'L"*zpugHHw7κy'$Zg&RIKr}{Tbk)gZ" z8D\s#T`>hJEhl+A'[epV5j2غ.?҅AͮuzTٶnJڢss .,eרqR/7šwSqJ?G!N\NТ~tu7_FB,_G_'=_7{n}vN=g־socw(gh#/ q mӨ;/]ؤ8:dvu1J76-$<[ຠ u 8Ϗ^bZ1}|6uXdB*?,;̵h4ƾ3bY<8qդϼ84oK=wu=XNK| c՛/wz@ٯkoë$$c"$?C$mI {C׵чgUjCnWĽVpe'USRD{Kv8oj:4ܩ6@5$d|-&YPA8eԔxLaȵ;p6[زCB8[]MgfqxTRwbǻ])~Tdœ"hʉ2+RC3АpdA[g炌/]{Vdx|/G>uʬbSq*3OȊ;/5jpUAsϑM龚5L⴬ e"IwwiuQAGAγ6Ykl1a["NFH'eHmYȧ<=^MZ/Х:it'87Ї*,G沃rp'@>w 7ߊ!O;Yg%WSٰ Lc!ӌQZ8myζ!l]f{ZݩS݀ LQVp:0bIIvVd)w"CM濎.Gyf%+KgԵ]}o!ZXk3<qx:maY/bF_aA>ei0D%qՄ"ϽyziS:eē^ʴزSC"@Hj5 r߅BGJ]L~b̛K>@0;A!-]lj3y ]i%Xm%+q=zA&t-lah쬫۴~RAAW^gEEA Y001ur$g2~,S\Zjrdw-R4']r2~u`PղX@{+UgM>",O(*>+*WOs5FkbUT~$䫝ޣ~nGqS7*biT?څrͯ,?k#5ȿ\ <㫇ÃAƒ هȥ:LqܚbC1ZC~?qD-QỈR")9kJ2hZR^NKd&\ጮА\*gT@H%Pj@H~ٴu63BU 4jtsmE݊c@P5mDPP%7m]yu g'%:LӏDv"c2vٌc: i3 E$_Ł*$zv3wVM֒7)vʮ'RbS:0|Vr5%ojujP :*owt7S^!*t4ũ|;j7QQ Br/Q*ļg80vֳV!ULT9. toiҾ? ֥IJB<_*+&zTT{*n~4OuUM&,^8kԩM7F8-%]6gCn+W8+4r;H&wEFS0&jQ{BSf ` KGdC;&6ř'/\l-8҄)7X}"$' JXoǦnb^y[D&?Ҳ caNK5uE pJ4Sq w5NLW1.{' BHwjZW#]E5ZVmSK%&L ez5iv#Vl'k~8ԶҦ9y{MWE{8W݌-,qRbH 1 $!@x:uW颫ح Š6^[yv~1C27GHxclc {oyiξuaB9R4w}'Ԯɍ5^VӹܬOACB;;Khޔdny^hU(8P{#{?H~J&*61B6gΚ5~cg J/6u}TwrTΥ|?ؤEg`RGbZ{{$A <#4=z왳t$ u`0+z|.0Ǘ~=:ޣC^IQ}*lKʻ+]z1,yDe|nCvMڔ _$mrtP3҇V[oxHz)'n:6z N ͌āCuTfv eG6S,Ӳ"$*T f8j7䑯jԲ+|*Od\< M:S}jsx6(<?}mR] W6%őRh7лm/7!{Q!؀L414!.5a5s罒7+kR*\vaC}2vmdysTDrNС[23>?̱yq|[GZ6=3Mf8Qu^C:i_A0'΄ȸ5݄7]kj/ְ:ݕWq^IQ1,($U;E)=vѣ=B Jqw|mi<,8>p:l(1m3j1ԑKEGY=PxvDSIHp $LnJ#pRL&n> 9>"rV!N]htn[y=If#MIU凎۔vm\/v.! Ö7~[7=};lOꙦl "ge0t&w~ VFh sREXÎ=:P⤕5c>;*&( d6rV0TVNSNrUٯ6Rh|3 T& r|R٩z+jr|5:U~6`}wb߾SK]XܥrsK_ Fbzmw^S5ɭG䝎r2tޢCŨSv O*r+-+ \zrwþ U0g5$qӾnF ܵ{oLޗ[!ֶQ$0r*yZ0׿7Nv`#_1;̗)dmY6eDž7#B`g, +?פt ߉gJ_!ڶWH\z_5 WKvD%_Vm.` ١ZtQ $+R/[N}3z57Ȋ{4c/ ]h3/3~ ҡ'WfgO>@w5Z8V{h#&ӤtW'kSwsOM aHN- .uAH $@H $@H%4jGqɥqvҫNXvBXcU7 S2}Ƭ\iLLx)#q $@H |3}-E=5Sܬg~͚w3V?Naj'\>?Ǫ[.={X~Tr2< <@H $j Z[zYM2$227MbX^{K+e s '}4ϕ m@H $V7Ĩ!Vư$,SϬ~wE ,Dh @uѠK6-My4em:|T~!舘ڬ?ST@Z|;߀[uрC<g$-h  0 c75<^LMQ)lj$@H $_"&ngny9t% kJ:z|s/'7v87\3Q{]+[g?,kZt901dI :Bf}=k x|a@,OC8F|$YqUνE:=v/Dbxܓ;dϴ_? ~ >j;DɁ̪ڢqvjbjH@H $OPozjkd,-9MM{5teU.}MXΥ'7al8owmC-n^? ~Lm^j/ ;3c J2ŖTy?re;^v`.-YI)IqOz4pˈ~a$UxP[;lI~{eӟ~$!)'tlm67O-feUHP"<@H $@ _m5װNfxԫik7YJ $0Ƈ:l6% A>9ڣZe$:\q6#)"VH'qI騤ע97jSѽnM^WԪ8 Ćb O~BzA7ւOGV-Wa[[S+bP=#M@@H $ZIkZn_M2Mu.#\7ߦKaNtiq n|'-pFtk+*"O*nł&Ǒ riWPnh4{v g3ҿNKR+¾T~n׳Nwd [-tL\;t`g861@HΉdl-ys~%zkъ 3R`2|',R~ $@H T::PK~ȓLliQt--~=L5o^EVnc[ַlnM^z-iMrۺaR;t&e,-ʿX'%]HG_MmT8+oafri١_Q}w$+]<,=Ԗs4sۋ2YMG0 zweQЎ Ld'moXbR3 ٧y=l] nԈ "$@H } z2w\~B$ic3{M ۬q]4.+eԪJmᚙԮ/ &=[#$Rt;vNnR^Kf4\I7o0;b]z\rVnn͌ۿ+s|07ƶ8%gVײ̈́L9WUA0O-&5#F 4?$e"DaBH $@Zn> W4V@yܶrMe~PӾܼ|OfOid]_%wM jy[+%5v+UQ r%1Q۲XhUi6W7=LA^uҕ/rט[ڈ/tF5vq*|xw 伨F^ldgƍ~p*88Ok ^rwMc{sT':e؋gxI H $LnJ-Mwesp ʘ ۣ#`-WSƀ7mZbη1dϵg}R47blv͝bn 94􇽌nXVJye@Vc뵈WP5P;&?QL~j^֓JXp楢NoI 󎄂fNf&l"/t((F^@Gyae%>SUGUu]2sYgM%sh&#$@H'@<7 P(r I9X{/iVCĽHw9Җ#[h/1"2Uk;jyyb2iu%0PfSiX̀&%VbXuMY,+N7|cBN~L\XyUkga}[Y>!?Tn@@ Oû6|~XddII9jȵQ } |W˥CuoTS<$)KIH'lHπ0i&sɠiӡԻaԔTiUc_MXvЛ])jvо31x4en{Wk$ڒrB]fM˜2.QׄE3=}E` l*a56qE%J^>\dj;~YϚ N^FJUD)Nu:ý]-;ɟ5sdЮW mckS0.Dg_*%eN5tBNkyތE}doa歠F-;'5w%U̜7aZֳ֧ g~ȹP}+7.qj;UX .*=ǍP ISH'Ƥ$|݆ZUXI\y"q|-qgDvӥ0=8C§ @H $*U :)#kڱ'Cغ UHtml6)?9f/wZ)`AUy|F>;h ZסI1Y\]Ӳ+1 DQVRijM+Vغp2kgɔAϡvS8 ;<@M}kC!Gbo; Q2hj1R:--e f"WH|vi>ȯg-zP;r8+ڕqd~yr)UȚeȑ{lyj4a,m9|F jDuo7[l~n4~mE>yrZ9\ݵcOX'歚4SދfffNwm<CM\\>v00u"'mr$x뻏?E^~H]E4#){Xb3EǍiV\z|6ԈPG"s/>[/`XӂU?H̄ީ0aԙ3N75ͅN2n"A)k3.5a5sWزSysg#SV9Xૠf) kA "عi~bDNy%+tw/qb ܒLar77%_g;RS ɔR2_BKÔ 'FFX!u$3g3ba ]"QNz[Qw@H $ݔ&_6)w*aӀ#BN-7ok% ԅMד*@Sq?=4mݽ{wŨwMw u"vm+\ wUg|;PDTsUwh@ =6ULI]C'wKprѩj OU94нusyg2V[Y +&5hzWAJ.T}R3 +/;Tv'VH $P{n/ɋ ^0o7ip ]D&͌tuސ@H $@H $@H $X3_{¼e*|V@H $@_szY1.@H $@H5>@ QFj*ls~[ÈB+RUH $@H ZkE[c;~JjFVDuׯx*@H $@HS -0TXadptxNs>!#$@H $njIO=KO븯d=@H $@Hsu2%FWY[Rp +@H $MȆ }"P=*;Фr$@H $Oư۱ٹ9uM-LYͨ'UH $@H $[[m2{0Hm|*@H $@H V|ҡźC|̑$/D@H $otyiG Fj*\veQ@H $ *>?e5 )$@H $WA@t|ЉMoG%}e @H $@/V$u[P.D @H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $@H $g!tzx$IENDB`sshkit-1.9.0.rc1/examples/images/logo.png000066400000000000000000000115531266312556600202640ustar00rootroot00000000000000PNG  IHDR=* pHYsgRtEXtSoftwareAdobe ImageReadyqe<IDATxMzW`9݃3 YfqVg 4 pVXA`0 XA`QǸhm՟:MۢTu:sӢ\+` ]^YѷkԷW?._Es h>epyTgE_q(;ۢs _0kU'!@Ϸ/)yz[' sv->M4 9EEpQX;Hsڀ㄀D2  B B B B B B B B B B B BSB B B B B B B B B B B B B B B B B B {O`X;wn޼={lɓՇ ji ۼyf}rr !oS@اw5>g$PL|qn C&^z{sIǏ+ӧ@){_Qe Ma A z ee~jl% C@@`Rd!`/rQtd]pz/^}nHq0@?B%>\;!KrƍO?tQ<8}%A(!-ֶ^?==]n^"c<~ tC+cBqy>@wkmڭ3YЋ:?cZwQtt7ovly-򚴕)h{~TEXjt>?[|mUq=kWmU}{(,,+2I'އ:]Zq5*alsYcC@kfkoy֭[gaxy M.RnPׯ/^]ez诿:~9Ӯ8Y^˪ Nr}.a 4 MI:>Hإ3KȚ}wk8ԹLm.c \{˷sN}&AN3r aA 1׹ Tԍ LRyU% 3 ꮒ*M;*@N͐q}qBpy)ƏuaӧnGOT2,]:t(czQ9TIǕ8t餚{uWLcrlMn߾}qΧ_-0y_!!Ry?~\:6!9k,H]}£Ju"ZSUtzgUy4kRߤ=d*ŔǺ1ǾyfIn_mf 9; Eݹ{&H⾯i)isT![]G^m@h*2ژb3+Uéwm;>C@q5 OB&hn鼺ҲN $㻺OCKu15 ]kyyC\!@Q|u]]l\ao.\ m0iۼ&})hB6u`U;~ y9Ǵc4Yc]y뵭m!A>W` VXnP1LJ*8el霻DX7;mUYs~B! ]ȴ,2l }]i2EqHy֍tS`CM9gWz= +)/Q8tX!ч6fuUn}~ZHŮ2@[TFp R.wn|ʥ~FwlZ-oɡB@^ԩTIrE|Ql*4Pv8uW v7>;&WS2Suns4]%P:tHm;ϟj*"ܤsG!cJSδ(+ݺ]Sߒ pM(Q͆rp͐zv3 dárz.y 5K+lw@ؐ .:t`mP2[w[L!L9jAQBRۙɨE_Eoi.f?v@X:Mlf(:9I{8>P:.S.5j1{jDpmrǺDS>߾+~ [TnչʲMre5{hY;* شeSMvȚ.Sm UXNozUտ曋)VTC]h[+y 57WelD7šBL0ο߶/+˺+߾})kRk\(40[|. tHR?D(@:)jԹ}* ͒UƺT3"3i7M =j笙K=C Z3[M\\Vj4NZjC6zm;]͛7/@7߻py՝! >MںVO薫&@f4]RV`W7!@kP0^ocCs+w!^ 4s;uAN:-:iQ3TL )6Ku\*ºe~Ӂ/V]Xʪe yWfy-A`/rE[V[;ݕw: ulf\?WsJVG0UdZp[!YG>/Dw>dGN;Y)m&4j:KZa]w[!I*+/?|nj&W &oc쫓Y!Ayzp!`f/|{uEWWWu7I N[4uAheekINƂ $[6Y@fׅ`ٶ|p{ՂCۮ:.Tjk,Tw>m,+\}*@?f*=W@+F:whdH2ØY*yH>nO m(3l>"6ȱWmE-z5B Z.Ll4DHɇ|vDt7uiʕe8+$wB'W}u̻$&UtPNABq~' 6 ?!!W~ʾ-R2 <& t *@ϖ+E*з@ۆ|o'h8f-bAm:V9*-W\s<8)\qgWymA:}{$BCPB B B B B B B B B B B B B B B B B B B B B B B BAB{]B;^:,eB3y" Y %yr.Ex^}_:/0{Ns[ vsH@y[ E?ak??L!2~'w>G*dBFeh϶uC@pZPpMѯk/GvIENDB`sshkit-1.9.0.rc1/lib/000077500000000000000000000000001266312556600142745ustar00rootroot00000000000000sshkit-1.9.0.rc1/lib/core_ext/000077500000000000000000000000001266312556600161045ustar00rootroot00000000000000sshkit-1.9.0.rc1/lib/core_ext/array.rb000066400000000000000000000001171266312556600175460ustar00rootroot00000000000000class Array def extract_options! last.is_a?(::Hash) ? pop : {} end end sshkit-1.9.0.rc1/lib/core_ext/hash.rb000066400000000000000000000003421266312556600173530ustar00rootroot00000000000000class Hash def symbolize_keys inject({}) do |options, (key, value)| options[(key.to_sym rescue key) || key] = value options end end def symbolize_keys! self.replace(self.symbolize_keys) end end sshkit-1.9.0.rc1/lib/sshkit.rb000066400000000000000000000005361266312556600161320ustar00rootroot00000000000000module SSHKit StandardError = Class.new(::StandardError) class << self attr_accessor :config def configure @@config ||= Configuration.new yield config end def config @@config ||= Configuration.new end def reset_configuration! @@config = nil end end end require_relative 'sshkit/all' sshkit-1.9.0.rc1/lib/sshkit/000077500000000000000000000000001266312556600156015ustar00rootroot00000000000000sshkit-1.9.0.rc1/lib/sshkit/all.rb000066400000000000000000000020151266312556600166740ustar00rootroot00000000000000require_relative '../core_ext/array' require_relative '../core_ext/hash' require_relative 'host' require_relative 'color' require_relative 'command' require_relative 'command_map' require_relative 'configuration' require_relative 'coordinator' require_relative 'deprecation_logger' require_relative 'dsl' require_relative 'exception' require_relative 'logger' require_relative 'log_message' require_relative 'mapping_interaction_handler' require_relative 'formatters/abstract' require_relative 'formatters/black_hole' require_relative 'formatters/pretty' require_relative 'formatters/simple_text' require_relative 'formatters/dot' require_relative 'runners/abstract' require_relative 'runners/sequential' require_relative 'runners/parallel' require_relative 'runners/group' require_relative 'runners/null' require_relative 'backends/abstract' require_relative 'backends/connection_pool' require_relative 'backends/printer' require_relative 'backends/netssh' require_relative 'backends/local' require_relative 'backends/skipper' sshkit-1.9.0.rc1/lib/sshkit/backends/000077500000000000000000000000001266312556600173535ustar00rootroot00000000000000sshkit-1.9.0.rc1/lib/sshkit/backends/abstract.rb000066400000000000000000000100241266312556600215000ustar00rootroot00000000000000module SSHKit module Backend MethodUnavailableError = Class.new(SSHKit::StandardError) # The Backend instance that is running in the current thread. If no Backend # is running, returns `nil` instead. # # Example: # # on(:local) do # self == SSHKit::Backend.current # => true # end # def self.current Thread.current["sshkit_backend"] end class Abstract extend Forwardable def_delegators :output, :log, :fatal, :error, :warn, :info, :debug attr_reader :host def run Thread.current["sshkit_backend"] = self instance_exec(@host, &@block) ensure Thread.current["sshkit_backend"] = nil end def initialize(host, &block) raise "Must pass a Host object" unless host.is_a? Host @host = host @block = block end def make(commands=[]) execute :make, commands end def rake(commands=[]) execute :rake, commands end def test(*args) options = args.extract_options!.merge(raise_on_non_zero_exit: false, verbosity: Logger::DEBUG) create_command_and_execute(args, options).success? end def capture(*args) options = { verbosity: Logger::DEBUG, strip: true }.merge(args.extract_options!) result = create_command_and_execute(args, options).full_stdout options[:strip] ? result.strip : result end def background(*args) SSHKit.config.deprecation_logger.log( 'The background method is deprecated. Blame badly behaved pseudo-daemons!' ) options = args.extract_options!.merge(run_in_background: true) create_command_and_execute(args, options).success? end def execute(*args) options = args.extract_options! create_command_and_execute(args, options).success? end def within(directory, &_block) (@pwd ||= []).push directory.to_s execute <<-EOTEST, verbosity: Logger::DEBUG if test ! -d #{File.join(@pwd)} then echo "Directory does not exist '#{File.join(@pwd)}'" 1>&2 false fi EOTEST yield ensure @pwd.pop end def with(environment, &_block) @_env = (@env ||= {}) @env = @_env.merge environment yield ensure @env = @_env remove_instance_variable(:@_env) end def as(who, &_block) if who.is_a? Hash @user = who[:user] || who["user"] @group = who[:group] || who["group"] else @user = who @group = nil end execute <<-EOTEST, verbosity: Logger::DEBUG if ! sudo -u #{@user} whoami > /dev/null then echo "You cannot switch to user '#{@user}' using sudo, please check the sudoers file" 1>&2 false fi EOTEST yield ensure remove_instance_variable(:@user) remove_instance_variable(:@group) end class << self def config @config ||= OpenStruct.new end def configure yield config end end # Backends which extend the Abstract backend should implement the following methods: def upload!(_local, _remote, _options = {}) raise MethodUnavailableError end def download!(_remote, _local=nil, _options = {}) raise MethodUnavailableError end def execute_command(_cmd) raise MethodUnavailableError end private :execute_command # Can inline after Ruby 2.1 private def output SSHKit.config.output end def create_command_and_execute(args, options) command(args, options).tap { |cmd| execute_command(cmd) } end def pwd_path if @pwd.nil? || @pwd.empty? nil else File.join(@pwd) end end def command(args, options) SSHKit::Command.new(*[*args, options.merge({in: pwd_path, env: @env, host: @host, user: @user, group: @group})]) end end end end sshkit-1.9.0.rc1/lib/sshkit/backends/connection_pool.rb000066400000000000000000000123741266312556600230770ustar00rootroot00000000000000require "monitor" require "thread" # Since we call to_s on new connection arguments and use that as a cache key, we # need to make sure the memory address of the object is not used as part of the # key. Otherwise identical objects with different memory address won't reuse the # cache. # # In the case of proxy commands, this can lead to proxy processes leaking, and # in severe cases can cause deploys to fail due to default file descriptor # limits. An alternate solution would be to use a different means of generating # hash keys. # require "net/ssh/proxy/command" class Net::SSH::Proxy::Command # Ensure a stable string value is used, rather than memory address. def inspect @command_line_template end end # The ConnectionPool caches connections and allows them to be reused, so long as # the reuse happens within the `idle_timeout` period. Timed out connections are # closed, forcing a new connection to be used in that case. # # Additionally, a background thread is started to check for abandoned # connections that have timed out without any attempt at being reused. These # are eventually closed as well and removed from the cache. # # If `idle_timeout` set to `false`, `0`, or `nil`, no caching is performed, and # a new connection is created and then immediately closed each time. The default # timeout is 30 (seconds). # # There is a single public method: `with`. Example usage: # # pool = SSHKit::Backend::ConnectionPool.new # pool.with(Net::SSH.method(:start), "host", "username") do |connection| # # do stuff with connection # end # class SSHKit::Backend::ConnectionPool attr_accessor :idle_timeout def initialize(idle_timeout=30) @idle_timeout = idle_timeout @caches = {} @caches.extend(MonitorMixin) @timed_out_connections = Queue.new Thread.new { run_eviction_loop } end # Creates a new connection or reuses a cached connection (if possible) and # yields the connection to the given block. Connections are created by # invoking the `connection_factory` proc with the given `args`. The arguments # are used to construct a key used for caching. def with(connection_factory, *args) cache = find_cache(args) conn = cache.pop || begin connection_factory.call(*args) end yield(conn) ensure cache.push(conn) unless conn.nil? end # Immediately remove all cached connections, without closing them. This only # exists for unit test purposes. def flush_connections caches.synchronize { caches.clear } end # Immediately close all cached connections and empty the pool. def close_connections caches.synchronize do caches.values.each(&:clear) caches.clear process_deferred_close end end private attr_reader :caches, :timed_out_connections def cache_enabled? idle_timeout && idle_timeout > 0 end # Look up a Cache that matches the given connection arguments. def find_cache(args) if cache_enabled? key = args.to_s caches[key] || thread_safe_find_or_create_cache(key) else NilCache.new(method(:silently_close_connection)) end end # Cache creation needs to happen in a mutex, because otherwise a race # condition might cause two identical caches to be created for the same key. def thread_safe_find_or_create_cache(key) caches.synchronize do caches[key] ||= begin Cache.new(idle_timeout, method(:silently_close_connection_later)) end end end # Loops indefinitely to close connections and to find abandoned connections # that need to be closed. def run_eviction_loop loop do process_deferred_close # Periodically sweep all Caches to evict stale connections sleep([idle_timeout, 5].min) caches.values.each(&:evict) end end # Immediately close any connections that are pending closure. # rubocop:disable Lint/HandleExceptions def process_deferred_close until timed_out_connections.empty? connection = timed_out_connections.pop(true) silently_close_connection(connection) end rescue ThreadError # Queue#pop(true) raises ThreadError if the queue is empty. # This could only happen if `close_connections` is called at the same time # the background eviction thread has woken up to close connections. In any # case, it is not something we need to care about, since an empty queue is # perfectly OK. end # rubocop:enable Lint/HandleExceptions # Adds the connection to a queue that is processed asynchronously by a # background thread. The connection will eventually be closed. def silently_close_connection_later(connection) timed_out_connections << connection end # Close the given `connection` immediately, assuming it responds to a `close` # method. If it doesn't, or if `nil` is provided, it is silently ignored. Any # `StandardError` is also silently ignored. Returns `true` if the connection # was closed; `false` if it was already closed or could not be closed due to # an error. def silently_close_connection(connection) return false unless connection.respond_to?(:close) return false if connection.respond_to?(:closed?) && connection.closed? connection.close true rescue StandardError false end end require "sshkit/backends/connection_pool/cache" require "sshkit/backends/connection_pool/nil_cache" sshkit-1.9.0.rc1/lib/sshkit/backends/connection_pool/000077500000000000000000000000001266312556600225435ustar00rootroot00000000000000sshkit-1.9.0.rc1/lib/sshkit/backends/connection_pool/cache.rb000066400000000000000000000034621266312556600241400ustar00rootroot00000000000000# A Cache holds connections for a given key. Each connection is stored along # with an expiration time so that its idle duration can be measured. class SSHKit::Backend::ConnectionPool::Cache def initialize(idle_timeout, closer) @connections = [] @connections.extend(MonitorMixin) @idle_timeout = idle_timeout @closer = closer end # Remove and return a fresh connection from this Cache. Returns `nil` if # the Cache is empty or if all existing connections have gone stale. def pop connections.synchronize do evict _, connection = connections.pop connection end end # Return a connection to this Cache. def push(conn) # No need to cache if the connection has already been closed. return if closed?(conn) connections.synchronize do connections.push([Time.now + idle_timeout, conn]) end end # Close and remove any connections in this Cache that have been idle for # too long. def evict # Peek at the first connection to see if it is still fresh. If so, we can # return right away without needing to use `synchronize`. first_expires_at, _connection = connections.first return if first_expires_at.nil? || fresh?(first_expires_at) connections.synchronize do fresh, stale = connections.partition do |expires_at, _| fresh?(expires_at) end connections.replace(fresh) stale.each { |_, conn| closer.call(conn) } end end # Close all connections and completely clear the cache. def clear connections.synchronize do connections.map(&:last).each(&closer) connections.clear end end private attr_reader :connections, :idle_timeout, :closer def fresh?(expires_at) expires_at > Time.now end def closed?(conn) conn.respond_to?(:closed?) && conn.closed? end end sshkit-1.9.0.rc1/lib/sshkit/backends/connection_pool/nil_cache.rb000066400000000000000000000003541266312556600247770ustar00rootroot00000000000000# A cache that holds no connections. Any connection provided to this cache # is simply closed. SSHKit::Backend::ConnectionPool::NilCache = Struct.new(:closer) do def pop nil end def push(conn) closer.call(conn) end end sshkit-1.9.0.rc1/lib/sshkit/backends/local.rb000066400000000000000000000027671266312556600210060ustar00rootroot00000000000000require 'open3' require 'fileutils' module SSHKit module Backend class Local < Abstract def initialize(_ = nil, &block) @host = Host.new(:local) # just for logging @block = block end def upload!(local, remote, _options = {}) if local.is_a?(String) FileUtils.cp(local, remote) else File.open(remote, "wb") do |f| IO.copy_stream(local, f) end end end def download!(remote, local=nil, _options = {}) if local.nil? FileUtils.cp(remote, File.basename(remote)) else File.open(remote, "rb") do |f| IO.copy_stream(f, local) end end end private def execute_command(cmd) output.log_command_start(cmd) cmd.started = Time.now Open3.popen3(cmd.to_command) do |stdin, stdout, stderr, wait_thr| stdout_thread = Thread.new do while (line = stdout.gets) do cmd.on_stdout(stdin, line) output.log_command_data(cmd, :stdout, line) end end stderr_thread = Thread.new do while (line = stderr.gets) do cmd.on_stderr(stdin, line) output.log_command_data(cmd, :stderr, line) end end stdout_thread.join stderr_thread.join cmd.exit_status = wait_thr.value.to_i output.log_command_exit(cmd) end end end end end sshkit-1.9.0.rc1/lib/sshkit/backends/netssh.rb000066400000000000000000000077661266312556600212240ustar00rootroot00000000000000require 'net/ssh' require 'net/scp' module Net module SSH class Config class << self def default_files @@default_files + [File.join(Dir.pwd, '.ssh/config')] end end end end end module SSHKit module Backend class Netssh < Abstract class Configuration attr_accessor :connection_timeout, :pty attr_writer :ssh_options def ssh_options @ssh_options || {} end end def upload!(local, remote, options = {}) summarizer = transfer_summarizer('Uploading') with_ssh do |ssh| ssh.scp.upload!(local, remote, options, &summarizer) end end def download!(remote, local=nil, options = {}) summarizer = transfer_summarizer('Downloading') with_ssh do |ssh| ssh.scp.download!(remote, local, options, &summarizer) end end @pool = SSHKit::Backend::ConnectionPool.new class << self attr_accessor :pool def configure yield config end def config @config ||= Configuration.new end end private def transfer_summarizer(action) last_name = nil last_percentage = nil proc do |_ch, name, transferred, total| percentage = (transferred.to_f * 100 / total.to_f) unless percentage.nan? message = "#{action} #{name} #{percentage.round(2)}%" percentage_r = (percentage / 10).truncate * 10 if percentage_r > 0 && (last_name != name || last_percentage != percentage_r) info message last_name = name last_percentage = percentage_r else debug message end else warn "Error calculating percentage #{transferred}/#{total}, " << "is #{name} empty?" end end end def execute_command(cmd) output.log_command_start(cmd) cmd.started = true exit_status = nil with_ssh do |ssh| ssh.open_channel do |chan| chan.request_pty if Netssh.config.pty chan.exec cmd.to_command do |_ch, _success| chan.on_data do |ch, data| cmd.on_stdout(ch, data) output.log_command_data(cmd, :stdout, data) end chan.on_extended_data do |ch, _type, data| cmd.on_stderr(ch, data) output.log_command_data(cmd, :stderr, data) end chan.on_request("exit-status") do |_ch, data| exit_status = data.read_long end #chan.on_request("exit-signal") do |ch, data| # # TODO: This gets called if the program is killed by a signal # # might also be a worthwhile thing to report # exit_signal = data.read_string.to_i # warn ">>> " + exit_signal.inspect # output.log_command_killed(cmd, exit_signal) #end chan.on_open_failed do |_ch| # TODO: What do do here? # I think we should raise something end chan.on_process do |_ch| # TODO: I don't know if this is useful end chan.on_eof do |_ch| # TODO: chan sends EOF before the exit status has been # writtend end end chan.wait end ssh.loop end # Set exit_status and log the result upon completion if exit_status cmd.exit_status = exit_status output.log_command_exit(cmd) end end def with_ssh(&block) host.ssh_options = self.class.config.ssh_options.merge(host.ssh_options || {}) self.class.pool.with( Net::SSH.method(:start), String(host.hostname), host.username, host.netssh_options, &block ) end end end end sshkit-1.9.0.rc1/lib/sshkit/backends/printer.rb000066400000000000000000000005211266312556600213610ustar00rootroot00000000000000module SSHKit module Backend # Printer is used to implement --dry-run in Capistrano class Printer < Abstract def execute_command(cmd) output.log_command_start(cmd) end alias :upload! :execute alias :download! :execute def test(*) super true end end end end sshkit-1.9.0.rc1/lib/sshkit/backends/skipper.rb000066400000000000000000000007751266312556600213660ustar00rootroot00000000000000module SSHKit module Backend class Skipper < Abstract def initialize(&block) @block = block end def execute_command(cmd) warn "[SKIPPING] No Matching Host for #{cmd}" end alias :upload! :execute alias :download! :execute alias :test :execute def info(_messages) # suppress all messages except `warn` end alias :log :info alias :fatal :info alias :error :info alias :debug :info end end end sshkit-1.9.0.rc1/lib/sshkit/color.rb000066400000000000000000000035311266312556600172460ustar00rootroot00000000000000module SSHKit # Very basic support for ANSI color, so that we don't have to rely on # any external dependencies. This class handles colorizing strings, and # automatically disabling color if the underlying output is not a tty. # class Color COLOR_CODES = { :black => 30, :red => 31, :green => 32, :yellow => 33, :blue => 34, :magenta => 35, :cyan => 36, :white => 37, :light_black => 90, :light_red => 91, :light_green => 92, :light_yellow => 93, :light_blue => 94, :light_magenta => 95, :light_cyan => 96, :light_white => 97 }.freeze def initialize(output, env=ENV) @output, @env = output, env end # Converts the given obj to string and surrounds in the appropriate ANSI # color escape sequence, based on the specified color and mode. The color # must be a symbol (see COLOR_CODES for a complete list). # # If the underlying output does not support ANSI color (see `colorize?), # the string will be not be colorized. Likewise if the specified color # symbol is unrecognized, the string will not be colorized. # # Note that the only mode currently support is :bold. All other values # will be silently ignored (i.e. treated the same as mode=nil). # def colorize(obj, color, mode=nil) string = obj.to_s return string unless colorize? return string unless COLOR_CODES.key?(color) result = mode == :bold ? "\e[1;" : "\e[0;" result << COLOR_CODES.fetch(color).to_s result << ";49m#{string}\e[0m" end # Returns `true` if the underlying output is a tty, or if the SSHKIT_COLOR # environment variable is set. # def colorize? @env['SSHKIT_COLOR'] || (@output.respond_to?(:tty?) && @output.tty?) end end end sshkit-1.9.0.rc1/lib/sshkit/command.rb000066400000000000000000000151241266312556600175470ustar00rootroot00000000000000require 'digest/sha1' require 'securerandom' # @author Lee Hambley module SSHKit # @author Lee Hambley class Command Failed = Class.new(SSHKit::StandardError) attr_reader :command, :args, :options, :started_at, :started, :exit_status, :full_stdout, :full_stderr # Initialize a new Command object # # @param [Array] A list of arguments, the first is considered to be the # command name, with optional variadaric args # @return [Command] An un-started command object with no exit staus, and # nothing in stdin or stdout # def initialize(*args) raise ArgumentError, "Must pass arguments to Command.new" if args.empty? @options = default_options.merge(args.extract_options!) @command = args.shift.to_s.strip.to_sym @args = args @options.symbolize_keys! sanitize_command! @stdout, @stderr, @full_stdout, @full_stderr = String.new, String.new, String.new, String.new end def complete? !exit_status.nil? end alias :finished? :complete? def started? started end def started=(new_started) @started_at = Time.now @started = new_started end def uuid @uuid ||= Digest::SHA1.hexdigest(SecureRandom.random_bytes(10))[0..7] end def success? exit_status.nil? ? false : exit_status.to_i == 0 end alias :successful? :success? def failure? exit_status.to_i > 0 end alias :failed? :failure? def stdout log_reader_deprecation('stdout') @stdout end def stdout=(new_value) log_writer_deprecation('stdout') @stdout = new_value end def stderr log_reader_deprecation('stderr') @stderr end def stderr=(new_value) log_writer_deprecation('stderr') @stderr = new_value end def on_stdout(channel, data) @stdout = data @full_stdout += data call_interaction_handler(:stdout, data, channel) end def on_stderr(channel, data) @stderr = data @full_stderr += data call_interaction_handler(:stderr, data, channel) end def exit_status=(new_exit_status) @finished_at = Time.now @exit_status = new_exit_status if options[:raise_on_non_zero_exit] && exit_status > 0 message = "" message += "#{command} exit status: " + exit_status.to_s + "\n" message += "#{command} stdout: " + (full_stdout.strip.empty? ? "Nothing written" : full_stdout.strip) + "\n" message += "#{command} stderr: " + (full_stderr.strip.empty? ? 'Nothing written' : full_stderr.strip) + "\n" raise Failed, message end end def runtime return nil unless complete? @finished_at - @started_at end def to_hash { command: self.to_s, args: args, options: options, exit_status: exit_status, stdout: full_stdout, stderr: full_stderr, started_at: @started_at, finished_at: @finished_at, runtime: runtime, uuid: uuid, started: started?, finished: finished?, successful: successful?, failed: failed? } end def host options[:host] end def verbosity if (vb = options[:verbosity]) case vb.class.name when 'Symbol' then return Logger.const_get(vb.to_s.upcase) when 'Fixnum' then return vb end else Logger::INFO end end def should_map? !command.match(/\s/) end def within(&_block) return yield unless options[:in] sprintf("cd #{options[:in]} && %s", yield) end def environment_hash (SSHKit.config.default_env || {}).merge(options[:env] || {}) end def environment_string environment_hash.collect do |key,value| key_string = key.is_a?(Symbol) ? key.to_s.upcase : key.to_s escaped_value = value.to_s.gsub(/"/, '\"') %{#{key_string}="#{escaped_value}"} end.join(' ') end def with(&_block) return yield unless environment_hash.any? sprintf("( export #{environment_string} ; %s )", yield) end def user(&_block) return yield unless options[:user] "sudo -u #{options[:user]} #{environment_string + " " unless environment_string.empty?}-- sh -c '%s'" % %Q{#{yield}} end def in_background(&_block) return yield unless options[:run_in_background] sprintf("( nohup %s > /dev/null & )", yield) end def umask(&_block) return yield unless SSHKit.config.umask sprintf("umask #{SSHKit.config.umask} && %s", yield) end def group(&_block) return yield unless options[:group] "sg #{options[:group]} -c \\\"%s\\\"" % %Q{#{yield}} # We could also use the so-called heredoc format perhaps: #"newgrp #{options[:group]} <= SSHKit.config.output_verbosity end end end end sshkit-1.9.0.rc1/lib/sshkit/formatters/simple_text.rb000066400000000000000000000005301266312556600226470ustar00rootroot00000000000000module SSHKit module Formatter class SimpleText < Pretty # Historically, SimpleText formatter was used to disable coloring, so we maintain that behaviour def colorize(obj, _color, _mode=nil) obj.to_s end def format_message(_verbosity, message, _uuid=nil) message end end end end sshkit-1.9.0.rc1/lib/sshkit/host.rb000066400000000000000000000073031266312556600171060ustar00rootroot00000000000000require 'ostruct' module SSHKit UnparsableHostStringError = Class.new(SSHKit::StandardError) class Host attr_accessor :password, :hostname, :port, :user, :ssh_options def key=(new_key) @keys = [new_key] end def keys=(new_keys) @keys = new_keys end def keys Array(@keys) end def initialize(host_string_or_options_hash) if host_string_or_options_hash == :local @local = true @hostname = "localhost" @user = ENV['USER'] || ENV['LOGNAME'] || ENV['USERNAME'] elsif !host_string_or_options_hash.is_a?(Hash) suitable_parsers = [ SimpleHostParser, HostWithPortParser, HostWithUsernameAndPortParser, IPv6HostWithPortParser, HostWithUsernameParser, ].select do |p| p.suitable?(host_string_or_options_hash) end if suitable_parsers.any? suitable_parsers.first.tap do |parser| @user, @hostname, @port = parser.new(host_string_or_options_hash).attributes end else raise UnparsableHostStringError, "Cannot parse host string #{host_string_or_options_hash}" end else host_string_or_options_hash.each do |key, value| if self.respond_to?("#{key}=") send("#{key}=", value) else raise ArgumentError, "Unknown host property #{key}" end end end end def local? @local end def hash user.hash ^ hostname.hash ^ port.hash end def username user end def eql?(other_host) other_host.hash == hash end alias :== :eql? alias :equal? :eql? def to_s hostname end def netssh_options {}.tap do |sho| sho[:keys] = keys if keys.any? sho[:port] = port if port sho[:user] = user if user sho[:password] = password if password sho[:forward_agent] = true end .merge(ssh_options || {}) end def properties @properties ||= OpenStruct.new end end # @private # :nodoc: class SimpleHostParser def self.suitable?(host_string) !host_string.match(/[:|@]/) end def initialize(host_string) @host_string = host_string end def username end def port end def hostname @host_string end def attributes [username, hostname, port] end end class HostWithPortParser < SimpleHostParser def self.suitable?(host_string) !host_string.match(/[@|\[|\]]/) end def port @host_string.split(':').last.to_i end def hostname @host_string.split(':').first end end # @private # :nodoc: class HostWithUsernameAndPortParser < SimpleHostParser def self.suitable?(host_string) host_string.match(/@.*:\d+/) end def username @host_string.split(/:|@/)[0] end def hostname @host_string.split(/:|@/)[1] end def port @host_string.split(/:|@/)[2].to_i end end # @private # :nodoc: class IPv6HostWithPortParser < SimpleHostParser def self.suitable?(host_string) host_string.match(/[a-fA-F0-9:]+:\d+/) end def port @host_string.split(':').last.to_i end def hostname @host_string.gsub!(/\[|\]/, '') @host_string.split(':')[0..-2].join(':') end end # @private # :nodoc: class HostWithUsernameParser < SimpleHostParser def self.suitable?(host_string) host_string.match(/@/) && !host_string.match(/\:/) end def username @host_string.split('@').first end def hostname @host_string.split('@').last end end end sshkit-1.9.0.rc1/lib/sshkit/log_message.rb000066400000000000000000000003401266312556600204100ustar00rootroot00000000000000module SSHKit class LogMessage attr_reader :verbosity, :message def initialize(verbosity, message) @verbosity, @message = verbosity, message end def to_s @message.to_s.strip end end end sshkit-1.9.0.rc1/lib/sshkit/logger.rb000066400000000000000000000001551266312556600174060ustar00rootroot00000000000000module SSHKit class Logger DEBUG = 0 INFO = 1 WARN = 2 ERROR = 3 FATAL = 4 end end sshkit-1.9.0.rc1/lib/sshkit/mapping_interaction_handler.rb000066400000000000000000000026501266312556600236600ustar00rootroot00000000000000module SSHKit class MappingInteractionHandler def initialize(mapping, log_level=nil) @log_level = log_level @mapping_proc = \ case mapping when Hash lambda do |server_output| first_matching_key_value = mapping.find { |k, _v| k === server_output } first_matching_key_value.nil? ? nil : first_matching_key_value.last end when Proc mapping else raise "Unsupported mapping type: #{mapping.class} - only Hash and Proc mappings are supported" end end def on_data(_command, stream_name, data, channel) log("Looking up response for #{stream_name} message #{data.inspect}") response_data = @mapping_proc.call(data) if response_data.nil? log("Unable to find interaction handler mapping for #{stream_name}: #{data.inspect} so no response was sent") else log("Sending #{response_data.inspect}") if channel.respond_to?(:send_data) # Net SSH Channel channel.send_data(response_data) elsif channel.respond_to?(:write) # Local IO channel.write(response_data) else raise "Unable to write response data to channel #{channel.inspect} - does not support 'send_data' or 'write'" end end end private def log(message) SSHKit.config.output.send(@log_level, message) unless @log_level.nil? end end end sshkit-1.9.0.rc1/lib/sshkit/runners/000077500000000000000000000000001266312556600172755ustar00rootroot00000000000000sshkit-1.9.0.rc1/lib/sshkit/runners/abstract.rb000066400000000000000000000007411266312556600214270ustar00rootroot00000000000000module SSHKit module Runner class Abstract attr_reader :hosts, :options, :block def initialize(hosts, options = nil, &block) @hosts = Array(hosts) @options = options || {} @block = block end private def backend(host, &block) if host.local? SSHKit::Backend::Local.new(&block) else SSHKit.config.backend.new(host, &block) end end end end end sshkit-1.9.0.rc1/lib/sshkit/runners/group.rb000066400000000000000000000006101266312556600207530ustar00rootroot00000000000000module SSHKit module Runner class Group < Sequential attr_writer :group_size def execute hosts.each_slice(group_size).collect do |group_hosts| Parallel.new(group_hosts, &block).execute sleep wait_interval end.flatten end private def group_size @group_size || options[:limit] || 2 end end end end sshkit-1.9.0.rc1/lib/sshkit/runners/null.rb000066400000000000000000000002321266312556600205710ustar00rootroot00000000000000module SSHKit module Runner class Null < Abstract def execute SSHKit::Backend::Skipper.new(&block).run end end end end sshkit-1.9.0.rc1/lib/sshkit/runners/parallel.rb000066400000000000000000000010121266312556600214100ustar00rootroot00000000000000require 'thread' module SSHKit module Runner class Parallel < Abstract def execute threads = hosts.map do |host| Thread.new(host) do |h| begin backend(h, &block).run rescue StandardError => e e2 = ExecuteError.new e raise e2, "Exception while executing #{host.user ? "as #{host.user}@" : "on host "}#{host}: #{e.message}" end end end threads.each(&:join) end end end end sshkit-1.9.0.rc1/lib/sshkit/runners/sequential.rb000066400000000000000000000013201266312556600217700ustar00rootroot00000000000000module SSHKit module Runner class Sequential < Abstract attr_writer :wait_interval def execute last_host = hosts.pop hosts.each do |host| run_backend(host, &block) sleep wait_interval end unless last_host.nil? run_backend(last_host, &block) end end private def run_backend(host, &block) backend(host, &block).run rescue StandardError => e e2 = ExecuteError.new e raise e2, "Exception while executing #{host.user ? "as #{host.user}@" : "on host "}#{host}: #{e.message}" end def wait_interval @wait_interval || options[:wait] || 2 end end end end sshkit-1.9.0.rc1/lib/sshkit/version.rb000066400000000000000000000000521266312556600176100ustar00rootroot00000000000000module SSHKit VERSION = "1.9.0.rc1" end sshkit-1.9.0.rc1/sshkit.gemspec000066400000000000000000000023371266312556600164050ustar00rootroot00000000000000# -*- encoding: utf-8 -*- require File.expand_path('../lib/sshkit/version', __FILE__) Gem::Specification.new do |gem| gem.authors = ["Lee Hambley", "Tom Clements"] gem.email = ["lee.hambley@gmail.com", "seenmyfate@gmail.com"] gem.summary = %q{SSHKit makes it easy to write structured, testable SSH commands in Ruby} gem.description = %q{A comprehensive toolkit for remotely running commands in a structured manner on groups of servers.} gem.homepage = "http://github.com/capistrano/sshkit" gem.license = "MIT" gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } gem.files = `git ls-files`.split("\n") gem.test_files = `git ls-files -- test/*`.split("\n") gem.name = "sshkit" gem.require_paths = ["lib"] gem.version = SSHKit::VERSION gem.add_runtime_dependency('net-ssh', '>= 2.8.0') gem.add_runtime_dependency('net-scp', '>= 1.1.2') gem.add_development_dependency('minitest', '>= 5.0.0') gem.add_development_dependency('minitest-reporters') gem.add_development_dependency('rake') gem.add_development_dependency('rubocop') gem.add_development_dependency('unindent') gem.add_development_dependency('mocha') end sshkit-1.9.0.rc1/test/000077500000000000000000000000001266312556600145055ustar00rootroot00000000000000sshkit-1.9.0.rc1/test/boxes.json000066400000000000000000000003331266312556600165170ustar00rootroot00000000000000[ { "name": "one", "port": 3001, "user": "vagrant", "password": "vagrant", "hostname": "localhost" }, { "name": "two", "port": 3002 }, { "name": "three", "port": 3003 } ] sshkit-1.9.0.rc1/test/functional/000077500000000000000000000000001266312556600166475ustar00rootroot00000000000000sshkit-1.9.0.rc1/test/functional/backends/000077500000000000000000000000001266312556600204215ustar00rootroot00000000000000sshkit-1.9.0.rc1/test/functional/backends/test_local.rb000066400000000000000000000031341266312556600231000ustar00rootroot00000000000000require 'helper' module SSHKit module Backend class TestLocal < Minitest::Test def setup super SSHKit.config.output = SSHKit::Formatter::BlackHole.new($stdout) end def test_capture captured_command_result = '' Local.new do captured_command_result = capture(:echo, 'foo', strip: false) end.run assert_equal "foo\n", captured_command_result end def test_execute_raises_on_non_zero_exit_status_and_captures_stdout_and_stderr err = assert_raises SSHKit::Command::Failed do Local.new do execute :echo, "'Test capturing stderr' 1>&2; false" end.run end assert_equal "echo exit status: 256\necho stdout: Nothing written\necho stderr: Test capturing stderr\n", err.message end def test_test succeeded_test_result = failed_test_result = nil Local.new do succeeded_test_result = test('[ -d ~ ]') failed_test_result = test('[ -f ~ ]') end.run assert_equal true, succeeded_test_result assert_equal false, failed_test_result end def test_interaction_handler captured_command_result = nil Local.new do command = 'echo Enter Data; read the_data; echo Captured $the_data;' captured_command_result = capture(command, interaction_handler: { "Enter Data\n" => "SOME DATA\n", "Captured SOME DATA\n" => nil }) end.run assert_equal("Enter Data\nCaptured SOME DATA", captured_command_result) end end end end sshkit-1.9.0.rc1/test/functional/backends/test_netssh.rb000066400000000000000000000112211266312556600233060ustar00rootroot00000000000000require 'helper' require 'securerandom' require 'benchmark' module SSHKit module Backend class TestNetssh < FunctionalTest def setup super @output = String.new SSHKit.config.output_verbosity = :debug SSHKit.config.output = SSHKit::Formatter::SimpleText.new(@output) end def a_host VagrantWrapper.hosts['one'] end def test_simple_netssh Netssh.new(a_host) do execute 'date' execute :ls, '-l' with rails_env: :production do within '/tmp' do as :root do execute :touch, 'restart.txt' end end end end.run command_lines = @output.lines.select { |line| line.start_with?('Command:') } assert_equal <<-EOEXPECTED.unindent, command_lines.join Command: /usr/bin/env date Command: /usr/bin/env ls -l Command: if test ! -d /tmp; then echo \"Directory does not exist '/tmp'\" 1>&2; false; fi Command: if ! sudo -u root whoami > /dev/null; then echo \"You cannot switch to user 'root' using sudo, please check the sudoers file\" 1>&2; false; fi Command: cd /tmp && ( export RAILS_ENV="production" ; sudo -u root RAILS_ENV="production" -- sh -c '/usr/bin/env touch restart.txt' ) EOEXPECTED end def test_capture captured_command_result = nil Netssh.new(a_host) do |_host| captured_command_result = capture(:uname) end.run assert_includes %W(Linux Darwin), captured_command_result end def test_ssh_option_merge a_host.ssh_options = { paranoid: true } host_ssh_options = {} SSHKit::Backend::Netssh.config.ssh_options = { forward_agent: false } Netssh.new(a_host) do |host| capture(:uname) host_ssh_options = host.ssh_options end.run assert_equal({ forward_agent: false, paranoid: true }, host_ssh_options) end def test_env_vars_substituion_in_subshell captured_command_result = nil Netssh.new(a_host) do |_host| with some_env_var: :some_value do captured_command_result = capture(:echo, '$SOME_ENV_VAR') end end.run assert_equal "some_value", captured_command_result end def test_execute_raises_on_non_zero_exit_status_and_captures_stdout_and_stderr err = assert_raises SSHKit::Command::Failed do Netssh.new(a_host) do |_host| execute :echo, "'Test capturing stderr' 1>&2; false" end.run end assert_equal "echo exit status: 1\necho stdout: Nothing written\necho stderr: Test capturing stderr\n", err.message end def test_test_does_not_raise_on_non_zero_exit_status Netssh.new(a_host) do |_host| test :false end.run end def test_upload_and_then_capture_file_contents actual_file_contents = "" file_name = File.join("/tmp", SecureRandom.uuid) File.open file_name, 'w+' do |f| f.write "Some Content\nWith a newline and trailing spaces \n " end Netssh.new(a_host) do upload!(file_name, file_name) actual_file_contents = capture(:cat, file_name, strip: false) end.run assert_equal "Some Content\nWith a newline and trailing spaces \n ", actual_file_contents end def test_upload_string_io file_contents = "" Netssh.new(a_host) do |_host| file_name = File.join("/tmp", SecureRandom.uuid) upload!(StringIO.new('example_io'), file_name) file_contents = download!(file_name) end.run assert_equal "example_io", file_contents end def test_upload_large_file size = 25 fills = SecureRandom.random_bytes(1024*1024) file_name = "/tmp/file-#{size}.txt" File.open(file_name, 'w') do |f| (size).times {f.write(fills) } end file_contents = "" Netssh.new(a_host) do upload!(file_name, file_name) file_contents = download!(file_name) end.run assert_equal File.open(file_name).read, file_contents end def test_interaction_handler captured_command_result = nil Netssh.new(a_host) do command = 'echo Enter Data; read the_data; echo Captured $the_data;' captured_command_result = capture(command, interaction_handler: { "Enter Data\n" => "SOME DATA\n", "Captured SOME DATA\n" => nil }) end.run assert_equal("Enter Data\nCaptured SOME DATA", captured_command_result) end end end end sshkit-1.9.0.rc1/test/functional/test_ssh_server_comes_up_for_functional_tests.rb000066400000000000000000000007461266312556600306310ustar00rootroot00000000000000require 'helper' module SSHKit class TestHost < FunctionalTest def host @_host ||= Host.new('') end def test_that_it_works assert true end def test_creating_a_user_gives_us_back_his_private_key_as_a_string skip 'It is not safe to create an user for non vagrant envs' unless VagrantWrapper.running? keys = create_user_with_key(:peter) assert_equal [:one, :two, :three], keys.keys assert keys.values.all? end end end sshkit-1.9.0.rc1/test/helper.rb000066400000000000000000000046551266312556600163230ustar00rootroot00000000000000require 'rubygems' require 'bundler/setup' require 'tempfile' require 'minitest/autorun' require 'minitest/reporters' require 'mocha/setup' require 'unindent' require 'stringio' require 'json' $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'sshkit' Dir[File.expand_path('test/support/*.rb')].each { |file| require file } class UnitTest < Minitest::Test def setup SSHKit.reset_configuration! end SSHKit::Backend::ConnectionPool.class_eval do def flush_connections Thread.current[:sshkit_pool] = {} end end end class FunctionalTest < Minitest::Test def setup unless VagrantWrapper.running? warn "Vagrant VMs are not running. Please, start it manually with `vagrant up`" end end private def create_user_with_key(username, password = :secret) username, password = username.to_s, password.to_s keys = VagrantWrapper.hosts.collect do |_name, host| Net::SSH.start(host.hostname, host.user, port: host.port, password: host.password) do |ssh| # Remove the user, make it again, force-generate a key for him # short keys save us a few microseconds ssh.exec!("sudo userdel -rf #{username}; true") # The `rescue nil` of the shell world ssh.exec!("sudo useradd -m #{username}") ssh.exec!("sudo echo y | ssh-keygen -b 1024 -f #{username} -N ''") ssh.exec!("sudo chown vagrant:vagrant #{username}*") ssh.exec!("sudo echo #{username}:#{password} | chpasswd") # Make the .ssh directory, change the ownership and the ssh.exec!("sudo mkdir -p /home/#{username}/.ssh") ssh.exec!("sudo chown #{username}:#{username} /home/#{username}/.ssh") ssh.exec!("sudo chmod 700 /home/#{username}/.ssh") # Move the key to authorized keys and chown and chmod it ssh.exec!("sudo cat #{username}.pub > /home/#{username}/.ssh/authorized_keys") ssh.exec!("sudo chown #{username}:#{username} /home/#{username}/.ssh/authorized_keys") ssh.exec!("sudo chmod 600 /home/#{username}/.ssh/authorized_keys") key = ssh.exec!("cat /home/vagrant/#{username}") # Clean Up Files ssh.exec!("sudo rm #{username} #{username}.pub") key end end Hash[VagrantWrapper.hosts.collect { |n, _h| n.to_sym }.zip(keys)] end end # # Force colours in Autotest # Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new sshkit-1.9.0.rc1/test/support/000077500000000000000000000000001266312556600162215ustar00rootroot00000000000000sshkit-1.9.0.rc1/test/support/vagrant_wrapper.rb000066400000000000000000000020311266312556600217440ustar00rootroot00000000000000class VagrantWrapper class << self def hosts @vm_hosts ||= begin result = {} boxes = boxes_list unless running? boxes.map! do |box| box['user'] = ENV['USER'] box['port'] = '22' box end end boxes.each do |vm| result[vm['name']] = vm_host(vm) end result end end def running? @running ||= begin status = `#{vagrant_binary} status` status.include?('running') end end def boxes_list json_config_path = File.join('test', 'boxes.json') boxes = File.open(json_config_path).read JSON.parse(boxes) end def vagrant_binary 'vagrant' end private def vm_host(vm) host_options = { user: vm['user'] || 'vagrant', hostname: vm['hostname'] || 'localhost', port: vm['port'] || '22', password: vm['password'] || 'vagrant' } SSHKit::Host.new(host_options) end end end sshkit-1.9.0.rc1/test/unit/000077500000000000000000000000001266312556600154645ustar00rootroot00000000000000sshkit-1.9.0.rc1/test/unit/backends/000077500000000000000000000000001266312556600172365ustar00rootroot00000000000000sshkit-1.9.0.rc1/test/unit/backends/test_abstract.rb000066400000000000000000000115401266312556600224260ustar00rootroot00000000000000require 'helper' module SSHKit module Backend class TestAbstract < UnitTest def test_make backend = ExampleBackend.new do make %w(some command) end backend.run assert_equal '/usr/bin/env make some command', backend.executed_command.to_command end def test_rake backend = ExampleBackend.new do rake %w(a command) end backend.run assert_equal '/usr/bin/env rake a command', backend.executed_command.to_command end def test_execute_creates_and_executes_command_with_default_options backend = ExampleBackend.new do execute :ls, '-l', '/some/directory' end backend.run assert_equal '/usr/bin/env ls -l /some/directory', backend.executed_command.to_command assert_equal( {:raise_on_non_zero_exit=>true, :run_in_background=>false, :in=>nil, :env=>nil, :host=>ExampleBackend.example_host, :user=>nil, :group=>nil}, backend.executed_command.options ) end def test_test_method_creates_and_executes_command_with_false_raise_on_non_zero_exit backend = ExampleBackend.new do test '[ -d /some/file ]' end backend.run assert_equal '[ -d /some/file ]', backend.executed_command.to_command assert_equal false, backend.executed_command.options[:raise_on_non_zero_exit], 'raise_on_non_zero_exit option' end def test_capture_creates_and_executes_command_and_returns_stripped_output output = nil backend = ExampleBackend.new do output = capture :cat, '/a/file' end backend.full_stdout = "Some stdout\n " backend.run assert_equal '/usr/bin/env cat /a/file', backend.executed_command.to_command assert_equal 'Some stdout', output end def test_capture_supports_disabling_strip output = nil backend = ExampleBackend.new do output = capture :cat, '/a/file', :strip => false end backend.full_stdout = "Some stdout\n " backend.run assert_equal '/usr/bin/env cat /a/file', backend.executed_command.to_command assert_equal "Some stdout\n ", output end def test_within_properly_clears backend = ExampleBackend.new do within 'a' do execute :cat, 'file', :strip => false end execute :cat, 'file', :strip => false end backend.run assert_equal '/usr/bin/env cat file', backend.executed_command.to_command end def test_background_logs_deprecation_warnings deprecation_out = '' SSHKit.config.deprecation_output = deprecation_out ExampleBackend.new do background :ls end.run lines = deprecation_out.lines.to_a assert_equal 2, lines.length assert_equal("[Deprecated] The background method is deprecated. Blame badly behaved pseudo-daemons!\n", lines[0]) assert_match(/ \(Called from.*test_abstract.rb:\d+:in `block in test_background_logs_deprecation_warnings'\)\n/, lines[1]) end def test_calling_abstract_with_undefined_execute_command_raises_exception abstract = Abstract.new(ExampleBackend.example_host) do execute(:some_command) end assert_raises(SSHKit::Backend::MethodUnavailableError) do abstract.run end end def test_abstract_backend_can_be_configured Abstract.configure do |config| config.some_option = 100 end assert_equal 100, Abstract.config.some_option end def test_invoke_raises_no_method_error assert_raises NoMethodError do ExampleBackend.new.invoke :echo end end def test_current_refers_to_currently_executing_backend backend = nil current = nil backend = ExampleBackend.new do backend = self current = SSHKit::Backend.current end backend.run assert_equal(backend, current) end def test_current_is_nil_outside_of_the_block backend = ExampleBackend.new do # nothing end backend.run assert_nil(SSHKit::Backend.current) end # Use a concrete ExampleBackend rather than a mock for improved assertion granularity class ExampleBackend < Abstract attr_writer :full_stdout attr_reader :executed_command def initialize(&block) block = block.nil? ? lambda {} : block super(ExampleBackend.example_host, &block) end def execute_command(command) @executed_command = command command.on_stdout(nil, @full_stdout) unless @full_stdout.nil? end def ExampleBackend.example_host Host.new(:'example.com') end end end end end sshkit-1.9.0.rc1/test/unit/backends/test_connection_pool.rb000066400000000000000000000066051266312556600240210ustar00rootroot00000000000000require 'helper' require 'ostruct' module SSHKit module Backend class TestConnectionPool < UnitTest def setup super pool.flush_connections end def pool @pool ||= SSHKit::Backend::ConnectionPool.new end def connect ->(*_args) { Object.new } end def connect_and_close ->(*_args) { OpenStruct.new(:closed? => true) } end def echo_args ->(*args) { args } end def test_default_idle_timeout assert_equal 30, pool.idle_timeout end def test_connection_factory_receives_args args = %w(a b c) conn = pool.with(echo_args, *args) { |c| c } assert_equal args, conn end def test_connections_are_not_reused_if_not_checked_in conn1 = nil conn2 = nil pool.with(connect, "conn") do |yielded_conn_1| conn1 = yielded_conn_1 conn2 = pool.with(connect, "conn") { |c| c } end refute_equal conn1, conn2 end def test_connections_are_reused_if_checked_in conn1 = pool.with(connect, "conn") {} conn2 = pool.with(connect, "conn") {} assert_equal conn1, conn2 end def test_connections_are_reused_across_threads_multiple_times t1 = Thread.new do pool.with(connect, "conn") { |c| c } end t2 = Thread.new do pool.with(connect, "conn") { |c| c } end t3 = Thread.new do pool.with(connect, "conn") { |c| c } end refute_nil t1.value assert_equal t1.value, t2.value assert_equal t2.value, t3.value end def test_zero_idle_timeout_disables_pooling pool.idle_timeout = 0 conn1 = pool.with(connect, "conn") { |c| c } conn2 = pool.with(connect, "conn") { |c| c } refute_equal conn1, conn2 end def test_expired_connection_is_not_reused pool.idle_timeout = 0.1 conn1 = pool.with(connect, "conn") { |c| c } sleep(pool.idle_timeout) conn2 = pool.with(connect, "conn") { |c| c } refute_equal conn1, conn2 end def test_expired_connection_is_closed pool.idle_timeout = 0.1 conn1 = mock conn1.expects(:closed?).twice.returns(false) conn1.expects(:close) pool.with(->(*) { conn1 }, "conn1") {} # Pause to allow the background thread to wake and close the conn sleep(5 + pool.idle_timeout) end def test_closed_connection_is_not_reused conn1 = pool.with(connect_and_close, "conn") { |c| c } conn2 = pool.with(connect, "conn") { |c| c } refute_equal conn1, conn2 end def test_connections_with_different_args_are_not_reused conn1 = pool.with(connect, "conn1") { |c| c } conn2 = pool.with(connect, "conn2") { |c| c } refute_equal conn1, conn2 end def test_close_connections conn1 = mock conn1.expects(:closed?).twice.returns(false) conn1.expects(:close) conn2 = mock conn2.expects(:closed?).returns(false) conn2.expects(:close).never pool.with(->(*) { conn1 }, "conn1") {} # We are using conn2 when close_connections is called, so it should # not be closed. pool.with(->(*) { conn2 }, "conn2") do pool.close_connections end end end end end sshkit-1.9.0.rc1/test/unit/backends/test_local.rb000066400000000000000000000006411266312556600217150ustar00rootroot00000000000000require 'helper' module SSHKit module Backend class TestLocal < UnitTest def local @local ||= Local.new end def test_host assert_equal 'localhost', local.host.to_s end def test_execute assert_equal true, local.execute('uname -a') assert_equal true, local.execute assert_equal true, local.execute('cd && pwd') end end end end sshkit-1.9.0.rc1/test/unit/backends/test_netssh.rb000066400000000000000000000035121266312556600221270ustar00rootroot00000000000000require 'helper' module SSHKit module Backend class TestNetssh < UnitTest def backend @backend ||= Netssh end def test_net_ssh_configuration_options backend.configure do |ssh| ssh.pty = true ssh.connection_timeout = 30 ssh.ssh_options = { keys: %w(/home/user/.ssh/id_rsa), forward_agent: false, auth_methods: %w(publickey password) } end assert_equal 30, backend.config.connection_timeout assert_equal true, backend.config.pty assert_equal %w(/home/user/.ssh/id_rsa), backend.config.ssh_options[:keys] assert_equal false, backend.config.ssh_options[:forward_agent] assert_equal %w(publickey password), backend.config.ssh_options[:auth_methods] end def test_netssh_ext assert_includes Net::SSH::Config.default_files, "#{Dir.pwd}/.ssh/config" end def test_transfer_summarizer netssh = Netssh.new(Host.new('fake')) summarizer = netssh.send(:transfer_summarizer,'Transferring') [ [1, 1000, :debug, 'Transferring afile 0.1%'], [1, 100, :debug, 'Transferring afile 1.0%'], [99, 1000, :debug, 'Transferring afile 9.9%'], [15, 100, :info, 'Transferring afile 15.0%'], [1, 3, :info, 'Transferring afile 33.33%'], [0, 1, :debug, 'Transferring afile 0.0%'], [1, 2, :info, 'Transferring afile 50.0%'], [0, 0, :warn, 'percentage 0/0'], [1023, 343, :info, 'Transferring'], ].each do |transferred,total,method,substring| netssh.expects(method).with { |msg| msg.include?(substring) } summarizer.call(nil,'afile',transferred,total) end end end end end sshkit-1.9.0.rc1/test/unit/backends/test_printer.rb000066400000000000000000000035651266312556600223160ustar00rootroot00000000000000require 'helper' module SSHKit module Backend class TestPrinter < UnitTest def setup super SSHKit.config.output = SSHKit::Formatter::Pretty.new(output) SSHKit.config.output_verbosity = Logger::DEBUG Command.any_instance.stubs(:uuid).returns('aaaaaa') end def output @output ||= String.new end def printer @printer ||= Printer.new(Host.new('example.com')) end def test_execute printer.execute 'uname -a' assert_output_lines( ' INFO [aaaaaa] Running uname -a on example.com', ' DEBUG [aaaaaa] Command: uname -a' ) end def test_test_method assert printer.test('[ -d /some/file ]'), 'test should return true' assert_output_lines( ' DEBUG [aaaaaa] Running [ -d /some/file ] on example.com', ' DEBUG [aaaaaa] Command: [ -d /some/file ]' ) end def test_capture result = printer.capture 'ls -l' assert_equal '', result assert_output_lines( ' DEBUG [aaaaaa] Running ls -l on example.com', ' DEBUG [aaaaaa] Command: ls -l' ) end def test_upload printer.upload! '/some/file', '/remote' assert_output_lines( ' INFO [aaaaaa] Running /usr/bin/env /some/file /remote on example.com', ' DEBUG [aaaaaa] Command: /usr/bin/env /some/file /remote' ) end def test_download printer.download! 'remote/file', '/local/path' assert_output_lines( ' INFO [aaaaaa] Running /usr/bin/env remote/file /local/path on example.com', ' DEBUG [aaaaaa] Command: /usr/bin/env remote/file /local/path' ) end private def assert_output_lines(*expected_lines) assert_equal(expected_lines, output.split("\n")) end end end end sshkit-1.9.0.rc1/test/unit/core_ext/000077500000000000000000000000001266312556600172745ustar00rootroot00000000000000sshkit-1.9.0.rc1/test/unit/core_ext/test_string.rb000066400000000000000000000003461266312556600221710ustar00rootroot00000000000000class String def unindent indent = self.split("\n").select do |line| !line.strip.empty? end.map do |line| line.index(/[^\s]/) end.compact.min || 0 self.gsub(/^[[:blank:]]{#{indent}}/, '') end end sshkit-1.9.0.rc1/test/unit/formatters/000077500000000000000000000000001266312556600176525ustar00rootroot00000000000000sshkit-1.9.0.rc1/test/unit/formatters/test_custom.rb000066400000000000000000000030131266312556600225450ustar00rootroot00000000000000require 'helper' module SSHKit # Try to maintain backwards compatibility with Custom formatters defined by other people class TestCustom < UnitTest def setup super SSHKit.config.output_verbosity = Logger::DEBUG end def output @output ||= String.new end def custom @custom ||= CustomFormatter.new(output) end { log: 'LM 1 Test', fatal: 'LM 4 Test', error: 'LM 3 Test', warn: 'LM 2 Test', info: 'LM 1 Test', debug: 'LM 0 Test' }.each do |level, expected_output| define_method("test_#{level}_logging") do custom.send(level, 'Test') assert_log_output expected_output end end def test_write_logs_commands custom.write(Command.new(:ls)) assert_log_output 'C 1 /usr/bin/env ls' end def test_double_chevron_logs_commands custom << Command.new(:ls) assert_log_output 'C 1 /usr/bin/env ls' end def test_accepts_options_hash custom = CustomFormatter.new(output, :foo => 'value') assert_equal('value', custom.options[:foo]) end private def assert_log_output(expected_output) assert_equal expected_output, output end end class CustomFormatter < SSHKit::Formatter::Abstract def write(obj) original_output << \ case obj when SSHKit::Command then "C #{obj.verbosity} #{obj}" when SSHKit::LogMessage then "LM #{obj.verbosity} #{obj}" end end alias :<< :write end end sshkit-1.9.0.rc1/test/unit/formatters/test_dot.rb000066400000000000000000000024601266312556600220260ustar00rootroot00000000000000require 'helper' module SSHKit class TestDot < UnitTest def setup super SSHKit.config.output_verbosity = Logger::DEBUG end def output @output ||= String.new end def dot @dot ||= SSHKit::Formatter::Dot.new(output) end %w(fatal error warn info debug).each do |level| define_method("test_#{level}_output") do dot.send(level, 'Test') assert_log_output('') end end def test_log_command_start dot.log_command_start(SSHKit::Command.new(:ls)) assert_log_output('') end def test_log_command_data dot.log_command_data(SSHKit::Command.new(:ls), :stdout, 'Some output') assert_log_output('') end def test_command_success output.stubs(:tty?).returns(true) command = SSHKit::Command.new(:ls) command.exit_status = 0 dot.log_command_exit(command) assert_log_output("\e[0;32;49m.\e[0m") end def test_command_failure output.stubs(:tty?).returns(true) command = SSHKit::Command.new(:ls, {raise_on_non_zero_exit: false}) command.exit_status = 1 dot.log_command_exit(command) assert_log_output("\e[0;31;49m.\e[0m") end private def assert_log_output(expected_output) assert_equal expected_output, output end end end sshkit-1.9.0.rc1/test/unit/formatters/test_pretty.rb000066400000000000000000000114271266312556600225720ustar00rootroot00000000000000require 'helper' module SSHKit class TestPretty < UnitTest def setup super SSHKit.config.output_verbosity = Logger::DEBUG Command.any_instance.stubs(:uuid).returns('aaaaaa') end def output @output ||= String.new end def pretty @pretty ||= SSHKit::Formatter::Pretty.new(output) end { log: "\e[0;34;49mINFO\e[0m Test\n", fatal: "\e[0;31;49mFATAL\e[0m Test\n", error: "\e[0;31;49mERROR\e[0m Test\n", warn: "\e[0;33;49mWARN\e[0m Test\n", info: "\e[0;34;49mINFO\e[0m Test\n", debug: "\e[0;30;49mDEBUG\e[0m Test\n" }.each do |level, expected_output| define_method("test_#{level}_output_with_color") do output.stubs(:tty?).returns(true) pretty.send(level, 'Test') assert_log_output(expected_output) end end def test_command_lifecycle_logging_with_color output.stubs(:tty?).returns(true) simulate_command_lifecycle(pretty) expected_log_lines = [ "\e[0;34;49mINFO\e[0m [\e[0;32;49maaaaaa\e[0m] Running \e[1;33;49m/usr/bin/env a_cmd some args\e[0m as \e[0;34;49muser\e[0m@\e[0;34;49mlocalhost\e[0m", "\e[0;30;49mDEBUG\e[0m [\e[0;32;49maaaaaa\e[0m] Command: \e[0;34;49m/usr/bin/env a_cmd some args\e[0m", "\e[0;30;49mDEBUG\e[0m [\e[0;32;49maaaaaa\e[0m] \e[0;32;49m\tstdout message\e[0m", "\e[0;30;49mDEBUG\e[0m [\e[0;32;49maaaaaa\e[0m] \e[0;31;49m\tstderr message\e[0m", "\e[0;34;49mINFO\e[0m [\e[0;32;49maaaaaa\e[0m] Finished in 1.000 seconds with exit status 0 (\e[1;32;49msuccessful\e[0m)." ] assert_equal expected_log_lines, output.split("\n") end { log: " INFO Test\n", fatal: " FATAL Test\n", error: " ERROR Test\n", warn: " WARN Test\n", info: " INFO Test\n", debug: " DEBUG Test\n" }.each do |level, expected_output| define_method("test_#{level}_output_without_color") do pretty.send(level, "Test") assert_log_output expected_output end end def test_logging_message_with_leading_and_trailing_space pretty.log(" some spaces\n\n \t") assert_log_output " INFO some spaces\n" end def test_can_log_non_strings pretty.log(Pathname.new('/var/log/my.log')) assert_log_output " INFO /var/log/my.log\n" end def test_command_lifecycle_logging_without_color simulate_command_lifecycle(pretty) expected_log_lines = [ ' INFO [aaaaaa] Running /usr/bin/env a_cmd some args as user@localhost', ' DEBUG [aaaaaa] Command: /usr/bin/env a_cmd some args', " DEBUG [aaaaaa] \tstdout message", " DEBUG [aaaaaa] \tstderr message", ' INFO [aaaaaa] Finished in 1.000 seconds with exit status 0 (successful).' ] assert_equal expected_log_lines, output.split("\n") end def test_unsupported_class raised_error = assert_raises RuntimeError do pretty << Pathname.new('/tmp') end assert_equal('write only supports formatting SSHKit::LogMessage, called with Pathname: #', raised_error.message) end def test_does_not_log_message_when_verbosity_is_too_low SSHKit.config.output_verbosity = Logger::WARN pretty.info('Some info') assert_log_output('') SSHKit.config.output_verbosity = Logger::INFO pretty.info('Some other info') assert_log_output(" INFO Some other info\n") end def test_does_not_log_command_when_verbosity_is_too_low SSHKit.config.output_verbosity = Logger::WARN command = Command.new(:ls, host: Host.new('user@localhost'), verbosity: Logger::INFO) pretty.log_command_start(command) assert_log_output('') SSHKit.config.output_verbosity = Logger::INFO pretty.log_command_start(command) assert_log_output(" INFO [aaaaaa] Running /usr/bin/env ls as user@localhost\n") end def test_can_write_to_output_which_just_supports_append # Note output doesn't have to be an IO, it only needs to support << output = stub(:<<) pretty = SSHKit::Formatter::Pretty.new(output) simulate_command_lifecycle(pretty) end private def simulate_command_lifecycle(pretty) command = SSHKit::Command.new(:a_cmd, 'some args', host: Host.new('user@localhost')) command.stubs(:runtime).returns(1) pretty.log_command_start(command) command.started = true command.on_stdout(nil, 'stdout message') pretty.log_command_data(command, :stdout, 'stdout message') command.on_stderr(nil, 'stderr message') pretty.log_command_data(command, :stderr, 'stderr message') command.exit_status = 0 pretty.log_command_exit(command) end def assert_log_output(expected_output) assert_equal expected_output, output end end end sshkit-1.9.0.rc1/test/unit/formatters/test_simple_text.rb000066400000000000000000000045351266312556600236020ustar00rootroot00000000000000require 'helper' module SSHKit class TestSimpleText < UnitTest def setup super SSHKit.config.output_verbosity = Logger::DEBUG end def output @output ||= String.new end def simple @simple ||= SSHKit::Formatter::SimpleText.new(output) end %w(fatal error warn info debug).each do |level| define_method("test_#{level}_output") do simple.send(level, 'Test') assert_log_output "Test\n" end end def test_logging_message_with_leading_and_trailing_space simple.log(" some spaces\n\n \t") assert_log_output "some spaces\n" end def test_can_log_non_strings simple.log(Pathname.new('/var/log/my.log')) assert_log_output "/var/log/my.log\n" end def test_command_lifecycle_logging command = SSHKit::Command.new(:a_cmd, 'some args', host: Host.new('user@localhost')) command.stubs(:uuid).returns('aaaaaa') command.stubs(:runtime).returns(1) simple.log_command_start(command) command.started = true command.on_stdout(nil, 'stdout message') simple.log_command_data(command, :stdout, 'stdout message') command.on_stderr(nil, 'stderr message') simple.log_command_data(command, :stderr, 'stderr message') command.exit_status = 0 simple.log_command_exit(command) expected_log_lines = [ 'Running /usr/bin/env a_cmd some args as user@localhost', 'Command: /usr/bin/env a_cmd some args', "\tstdout message", "\tstderr message", 'Finished in 1.000 seconds with exit status 0 (successful).' ] assert_equal expected_log_lines, output.split("\n") end def test_unsupported_class raised_error = assert_raises RuntimeError do simple << Pathname.new('/tmp') end assert_equal('write only supports formatting SSHKit::LogMessage, called with Pathname: #', raised_error.message) end def test_does_not_log_when_verbosity_is_too_low SSHKit.config.output_verbosity = Logger::WARN simple.info('Some info') assert_log_output('') SSHKit.config.output_verbosity = Logger::INFO simple.info('Some other info') assert_log_output("Some other info\n") end private def assert_log_output(expected_output) assert_equal expected_output, output end end end sshkit-1.9.0.rc1/test/unit/test_color.rb000066400000000000000000000077201266312556600201740ustar00rootroot00000000000000require 'helper' require 'sshkit' module SSHKit class TestColor < UnitTest def test_colorize_when_tty_available color = SSHKit::Color.new(stub(tty?: true), {}) assert_equal "\e[1;32;49mhi\e[0m", color.colorize('hi', :green, :bold) end def test_colorize_when_SSHKIT_COLOR_present color = SSHKit::Color.new(stub(tty?: false), {'SSHKIT_COLOR' => 'a'}) assert_equal "\e[0;31;49mhi\e[0m", color.colorize('hi', :red) end def test_does_not_colorize_when_no_tty_and_SSHKIT_COLOR_not_present color = SSHKit::Color.new(stub(tty?: false), {}) assert_equal 'hi', color.colorize('hi', :red) end # The output parameter may not define the tty method eg if it is a Logger. # In this case we assume showing colors would not be supported # https://github.com/capistrano/sshkit/pull/246#issuecomment-100358122 def test_does_not_colorize_when_tty_method_not_defined_and_SSHKIT_COLOR_not_present color = SSHKit::Color.new(stub(), {}) assert_equal 'hi', color.colorize('hi', :red) end def test_colorize_colors color = SSHKit::Color.new(stub(tty?: true), {}) assert_equal "\e[0;30;49mhi\e[0m", color.colorize('hi', :black) assert_equal "\e[0;31;49mhi\e[0m", color.colorize('hi', :red) assert_equal "\e[0;32;49mhi\e[0m", color.colorize('hi', :green) assert_equal "\e[0;33;49mhi\e[0m", color.colorize('hi', :yellow) assert_equal "\e[0;34;49mhi\e[0m", color.colorize('hi', :blue) assert_equal "\e[0;35;49mhi\e[0m", color.colorize('hi', :magenta) assert_equal "\e[0;36;49mhi\e[0m", color.colorize('hi', :cyan) assert_equal "\e[0;37;49mhi\e[0m", color.colorize('hi', :white) assert_equal "\e[0;90;49mhi\e[0m", color.colorize('hi', :light_black) assert_equal "\e[0;91;49mhi\e[0m", color.colorize('hi', :light_red) assert_equal "\e[0;92;49mhi\e[0m", color.colorize('hi', :light_green) assert_equal "\e[0;93;49mhi\e[0m", color.colorize('hi', :light_yellow) assert_equal "\e[0;94;49mhi\e[0m", color.colorize('hi', :light_blue) assert_equal "\e[0;95;49mhi\e[0m", color.colorize('hi', :light_magenta) assert_equal "\e[0;96;49mhi\e[0m", color.colorize('hi', :light_cyan) assert_equal "\e[0;97;49mhi\e[0m", color.colorize('hi', :light_white) end def test_colorize_bold_colors color = SSHKit::Color.new(stub(tty?: true), {}) assert_equal "\e[1;30;49mhi\e[0m", color.colorize('hi', :black, :bold) assert_equal "\e[1;31;49mhi\e[0m", color.colorize('hi', :red, :bold) assert_equal "\e[1;32;49mhi\e[0m", color.colorize('hi', :green, :bold) assert_equal "\e[1;33;49mhi\e[0m", color.colorize('hi', :yellow, :bold) assert_equal "\e[1;34;49mhi\e[0m", color.colorize('hi', :blue, :bold) assert_equal "\e[1;35;49mhi\e[0m", color.colorize('hi', :magenta, :bold) assert_equal "\e[1;36;49mhi\e[0m", color.colorize('hi', :cyan, :bold) assert_equal "\e[1;37;49mhi\e[0m", color.colorize('hi', :white, :bold) assert_equal "\e[1;90;49mhi\e[0m", color.colorize('hi', :light_black, :bold) assert_equal "\e[1;91;49mhi\e[0m", color.colorize('hi', :light_red, :bold) assert_equal "\e[1;92;49mhi\e[0m", color.colorize('hi', :light_green, :bold) assert_equal "\e[1;93;49mhi\e[0m", color.colorize('hi', :light_yellow, :bold) assert_equal "\e[1;94;49mhi\e[0m", color.colorize('hi', :light_blue, :bold) assert_equal "\e[1;95;49mhi\e[0m", color.colorize('hi', :light_magenta, :bold) assert_equal "\e[1;96;49mhi\e[0m", color.colorize('hi', :light_cyan, :bold) assert_equal "\e[1;97;49mhi\e[0m", color.colorize('hi', :light_white, :bold) end def test_ignores_unrecognized_color color = SSHKit::Color.new(stub(tty?: true), {}) assert_equal 'hi', color.colorize('hi', :tangerine) end def test_ignores_unrecognized_mode color = SSHKit::Color.new(stub(tty?: true), {}) assert_equal "\e[0;31;49mhi\e[0m", color.colorize('hi', :red, :underline) end end end sshkit-1.9.0.rc1/test/unit/test_command.rb000066400000000000000000000175161266312556600205000ustar00rootroot00000000000000require 'helper' require 'sshkit' module SSHKit class TestCommand < UnitTest def test_maps_a_command c = Command.new('example') assert_equal '/usr/bin/env example', c.to_command end def test_not_mapping_a_builtin %w{if test time}.each do |builtin| c = Command.new(builtin) assert_equal builtin, c.to_command end end def test_using_a_heredoc c = Command.new <<-EOHEREDOC if test ! -d /var/log; then echo "Example" fi EOHEREDOC assert_equal "if test ! -d /var/log; then; echo \"Example\"; fi", c.to_command end def test_including_the_env SSHKit.config = nil c = Command.new(:rails, 'server', env: {rails_env: :production}) assert_equal %{( export RAILS_ENV="production" ; /usr/bin/env rails server )}, c.to_command end def test_including_the_env_with_multiple_keys SSHKit.config = nil c = Command.new(:rails, 'server', env: {rails_env: :production, foo: 'bar'}) assert_equal %{( export RAILS_ENV="production" FOO="bar" ; /usr/bin/env rails server )}, c.to_command end def test_including_the_env_with_string_keys SSHKit.config = nil c = Command.new(:rails, 'server', env: {'FACTER_env' => :production, foo: 'bar'}) assert_equal %{( export FACTER_env="production" FOO="bar" ; /usr/bin/env rails server )}, c.to_command end def test_double_quotes_are_escaped_in_env SSHKit.config = nil c = Command.new(:rails, 'server', env: {foo: 'asdf"hjkl'}) assert_equal %{( export FOO="asdf\\\"hjkl" ; /usr/bin/env rails server )}, c.to_command end def test_including_the_env_doesnt_addressively_escape SSHKit.config = nil c = Command.new(:rails, 'server', env: {path: '/example:$PATH'}) assert_equal %{( export PATH="/example:$PATH" ; /usr/bin/env rails server )}, c.to_command end def test_global_env SSHKit.config = nil SSHKit.config.default_env = { default: 'env' } c = Command.new(:rails, 'server', env: {}) assert_equal %{( export DEFAULT="env" ; /usr/bin/env rails server )}, c.to_command end def test_default_env_is_overwritten_with_locally_defined SSHKit.config.default_env = { foo: 'bar', over: 'under' } c = Command.new(:rails, 'server', env: { over: 'write'}) assert_equal %{( export FOO="bar" OVER="write" ; /usr/bin/env rails server )}, c.to_command end def test_working_in_a_given_directory c = Command.new(:ls, '-l', in: "/opt/sites") assert_equal "cd /opt/sites && /usr/bin/env ls -l", c.to_command end def test_working_in_a_given_directory_with_env c = Command.new(:ls, '-l', in: "/opt/sites", env: {a: :b}) assert_equal %{cd /opt/sites && ( export A="b" ; /usr/bin/env ls -l )}, c.to_command end def test_having_a_host_passed refute Command.new(:date).host assert Command.new(:date, host: :foo) assert_equal :foo, Command.new(host: :foo).host end def test_working_as_a_given_user c = Command.new(:whoami, user: :anotheruser) assert_equal "sudo -u anotheruser -- sh -c '/usr/bin/env whoami'", c.to_command end def test_working_as_a_given_group c = Command.new(:whoami, group: :devvers) assert_equal "sg devvers -c \\\"/usr/bin/env whoami\\\"", c.to_command end def test_working_as_a_given_user_and_group c = Command.new(:whoami, user: :anotheruser, group: :devvers) assert_equal "sudo -u anotheruser -- sh -c 'sg devvers -c \\\"/usr/bin/env whoami\\\"'", c.to_command end def test_umask SSHKit.config.umask = '007' c = Command.new(:touch, 'somefile') assert_equal "umask 007 && /usr/bin/env touch somefile", c.to_command end def test_umask_with_working_directory SSHKit.config.umask = '007' c = Command.new(:touch, 'somefile', in: '/opt') assert_equal "cd /opt && umask 007 && /usr/bin/env touch somefile", c.to_command end def test_umask_with_working_directory_and_user SSHKit.config.umask = '007' c = Command.new(:touch, 'somefile', in: '/var', user: 'alice') assert_equal "cd /var && umask 007 && sudo -u alice -- sh -c '/usr/bin/env touch somefile'", c.to_command end def test_umask_with_env_and_working_directory_and_user SSHKit.config.umask = '007' c = Command.new(:touch, 'somefile', user: 'bob', env: {a: 'b'}, in: '/var') assert_equal %{cd /var && umask 007 && ( export A="b" ; sudo -u bob A="b" -- sh -c '/usr/bin/env touch somefile' )}, c.to_command end def test_verbosity_defaults_to_logger_info assert_equal Logger::INFO, Command.new(:ls).verbosity end def test_overriding_verbosity_level_with_a_constant assert_equal Logger::DEBUG, Command.new(:ls, verbosity: Logger::DEBUG).verbosity end def test_overriding_verbosity_level_with_a_symbol assert_equal Logger::DEBUG, Command.new(:ls, verbosity: :debug).verbosity end def test_complete? c = Command.new(:whoami, raise_on_non_zero_exit: false) refute c.complete? c.exit_status = 1 assert c.complete? c.exit_status = 0 assert c.complete? end def test_successful? c = Command.new(:whoami) refute c.successful? refute c.success? c.exit_status = 0 assert c.successful? assert c.success? end def test_failure? c = Command.new(:whoami, raise_on_non_zero_exit: false) refute c.failure? refute c.failed? c.exit_status = 1 assert c.failure? assert c.failed? c.exit_status = 127 assert c.failure? assert c.failed? end def test_on_stdout c = Command.new(:whoami) c.on_stdout(nil, "test\n") c.on_stdout(nil, 'test2') c.on_stdout(nil, 'test3') assert_equal "test\ntest2test3", c.full_stdout end def test_on_stderr c = Command.new(:whoami) c.on_stderr(nil, 'test') assert_equal 'test', c.full_stderr end def test_deprecated_stdtream_accessors deprecation_out = '' SSHKit.config.deprecation_output = deprecation_out c = Command.new(:whoami) c.stdout='a test' assert_equal('a test', c.stdout) c.stderr='another test' assert_equal('another test', c.stderr) deprecation_lines = deprecation_out.lines.to_a assert_equal 8, deprecation_lines.size assert_equal( '[Deprecated] The stdout= method on Command is deprecated. ' + "The @stdout attribute will be removed in a future release.\n", deprecation_lines[0]) assert_equal( '[Deprecated] The stdout method on Command is deprecated. ' + "The @stdout attribute will be removed in a future release. Use full_stdout() instead.\n", deprecation_lines[2]) assert_equal( '[Deprecated] The stderr= method on Command is deprecated. ' + "The @stderr attribute will be removed in a future release.\n", deprecation_lines[4]) assert_equal( '[Deprecated] The stderr method on Command is deprecated. ' + "The @stderr attribute will be removed in a future release. Use full_stderr() instead.\n", deprecation_lines[6]) end def test_setting_exit_status c = Command.new(:whoami, raise_on_non_zero_exit: false) assert_equal nil, c.exit_status assert c.exit_status = 1 assert_equal 1, c.exit_status end def test_command_has_a_guid assert Command.new(:whosmi).uuid end def test_wont_take_no_args assert_raises ArgumentError do Command.new end end def test_command_raises_command_failed_error_when_non_zero_exit error = assert_raises SSHKit::Command::Failed do Command.new(:whoami).exit_status = 1 end assert_equal "whoami exit status: 1\nwhoami stdout: Nothing written\nwhoami stderr: Nothing written\n", error.message end end end sshkit-1.9.0.rc1/test/unit/test_command_map.rb000066400000000000000000000036431266312556600213310ustar00rootroot00000000000000require 'helper' require 'sshkit' module SSHKit class TestCommandMap < UnitTest def test_defaults map = CommandMap.new assert_equal map[:rake], "/usr/bin/env rake" assert_equal map[:test], "test" end def test_setter map = CommandMap.new map[:rake] = "/usr/local/rbenv/shims/rake" assert_equal map[:rake], "/usr/local/rbenv/shims/rake" end def test_prefix map = CommandMap.new map.prefix[:rake].push("/home/vagrant/.rbenv/bin/rbenv exec") map.prefix[:rake].push("bundle exec") assert_equal map[:rake], "/home/vagrant/.rbenv/bin/rbenv exec bundle exec rake" end def test_prefix_procs map = CommandMap.new map.prefix[:rake].push("/home/vagrant/.rbenv/bin/rbenv exec") map.prefix[:rake].push(proc{ "bundle exec" }) assert_equal map[:rake], "/home/vagrant/.rbenv/bin/rbenv exec bundle exec rake" end def test_prefix_unshift map = CommandMap.new map.prefix[:rake].push("bundle exec") map.prefix[:rake].unshift("/home/vagrant/.rbenv/bin/rbenv exec") assert_equal map[:rake], "/home/vagrant/.rbenv/bin/rbenv exec bundle exec rake" end def test_indifferent_setter map = CommandMap.new map[:rake] = "/usr/local/rbenv/shims/rake" map["rake"] = "/usr/local/rbenv/shims/rake2" assert_equal "/usr/local/rbenv/shims/rake2", map[:rake] end def test_indifferent_prefix map = CommandMap.new map.prefix[:rake].push("/home/vagrant/.rbenv/bin/rbenv exec") map.prefix["rake"].push("bundle exec") assert_equal map[:rake], "/home/vagrant/.rbenv/bin/rbenv exec bundle exec rake" end def test_prefix_initialization_is_thread_safe map = CommandMap.new threads = Array.new(3) do Thread.new do (1..1_000).each { |i| assert_equal([], map.prefix[i.to_s]) } end end threads.each(&:join) end end end sshkit-1.9.0.rc1/test/unit/test_configuration.rb000066400000000000000000000063761266312556600217330ustar00rootroot00000000000000require 'helper' module SSHKit class TestConfiguration < UnitTest def setup super SSHKit.config.command_map.clear SSHKit.config.output = SSHKit::Formatter::Pretty.new($stdout) end def test_deprecation_output output = '' SSHKit.config.deprecation_output = output SSHKit.config.deprecation_logger.log('Test') assert_equal "[Deprecated] Test\n", output.lines.first end def test_default_deprecation_output SSHKit.config.deprecation_logger.log('Test') end def test_nil_deprecation_output SSHKit.config.deprecation_output = nil SSHKit.config.deprecation_logger.log('Test') end def test_output assert SSHKit.config.output.is_a? SSHKit::Formatter::Pretty assert SSHKit.config.output = $stderr end def test_umask assert SSHKit.config.umask.nil? assert SSHKit.config.umask = '007' assert_equal '007', SSHKit.config.umask end def test_output_verbosity assert_equal Logger::INFO, SSHKit.config.output_verbosity assert SSHKit.config.output_verbosity = :debug assert_equal Logger::DEBUG, SSHKit.config.output_verbosity assert SSHKit.config.output_verbosity = Logger::INFO assert_equal Logger::INFO, SSHKit.config.output_verbosity assert SSHKit.config.output_verbosity = 0 assert_equal Logger::DEBUG, SSHKit.config.output_verbosity end def test_default_env assert SSHKit.config.default_env end def test_default_runner assert_equal :parallel, SSHKit.config.default_runner SSHKit.config.default_runner = :sequence assert_equal :sequence, SSHKit.config.default_runner end def test_backend assert_equal SSHKit::Backend::Netssh, SSHKit.config.backend assert SSHKit.config.backend = SSHKit::Backend::Printer assert_equal SSHKit::Backend::Printer, SSHKit.config.backend end def test_command_map assert_equal SSHKit.config.command_map.is_a?(SSHKit::CommandMap), true cm = Hash.new { |h,k| h[k] = "/opt/sites/example/current/bin #{k}"} assert SSHKit.config.command_map = cm assert_equal SSHKit.config.command_map.is_a?(SSHKit::CommandMap), true assert_equal "/opt/sites/example/current/bin ruby", SSHKit.config.command_map[:ruby] end def test_setting_formatter_types { dot: SSHKit::Formatter::Dot, blackhole: SSHKit::Formatter::BlackHole, simpletext: SSHKit::Formatter::SimpleText, }.each do |format, expected_class| SSHKit.config.format = format assert SSHKit.config.output.is_a? expected_class end end def test_prohibits_unknown_formatter_type_with_exception assert_raises(NameError) do SSHKit.config.format = :doesnotexist end end def test_options_can_be_provided_to_formatter SSHKit.config.use_format(TestFormatter, :color => false) formatter = SSHKit.config.output assert_instance_of(TestFormatter, formatter) assert_equal($stdout, formatter.output) assert_equal({ :color => false }, formatter.options) end class TestFormatter attr_accessor :output, :options def initialize(output, options={}) @output = output @options = options end end end end sshkit-1.9.0.rc1/test/unit/test_coordinator.rb000066400000000000000000000077111266312556600214010ustar00rootroot00000000000000require 'time' require 'helper' module SSHKit class TestCoordinator < UnitTest def setup super @output = String.new SSHKit.config.output_verbosity = :debug SSHKit.config.output = SSHKit::Formatter::SimpleText.new(@output) SSHKit.config.backend = SSHKit::Backend::Printer end def echo_time lambda do |_host| execute "echo #{Time.now.to_f}" end end def test_the_connection_manager_handles_empty_argument Coordinator.new([]).each do raise "This should not be executed" end end def test_connection_manager_handles_a_single_argument h = Host.new('1.example.com') Host.expects(:new).with('1.example.com').once().returns(h) Coordinator.new '1.example.com' end def test_connection_manager_resolves_hosts h = Host.new('n.example.com') Host.expects(:new).times(3).returns(h) Coordinator.new %w{1.example.com 2.example.com 3.example.com} end def test_the_connection_manager_yields_the_host_to_each_connection_instance Coordinator.new(%w{1.example.com}).each do |host| execute "echo #{host.hostname}" end assert_equal "Command: echo 1.example.com\n", actual_output_commands.last end def test_the_connection_manaager_runs_things_in_parallel_by_default Coordinator.new(%w{1.example.com 2.example.com}).each(&echo_time) assert_equal 2, actual_execution_times.length assert_within_10_ms(actual_execution_times) end def test_the_connection_manager_can_run_things_in_sequence Coordinator.new(%w{1.example.com 2.example.com}).each in: :sequence, &echo_time assert_equal 2, actual_execution_times.length assert_at_least_1_sec_apart(actual_execution_times.first, actual_execution_times.last) end class MyRunner < SSHKit::Runner::Parallel def execute threads = hosts.map do |host| Thread.new(host) do |h| b = backend(h, &block) b.run b.warn "custom runner out" end end threads.each(&:join) end end def test_the_connection_manager_can_run_things_in_custom_runner begin $original_runner = SSHKit.config.default_runner SSHKit.config.default_runner = MyRunner Coordinator.new(%w{1.example.com 2.example.com}).each(&echo_time) assert_equal 2, actual_execution_times.length assert_within_10_ms(actual_execution_times) assert_match(/custom runner out/, @output) ensure SSHKit.config.default_runner = $original_runner end end def test_the_connection_manager_can_run_things_in_sequence_with_wait start = Time.now Coordinator.new(%w{1.example.com 2.example.com}).each in: :sequence, wait: 10, &echo_time stop = Time.now assert_operator(stop - start, :>=, 10.0) end def test_the_connection_manager_can_run_things_in_groups Coordinator.new( %w{ 1.example.com 2.example.com 3.example.com 4.example.com 5.example.com 6.example.com } ).each in: :groups, &echo_time assert_equal 6, actual_execution_times.length assert_within_10_ms(actual_execution_times[0..1]) assert_within_10_ms(actual_execution_times[2..3]) assert_within_10_ms(actual_execution_times[4..5]) assert_at_least_1_sec_apart(actual_execution_times[1], actual_execution_times[2]) assert_at_least_1_sec_apart(actual_execution_times[3], actual_execution_times[4]) end private def assert_at_least_1_sec_apart(first_time, last_time) assert_operator(last_time - first_time, :>, 1.0) end def assert_within_10_ms(array) assert_in_delta(*array, 0.01) # 10 msec end def actual_execution_times actual_output_commands.map { |line| line.split(' ').last.to_f } end def actual_output_commands @output.lines.select { |line| line.start_with?('Command:') } end end end sshkit-1.9.0.rc1/test/unit/test_deprecation_logger.rb000066400000000000000000000014551266312556600227110ustar00rootroot00000000000000require 'helper' module SSHKit class TestDeprecationLogger < UnitTest def output @output ||= String.new end def logger @logger ||= DeprecationLogger.new(output) end def test_hides_duplicate_deprecation_warnings line_number = generate_warning generate_warning actual_lines = output.lines.to_a assert_equal(2, actual_lines.size) assert_equal "[Deprecated] Some message\n", actual_lines[0] assert_match %r{ \(Called from .*sshkit/test/unit/test_deprecation_logger.rb:#{line_number}:in `generate_warning'\)\n}, actual_lines[1] end def test_handles_nil_output DeprecationLogger.new(nil).log('Some message') end private def generate_warning logger.log('Some message') __LINE__-1 end end end sshkit-1.9.0.rc1/test/unit/test_dsl.rb000066400000000000000000000007071266312556600176360ustar00rootroot00000000000000require 'helper' module SSHKit class TestDSL < UnitTest include SSHKit::DSL def test_dsl_on coordinator = mock Coordinator.stubs(:new).returns coordinator coordinator.expects(:each).at_least_once on('1.2.3.4') end def test_dsl_run_locally local_backend = mock Backend::Local.stubs(:new).returns local_backend local_backend.expects(:run).at_least_once run_locally end end end sshkit-1.9.0.rc1/test/unit/test_host.rb000066400000000000000000000105601266312556600200270ustar00rootroot00000000000000require 'helper' module SSHKit class TestHost < UnitTest def test_raises_on_unparsable_string assert_raises UnparsableHostStringError do Host.new(":@hello@:") end end def test_regular_hosts h = Host.new 'example.com' assert_equal 'example.com', h.hostname end def test_ipv4_with_username_and_port h = Host.new 'user@127.0.0.1:2222' assert_equal 2222, h.port assert_equal 'user', h.username assert_equal '127.0.0.1', h.hostname end def test_host_with_port h = Host.new 'example.com:2222' assert_equal 2222, h.port assert_equal 'example.com', h.hostname end def test_host_with_username h = Host.new 'root@example.com' assert_equal 'root', h.username assert_equal 'example.com', h.hostname end def test_host_with_username_and_port h = Host.new 'user@example.com:123' assert_equal 123, h.port assert_equal 'user', h.username assert_equal 'example.com', h.hostname end def test_host_local h = Host.new :local assert h.local? assert_nil h.port username_candidates = ENV['USER'] || ENV['LOGNAME'] || ENV['USERNAME'] assert_equal username_candidates, h.username assert_equal 'localhost', h.hostname end def test_does_not_confuse_ipv6_hosts_with_port_specification h = Host.new '[1fff:0:a88:85a3::ac1f]:8001' assert_equal 8001, h.port assert_equal '1fff:0:a88:85a3::ac1f', h.hostname end def testing_host_casting_to_a_string assert_equal "example.com", Host.new('user@example.com:1234').to_s end def test_assert_hosts_hash_equally assert_equal Host.new('example.com').hash, Host.new('example.com').hash end def test_assert_hosts_compare_equal h1 = Host.new('example.com') h2 = Host.new('example.com') assert h1 == h2 assert h1.eql? h2 assert h1.equal? h2 end def test_arbitrary_host_properties h = Host.new('example.com') assert_equal nil, h.properties.roles assert h.properties.roles = [:web, :app] assert_equal [:web, :app], h.properties.roles end def test_setting_up_a_host_with_a_hash h = Host.new(hostname: 'example.com', port: 1234, key: "~/.ssh/example_com.key") assert_equal "example.com", h.hostname assert_equal 1234, h.port assert_equal "~/.ssh/example_com.key", h.keys.first end def test_setting_up_a_host_with_a_hash_raises_on_unknown_keys assert_raises ArgumentError do Host.new({this_key_doesnt_exist: nil}) end end def test_turning_a_host_into_ssh_options Host.new('someuser@example.com:2222').tap do |host| host.password = "andthisdoesntevenmakeanysense" host.keys = ["~/.ssh/some_key_here"] host.netssh_options.tap do |sho| assert_equal 2222, sho.fetch(:port) assert_equal 'andthisdoesntevenmakeanysense', sho.fetch(:password) assert_equal ['~/.ssh/some_key_here'], sho.fetch(:keys) end end end def test_host_ssh_options_are_simply_missing_when_they_have_no_value Host.new('my_config_is_in_the_ssh_config_file').tap do |host| host.netssh_options.tap do |sho| refute sho.has_key?(:port) refute sho.has_key?(:password) refute sho.has_key?(:keys) refute sho.has_key?(:user) end end end def test_turning_a_host_into_ssh_options_when_extra_options_are_set ssh_options = { port: 3232, keys: %w(/home/user/.ssh/id_rsa), forward_agent: false, auth_methods: %w(publickey password) } Host.new('someuser@example.com:2222').tap do |host| host.password = "andthisdoesntevenmakeanysense" host.keys = ["~/.ssh/some_key_here"] host.ssh_options = ssh_options host.netssh_options.tap do |sho| assert_equal 3232, sho.fetch(:port) assert_equal 'andthisdoesntevenmakeanysense', sho.fetch(:password) assert_equal %w(/home/user/.ssh/id_rsa), sho.fetch(:keys) assert_equal false, sho.fetch(:forward_agent) assert_equal %w(publickey password), sho.fetch(:auth_methods) end end end end end sshkit-1.9.0.rc1/test/unit/test_logger.rb000066400000000000000000000004641266312556600203330ustar00rootroot00000000000000require 'helper' module SSHKit class TestLogger < UnitTest def test_logger_severity_constants assert_equal Logger::DEBUG, 0 assert_equal Logger::INFO, 1 assert_equal Logger::WARN, 2 assert_equal Logger::ERROR, 3 assert_equal Logger::FATAL, 4 end end end sshkit-1.9.0.rc1/test/unit/test_mapping_interaction_handler.rb000066400000000000000000000070131266312556600246000ustar00rootroot00000000000000require 'helper' module SSHKit class TestMappingInteractionHandler < UnitTest def channel @channel ||= mock end def setup super @output = stub() SSHKit.config.output = @output end def test_calls_send_data_with_mapped_input_when_stdout_matches handler = MappingInteractionHandler.new('Server output' => "some input\n") channel.expects(:send_data).with("some input\n") handler.on_data(nil, :stdout, 'Server output', channel) end def test_calls_send_data_with_mapped_input_when_stderr_matches handler = MappingInteractionHandler.new('Server output' => "some input\n") channel.expects(:send_data).with("some input\n") handler.on_data(nil, :stderr, 'Server output', channel) end def test_logs_unmatched_interaction_if_constructed_with_a_log_level @output.expects(:debug).with('Looking up response for stdout message "Server output\n"') @output.expects(:debug).with('Unable to find interaction handler mapping for stdout: "Server output\n" so no response was sent') MappingInteractionHandler.new({}, :debug).on_data(nil, :stdout, "Server output\n", channel) end def test_logs_matched_interaction_if_constructed_with_a_log_level handler = MappingInteractionHandler.new({"Server output\n" => "Some input\n"}, :debug) channel.stubs(:send_data) @output.expects(:debug).with('Looking up response for stdout message "Server output\n"') @output.expects(:debug).with('Sending "Some input\n"') handler.on_data(nil, :stdout, "Server output\n", channel) end def test_supports_regex_keys handler = MappingInteractionHandler.new({/Some \w+ output\n/ => "Input\n"}) channel.expects(:send_data).with("Input\n") handler.on_data(nil, :stdout, "Some lovely output\n", channel) end def test_supports_lambda_mapping channel.expects(:send_data).with("GREAT Input\n") mapping = lambda do |server_output| case server_output when /Some (\w+) output\n/ "#{$1.upcase} Input\n" end end MappingInteractionHandler.new(mapping).on_data(nil, :stdout, "Some great output\n", channel) end def test_matches_keys_in_ofer interaction_handler = MappingInteractionHandler.new({ "Specific output\n" => "Specific Input\n", /.*/ => "Default Input\n" }) channel.expects(:send_data).with("Specific Input\n") interaction_handler.on_data(nil, :stdout, "Specific output\n", channel) end def test_supports_default_mapping interaction_handler = MappingInteractionHandler.new({ "Specific output\n" => "Specific Input\n", /.*/ => "Default Input\n" }) channel.expects(:send_data).with("Specific Input\n") interaction_handler.on_data(nil, :stdout, "Specific output\n", channel) end def test_raises_for_unsupported_mapping_type raised_error = assert_raises RuntimeError do MappingInteractionHandler.new(Object.new) end assert_equal('Unsupported mapping type: Object - only Hash and Proc mappings are supported', raised_error.message) end def test_raises_for_unsupported_channel_type handler = MappingInteractionHandler.new({"Some output\n" => "Whatever"}) raised_error = assert_raises RuntimeError do handler.on_data(nil, :stdout, "Some output\n", Object.new) end assert_match(/Unable to write response data to channel # - does not support 'send_data' or 'write'/, raised_error.message) end end end