pax_global_header00006660000000000000000000000064136451254720014523gustar00rootroot0000000000000052 comment=1b09580afe5fcc5b3e4c0e0865b3fe0929cc24b8 train-3.2.28/000077500000000000000000000000001364512547200127345ustar00rootroot00000000000000train-3.2.28/.codeclimate.yml000066400000000000000000000003671364512547200160140ustar00rootroot00000000000000version: "2" checks: file-lines: enabled: false plugins: fixme: enabled: true config: strings: - TODO - rubocop:disable flog: enabled: true markdownlint: enabled: true rubocop: enabled: true train-3.2.28/.expeditor/000077500000000000000000000000001364512547200150155ustar00rootroot00000000000000train-3.2.28/.expeditor/buildkite/000077500000000000000000000000001364512547200167715ustar00rootroot00000000000000train-3.2.28/.expeditor/buildkite/cache_support.sh000077500000000000000000000026751364512547200222010ustar00rootroot00000000000000#!/bin/bash set -ue S3_URL="s3://public-cd-buildkite-cache/${BUILDKITE_PIPELINE_SLUG}/${BUILDKITE_LABEL}" pull_s3_file() { aws s3 cp "${S3_URL}/$1" "$1" || echo "Could not pull $1 from S3" } push_s3_file() { if [ -f "$1" ]; then aws s3 cp "$1" "${S3_URL}/$1" || echo "Could not push $1 to S3 for caching." fi } install_cache_deps() { apt-get update -y if [ -n "${1:-}" ]; then apt-get install "$@" -y fi if [ -z "${SKIP_BUNDLE_CACHE:-}" ]; then apt-get install awscli -y fi } pull_bundle() { if [ -z "${SKIP_BUNDLE_CACHE:-}" ]; then pull_s3_file "bundle.tar.gz" pull_s3_file "bundle.sha256" if [ -f bundle.tar.gz ]; then tar -xzf bundle.tar.gz mv Gemfile.lock Gemfile.lock.old || true fi if [ -n "${RESET_BUNDLE_CACHE:-}" ]; then rm bundle.sha256 fi fi } push_bundle() { if [ -z "${SKIP_BUNDLE_CACHE:-}" ]; then if test -f bundle.sha256 && shasum --check bundle.sha256 --status; then echo "Bundled gems have not changed. Skipping upload to s3" else echo "Bundled gems have changed. Uploading to s3" diff -u Gemfile.lock.old Gemfile.lock || true shasum -a 256 Gemfile.lock > bundle.sha256 tar -czf bundle.tar.gz Gemfile.lock vendor/ push_s3_file bundle.tar.gz push_s3_file bundle.sha256 fi fi } train-3.2.28/.expeditor/buildkite/coverage.sh000077500000000000000000000004071364512547200211240ustar00rootroot00000000000000#!/bin/bash set -ueo pipefail echo "--- system details" uname -a ruby -v bundle --version echo "--- system environment" env echo "--- bundle install" bundle install --jobs=7 --retry=3 --without tools integration echo "+++ bundle exec rake" bundle exec rake train-3.2.28/.expeditor/buildkite/verify.ps1000066400000000000000000000005651364512547200207300ustar00rootroot00000000000000echo "--- system details" $Properties = 'Caption', 'CSName', 'Version', 'BuildType', 'OSArchitecture' Get-CimInstance Win32_OperatingSystem | Select-Object $Properties | Format-Table -AutoSize ruby -v bundle --version echo "--- bundle install" bundle install --jobs=7 --retry=3 --without tools integration echo "+++ bundle exec rake" bundle exec rake exit $LASTEXITCODE train-3.2.28/.expeditor/buildkite/verify.sh000077500000000000000000000006601364512547200206360ustar00rootroot00000000000000#!/bin/bash echo "--- dependencies" . .expeditor/buildkite/cache_support.sh install_cache_deps echo "--- system details" uname -a ruby -v bundle --version echo "--- pull bundle cache" pull_bundle echo "--- bundle install" bundle config --local path vendor/bundle bundle install --jobs=7 --retry=3 --without tools integration echo "--- push bundle cache" push_bundle echo "+++ bundle exec rake" bundle exec rake ${RAKE_TASK:-} train-3.2.28/.expeditor/config.yml000066400000000000000000000043721364512547200170130ustar00rootroot00000000000000# Documentation available at https://expeditor.chef.io/docs/getting-started/ --- # Slack channel in Chef Software slack to send notifications about build failures, etc slack: notify_channel: - chef-found-notify - inspec-notify # This publish is triggered by the `built_in:publish_rubygems` artifact_action. rubygems: - train - train-core github: # This deletes the GitHub PR branch after successfully merged into the release branch delete_branch_on_merge: true # The tag format to use (e.g. v1.0.0) version_tag_format: "v{{version}}" # allow bumping the minor release via label minor_bump_labels: - "Expeditor: Bump Minor Version" major_bump_labels: - "Expeditor: Bump Major Version" release_branch: - 1-stable: version_constraint: 1.* - 2-stable: version_constraint: 2.* - master: version_constraint: 3.* changelog: rollup_header: Changes not yet released to rubygems.org # These actions are taken, in order they are specified, anytime a Pull Request is merged. merge_actions: - built_in:bump_version: ignore_labels: - "Expeditor: Skip Version Bump" - "Expeditor: Skip All" - bash:.expeditor/update_version.sh: only_if: built_in:bump_version - built_in:update_changelog: ignore_labels: - "Expeditor: Skip Changelog" - "Expeditor: Skip All" - built_in:build_gem: only_if: built_in:bump_version - trigger_pipeline:coverage: post_commit: true subscriptions: - workload: pull_request_opened:{{agent_id}}:* actions: - post_github_comment:.expeditor/templates/pull_request.mustache: ignore_team_members: - inspec/owners - inspec/inspec-core-team - built_in:github_auto_assign_author: only_if_team_member: - inspec/owners - inspec/inspec-core-team - workload: project_promoted:{{agent_id}}:* actions: - built_in:rollover_changelog - built_in:publish_rubygems # - built_in:create_github_release # This is not yet supported for rubygems - see https://github.com/chef/expeditor/pull/1182 pipelines: - verify: description: Pull Request validation tests public: true - coverage: description: Generate test coverage report train-3.2.28/.expeditor/coverage.pipeline.yml000066400000000000000000000007631364512547200211450ustar00rootroot00000000000000--- steps: - label: coverage commands: - /workdir/.expeditor/buildkite/coverage.sh expeditor: secrets: COVERALLS_REPO_TOKEN: path: secret/coveralls/inspec/train field: repo_token executor: docker: environment: - CI_ENABLE_COVERAGE=true - CI_NAME=Buildkite - CI_BUILD_NUMBER=$BUILDKITE_BUILD_NUMBER - CI_BUILD_URL=$BUILDKITE_BUILD_URL - CI_BRANCH=$BUILDKITE_BRANCH train-3.2.28/.expeditor/templates/000077500000000000000000000000001364512547200170135ustar00rootroot00000000000000train-3.2.28/.expeditor/templates/pull_request.mustache000066400000000000000000000012661364512547200232770ustar00rootroot00000000000000Hello {{author}}! Thanks for the pull request! Here is what will happen next: 1. Your PR will be reviewed by the maintainers. 2. Possible Outcomes a. If everything looks good, one of them will approve it, and your PR will be merged. b. The maintainer may request follow-on work (e.g. code fix, linting, etc). We would encourage you to address this work in 2-3 business days to keep the conversation going and to get your contribution in sooner. c. Cases exist where a PR is neither aligned to Chef InSpec's product roadmap, or something the team can own or maintain long-term. In these cases, the maintainer will provide justification and close out the PR. Thank you for contributing! train-3.2.28/.expeditor/update_version.sh000077500000000000000000000006711364512547200204070ustar00rootroot00000000000000#!/bin/bash # # After a PR merge, Chef Expeditor will bump the PATCH version in the VERSION file. # It then executes this file to update any other files/components with that new version. # set -evx sed -i -r "s/VERSION = \".*\"/VERSION = \"$(cat VERSION)\"/" lib/train/version.rb # Once Expeditor finshes executing this script, it will commit the changes and push # the commit as a new tag corresponding to the value in the VERSION file. train-3.2.28/.expeditor/verify.pipeline.yml000066400000000000000000000021301364512547200206440ustar00rootroot00000000000000--- steps: - label: lint-ruby-2.6 command: - RAKE_TASK=lint /workdir/.expeditor/buildkite/verify.sh expeditor: executor: docker: image: ruby:2.6 - label: run-tests-ruby-2.4 command: - /workdir/.expeditor/buildkite/verify.sh expeditor: executor: docker: image: ruby:2.4 - label: run-tests-ruby-2.5 command: - /workdir/.expeditor/buildkite/verify.sh expeditor: executor: docker: image: ruby:2.5 - label: run-tests-ruby-2.6 command: - /workdir/.expeditor/buildkite/verify.sh expeditor: executor: docker: image: ruby:2.6 - label: run-tests-ruby-2.7 command: - /workdir/.expeditor/buildkite/verify.sh expeditor: executor: docker: image: ruby:2.7 - label: run-tests-ruby-2.6-windows command: - /workdir/.expeditor/buildkite/verify.ps1 expeditor: executor: docker: environment: - BUILDKITE host_os: windows shell: ["powershell", "-Command"] train-3.2.28/.github/000077500000000000000000000000001364512547200142745ustar00rootroot00000000000000train-3.2.28/.github/CODEOWNERS000066400000000000000000000002211364512547200156620ustar00rootroot00000000000000# Order is important. The last matching pattern has the most precedence. * @inspec/inspec-core-team @chef/foundation-team-reviewers train-3.2.28/.github/ISSUE_TEMPLATE/000077500000000000000000000000001364512547200164575ustar00rootroot00000000000000train-3.2.28/.github/ISSUE_TEMPLATE/BUG_TEMPLATE.md000066400000000000000000000011521364512547200207100ustar00rootroot00000000000000--- name: � Bug Report about: If something isn't working as expected �. labels: "Status: Untriaged" --- # Version: [Version of the project installed] # Environment: [Details about the environment such as the Operating System, cookbook details, etc...] # Scenario: [What you are trying to achieve and you can't?] # Steps to Reproduce: [If you are filing an issue what are the things we need to do in order to repro your problem?] # Expected Result: [What are you expecting to happen as the consequence of above reproduction steps?] # Actual Result: [What actually happens after the reproduction steps?] train-3.2.28/.github/ISSUE_TEMPLATE/DESIGN_PROPOSAL.md000066400000000000000000000023431364512547200212730ustar00rootroot00000000000000--- name: Design Proposal about: I have a significant change I would like to propose and discuss before starting labels: "Status: Untriaged" --- ### When a Change Needs a Design Proposal A design proposal should be opened any time a change meets one of the following qualifications: - Significantly changes the user experience of a project in a way that impacts users. - Significantly changes the underlying architecture of the project in a way that impacts other developers. - Changes the development or testing process of the project such as a change of CI systems or test frameworks. ### Why We Use This Process - Allows all interested parties (including any community member) to discuss large impact changes to a project. - Serves as a durable paper trail for discussions regarding project architecture. - Forces design discussions to occur before PRs are created. - Reduces PR refactoring and rejected PRs. --- ## Motivation ## Specification ## Downstream Impact train-3.2.28/.github/ISSUE_TEMPLATE/ENHANCEMENT_REQUEST_TEMPLATE.md000066400000000000000000000014071364512547200232330ustar00rootroot00000000000000--- name: 🚀 Enhancement Request about: I have a suggestion (and may want to implement it 🙂)! labels: "Status: Untriaged" --- ### Describe the Enhancement: ### Describe the Need: ### Current Alternative ### Can We Help You Implement This?: train-3.2.28/.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md000066400000000000000000000006601364512547200215060ustar00rootroot00000000000000--- name: 🤗 Support Question about: If you have a question 💬, please check out our Slack! --- We use GitHub issues to track bugs and feature requests. If you need help please post to our Mailing List or join the Chef Community Slack. * Chef Community Slack at http://community-slack.chef.io/. * Chef Mailing List https://discourse.chef.io/ Support issues opened here will be closed and redirected to Slack or Discourse. train-3.2.28/.github/lock.yml000066400000000000000000000000221364512547200157410ustar00rootroot00000000000000daysUntilLock: 60 train-3.2.28/.gitignore000066400000000000000000000002001364512547200147140ustar00rootroot00000000000000train-*.gem r-train-*.gem Gemfile.lock Gemfile.local .kitchen/ TAGS terraform.tfstate.backup .terraform .bundle .gems coverage/ train-3.2.28/CHANGELOG.md000066400000000000000000002120171364512547200145500ustar00rootroot00000000000000 ## [v3.2.28](https://github.com/inspec/train/tree/v3.2.28) (2020-04-13) #### Merged Pull Requests - [chef#9635] Add reason for sudo root password [#583](https://github.com/inspec/train/pull/583) ([vsingh-msys](https://github.com/vsingh-msys)) ### Changes not yet released to rubygems.org #### Merged Pull Requests - [chef#9635] Add reason for sudo root password [#583](https://github.com/inspec/train/pull/583) ([vsingh-msys](https://github.com/vsingh-msys)) ## [v3.2.27](https://github.com/inspec/train/tree/v3.2.27) (2020-04-08) #### Merged Pull Requests - Silence deprecation warning under ruby 2.7 [#582](https://github.com/inspec/train/pull/582) ([clintoncwolfe](https://github.com/clintoncwolfe)) ## [v3.2.26](https://github.com/inspec/train/tree/v3.2.26) (2020-03-20) #### Merged Pull Requests - Update rake dep for security issue [#577](https://github.com/inspec/train/pull/577) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Move dependency on inifile from train-core to train [#579](https://github.com/inspec/train/pull/579) ([terceiro](https://github.com/terceiro)) - Avoid explicit /tmp in favor of $TMPDIR [#578](https://github.com/inspec/train/pull/578) ([majioa](https://github.com/majioa)) ## [v3.2.23](https://github.com/inspec/train/tree/v3.2.23) (2020-03-02) #### Merged Pull Requests - Expeditor - Disable nonfunctional github release option [#573](https://github.com/inspec/train/pull/573) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Attempt to fix --sudo. [#576](https://github.com/inspec/train/pull/576) ([skpaterson](https://github.com/skpaterson)) ## [v3.2.22](https://github.com/inspec/train/tree/v3.2.22) (2020-02-18) #### Merged Pull Requests - Revert to regular require to fix transport loading across gem boundary [#572](https://github.com/inspec/train/pull/572) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Include the LICENSE file in the gem [#571](https://github.com/inspec/train/pull/571) ([btm](https://github.com/btm)) ## [v3.2.20](https://github.com/inspec/train/tree/v3.2.20) (2020-02-06) #### Merged Pull Requests - Kali Linux platform detection support [#556](https://github.com/inspec/train/pull/556) ([mattray](https://github.com/mattray)) - Refactor OS detection. [#561](https://github.com/inspec/train/pull/561) ([zenspider](https://github.com/zenspider)) - Unified gemspec and fixed dependencies across train and train-core. [#563](https://github.com/inspec/train/pull/563) ([zenspider](https://github.com/zenspider)) - Rebase #339 [#566](https://github.com/inspec/train/pull/566) ([zenspider](https://github.com/zenspider)) - Improve debugging experience by making platform and connection less noisy. [#565](https://github.com/inspec/train/pull/565) ([zenspider](https://github.com/zenspider)) - Added a blank line to the readme where we needed one. [#567](https://github.com/inspec/train/pull/567) ([zenspider](https://github.com/zenspider)) ## [v3.2.14](https://github.com/inspec/train/tree/v3.2.14) (2020-01-23) #### Merged Pull Requests - Substitute require for require_relative [#549](https://github.com/inspec/train/pull/549) ([tas50](https://github.com/tas50)) - allow overriding follow_symlink on Train::File [#550](https://github.com/inspec/train/pull/550) ([miah](https://github.com/miah)) - Fix README typo [#551](https://github.com/inspec/train/pull/551) ([multani](https://github.com/multani)) - LinuxCommand#verify cleaned up [#530](https://github.com/inspec/train/pull/530) ([vsingh-msys](https://github.com/vsingh-msys)) - Add azure_mgmt_storage to train.gemspec [#552](https://github.com/inspec/train/pull/552) ([rmoles](https://github.com/rmoles)) - Refactor with_sudo_pty to BaseConnection (no-op) and SshConnection. [#554](https://github.com/inspec/train/pull/554) ([zenspider](https://github.com/zenspider)) - Yocto Project family and Yocto Linux and balenaOS platform detection [#558](https://github.com/inspec/train/pull/558) ([mattray](https://github.com/mattray)) - Make stat command use '-c' for Yocto OS [#559](https://github.com/inspec/train/pull/559) ([michaellihs](https://github.com/michaellihs)) - Fix verify step for sudo [#557](https://github.com/inspec/train/pull/557) ([zenspider](https://github.com/zenspider)) ## [v3.2.5](https://github.com/inspec/train/tree/v3.2.5) (2019-12-12) #### Merged Pull Requests - Add extended metadata [#546](https://github.com/inspec/train/pull/546) ([tas50](https://github.com/tas50)) - Move built_in:create_github_release to a workload? [#547](https://github.com/inspec/train/pull/547) ([zenspider](https://github.com/zenspider)) ## [v3.2.3](https://github.com/inspec/train/tree/v3.2.3) (2019-12-12) #### Merged Pull Requests - Correctly verify ssh w/ sudo [#544](https://github.com/inspec/train/pull/544) ([zenspider](https://github.com/zenspider)) - Adding bundle artifact caching to BK. [#545](https://github.com/inspec/train/pull/545) ([zenspider](https://github.com/zenspider)) - Return exit code for local Windows command [#533](https://github.com/inspec/train/pull/533) ([james-stocks](https://github.com/james-stocks)) ## [v3.2.0](https://github.com/inspec/train/tree/v3.2.0) (2019-12-02) #### Merged Pull Requests - Fix inspec detect on SLES [#515](https://github.com/inspec/train/pull/515) ([christian-wtd](https://github.com/christian-wtd)) - Added create_github_release action to stable promotion. [#536](https://github.com/inspec/train/pull/536) ([zenspider](https://github.com/zenspider)) - Minor cleanup [#537](https://github.com/inspec/train/pull/537) ([zenspider](https://github.com/zenspider)) - Rolling back #515. [#539](https://github.com/inspec/train/pull/539) ([zenspider](https://github.com/zenspider)) - Add azure_mgmt_security to train.gemspec [#541](https://github.com/inspec/train/pull/541) ([rmoles](https://github.com/rmoles)) - Bump minor version [#543](https://github.com/inspec/train/pull/543) ([clintoncwolfe](https://github.com/clintoncwolfe)) ## [v3.1.8](https://github.com/inspec/train/tree/v3.1.8) (2019-11-18) #### Merged Pull Requests - Stop using global expectation methods and switch to using _. [#524](https://github.com/inspec/train/pull/524) ([zenspider](https://github.com/zenspider)) - Adds activesupport gem to allow XML>JSON parsing from Azure APIs [#534](https://github.com/inspec/train/pull/534) ([r-fennell](https://github.com/r-fennell)) - Update google-api-client version. [#531](https://github.com/inspec/train/pull/531) ([skpaterson](https://github.com/skpaterson)) - Fix contributor url pointing to 404 [#532](https://github.com/inspec/train/pull/532) ([vsingh-msys](https://github.com/vsingh-msys)) ## [v3.1.4](https://github.com/inspec/train/tree/v3.1.4) (2019-10-10) #### Merged Pull Requests - Add powershell detection [#523](https://github.com/inspec/train/pull/523) ([miah](https://github.com/miah)) ## [v3.1.3](https://github.com/inspec/train/tree/v3.1.3) (2019-10-03) #### Merged Pull Requests - Let expeditor respond to pull request [#512](https://github.com/inspec/train/pull/512) ([miah](https://github.com/miah)) - add def forward_remote to Transports::SSH [#457](https://github.com/inspec/train/pull/457) ([sawanoboly](https://github.com/sawanoboly)) - Fix chefstyle warnings [#514](https://github.com/inspec/train/pull/514) ([tas50](https://github.com/tas50)) - Add reason field while raising Train::ClientError. [#517](https://github.com/inspec/train/pull/517) ([samshinde](https://github.com/samshinde)) - Fix method called on string in os parse function [#519](https://github.com/inspec/train/pull/519) ([noisleahcim](https://github.com/noisleahcim)) ## [v3.0.3](https://github.com/inspec/train/tree/v3.0.3) (2019-08-29) #### Merged Pull Requests - Ensure UserError is raised with a reason value [#511](https://github.com/inspec/train/pull/511) ([marcparadise](https://github.com/marcparadise)) ## [v3.0.2](https://github.com/inspec/train/tree/v3.0.2) (2019-08-15) #### Merged Pull Requests - Fix raise invalid arguments [#508](https://github.com/inspec/train/pull/508) ([vsingh-msys](https://github.com/vsingh-msys)) ## [v3.0.1](https://github.com/inspec/train/tree/v3.0.1) (2019-08-07) #### Merged Pull Requests - Add Windows to the verify pipeline [#484](https://github.com/inspec/train/pull/484) ([miah](https://github.com/miah)) - garbo [#485](https://github.com/inspec/train/pull/485) ([miah](https://github.com/miah)) - Fix up windows testing on buildkite [#487](https://github.com/inspec/train/pull/487) ([zenspider](https://github.com/zenspider)) - Remove coverage from general test runs and add its own pipeline [#486](https://github.com/inspec/train/pull/486) ([miah](https://github.com/miah)) - Testing coverage pipeline [#488](https://github.com/inspec/train/pull/488) ([miah](https://github.com/miah)) - yah-mail [#490](https://github.com/inspec/train/pull/490) ([miah](https://github.com/miah)) - Use our coverage.sh rather than embedded commands [#491](https://github.com/inspec/train/pull/491) ([miah](https://github.com/miah)) - chmod 755 coverage.sh [#492](https://github.com/inspec/train/pull/492) ([miah](https://github.com/miah)) - Load simplecov too [#493](https://github.com/inspec/train/pull/493) ([miah](https://github.com/miah)) - less clever to worry about here since this isn't InSpec ;) [#494](https://github.com/inspec/train/pull/494) ([miah](https://github.com/miah)) - Move coverage private and use our repo_token [#495](https://github.com/inspec/train/pull/495) ([miah](https://github.com/miah)) - Add newline to Rakefile [#496](https://github.com/inspec/train/pull/496) ([miah](https://github.com/miah)) - I understand how vault secrets in expeditor work now. [#497](https://github.com/inspec/train/pull/497) ([miah](https://github.com/miah)) - that image isnt supported for accounts/secrets [#498](https://github.com/inspec/train/pull/498) ([miah](https://github.com/miah)) - the secret doesnt seem to be propagating to docker? [#499](https://github.com/inspec/train/pull/499) ([miah](https://github.com/miah)) - correct the path to the secret in vault [#500](https://github.com/inspec/train/pull/500) ([miah](https://github.com/miah)) - Move this secrets stuff into the label [#501](https://github.com/inspec/train/pull/501) ([miah](https://github.com/miah)) - get more debugging details [#502](https://github.com/inspec/train/pull/502) ([miah](https://github.com/miah)) - Add codeclimate setup. [#460](https://github.com/inspec/train/pull/460) ([miah](https://github.com/miah)) - Minor cleanup to regexp for correctness. [#458](https://github.com/inspec/train/pull/458) ([zenspider](https://github.com/zenspider)) - Remove travis and appveyor [#503](https://github.com/inspec/train/pull/503) ([miah](https://github.com/miah)) - Remove WinRM support in favor of train-winrm plugin [#448](https://github.com/inspec/train/pull/448) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Use /etc/os-release for SUSE detection (Adopted) [#505](https://github.com/inspec/train/pull/505) ([clintoncwolfe](https://github.com/clintoncwolfe)) ## [v2.1.19](https://github.com/inspec/train/tree/v2.1.19) (2019-07-23) #### Merged Pull Requests - Resolve chefstyle warnings in 0.13 [#470](https://github.com/inspec/train/pull/470) ([tas50](https://github.com/tas50)) - Add verify script for pipeline [#477](https://github.com/inspec/train/pull/477) ([miah](https://github.com/miah)) - chefstyle -a [#478](https://github.com/inspec/train/pull/478) ([miah](https://github.com/miah)) - TRYING to get things normalized across chef, inspec, and train. [#480](https://github.com/inspec/train/pull/480) ([zenspider](https://github.com/zenspider)) - This fixes cisco_ios? being defined to return true by default [#481](https://github.com/inspec/train/pull/481) ([zenspider](https://github.com/zenspider)) - Add inspec-notify to the notify_channel list [#483](https://github.com/inspec/train/pull/483) ([zenspider](https://github.com/zenspider)) ## [v2.1.13](https://github.com/inspec/train/tree/v2.1.13) (2019-07-01) #### Merged Pull Requests - Add github templates & fix the expeditor version bump script [#469](https://github.com/inspec/train/pull/469) ([tas50](https://github.com/tas50)) ## [v2.1.12](https://github.com/inspec/train/tree/v2.1.12) (2019-06-26) #### Merged Pull Requests - Fixing inspec/train to be green again and improving test stability. [#463](https://github.com/inspec/train/pull/463) ([zenspider](https://github.com/zenspider)) - More cleanup of test output and stability fixes [#464](https://github.com/inspec/train/pull/464) ([zenspider](https://github.com/zenspider)) - Minor cleanup of logic in OSCommon#unix_uuid. [#465](https://github.com/inspec/train/pull/465) ([zenspider](https://github.com/zenspider)) - Apply Chefstyle to train [#459](https://github.com/inspec/train/pull/459) ([miah](https://github.com/miah)) - Fix to raise specific error when ssh user is not provided and root is used as default user. [#466](https://github.com/inspec/train/pull/466) ([Vasu1105](https://github.com/Vasu1105)) ## [v2.1.7](https://github.com/inspec/train/tree/v2.1.7) (2019-05-23) #### Merged Pull Requests - Add Code of Conduct to train [#453](https://github.com/inspec/train/pull/453) ([miah](https://github.com/miah)) - Add codeclimate to train [#454](https://github.com/inspec/train/pull/454) ([miah](https://github.com/miah)) - Fix failing unit tests verify_host_key in ssh [#452](https://github.com/inspec/train/pull/452) ([marcparadise](https://github.com/marcparadise)) - Fix fallback regex for OpenSuse [#451](https://github.com/inspec/train/pull/451) ([n-rodriguez](https://github.com/n-rodriguez)) - Set chef-foundation as the project owner and update expeditor [#456](https://github.com/inspec/train/pull/456) ([tas50](https://github.com/tas50)) ## [v2.1.2](https://github.com/inspec/train/tree/v2.1.2) (2019-05-15) #### Merged Pull Requests - Add Coveralls.io to Train [#440](https://github.com/inspec/train/pull/440) ([miah](https://github.com/miah)) - Rename ca_trust_file to ca_trust_path [#450](https://github.com/inspec/train/pull/450) ([marcparadise](https://github.com/marcparadise)) ## [v2.1.0](https://github.com/inspec/train/tree/v2.1.0) (2019-05-06) #### Merged Pull Requests - Ensure we're using the latest OS on Appveyor [#441](https://github.com/inspec/train/pull/441) ([miah](https://github.com/miah)) - Fixes enable password and catches failure [#383](https://github.com/inspec/train/pull/383) ([btm](https://github.com/btm)) - Update backend test to match updated appveyor os [#442](https://github.com/inspec/train/pull/442) ([miah](https://github.com/miah)) - The Windows release also changed.. [#443](https://github.com/inspec/train/pull/443) ([miah](https://github.com/miah)) - Detect windows also with ssh transport [#416](https://github.com/inspec/train/pull/416) ([StefanScherer](https://github.com/StefanScherer)) ## [v2.0.12](https://github.com/inspec/train/tree/v2.0.12) (2019-04-23) #### Merged Pull Requests - Add project state and SLA [#434](https://github.com/inspec/train/pull/434) ([miah](https://github.com/miah)) - Remove dependency on ed25519 gems [#439](https://github.com/inspec/train/pull/439) ([tas50](https://github.com/tas50)) - Require Ruby 2.4 or later [#438](https://github.com/inspec/train/pull/438) ([tas50](https://github.com/tas50)) - Add missed cisco_ios_connection to train-core. [#436](https://github.com/inspec/train/pull/436) ([marcparadise](https://github.com/marcparadise)) ## [v2.0.8](https://github.com/inspec/train/tree/v2.0.8) (2019-04-22) #### Merged Pull Requests - Silence verify_host_key warning from net-ssh [#430](https://github.com/inspec/train/pull/430) ([clintoncwolfe](https://github.com/clintoncwolfe)) - add ssh/winrm to core [#433](https://github.com/inspec/train/pull/433) ([marcparadise](https://github.com/marcparadise)) - Require ed25519 and bcrypt_pbkdf gems for ed25519 support [#435](https://github.com/inspec/train/pull/435) ([tas50](https://github.com/tas50)) ## [v2.0.5](https://github.com/inspec/train/tree/v2.0.5) (2019-04-15) #### Merged Pull Requests - [SUSTAIN-955] Provide ssh option to enable host key verify [#427](https://github.com/inspec/train/pull/427) ([marcparadise](https://github.com/marcparadise)) - [SUSTAIN-955] Add data callbacks to WinRM and SSH (adopted) [#431](https://github.com/inspec/train/pull/431) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Add a passthrough option for winrm_operation_timeout under winrm [#432](https://github.com/inspec/train/pull/432) ([marcparadise](https://github.com/marcparadise)) ## [v2.0.2](https://github.com/inspec/train/tree/v2.0.2) (2019-03-19) #### Merged Pull Requests - Add v2 to expeditor [#407](https://github.com/inspec/train/pull/407) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Add api_call cache type [#411](https://github.com/inspec/train/pull/411) ([clintoncwolfe](https://github.com/clintoncwolfe)) - 2.x: Remove AWS transport [#408](https://github.com/inspec/train/pull/408) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Fix detection of 'SLES Expanded Support' of Red Hat [#361](https://github.com/inspec/train/pull/361) ([jabofh](https://github.com/jabofh)) - Update gem deps to allow mixlib-shellout 3.x and net-scp 2.x [#421](https://github.com/inspec/train/pull/421) ([tas50](https://github.com/tas50)) - [SUSTAIN-955] Add kerberos and additional winrm options [#426](https://github.com/inspec/train/pull/426) ([marcparadise](https://github.com/marcparadise)) ## [v1.7.5](https://github.com/inspec/train/tree/v1.7.5) (2019-03-13) #### Merged Pull Requests - Replace the usage of const_defined? in the WinRM detection helper [#406](https://github.com/inspec/train/pull/406) ([RoboticCheese](https://github.com/RoboticCheese)) ## [v1.7.4](https://github.com/inspec/train/tree/v1.7.4) (2019-03-01) #### Merged Pull Requests - Add v2 to expeditor [#407](https://github.com/inspec/train/pull/407) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Add api_call cache type [#411](https://github.com/inspec/train/pull/411) ([clintoncwolfe](https://github.com/clintoncwolfe)) ## [v1.7.2](https://github.com/inspec/train/tree/v1.7.2) (2019-01-31) #### Merged Pull Requests - Prepare train for using credential sets [#394](https://github.com/inspec/train/pull/394) ([clintoncwolfe](https://github.com/clintoncwolfe)) ## [v1.7.1](https://github.com/inspec/train/tree/v1.7.1) (2019-01-26) #### Merged Pull Requests - Update copyright year to 2019 [#403](https://github.com/inspec/train/pull/403) ([clintoncwolfe](https://github.com/clintoncwolfe)) ## [v1.7.0](https://github.com/inspec/train/tree/v1.7.0) (2019-01-26) #### Merged Pull Requests - drop ruby 2.0-2.2 support, allow bundler 2.x, bump integration gems, allow mixlib-shellout 3.x [#396](https://github.com/inspec/train/pull/396) ([lamont-granquist](https://github.com/lamont-granquist)) - Fix Azure Test by adding a require [#402](https://github.com/inspec/train/pull/402) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Remove compat with unsupported Ruby 1.9 in the Gemfile [#389](https://github.com/inspec/train/pull/389) ([tas50](https://github.com/tas50)) ## [v1.6.3](https://github.com/inspec/train/tree/v1.6.3) (2018-12-19) #### Merged Pull Requests - Expose additional winrm options [#392](https://github.com/inspec/train/pull/392) ([frezbo](https://github.com/frezbo)) - Only ship the runtime files in the gem to slim install sizes [#388](https://github.com/inspec/train/pull/388) ([tas50](https://github.com/tas50)) - Small style/spelling changes for Train example plugin [#364](https://github.com/inspec/train/pull/364) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Update Travis config for Xenial / Ruby 2.6 and latest patchlevels of Ruby 2.2-2.5 [#390](https://github.com/inspec/train/pull/390) ([tas50](https://github.com/tas50)) ## [v1.5.11](https://github.com/inspec/train/tree/v1.5.11) (2018-12-10) #### Merged Pull Requests - Add Google API application info [#378](https://github.com/inspec/train/pull/378) ([nathenharvey](https://github.com/nathenharvey)) - Fix shallow_link_path on remote unix [#373](https://github.com/inspec/train/pull/373) ([mheiges](https://github.com/mheiges)) - Remove `#local?` [#365](https://github.com/inspec/train/pull/365) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Added a new matcher for amazon linux 2 [#380](https://github.com/inspec/train/pull/380) ([artyomtkachenko](https://github.com/artyomtkachenko)) - Pass logger to Cisco IOS transport [#381](https://github.com/inspec/train/pull/381) ([btm](https://github.com/btm)) ## [v1.5.6](https://github.com/inspec/train/tree/v1.5.6) (2018-11-01) #### Merged Pull Requests - Fix Cisco IOS detection when banners lack a `\r\n` [#372](https://github.com/inspec/train/pull/372) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Adds cached_client method in BaseConnection [#371](https://github.com/inspec/train/pull/371) ([dmccown](https://github.com/dmccown)) ## [v1.5.4](https://github.com/inspec/train/tree/v1.5.4) (2018-10-18) #### Merged Pull Requests - Fixes the link pointing back to the plugin docs [#362](https://github.com/inspec/train/pull/362) ([cattywampus](https://github.com/cattywampus)) - Remove the legacy version bumping from the rakefile [#359](https://github.com/inspec/train/pull/359) ([tas50](https://github.com/tas50)) - Adds Azure Vault Client [#351](https://github.com/inspec/train/pull/351) ([r-fennell](https://github.com/r-fennell)) - Correct example plugin link [#363](https://github.com/inspec/train/pull/363) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) ## [v1.5.0](https://github.com/inspec/train/tree/v1.5.0) (2018-09-27) #### Merged Pull Requests - Update google-api-client version. [#348](https://github.com/inspec/train/pull/348) ([skpaterson](https://github.com/skpaterson)) - Adding GCP admin_client helper. [#349](https://github.com/inspec/train/pull/349) ([skpaterson](https://github.com/skpaterson)) - Plugins: Test harness, test fixture, docs, and local-type example [#356](https://github.com/inspec/train/pull/356) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Bump minor version. [#357](https://github.com/inspec/train/pull/357) ([jquick](https://github.com/jquick)) ## [v1.4.37](https://github.com/inspec/train/tree/v1.4.37) (2018-09-13) #### Merged Pull Requests - Rescues SystemCallError instead of Errno [#346](https://github.com/inspec/train/pull/346) ([dmccown](https://github.com/dmccown)) - Add a export method for platforms [#347](https://github.com/inspec/train/pull/347) ([jquick](https://github.com/jquick)) ## [v1.4.35](https://github.com/inspec/train/tree/v1.4.35) (2018-08-23) #### Merged Pull Requests - Ensure unique_identifier returns something meaningful for service acc… [#338](https://github.com/inspec/train/pull/338) ([skpaterson](https://github.com/skpaterson)) - Modify Cisco UUID detection to use processor ID [#342](https://github.com/inspec/train/pull/342) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Fixes failing test when you have a cred file [#343](https://github.com/inspec/train/pull/343) ([dmccown](https://github.com/dmccown)) - Adds connection to Graph RBAC API [#327](https://github.com/inspec/train/pull/327) ([r-fennell](https://github.com/r-fennell)) ## [v1.4.31](https://github.com/inspec/train/tree/v1.4.31) (2018-08-17) #### Merged Pull Requests - Fixes an issue where the credential file was nil [#337](https://github.com/inspec/train/pull/337) ([dmccown](https://github.com/dmccown)) - Enable using rubygems as plugins [#335](https://github.com/inspec/train/pull/335) ([clintoncwolfe](https://github.com/clintoncwolfe)) ## [v1.4.29](https://github.com/inspec/train/tree/v1.4.29) (2018-08-15) #### Features & Enhancements - Pulls file credentials parsing out of Azure class [#324](https://github.com/inspec/train/pull/324) ([dmccown](https://github.com/dmccown)) #### Merged Pull Requests - Modify checksum logic to use system binaries [#251](https://github.com/inspec/train/pull/251) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Require Ruby 2.0 and allow net-ssh 5.0 [#334](https://github.com/inspec/train/pull/334) ([tas50](https://github.com/tas50)) - Add non_interactive support for SSH [#336](https://github.com/inspec/train/pull/336) ([marcparadise](https://github.com/marcparadise)) ## [v1.4.25](https://github.com/inspec/train/tree/v1.4.25) (2018-08-01) #### Merged Pull Requests - Remove not needed google-cloud dependency (see #328) and correct GCP … [#329](https://github.com/inspec/train/pull/329) ([skpaterson](https://github.com/skpaterson)) ## [v1.4.24](https://github.com/inspec/train/tree/v1.4.24) (2018-07-26) #### Merged Pull Requests - Add shallow_link_path to inspect symlink direct link [#309](https://github.com/inspec/train/pull/309) ([ColinHebert](https://github.com/ColinHebert)) - Retry SSH command on IOError (Cisco IOS specific) [#326](https://github.com/inspec/train/pull/326) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) ## [v1.4.22](https://github.com/inspec/train/tree/v1.4.22) (2018-07-16) #### Merged Pull Requests - Add VMware transport [#321](https://github.com/inspec/train/pull/321) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) ## [v1.4.21](https://github.com/inspec/train/tree/v1.4.21) (2018-07-05) #### Merged Pull Requests - Remove the delivery cookbook [#317](https://github.com/inspec/train/pull/317) ([tas50](https://github.com/tas50)) - Modify `WindowsPipeRunner` stderr to use String [#320](https://github.com/inspec/train/pull/320) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) ## [v1.4.19](https://github.com/inspec/train/tree/v1.4.19) (2018-06-29) #### Merged Pull Requests - Fix detection of amazon linux 2 [#312](https://github.com/inspec/train/pull/312) ([artem-sidorenko](https://github.com/artem-sidorenko)) - Adding proper bastion support [#310](https://github.com/inspec/train/pull/310) ([frezbo](https://github.com/frezbo)) - Remove github_changelog_generator [#313](https://github.com/inspec/train/pull/313) ([tas50](https://github.com/tas50)) - Remove the deploy config from Travis [#315](https://github.com/inspec/train/pull/315) ([tas50](https://github.com/tas50)) ## [v1.4.15](https://github.com/inspec/train/tree/v1.4.15) (2018-06-14) #### Merged Pull Requests - Allow TrainError to provide a supplement reason [#303](https://github.com/inspec/train/pull/303) ([marcparadise](https://github.com/marcparadise)) - Adding Oneview to platform detection. [#307](https://github.com/inspec/train/pull/307) ([skpaterson](https://github.com/skpaterson)) - Add the mock transport to train-core [#308](https://github.com/inspec/train/pull/308) ([jquick](https://github.com/jquick)) - Don't double-escape paths [#306](https://github.com/inspec/train/pull/306) ([voroniys](https://github.com/voroniys)) ## [v1.4.11](https://github.com/inspec/train/tree/v1.4.11) (2018-05-17) #### Merged Pull Requests - Add required env for azure shell msi headers [#302](https://github.com/inspec/train/pull/302) ([jquick](https://github.com/jquick)) ## [v1.4.10](https://github.com/inspec/train/tree/v1.4.10) (2018-05-17) #### Merged Pull Requests - support sudo passwords for cisco [#301](https://github.com/inspec/train/pull/301) ([arlimus](https://github.com/arlimus)) ## [v1.4.9](https://github.com/inspec/train/tree/v1.4.9) (2018-05-16) #### Bug Fixes - Allow nil password and www_form_encoded_password to work together. [#297](https://github.com/inspec/train/pull/297) ([marcparadise](https://github.com/marcparadise)) #### Merged Pull Requests - Support encoded passwords in target url [#296](https://github.com/inspec/train/pull/296) ([marcparadise](https://github.com/marcparadise)) - Initial import of transport for GCP. [#283](https://github.com/inspec/train/pull/283) ([skpaterson](https://github.com/skpaterson)) - Change Cisco IOS transport log level to INFO [#298](https://github.com/inspec/train/pull/298) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Unpin google-protobuf now that we are building it as a gem [#300](https://github.com/inspec/train/pull/300) ([scotthain](https://github.com/scotthain)) ## [v1.4.4](https://github.com/inspec/train/tree/v1.4.4) (2018-05-02) #### Merged Pull Requests - Enable expeditor release tasks [#294](https://github.com/inspec/train/pull/294) ([jquick](https://github.com/jquick)) - Split train into a core gem. [#293](https://github.com/inspec/train/pull/293) ([miah](https://github.com/miah)) # Change Log ## [1.4.2](https://github.com/inspec/train/tree/1.4.2) (2018-04-26) [Full Changelog](https://github.com/inspec/train/compare/v1.4.1...1.4.2) **Merged pull requests:** - switched method of determining aws account id to STS [\#286](https://github.com/inspec/train/pull/286) ([tkrueger](https://github.com/tkrueger)) ## [v1.4.1](https://github.com/inspec/train/tree/v1.4.1) (2018-04-19) [Full Changelog](https://github.com/inspec/train/compare/v1.4.0...v1.4.1) **Merged pull requests:** - Release 1.4.1 [\#287](https://github.com/inspec/train/pull/287) ([jquick](https://github.com/jquick)) - Add UUID for Cisco IOS and Nexus devices [\#285](https://github.com/inspec/train/pull/285) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Add handling for privileged exec mode [\#284](https://github.com/inspec/train/pull/284) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) ## [v1.4.0](https://github.com/inspec/train/tree/v1.4.0) (2018-04-12) [Full Changelog](https://github.com/inspec/train/compare/v1.3.0...v1.4.0) **Closed issues:** - Train reports directories with the archive bit set as files on the windows platform [\#274](https://github.com/inspec/train/issues/274) **Merged pull requests:** - Release 1.4.0 [\#282](https://github.com/inspec/train/pull/282) ([jquick](https://github.com/jquick)) - Add CloudLinux as a detected platform [\#281](https://github.com/inspec/train/pull/281) ([tarcinil](https://github.com/tarcinil)) - Move Cisco IOS connection under SSH transport [\#279](https://github.com/inspec/train/pull/279) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Initialize FileManager using '@service' [\#278](https://github.com/inspec/train/pull/278) ([marcparadise](https://github.com/marcparadise)) - small fix to make sure windows directories with the archive bit set a… [\#275](https://github.com/inspec/train/pull/275) ([devoptimist](https://github.com/devoptimist)) ## [v1.3.0](https://github.com/inspec/train/tree/v1.3.0) (2018-03-29) [Full Changelog](https://github.com/inspec/train/compare/v1.2.0...v1.3.0) **Implemented enhancements:** - Update errors to have a base type of Train::Error [\#273](https://github.com/inspec/train/pull/273) ([marcparadise](https://github.com/marcparadise)) **Closed issues:** - RFC: Generate unique uuid for platforms [\#264](https://github.com/inspec/train/issues/264) **Merged pull requests:** - Release Train 1.3.0 [\#276](https://github.com/inspec/train/pull/276) ([jquick](https://github.com/jquick)) - Add MSI connection option for azure. [\#272](https://github.com/inspec/train/pull/272) ([jquick](https://github.com/jquick)) - Add transport for Cisco IOS [\#271](https://github.com/inspec/train/pull/271) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Add platform uuid information. [\#270](https://github.com/inspec/train/pull/270) ([jquick](https://github.com/jquick)) ## [v1.2.0](https://github.com/inspec/train/tree/v1.2.0) (2018-03-15) [Full Changelog](https://github.com/inspec/train/compare/v1.1.1...v1.2.0) **Implemented enhancements:** - Change error message to use `connection` [\#263](https://github.com/inspec/train/pull/263) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) **Closed issues:** - Force 64bit powershell if using ruby32 on a 64bit os [\#265](https://github.com/inspec/train/issues/265) - Master OS detect family [\#260](https://github.com/inspec/train/issues/260) **Merged pull requests:** - Release train 1.2.0 [\#269](https://github.com/inspec/train/pull/269) ([jquick](https://github.com/jquick)) - Force 64bit powershell for 32bit ruby running on 64bit windows [\#266](https://github.com/inspec/train/pull/266) ([jquick](https://github.com/jquick)) - support cisco ios xe [\#262](https://github.com/inspec/train/pull/262) ([arlimus](https://github.com/arlimus)) - Create a master OS family and refactor specifications [\#261](https://github.com/inspec/train/pull/261) ([jquick](https://github.com/jquick)) - Support for Brocade FOS-based SAN devices [\#254](https://github.com/inspec/train/pull/254) ([marcelhuth](https://github.com/marcelhuth)) - ProxyCommand support [\#227](https://github.com/inspec/train/pull/227) ([cbeckr](https://github.com/cbeckr)) ## [v1.1.1](https://github.com/inspec/train/tree/v1.1.1) (2018-02-14) [Full Changelog](https://github.com/inspec/train/compare/v1.1.0...v1.1.1) **Merged pull requests:** - Release train 1.1.1 [\#259](https://github.com/inspec/train/pull/259) ([jquick](https://github.com/jquick)) - Add api sdk versions as platform release [\#258](https://github.com/inspec/train/pull/258) ([jquick](https://github.com/jquick)) - Add plat helper methods to api direct platforms. [\#257](https://github.com/inspec/train/pull/257) ([jquick](https://github.com/jquick)) ## [v1.1.0](https://github.com/inspec/train/tree/v1.1.0) (2018-02-08) [Full Changelog](https://github.com/inspec/train/compare/v1.0.0...v1.1.0) **Closed issues:** - Add azure:// target [\#233](https://github.com/inspec/train/issues/233) **Merged pull requests:** - Release train 1.1.0 [\#255](https://github.com/inspec/train/pull/255) ([jquick](https://github.com/jquick)) - Add qnx platform support [\#253](https://github.com/inspec/train/pull/253) ([jquick](https://github.com/jquick)) - Add azure transport [\#250](https://github.com/inspec/train/pull/250) ([jquick](https://github.com/jquick)) - Fix AIX and QNX file support [\#249](https://github.com/inspec/train/pull/249) ([adamleff](https://github.com/adamleff)) ## [v1.0.0](https://github.com/inspec/train/tree/v1.0.0) (2018-02-01) [Full Changelog](https://github.com/inspec/train/compare/v0.32.0...v1.0.0) **Closed issues:** - Add aws:// target [\#229](https://github.com/inspec/train/issues/229) **Merged pull requests:** - Update version to 1.0.0 [\#248](https://github.com/inspec/train/pull/248) ([jquick](https://github.com/jquick)) - cisco nexus + ios12 [\#247](https://github.com/inspec/train/pull/247) ([arlimus](https://github.com/arlimus)) - Add a CONTRIBUTING.md to Train [\#245](https://github.com/inspec/train/pull/245) ([miah](https://github.com/miah)) - catch detect failing to parse json [\#243](https://github.com/inspec/train/pull/243) ([arlimus](https://github.com/arlimus)) - if ssh closes the session force it to reset and reopen [\#242](https://github.com/inspec/train/pull/242) ([arlimus](https://github.com/arlimus)) - Add AWS transport [\#239](https://github.com/inspec/train/pull/239) ([clintoncwolfe](https://github.com/clintoncwolfe)) - Fix detection of Scientific Linux [\#237](https://github.com/inspec/train/pull/237) ([schrd](https://github.com/schrd)) ## [v0.32.0](https://github.com/inspec/train/tree/v0.32.0) (2018-01-04) [Full Changelog](https://github.com/inspec/train/compare/v0.31.1...v0.32.0) **Fixed bugs:** - platform names should be lower case [\#191](https://github.com/inspec/train/issues/191) - Return platform name that is lower case and underscored [\#228](https://github.com/inspec/train/pull/228) ([jquick](https://github.com/jquick)) **Merged pull requests:** - Release 0.32.0 [\#232](https://github.com/inspec/train/pull/232) ([adamleff](https://github.com/adamleff)) - Set mock transport to use the platform instance variable [\#230](https://github.com/inspec/train/pull/230) ([jquick](https://github.com/jquick)) ## [v0.31.1](https://github.com/inspec/train/tree/v0.31.1) (2017-12-06) [Full Changelog](https://github.com/inspec/train/compare/v0.31.0...v0.31.1) **Merged pull requests:** - Release 0.31.1 [\#226](https://github.com/inspec/train/pull/226) ([adamleff](https://github.com/adamleff)) - Allow runner specifications for local connections [\#225](https://github.com/inspec/train/pull/225) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) ## [v0.31.0](https://github.com/inspec/train/tree/v0.31.0) (2017-12-05) [Full Changelog](https://github.com/inspec/train/compare/v0.30.0...v0.31.0) **Fixed bugs:** - Add release detect for failback debian [\#223](https://github.com/inspec/train/pull/223) ([jquick](https://github.com/jquick)) **Merged pull requests:** - Release 0.31.0 [\#224](https://github.com/inspec/train/pull/224) ([adamleff](https://github.com/adamleff)) - Use named pipe to decrease local Windows runtime [\#220](https://github.com/inspec/train/pull/220) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) ## [v0.30.0](https://github.com/inspec/train/tree/v0.30.0) (2017-12-04) [Full Changelog](https://github.com/inspec/train/compare/v0.29.2...v0.30.0) **Merged pull requests:** - Release 0.30.0 [\#222](https://github.com/inspec/train/pull/222) ([adamleff](https://github.com/adamleff)) - Change the mock transport name to be 'mock' [\#221](https://github.com/inspec/train/pull/221) ([jquick](https://github.com/jquick)) - Enable caching on connections [\#214](https://github.com/inspec/train/pull/214) ([jquick](https://github.com/jquick)) ## [v0.29.2](https://github.com/inspec/train/tree/v0.29.2) (2017-11-21) [Full Changelog](https://github.com/inspec/train/compare/v0.29.1...v0.29.2) **Fixed bugs:** - Add unix\_mode\_mask method to Train::File::Local::Unix [\#215](https://github.com/inspec/train/pull/215) ([adamleff](https://github.com/adamleff)) **Merged pull requests:** - Fix regressions in 0.29.1 [\#219](https://github.com/inspec/train/pull/219) ([adamleff](https://github.com/adamleff)) - Use the sanitized file path for remote linux files [\#218](https://github.com/inspec/train/pull/218) ([RoboticCheese](https://github.com/RoboticCheese)) - Remove bundler install during Appveyor tests [\#217](https://github.com/inspec/train/pull/217) ([adamleff](https://github.com/adamleff)) - Fix inspec mock tests [\#216](https://github.com/inspec/train/pull/216) ([jquick](https://github.com/jquick)) - Platform framework and detect DSL [\#209](https://github.com/inspec/train/pull/209) ([jquick](https://github.com/jquick)) ## [v0.29.1](https://github.com/inspec/train/tree/v0.29.1) (2017-11-13) [Full Changelog](https://github.com/inspec/train/compare/v0.29.0...v0.29.1) **Merged pull requests:** - Release 0.29.1 [\#213](https://github.com/inspec/train/pull/213) ([adamleff](https://github.com/adamleff)) - Allow for a nil value when mocking OS [\#212](https://github.com/inspec/train/pull/212) ([adamleff](https://github.com/adamleff)) - Ensure a `mounted?` method exists for all File classes, including Mock [\#211](https://github.com/inspec/train/pull/211) ([adamleff](https://github.com/adamleff)) ## [v0.29.0](https://github.com/inspec/train/tree/v0.29.0) (2017-11-13) [Full Changelog](https://github.com/inspec/train/compare/v0.28.0...v0.29.0) **Merged pull requests:** - Release 0.29.0 [\#210](https://github.com/inspec/train/pull/210) ([adamleff](https://github.com/adamleff)) - Reverting accidental push to master re: \#204 [\#208](https://github.com/inspec/train/pull/208) ([adamleff](https://github.com/adamleff)) - clearer error if no auth methods are available [\#207](https://github.com/inspec/train/pull/207) ([thommay](https://github.com/thommay)) - Build a complete mock OS object [\#206](https://github.com/inspec/train/pull/206) ([adamleff](https://github.com/adamleff)) - Platform framework and detect DSL [\#204](https://github.com/inspec/train/pull/204) ([jquick](https://github.com/jquick)) - add basic qnx support for train [\#203](https://github.com/inspec/train/pull/203) ([chris-rock](https://github.com/chris-rock)) - Add CODEOWNERS for train [\#202](https://github.com/inspec/train/pull/202) ([adamleff](https://github.com/adamleff)) - implement uploads and downloads for ssh and winrm [\#201](https://github.com/inspec/train/pull/201) ([thommay](https://github.com/thommay)) - \[MSYS-649\] Fix InSpec file size in Windows, refactor File classes [\#193](https://github.com/inspec/train/pull/193) ([Vasu1105](https://github.com/Vasu1105)) ## [v0.28.0](https://github.com/inspec/train/tree/v0.28.0) (2017-09-25) [Full Changelog](https://github.com/inspec/train/compare/v0.27.0...v0.28.0) **Merged pull requests:** - Release 0.28.0 [\#200](https://github.com/inspec/train/pull/200) ([adamleff](https://github.com/adamleff)) - Continue to support older net-ssh while fixing 4.2 deprecation [\#199](https://github.com/inspec/train/pull/199) ([adamleff](https://github.com/adamleff)) ## [v0.27.0](https://github.com/inspec/train/tree/v0.27.0) (2017-09-25) [Full Changelog](https://github.com/inspec/train/compare/v0.26.2...v0.27.0) **Merged pull requests:** - Release v0.27.0 [\#198](https://github.com/inspec/train/pull/198) ([adamleff](https://github.com/adamleff)) - Bump to net-ssh 4.2, fix bad net-ssh deprecation [\#197](https://github.com/inspec/train/pull/197) ([adamleff](https://github.com/adamleff)) ## [v0.26.2](https://github.com/inspec/train/tree/v0.26.2) (2017-09-05) [Full Changelog](https://github.com/inspec/train/compare/v0.26.1...v0.26.2) **Merged pull requests:** - Release 0.26.2 [\#195](https://github.com/inspec/train/pull/195) ([adamleff](https://github.com/adamleff)) - Fix inconsistent link\_path behavior [\#194](https://github.com/inspec/train/pull/194) ([adamleff](https://github.com/adamleff)) ## [v0.26.1](https://github.com/inspec/train/tree/v0.26.1) (2017-08-14) [Full Changelog](https://github.com/inspec/train/compare/v0.26.0...v0.26.1) **Merged pull requests:** - Release 0.26.1 [\#188](https://github.com/inspec/train/pull/188) ([adamleff](https://github.com/adamleff)) - Return non-zero exit code for unknown mock command [\#187](https://github.com/inspec/train/pull/187) ([chris-rock](https://github.com/chris-rock)) ## [v0.26.0](https://github.com/inspec/train/tree/v0.26.0) (2017-08-10) [Full Changelog](https://github.com/inspec/train/compare/v0.25.0...v0.26.0) **Fixed bugs:** - AIX operating system name is not detected properly [\#181](https://github.com/inspec/train/issues/181) **Closed issues:** - Add support for ssh-agent to ssh transport [\#129](https://github.com/inspec/train/issues/129) **Merged pull requests:** - Release v0.26.0 [\#186](https://github.com/inspec/train/pull/186) ([adamleff](https://github.com/adamleff)) - typo - should $g for group instead of doulbe $u [\#185](https://github.com/inspec/train/pull/185) ([aklyachkin](https://github.com/aklyachkin)) - update ruby requirements to 2.2 - 2.4 range [\#184](https://github.com/inspec/train/pull/184) ([arlimus](https://github.com/arlimus)) - detect operating system name for AIX [\#182](https://github.com/inspec/train/pull/182) ([chris-rock](https://github.com/chris-rock)) ## [v0.25.0](https://github.com/inspec/train/tree/v0.25.0) (2017-06-15) [Full Changelog](https://github.com/inspec/train/compare/v0.24.0...v0.25.0) **Merged pull requests:** - Fix CoreOS platform detection [\#180](https://github.com/inspec/train/pull/180) ([rarenerd](https://github.com/rarenerd)) - Remove autoloads in favor of eager loading [\#178](https://github.com/inspec/train/pull/178) ([Sharpie](https://github.com/Sharpie)) - Fixed IPv6 URI parsing [\#176](https://github.com/inspec/train/pull/176) ([zfjagann](https://github.com/zfjagann)) ## [v0.24.0](https://github.com/inspec/train/tree/v0.24.0) (2017-05-30) [Full Changelog](https://github.com/inspec/train/compare/v0.23.0...v0.24.0) **Merged pull requests:** - prevent sudo on localhost targets [\#179](https://github.com/inspec/train/pull/179) ([arlimus](https://github.com/arlimus)) ## [v0.23.0](https://github.com/inspec/train/tree/v0.23.0) (2017-03-29) [Full Changelog](https://github.com/inspec/train/compare/v0.22.1...v0.23.0) **Merged pull requests:** - Release 0.23.0 [\#173](https://github.com/inspec/train/pull/173) ([adamleff](https://github.com/adamleff)) - Fix Net::SSH warning when passing nil option values [\#172](https://github.com/inspec/train/pull/172) ([tylercloke](https://github.com/tylercloke)) - winrm: hide password [\#171](https://github.com/inspec/train/pull/171) ([crepetl](https://github.com/crepetl)) ## [v0.22.1](https://github.com/inspec/train/tree/v0.22.1) (2017-01-17) [Full Changelog](https://github.com/inspec/train/compare/v0.22.0...v0.22.1) **Merged pull requests:** - Release 0.22.1 [\#169](https://github.com/inspec/train/pull/169) ([tduffield](https://github.com/tduffield)) - Relax net-ssh dep to allow 4.0 [\#168](https://github.com/inspec/train/pull/168) ([tduffield](https://github.com/tduffield)) - Fix Oracle Linux detection [\#167](https://github.com/inspec/train/pull/167) ([carldjohnston](https://github.com/carldjohnston)) - Add support for parallels & virtuozzo linux [\#166](https://github.com/inspec/train/pull/166) ([jaxxstorm](https://github.com/jaxxstorm)) ## [v0.22.0](https://github.com/inspec/train/tree/v0.22.0) (2016-11-29) [Full Changelog](https://github.com/inspec/train/compare/v0.21.1...v0.22.0) **Implemented enhancements:** - Try to use ssh agent if no password or key files have been specified [\#165](https://github.com/inspec/train/pull/165) ([alexpop](https://github.com/alexpop)) **Merged pull requests:** - Add openvms detection [\#159](https://github.com/inspec/train/pull/159) ([briandoodyie](https://github.com/briandoodyie)) ## [v0.21.1](https://github.com/inspec/train/tree/v0.21.1) (2016-11-04) [Full Changelog](https://github.com/inspec/train/compare/v0.21.0...v0.21.1) **Closed issues:** - detect\_arista\_eos raises exception against unix host [\#160](https://github.com/inspec/train/issues/160) **Merged pull requests:** - ensure the os detection works in pty mode [\#161](https://github.com/inspec/train/pull/161) ([chris-rock](https://github.com/chris-rock)) ## [v0.21.0](https://github.com/inspec/train/tree/v0.21.0) (2016-11-04) [Full Changelog](https://github.com/inspec/train/compare/v0.20.1...v0.21.0) **Implemented enhancements:** - Train doesn't create a login shell [\#148](https://github.com/inspec/train/issues/148) **Merged pull requests:** - Add detection for Arista EOS [\#158](https://github.com/inspec/train/pull/158) ([jerearista](https://github.com/jerearista)) ## [v0.20.1](https://github.com/inspec/train/tree/v0.20.1) (2016-10-15) [Full Changelog](https://github.com/inspec/train/compare/v0.20.0...v0.20.1) **Fixed bugs:** - support empty URIs [\#154](https://github.com/inspec/train/pull/154) ([arlimus](https://github.com/arlimus)) ## [v0.20.0](https://github.com/inspec/train/tree/v0.20.0) (2016-09-21) [Full Changelog](https://github.com/inspec/train/compare/v0.19.1...v0.20.0) **Fixed bugs:** - get `Preparing modules for first use.` when I use train on Windows [\#153](https://github.com/inspec/train/issues/153) **Merged pull requests:** - `Preparing modules for first use.` error message on Windows [\#152](https://github.com/inspec/train/pull/152) ([chris-rock](https://github.com/chris-rock)) - Convert `wmic` architecture to a normal standard [\#151](https://github.com/inspec/train/pull/151) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) - Login shell [\#149](https://github.com/inspec/train/pull/149) ([jonathanmorley](https://github.com/jonathanmorley)) ## [v0.19.1](https://github.com/inspec/train/tree/v0.19.1) (2016-09-16) [Full Changelog](https://github.com/inspec/train/compare/v0.19.0...v0.19.1) **Implemented enhancements:** - hostname property for WinRM::Connection [\#128](https://github.com/inspec/train/issues/128) - Return hostname from WinRM::Connection same as SSH::Connection [\#150](https://github.com/inspec/train/pull/150) ([alexpop](https://github.com/alexpop)) ## [v0.19.0](https://github.com/inspec/train/tree/v0.19.0) (2016-09-05) [Full Changelog](https://github.com/inspec/train/compare/v0.18.0...v0.19.0) **Fixed bugs:** - use stat -c for alpine linux [\#146](https://github.com/inspec/train/pull/146) ([chris-rock](https://github.com/chris-rock)) **Merged pull requests:** - support ruby 2.2.1 [\#145](https://github.com/inspec/train/pull/145) ([chris-rock](https://github.com/chris-rock)) - Use winrm v2 implementation [\#122](https://github.com/inspec/train/pull/122) ([mwrock](https://github.com/mwrock)) ## [v0.18.0](https://github.com/inspec/train/tree/v0.18.0) (2016-08-26) [Full Changelog](https://github.com/inspec/train/compare/v0.17.0...v0.18.0) **Merged pull requests:** - Allow JSON 2.0 on Ruby 2.2 and above [\#144](https://github.com/inspec/train/pull/144) ([jkeiser](https://github.com/jkeiser)) - Enable Ruby 2.3 in Travis, make it default suite [\#143](https://github.com/inspec/train/pull/143) ([jkeiser](https://github.com/jkeiser)) - Add the darwin platform family [\#141](https://github.com/inspec/train/pull/141) ([tas50](https://github.com/tas50)) - update integration test dependencies [\#139](https://github.com/inspec/train/pull/139) ([tas50](https://github.com/tas50)) - Add badges to the readme [\#138](https://github.com/inspec/train/pull/138) ([tas50](https://github.com/tas50)) - use --decode on base64 command to maintain compatibility with Darwin. [\#137](https://github.com/inspec/train/pull/137) ([thomascate](https://github.com/thomascate)) ## [v0.17.0](https://github.com/inspec/train/tree/v0.17.0) (2016-08-19) [Full Changelog](https://github.com/inspec/train/compare/v0.16.0...v0.17.0) **Implemented enhancements:** - return owner for files on windows [\#132](https://github.com/inspec/train/pull/132) ([chris-rock](https://github.com/chris-rock)) **Closed issues:** - prefix powershell commands with `$ProgressPreference = "SilentlyContinue"` [\#134](https://github.com/inspec/train/issues/134) **Merged pull requests:** - CI improvements [\#133](https://github.com/inspec/train/pull/133) ([chris-rock](https://github.com/chris-rock)) - Rescue EPIPE on connect in ssh transport [\#130](https://github.com/inspec/train/pull/130) ([stevendanna](https://github.com/stevendanna)) ## [v0.16.0](https://github.com/inspec/train/tree/v0.16.0) (2016-08-08) [Full Changelog](https://github.com/inspec/train/compare/v0.15.1...v0.16.0) **Merged pull requests:** - provide file\_version and product\_version for windows files [\#127](https://github.com/inspec/train/pull/127) ([chris-rock](https://github.com/chris-rock)) - Bring train platform data more in line with ohai's platform data [\#126](https://github.com/inspec/train/pull/126) ([stevendanna](https://github.com/stevendanna)) ## [v0.15.1](https://github.com/inspec/train/tree/v0.15.1) (2016-07-11) [Full Changelog](https://github.com/inspec/train/compare/v0.15.0...v0.15.1) **Fixed bugs:** - bugfix: higher mode bits on local connection [\#125](https://github.com/inspec/train/pull/125) ([arlimus](https://github.com/arlimus)) **Merged pull requests:** - Test ruby 2.1 instead of 1.9.3 and only launch one test group per travis/appveyor [\#123](https://github.com/inspec/train/pull/123) ([mwrock](https://github.com/mwrock)) ## [v0.15.0](https://github.com/inspec/train/tree/v0.15.0) (2016-07-01) [Full Changelog](https://github.com/inspec/train/compare/v0.14.2...v0.15.0) **Implemented enhancements:** - have net-ssh request a pty [\#60](https://github.com/inspec/train/issues/60) **Merged pull requests:** - Allow requesting a PTY [\#121](https://github.com/inspec/train/pull/121) ([srenatus](https://github.com/srenatus)) ## [v0.14.2](https://github.com/inspec/train/tree/v0.14.2) (2016-06-28) [Full Changelog](https://github.com/inspec/train/compare/v0.14.1...v0.14.2) **Merged pull requests:** - do not log password in ssh connection output [\#120](https://github.com/inspec/train/pull/120) ([marcparadise](https://github.com/marcparadise)) ## [v0.14.1](https://github.com/inspec/train/tree/v0.14.1) (2016-06-27) [Full Changelog](https://github.com/inspec/train/compare/v0.14.0...v0.14.1) **Fixed bugs:** - bugfix: add mock backend initialization [\#119](https://github.com/inspec/train/pull/119) ([arlimus](https://github.com/arlimus)) ## [v0.14.0](https://github.com/inspec/train/tree/v0.14.0) (2016-06-27) [Full Changelog](https://github.com/inspec/train/compare/v0.13.1...v0.14.0) **Implemented enhancements:** - json in and out for base connection [\#118](https://github.com/inspec/train/pull/118) ([arlimus](https://github.com/arlimus)) - ESX support [\#116](https://github.com/inspec/train/pull/116) ([Anirudh-Gupta](https://github.com/Anirudh-Gupta)) **Fixed bugs:** - sporadic appveyor failure on `winrm delete ...` [\#105](https://github.com/inspec/train/issues/105) - bugfix: run frozen string commands via ssh [\#117](https://github.com/inspec/train/pull/117) ([arlimus](https://github.com/arlimus)) ## [v0.13.1](https://github.com/inspec/train/tree/v0.13.1) (2016-06-16) [Full Changelog](https://github.com/inspec/train/compare/v0.13.0...v0.13.1) **Implemented enhancements:** - use train as gem name. Thanks @halo [\#115](https://github.com/inspec/train/pull/115) ([chris-rock](https://github.com/chris-rock)) ## [v0.13.0](https://github.com/inspec/train/tree/v0.13.0) (2016-06-16) [Full Changelog](https://github.com/inspec/train/compare/v0.12.1...v0.13.0) **Implemented enhancements:** - provide uri-formatted information on all connections [\#113](https://github.com/inspec/train/pull/113) ([arlimus](https://github.com/arlimus)) **Fixed bugs:** - Authentication with SSH Server on OSX is failing [\#111](https://github.com/inspec/train/issues/111) **Merged pull requests:** - adding support for vmware's esx server [\#114](https://github.com/inspec/train/pull/114) ([Anirudh-Gupta](https://github.com/Anirudh-Gupta)) - add missing keyboard-interactive authentication method [\#112](https://github.com/inspec/train/pull/112) ([chris-rock](https://github.com/chris-rock)) ## [v0.12.1](https://github.com/inspec/train/tree/v0.12.1) (2016-05-23) [Full Changelog](https://github.com/inspec/train/compare/v0.12.0...v0.12.1) **Fixed bugs:** - loosen restriction for docker api [\#109](https://github.com/inspec/train/pull/109) ([chris-rock](https://github.com/chris-rock)) **Closed issues:** - docker-api conflict when using docker cookbook [\#108](https://github.com/inspec/train/issues/108) ## [v0.12.0](https://github.com/inspec/train/tree/v0.12.0) (2016-05-16) [Full Changelog](https://github.com/inspec/train/compare/v0.11.4...v0.12.0) **Merged pull requests:** - Custom sudo command [\#107](https://github.com/inspec/train/pull/107) ([jeremymv2](https://github.com/jeremymv2)) ## [v0.11.4](https://github.com/inspec/train/tree/v0.11.4) (2016-05-13) [Full Changelog](https://github.com/inspec/train/compare/v0.11.3...v0.11.4) **Fixed bugs:** - mount resource incorrect matching [\#103](https://github.com/inspec/train/issues/103) - Add a space to avoid matching partial paths [\#104](https://github.com/inspec/train/pull/104) ([alexpop](https://github.com/alexpop)) - Update README.md [\#102](https://github.com/inspec/train/pull/102) ([mcquin](https://github.com/mcquin)) **Merged pull requests:** - 0.11.4 [\#106](https://github.com/inspec/train/pull/106) ([arlimus](https://github.com/arlimus)) ## [v0.11.3](https://github.com/inspec/train/tree/v0.11.3) (2016-05-10) [Full Changelog](https://github.com/inspec/train/compare/v0.11.2...v0.11.3) **Fixed bugs:** - appveyor fixing... [\#98](https://github.com/inspec/train/pull/98) ([arlimus](https://github.com/arlimus)) **Merged pull requests:** - fix: winrm https listener is not configured anymore in appveyor [\#100](https://github.com/inspec/train/pull/100) ([chris-rock](https://github.com/chris-rock)) - use aix stats implementation for hpux as well [\#99](https://github.com/inspec/train/pull/99) ([Anirudh-Gupta](https://github.com/Anirudh-Gupta)) ## [v0.11.2](https://github.com/inspec/train/tree/v0.11.2) (2016-04-29) [Full Changelog](https://github.com/inspec/train/compare/v0.11.1...v0.11.2) **Fixed bugs:** - bugfix: windows file failed to initialize with new symlink handler [\#96](https://github.com/inspec/train/pull/96) ([arlimus](https://github.com/arlimus)) **Merged pull requests:** - 0.11.2 [\#97](https://github.com/inspec/train/pull/97) ([alexpop](https://github.com/alexpop)) ## [v0.11.1](https://github.com/inspec/train/tree/v0.11.1) (2016-04-28) [Full Changelog](https://github.com/inspec/train/compare/v0.11.0...v0.11.1) **Fixed bugs:** - fix nil file paths [\#94](https://github.com/inspec/train/pull/94) ([arlimus](https://github.com/arlimus)) **Merged pull requests:** - provide a source path for filecommon [\#95](https://github.com/inspec/train/pull/95) ([arlimus](https://github.com/arlimus)) - restructure docker tests to balance load between 2 runs [\#93](https://github.com/inspec/train/pull/93) ([arlimus](https://github.com/arlimus)) ## [v0.11.0](https://github.com/inspec/train/tree/v0.11.0) (2016-04-28) [Full Changelog](https://github.com/inspec/train/compare/v0.10.8...v0.11.0) **Implemented enhancements:** - Overhault file\(...\) and stat\(...\); point to destination of symlinks [\#92](https://github.com/inspec/train/pull/92) ([arlimus](https://github.com/arlimus)) **Fixed bugs:** - validate the backend configuration [\#91](https://github.com/inspec/train/pull/91) ([arlimus](https://github.com/arlimus)) ## [v0.10.8](https://github.com/inspec/train/tree/v0.10.8) (2016-04-25) [Full Changelog](https://github.com/inspec/train/compare/v0.10.7...v0.10.8) **Implemented enhancements:** - loose restriction for mixlib-shellout [\#89](https://github.com/inspec/train/pull/89) ([chris-rock](https://github.com/chris-rock)) **Merged pull requests:** - use gemspec for travis [\#90](https://github.com/inspec/train/pull/90) ([chris-rock](https://github.com/chris-rock)) - Don't strip off the second byte of the octal mode. [\#88](https://github.com/inspec/train/pull/88) ([justindossey](https://github.com/justindossey)) ## [v0.10.7](https://github.com/inspec/train/tree/v0.10.7) (2016-04-21) [Full Changelog](https://github.com/inspec/train/compare/v0.10.6...v0.10.7) **Merged pull requests:** - 0.10.7 [\#87](https://github.com/inspec/train/pull/87) ([arlimus](https://github.com/arlimus)) - Revert "add -L to get stat for symlink" [\#86](https://github.com/inspec/train/pull/86) ([arlimus](https://github.com/arlimus)) ## [v0.10.6](https://github.com/inspec/train/tree/v0.10.6) (2016-04-20) [Full Changelog](https://github.com/inspec/train/compare/v0.10.5...v0.10.6) **Merged pull requests:** - add -L to get stat for symlink [\#85](https://github.com/inspec/train/pull/85) ([vjeffrey](https://github.com/vjeffrey)) - release via travis + test via rubygems [\#84](https://github.com/inspec/train/pull/84) ([arlimus](https://github.com/arlimus)) ## [v0.10.5](https://github.com/inspec/train/tree/v0.10.5) (2016-04-18) [Full Changelog](https://github.com/inspec/train/compare/v0.10.4...v0.10.5) **Merged pull requests:** - 0.10.5 [\#83](https://github.com/inspec/train/pull/83) ([srenatus](https://github.com/srenatus)) - detection for hp-ux machine [\#82](https://github.com/inspec/train/pull/82) ([Anirudh-Gupta](https://github.com/Anirudh-Gupta)) ## [v0.10.4](https://github.com/inspec/train/tree/v0.10.4) (2016-03-31) [Full Changelog](https://github.com/inspec/train/compare/v0.10.3...v0.10.4) **Fixed bugs:** - bugfix: do not use unix path escape for windows [\#79](https://github.com/inspec/train/pull/79) ([chris-rock](https://github.com/chris-rock)) **Merged pull requests:** - 0.10.4 [\#80](https://github.com/inspec/train/pull/80) ([arlimus](https://github.com/arlimus)) ## [v0.10.3](https://github.com/inspec/train/tree/v0.10.3) (2016-03-07) [Full Changelog](https://github.com/inspec/train/compare/v0.10.1...v0.10.3) **Fixed bugs:** - set default value for ssh compression to false [\#77](https://github.com/inspec/train/pull/77) ([chris-rock](https://github.com/chris-rock)) - avoid mock backend error on nil commands [\#75](https://github.com/inspec/train/pull/75) ([arlimus](https://github.com/arlimus)) **Merged pull requests:** - 0.10.3 [\#78](https://github.com/inspec/train/pull/78) ([chris-rock](https://github.com/chris-rock)) - 0.10.2 [\#76](https://github.com/inspec/train/pull/76) ([arlimus](https://github.com/arlimus)) ## [v0.10.1](https://github.com/inspec/train/tree/v0.10.1) (2016-02-29) [Full Changelog](https://github.com/inspec/train/compare/v0.10.0...v0.10.1) **Merged pull requests:** - 0.10.1 [\#74](https://github.com/inspec/train/pull/74) ([chris-rock](https://github.com/chris-rock)) - fix gem build license warning [\#73](https://github.com/inspec/train/pull/73) ([chris-rock](https://github.com/chris-rock)) - depend on docker-api 1.26.2 [\#72](https://github.com/inspec/train/pull/72) ([someara](https://github.com/someara)) ## [v0.10.0](https://github.com/inspec/train/tree/v0.10.0) (2016-02-19) [Full Changelog](https://github.com/inspec/train/compare/v0.9.7...v0.10.0) **Implemented enhancements:** - show mock failures for commands [\#69](https://github.com/inspec/train/pull/69) ([arlimus](https://github.com/arlimus)) - update gems and rubocop [\#68](https://github.com/inspec/train/pull/68) ([arlimus](https://github.com/arlimus)) **Fixed bugs:** - complete rewrite of windows version detection [\#70](https://github.com/inspec/train/pull/70) ([chris-rock](https://github.com/chris-rock)) **Merged pull requests:** - 0.10.0 [\#71](https://github.com/inspec/train/pull/71) ([chris-rock](https://github.com/chris-rock)) ## [v0.9.7](https://github.com/inspec/train/tree/v0.9.7) (2016-02-05) [Full Changelog](https://github.com/inspec/train/compare/v0.9.6...v0.9.7) **Implemented enhancements:** - feature: add file.basename [\#64](https://github.com/inspec/train/pull/64) ([arlimus](https://github.com/arlimus)) - add `requiretty` workaround measures [\#63](https://github.com/inspec/train/pull/63) ([srenatus](https://github.com/srenatus)) **Fixed bugs:** - ensure bundler is installed on travis [\#66](https://github.com/inspec/train/pull/66) ([chris-rock](https://github.com/chris-rock)) **Merged pull requests:** - 0.9.7 [\#67](https://github.com/inspec/train/pull/67) ([chris-rock](https://github.com/chris-rock)) ## [v0.9.6](https://github.com/inspec/train/tree/v0.9.6) (2016-01-29) [Full Changelog](https://github.com/inspec/train/compare/v0.9.5...v0.9.6) **Implemented enhancements:** - add solaris support [\#61](https://github.com/inspec/train/pull/61) ([chris-rock](https://github.com/chris-rock)) **Merged pull requests:** - 0.9.6 [\#62](https://github.com/inspec/train/pull/62) ([chris-rock](https://github.com/chris-rock)) ## [v0.9.5](https://github.com/inspec/train/tree/v0.9.5) (2016-01-25) [Full Changelog](https://github.com/inspec/train/compare/v0.9.4...v0.9.5) **Implemented enhancements:** - use minitest for windows tests [\#56](https://github.com/inspec/train/pull/56) ([chris-rock](https://github.com/chris-rock)) - use negotiate auth for winrm and not basic\_auth [\#55](https://github.com/inspec/train/pull/55) ([mwrock](https://github.com/mwrock)) - bugfix: pin net-ssh 2.9 in gem file [\#54](https://github.com/inspec/train/pull/54) ([chris-rock](https://github.com/chris-rock)) - Add appveyor and Windows test [\#53](https://github.com/inspec/train/pull/53) ([chris-rock](https://github.com/chris-rock)) - Deprecating winrm-tansport gem [\#46](https://github.com/inspec/train/pull/46) ([mwrock](https://github.com/mwrock)) **Fixed bugs:** - Cannot install train on Windows with ChefDK if username \>9 chars in length due to spec filename lengths in docker-api gem. [\#28](https://github.com/inspec/train/issues/28) - Properly wrap commands in powershell for local backend [\#57](https://github.com/inspec/train/pull/57) ([chris-rock](https://github.com/chris-rock)) - Copying https://github.com/test-kitchen/test-kitchen/pull/919 to this repo [\#52](https://github.com/inspec/train/pull/52) ([tyler-ball](https://github.com/tyler-ball)) **Merged pull requests:** - 0.9.5 [\#58](https://github.com/inspec/train/pull/58) ([chris-rock](https://github.com/chris-rock)) ## [v0.9.4](https://github.com/inspec/train/tree/v0.9.4) (2016-01-15) [Full Changelog](https://github.com/inspec/train/compare/v0.9.3...v0.9.4) **Implemented enhancements:** - 0.9.3 is empty on Windows [\#48](https://github.com/inspec/train/pull/48) ([tyler-ball](https://github.com/tyler-ball)) - Updating to the latest release of net-ssh to consume https://github.com/net-ssh/net-ssh/pull/280 [\#47](https://github.com/inspec/train/pull/47) ([tyler-ball](https://github.com/tyler-ball)) **Fixed bugs:** - bugfix: command wrapper always return nil [\#50](https://github.com/inspec/train/pull/50) ([chris-rock](https://github.com/chris-rock)) **Merged pull requests:** - 0.9.4 [\#51](https://github.com/inspec/train/pull/51) ([chris-rock](https://github.com/chris-rock)) ## [v0.9.3](https://github.com/inspec/train/tree/v0.9.3) (2016-01-03) [Full Changelog](https://github.com/inspec/train/compare/v0.9.2...v0.9.3) **Implemented enhancements:** - introduce `mounted` as a separate method to retrieve the content [\#44](https://github.com/inspec/train/pull/44) ([chris-rock](https://github.com/chris-rock)) - Support for local transport on Windows [\#43](https://github.com/inspec/train/pull/43) ([chris-rock](https://github.com/chris-rock)) - Split integration test preparation from executing [\#42](https://github.com/inspec/train/pull/42) ([chris-rock](https://github.com/chris-rock)) - Support for AIX and targeted SSH testing [\#41](https://github.com/inspec/train/pull/41) ([foobarbam](https://github.com/foobarbam)) **Merged pull requests:** - 0.9.3 [\#45](https://github.com/inspec/train/pull/45) ([chris-rock](https://github.com/chris-rock)) ## [v0.9.2](https://github.com/inspec/train/tree/v0.9.2) (2015-12-11) [Full Changelog](https://github.com/inspec/train/compare/v0.9.1...v0.9.2) **Implemented enhancements:** - add changelog [\#38](https://github.com/inspec/train/pull/38) ([chris-rock](https://github.com/chris-rock)) - activate integration tests in travis [\#37](https://github.com/inspec/train/pull/37) ([chris-rock](https://github.com/chris-rock)) - Adding support for Wind River Linux in support of Cisco devices [\#33](https://github.com/inspec/train/pull/33) ([adamleff](https://github.com/adamleff)) **Fixed bugs:** - Integration test failures [\#34](https://github.com/inspec/train/issues/34) - Implemented WindowsFile\#exist? [\#36](https://github.com/inspec/train/pull/36) ([docwhat](https://github.com/docwhat)) - adapt integration test to changes in command\_wrapper [\#35](https://github.com/inspec/train/pull/35) ([srenatus](https://github.com/srenatus)) **Closed issues:** - WinRM plaintext transport is hardcoded \(cannot use SSL\) [\#29](https://github.com/inspec/train/issues/29) **Merged pull requests:** - 0.9.2 [\#40](https://github.com/inspec/train/pull/40) ([arlimus](https://github.com/arlimus)) - add rake version helpers [\#39](https://github.com/inspec/train/pull/39) ([arlimus](https://github.com/arlimus)) ## [v0.9.1](https://github.com/inspec/train/tree/v0.9.1) (2015-11-03) [Full Changelog](https://github.com/inspec/train/compare/0.9.0...v0.9.1) **Implemented enhancements:** - R train [\#27](https://github.com/inspec/train/pull/27) ([arlimus](https://github.com/arlimus)) - Update style of readme [\#26](https://github.com/inspec/train/pull/26) ([chris-rock](https://github.com/chris-rock)) - Add Apache 2.0 License [\#25](https://github.com/inspec/train/pull/25) ([jamesc](https://github.com/jamesc)) ## [0.9.0](https://github.com/inspec/train/tree/0.9.0) (2015-11-03) **Implemented enhancements:** - set windows name in :release [\#23](https://github.com/inspec/train/pull/23) ([arlimus](https://github.com/arlimus)) - basic file transport via winrm [\#21](https://github.com/inspec/train/pull/21) ([chris-rock](https://github.com/chris-rock)) - dont return nil on command errors stdout/stderr [\#20](https://github.com/inspec/train/pull/20) ([arlimus](https://github.com/arlimus)) - skip .delivery in gemspec [\#19](https://github.com/inspec/train/pull/19) ([arlimus](https://github.com/arlimus)) - Verify sudo is working and fail with error messages if it isn't [\#18](https://github.com/inspec/train/pull/18) ([arlimus](https://github.com/arlimus)) - improve file eposure [\#16](https://github.com/inspec/train/pull/16) ([chris-rock](https://github.com/chris-rock)) - add delivery [\#13](https://github.com/inspec/train/pull/13) ([arlimus](https://github.com/arlimus)) - Sudo [\#12](https://github.com/inspec/train/pull/12) ([arlimus](https://github.com/arlimus)) - Extract options handling for transport [\#11](https://github.com/inspec/train/pull/11) ([arlimus](https://github.com/arlimus)) - don't let mock commands return nil on stdout or stderr [\#10](https://github.com/inspec/train/pull/10) ([arlimus](https://github.com/arlimus)) - allow mock command to support sha256 mocking of commands [\#9](https://github.com/inspec/train/pull/9) ([arlimus](https://github.com/arlimus)) - register plugins with both names and symbols [\#8](https://github.com/inspec/train/pull/8) ([arlimus](https://github.com/arlimus)) - split of mock into transport and connection [\#7](https://github.com/inspec/train/pull/7) ([arlimus](https://github.com/arlimus)) - bugfix: add docker dependency to gemspec [\#6](https://github.com/inspec/train/pull/6) ([arlimus](https://github.com/arlimus)) - move train/plugins/common to train/extras [\#2](https://github.com/inspec/train/pull/2) ([arlimus](https://github.com/arlimus)) - add Travis [\#1](https://github.com/inspec/train/pull/1) ([arlimus](https://github.com/arlimus)) **Fixed bugs:** - bugfix: prevent debugging info to stdout on winrm [\#22](https://github.com/inspec/train/pull/22) ([arlimus](https://github.com/arlimus)) - bugfix: fail ssh connections correctly [\#17](https://github.com/inspec/train/pull/17) ([arlimus](https://github.com/arlimus)) - bugfix: initialize mock transport to correct family [\#14](https://github.com/inspec/train/pull/14) ([arlimus](https://github.com/arlimus)) **Merged pull requests:** - bump train version to 0.9.0 [\#24](https://github.com/inspec/train/pull/24) ([chris-rock](https://github.com/chris-rock))train-3.2.28/CODE_OF_CONDUCT.md000066400000000000000000000315461364512547200155440ustar00rootroot00000000000000# Chef Contributor Covenant Code of Conduct **Note**: If you are in a physical space (e.g. ChefConf, Meetup, etc), please see the [Physical Spaces Code of Conduct](physical-spaces-code-of-conduct.md) ## Our Pledge Diversity is one of the greatest strengths that a community can have, and many times that strength is born from the friction that can only come through sharing of differing perspectives. In the interest of fostering an open, welcoming, and encouraging environment, we as contributors, maintainers, and community members pledge to making participation in our projects and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, veteran status, or sexual identity and orientation. ## Our Standards The Chef community contains a diverse group of professionals and volunteers who come from all over the world to make Chef better. Community members may fulfill many roles including mentoring, teaching, and connecting with other members of the community. Be careful in the words that you choose. Be kind to others. Practice empathy. Don't insult or put down others. Remember that sexist, racist, ableist, ageist, and other exclusionary jokes can be offensive to those around you. If you think your conversation is making another community member uncomfortable _or_ if they tell you so, stop immediately, make amends, and move forward. As you are working with other members of the community, please keep in mind that the following guidelines apply equally to founders, mentors, those who submit new features/pull requests, and anyone who is seeking help and guidance. The following list isn’t exhaustive, but these few examples can help all of us communicate well, so that the community can work better together: - Use welcoming and inclusive language - Exercise patience and friendliness - Be respectful of differing viewpoints and experiences - Gracefully accept constructive criticism - Focus on what is best for the community - Show empathy towards other community members The previous list applies to all forms of communication: Slack (or any web chat), Discourse, the issue tracker, and any other forum that is used by the community. Please keep in mind that: - Your work will be used by other people, and you, in turn, will depend on the work of others - Decisions that you make often will affect others in the community - Disagreements happen, but should not be an excuse for poor behavior and bad manners. When disagreements do happen, let’s work together to solve them effectively and in a way that ensures that everyone understands what the disagreements were - Our community spans languages, cultures, perspectives (and continents!), and as such people may not understand jokes, sarcasm, and oblique references in the same way that you do. Remember that and be kind to the other members of the community - Be cautious about making assumptions about what someone does or does not know about something - assuming that someone does not understand an issue and over explaining can be condescending (even when not intended to be so) - Sexist, racist, ableist, ageist, and other prejudicial or exclusionary comments are not welcome in the community ## Unacceptable Behavior Harassment comes in many forms, including but not limited to: - Offensive comments related to gender, sexual orientation, age, disability, physical appearance, body size, race, veteran status, or religion - Posting/Exposing sexually explicit or violent images - Deliberate (or implied) intimidation - Trolling, insulting/derogatory comments, and personal or political attacks particularly those related to gender, sexual orientation, age, race, religion or disability - Publishing others' private information, such as a physical or electronic address, without explicit permission ("doxing") As a community that meets in physical public spaces, harassment also includes: - Stalking or persistent following - Intrusive or otherwise unwanted photography or recording - Sustained disruption of talks or other events - Inappropriate physical contact or unwelcome sexual attention **NOTE**: If you are in a physical space -- e.g. Chef Conf, Meetup, etc. -- please see the [Physical Spaces Code of Conduct](physical-spaces-code-of-conduct.md). If you have any lack of clarity about behaviors we include in the definition of "harassment", please read the [Citizen Code of Conduct](http://citizencodeofconduct.org/). In particular, we do not tolerate behavior that excludes people in socially marginalized groups. ## Enforcement/Getting Help Instances of abusive, harassing, or otherwise unacceptable behavior should be reported by contacting any of the Community Advocates directly. Each person's contact information and role is listed in the repo that links to this document. If you were not linked here, then contact the [individuals listed below](#roles). All complaints will be reviewed, investigated, and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Community Organizers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Roles The following are the various roles of our **Community Organizers** and the person(s) assigned to each role: - The **Deciders** have final say on community guidelines and final authority on correct actions and appeals - The **Community Advocates** may be assigned for each area where the community convenes online (Slack, email list, GitHub, etc.). Community Advocates are volunteers who have the best interests of our community in mind. They act in good faith to help enforce our community guidelines and respond to incidents when they occur - The **Project Maintainers** are expected to conduct their behavior in line with the Code of Conduct and are individually responsible for both escalating to a **Community Advocate** in case of witnessing an incident, and helping to foster the community - A **Community Member** is anyone who participates with the community whether in-person or via online channels. Community members are responsible for following the community guidelines, suggesting updates to the guidelines when warranted, and helping enforce community guidelines | Role | Name | Contact Info | | ------------------- | ------------------------ | ---------------------- | | Decider | Jenny Armstrong-Own | jowen@chef.io | | Community Advocate | Mandi Walls | mandi@chef.io | | Community Advocate | Nell Shamrell-Harrington | nshamrell@chef.io | | Community Advocate | Robb Kidd | rkidd@chef.io | ## Consequences of Unacceptable Behavior Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will *not* be tolerated. Anyone who is asked to stop unacceptable behavior is expected to comply immediately. If a community member engages in unacceptable behavior, the community organizers may take action that they deem appropriate -- up to and including a temporary ban or permanent expulsion from the community _without warning_ (and without refund, in the case of a paid event). If you have been involved in unacceptable behavior with current Chef community members outside the boundaries of the Chef Community, the Community Organizers retain the right to treat those external incidents in the same manner as internal incidents. Any physical violence _or_ intimidation, threatened or acted on, is a serious offense and will result in immediate exclusion from the community and appropriate follow up with law enforcement. No, we are not kidding. ## Procedure for Handling Disagreements and Incidents Disagreements are inherent to a group of impassioned people. When they occur, we seek to resolve disagreements and differing views constructively and with the help of the community and community processes. When disagreements escalate, we ask our Community Advocates to step in to moderate, mediate, and help resolve tense situations. The Chef Community Advocates are well informed on how to deal with incidents. Report the incident (preferably in writing) to one of the Community Advocates listed in the [Roles](#roles) section. ### Handling Incidents **NOTE**: If you are in a physical space -- e.g. Chef Conf, Meetup, etc. -- please see the [Physical Spaces Code of Conduct](physical-spaces-code-of-conduct.md). When a Community Organizer or Project Maintainer notices someone behaving in a way that is outside of our guidelines (a violator), the Community Advocate should make every reasonable attempt to help curtail that behavior. The Community Advocate may: - Remind the violator about our Community Code of Conduct and provide a link to this document - Ask the violator to stop the unacceptable behavior - Raise the issue with a maintainer, the community manager, or any member of the core project team - Allow time for the violator to correct the behavior The Community Advocate should take the following steps if the behavior is not brought in-line with our guidelines or the incident is not resolved: - Consult with another Community Organizer to make a judgment call about what reasonable corrective actions are warranted - In the case that no conclusion can be made, escalate to include the next level of Community Organizers - If still no conclusion can be made, report the incident to the **Deciders** listed above - Apply the corrective action - Document the incident as described below #### Documenting Incidents All incident reports will be kept in a private repository that is shared with the aforementioned Community Advocates and Deciders under the [Roles]((#roles) section. No other individuals or project contributors will be given access to these incident reports. **This repo will hold no personal information on the victim of an incident.** On the displacement of any Community Organizer in the [Roles](#roles) list above, that individual will immediately lose access to this repository and will terminate any local copies of the repository. The important information to report consists of: - Identifying information (name, email address, Slack username, etc.) of the person doing the harassing - The behavior that was in violation - The approximate time and date of the behavior - The circumstances surrounding the incident - Where applicable, contextual information/proof (email body, chat log, GitHub Issue, etc.) - Contact information for witnesses to the incident If you feel your safety is in jeopardy, please do not hesitate to contact local law enforcement. **Note:** Incidents that violate the Community Code of Conduct are extremely damaging to the community. The silver lining is that, in many cases, these incidents present a chance for the community as a whole to grow, learn, and become better. ## Our Responsibilities Community Organizers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Community Organizers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, messages, tweets, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope Our community will convene in both physical and virtual spaces. This Code of Conduct applies within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers and community organizers. ## Attribution This Code of Conduct is adapted from the following: - [Contributor Covenant](http://contributor-covenant.org), version 1.4, available [here](http://contributor-covenant.org/version/1/4/) - [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html) - [Citizen Code of Conduct](http://citizencodeofconduct.org/) ## Community Members in Crisis If a community member is in or appears to be in crisis, please refer to the [Community Members in Crisis Guide](./communication/community-members-in-crisis.md).train-3.2.28/CONTRIBUTING.md000066400000000000000000000165611364512547200151760ustar00rootroot00000000000000# Contributing to Train We are glad you want to contribute to Train! This document will help answer common questions you may have during your first contribution. ## Submitting Issues We utilize **Github Issues** for issue tracking and contributions. You can contribute in two ways: 1. Reporting an issue or making a feature request [here](https://github.com/inspec/train/issues/new). 2. Adding features or fixing bugs yourself and contributing your code to Train. We ask you not to submit security concerns via Github. For details on submitting potential security issues please see ## Contribution Process We have a 3 step process for contributions: 1. Commit changes to a git branch, making sure to sign-off those changes for the [Developer Certificate of Origin](#developer-certification-of-origin-dco). 2. Create a Github Pull Request for your change, following the instructions in the pull request template. 3. Perform a [Code Review](#code-review-process) with the project maintainers on the pull request. ### Pull Request Requirements Chef Projects are built to last. We strive to ensure high quality throughout the experience. In order to ensure this, we require that all pull requests to Chef projects meet these specifications: 1. **Tests:** To ensure high quality code and protect against future regressions, we require all the code in Chef Projects to have at least unit test coverage. See the [test/unit](https://github.com/chef/train/tree/master/test/unit) directory for the existing tests and use ```bundle exec rake test``` to run them. 2. **Green CI Tests:** We use [Travis CI](https://travis-ci.org/) and/or [AppVeyor](https://www.appveyor.com/) CI systems to test all pull requests. We require these test runs to succeed on every pull request before being merged. In addition to this it would be nice to include the description of the problem you are solving with your change. You can use [Issue Template](https://github.com/chef/train/tree/master/ISSUE_TEMPLATE.md) in the description section of the pull request. ### Code Review Process Code review takes place in Github pull requests. See [this article](https://help.github.com/articles/about-pull-requests/) if you're not familiar with Github Pull Requests. Once you open a pull request, project maintainers will review your code and respond to your pull request with any feedback they might have. The process at this point is as follows: 1. Two thumbs-up (:+1:) are required from project maintainers. See the master maintainers document for Train projects at . 2. When ready, your pull request will be merged into `master`, we may require you to rebase your PR to the latest `master`. 3. Once the PR is merged, you will be included in `CHANGELOG.md`. ### Developer Certification of Origin (DCO) Licensing is very important to open source projects. It helps ensure the software continues to be available under the terms that the author desired. Chef uses [the Apache 2.0 license](https://github.com/chef/chef/blob/master/LICENSE) to strike a balance between open contribution and allowing you to use the software however you would like to. The license tells you what rights you have that are provided by the copyright holder. It is important that the contributor fully understands what rights they are licensing and agrees to them. Sometimes the copyright holder isn't the contributor, such as when the contributor is doing work on behalf of a company. To make a good faith effort to ensure these criteria are met, Chef requires the Developer Certificate of Origin (DCO) process to be followed. The DCO is an attestation attached to every contribution made by every developer. In the commit message of the contribution, the developer simply adds a Signed-off-by statement and thereby agrees to the DCO, which you can find below or at . ``` Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as Indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` For more information on the change see the Chef Blog post [Introducing Developer Certificate of Origin](https://blog.chef.io/2016/09/19/introducing-developer-certificate-of-origin/) #### DCO Sign-Off Methods The DCO requires a sign-off message in the following format appear on each commit in the pull request: ``` Signed-off-by: Julia Child ``` The DCO text can either be manually added to your commit body, or you can add either **-s** or **--signoff** to your usual git commit commands. If you forget to add the sign-off you can also amend a previous commit with the sign-off by running **git commit --amend -s**. If you've pushed your changes to Github already you'll need to force push your branch after this with **git push -f**. ### Obvious Fix Policy Small contributions, such as fixing spelling errors, where the content is small enough to not be considered intellectual property, can be submitted without signing the contribution for the DCO. As a rule of thumb, changes are obvious fixes if they do not introduce any new functionality or creative thinking. Assuming the change does not affect functionality, some common obvious fix examples include the following: - Spelling / grammar fixes - Typo correction, white space and formatting changes - Comment clean up - Bug fixes that change default return values or error codes stored in constants - Adding logging messages or debugging output - Changes to 'metadata' files like Gemfile, .gitignore, build scripts, etc. - Moving source files from one directory or package to another **Whenever you invoke the "obvious fix" rule, please say so in your commit message:** ``` ------------------------------------------------------------------------ commit 370adb3f82d55d912b0cf9c1d1e99b132a8ed3b5 Author: Julia Child Date: Wed Sep 18 11:44:40 2015 -0700 Fix typo in the README. Obvious fix. ------------------------------------------------------------------------ ``` ## Train Community Train is made possible by a strong community of developers, system administrators, auditor and security experts. If you have any questions or if you would like to get involved in the Train community you can check out: - [Chef Community Slack](https://community-slack.chef.io/) Also here are some additional pointers to some awesome Chef content: - [Learn Chef](https://learn.chef.io/) - [Chef Website](https://www.chef.io/) train-3.2.28/Gemfile000066400000000000000000000020111364512547200142210ustar00rootroot00000000000000# encoding: utf-8 source "https://rubygems.org" gemspec name: "train" group :test do gem "coveralls", require: false gem "minitest", "~> 5.8" gem "rake", ["~> 12.3", ">= 12.3.3"] gem "chefstyle", "0.13.2" gem "simplecov", "~> 0.10" gem "concurrent-ruby", "~> 1.0" gem "pry-byebug" gem "m" gem "ed25519" # ed25519 ssh key support gem "bcrypt_pbkdf" # ed25519 ssh key support # This is not a true gem installation # (Gem::Specification.find_by_path('train-gem-fixture') will return nil) # but it's close enough to show the gempath handler can find a plugin # See test/unit/ gem "train-test-fixture", path: "test/fixtures/plugins/train-test-fixture" gem "mocha", "~> 1.1" end group :integration do gem "berkshelf", "~> 6.3.0" gem "test-kitchen", ">= 1.2.4" gem "kitchen-vagrant" end group :tools do gem "pry", "~> 0.10" gem "rb-readline" gem "license_finder" end # add these additional dependencies into Gemfile.local eval_gemfile(__FILE__ + ".local") if File.exist?(__FILE__ + ".local") train-3.2.28/LICENSE000066400000000000000000000251421364512547200137450ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. train-3.2.28/MAINTAINERS.md000066400000000000000000000013111364512547200150240ustar00rootroot00000000000000# Maintainers This file lists how the train project is maintained. When making changes to the project, this file tells you who needs to review your patch - you need at least two maintainers to provide a :+1: on your pull request. Additionally, you need to not receive a veto from a Lieutenant or the Project Lead. Check out [How Chef is Maintained](https://github.com/chef/chef-rfc/blob/master/rfc030-maintenance-policy.md#how-the-project-is-maintained) for details on the process, how to become a maintainer, lieutenant, or the project lead. ## List of Maintainers The [Maintainers of the InSpec project](https://github.com/inspec/inspec/blob/master/MAINTAINERS.md) serve as maintainers of the train project. train-3.2.28/README.md000066400000000000000000000137511364512547200142220ustar00rootroot00000000000000# Train - Transport Interface * **Project State: Active** * **Issues Response SLA: 3 business days** * **Pull Request Response SLA: 3 business days** For more information on project states and SLAs, see [this documentation](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md). [![Build status](https://badge.buildkite.com/ee9d6f49d995b23e943955b12d5aaadf4314d12b7cc99b03be.svg?branch=master)](https://buildkite.com/chef-oss/inspec-train-master-verify) [![Gem Version](https://badge.fury.io/rb/train.svg)](https://badge.fury.io/rb/train) [![Coverage Status](https://coveralls.io/repos/github/inspec/train/badge.svg?branch=master)](https://coveralls.io/github/inspec/train?branch=master) **Umbrella Project**: [Chef Foundation](https://github.com/chef/chef-oss-practices/blob/master/projects/chef-foundation.md) **Project State**: [Active](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md#active) **Issues [Response Time Maximum](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md)**: 14 days **Pull Request [Response Time Maximum](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md)**: 14 days Train lets you talk to your local or remote operating systems and APIs with a unified interface. It allows you to: * execute commands via `run_command` * interact with files via `file` * identify the target operating system via `os` * authenticate to API-based services and treat them like a platform Train supports: * Local execution * SSH * WinRM * Docker * Mock (for testing and debugging) * AWS as an API * Azure as an API * VMware via PowerCLI * Habitat # Examples ## Setup **Local** ```ruby require 'train' train = Train.create('local') ``` **SSH** ```ruby require 'train' train = Train.create('ssh', host: '1.2.3.4', port: 22, user: 'root', key_files: '/vagrant') ``` If you don't specify the `key_files` and `password` options, SSH agent authentication will be attempted. For example: ```ruby require 'train' train = Train.create('ssh', host: '1.2.3.4', port: 22, user: 'root') ``` **WinRM** ```ruby require 'train' train = Train.create('winrm', host: '1.2.3.4', user: 'Administrator', password: '...', ssl: true, self_signed: true) ``` **Docker** ```ruby require 'train' train = Train.create('docker', host: 'container_id...') ``` **AWS** To use AWS API authentication, setup an AWS client profile to store the Access Key ID and Secret Access Key. ```ruby require 'train' train = Train.create('aws', region: 'us-east-2', profile: 'my-profile') ``` You may also use the standard AWS CLI environment variables, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`. ```ruby require 'train' train = Train.create('aws') ``` **VMware** ```ruby require 'train' Train.create('vmware', viserver: '10.0.0.10', user: 'demouser', password: 'securepassword') ``` You may also use environment variables by setting `VISERVER`, `VISERVER__USERNAME`, and `VISERVER_PASSWORD` ```ruby require 'train' Train.create('vmware') ``` ## Configuration To get a list of available options for a plugin: ```ruby puts Train.options('ssh') ``` This will provide all configuration options: ```ruby { :host => { :required => true}, :port => { :default => 22, :required => true}, :user => { :default => "root", :required => true}, :keys => { :default => nil}, :password => { :default => nil}, ... ``` ## Usage ```ruby # start or reuse a connection conn = train.connection # run a command on Linux/Unix/Mac puts conn.run_command('whoami').stdout # get OS info puts conn.os[:family] puts conn.os[:release] # access files puts conn.file('/proc/version').content # access specific API client functionality ec2_client = train.connection.aws_client(Aws::EC2::Client) puts ec2_client.describe_instances # close the connection conn.close ``` # Testing We perform `unit`, `integration` and `windows` tests. * `unit` tests ensure the intended behaviour of the implementation * `integration` tests run against VMs and docker containers * `windows` tests that run on appveyor for windows integration tests ## Mac/Linux ``` bundle exec ruby -W -Ilib:test/unit test/unit/extras/stat_test.rb ``` ## Windows ``` # run windows tests bundle exec rake test:windows # run single tests bundle exec ruby -I .\test\windows\ .\test\windows\local_test.rb ``` # Kudos and Contributors Train is heavily based on the work of: * [test-kitchen](https://github.com/test-kitchen/test-kitchen) by [Fletcher Nichol](mailto:fnichol@nichol.ca) and [a great community of contributors](https://github.com/test-kitchen/test-kitchen/graphs/contributors) * [ohai](https://github.com/chef/ohai) by Adam Jacob, Chef Software Inc. and [a great community of contributors](https://github.com/chef/ohai/graphs/contributors) We also want to thank [halo](https://github.com/halo) who did a great contribution by handing over the `train` gem name. ## Contributing 1. Fork it 1. Create your feature branch (git checkout -b my-new-feature) 1. Commit your changes (git commit -am 'Add some feature') 1. Push to the branch (git push origin my-new-feature) 1. Create new Pull Request ## License | **Author:** | Dominik Richter () | **Author:** | Christoph Hartmann () | **Copyright:** | Copyright (c) 2015-2019 Chef Software Inc. | **Copyright:** | Copyright (c) 2015 Vulcano Security GmbH. | **License:** | Apache License, Version 2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. train-3.2.28/Rakefile000077500000000000000000000036601364512547200144110ustar00rootroot00000000000000#!/usr/bin/env rake # encoding: utf-8 require "bundler" require "bundler/gem_helper" require "rake/testtask" require "chefstyle" require "rubocop/rake_task" Bundler::GemHelper.install_tasks name: "train" RuboCop::RakeTask.new(:lint) do |task| task.options << "--display-cop-names" end # run tests task default: %i{test} Rake::TestTask.new do |t| t.libs << "test" t.pattern = "test/unit/**/*_test.rb" t.warning = false t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end namespace :test do task :docker do path = File.join(File.dirname(__FILE__), "test", "integration") sh("sh", "-c", "cd #{path} && ruby -I ../../lib docker_test.rb tests/*") end task :windows do Dir.glob("test/windows/*_test.rb").all? do |file| sh(Gem.ruby, "-w", '-I .\test\windows', file) end || raise("Failures") end task :vm do concurrency = ENV["CONCURRENCY"] || 4 path = File.join(File.dirname(__FILE__), "test", "integration") sh("sh", "-c", "cd #{path} && kitchen test -c #{concurrency}") end # Target required: # rake "test:ssh[user@server]" # sh -c cd /home/foobarbam/src/gems/train/test/integration \ # && target=user@server ruby -I ../../lib test_ssh.rb tests/* # ... # Turn debug logging back on: # debug=1 rake "test:ssh[user@server]" # Use a different ssh key: # key_files=/home/foobarbam/.ssh/id_rsa2 rake "test:ssh[user@server]" # Run with a specific test: # test=path_block_device_test.rb rake "test:ssh[user@server]" task :ssh, [:target] do |t, args| path = File.join(File.dirname(__FILE__), "test", "integration") key_files = ENV["key_files"] || File.join(ENV["HOME"], ".ssh", "id_rsa") sh_cmd = "cd #{path} && target=#{args[:target]} key_files=#{key_files}" sh_cmd += " debug=#{ENV["debug"]}" if ENV["debug"] sh_cmd += " ruby -I ../../lib test_ssh.rb tests/" sh_cmd += ENV["test"] || "*" sh("sh", "-c", sh_cmd) end end train-3.2.28/VERSION000066400000000000000000000000061364512547200140000ustar00rootroot000000000000003.2.28train-3.2.28/contrib/000077500000000000000000000000001364512547200143745ustar00rootroot00000000000000train-3.2.28/contrib/fixup_requiretty.rb000066400000000000000000000025741364512547200203610ustar00rootroot00000000000000# encoding: utf-8 # author: Stephan Renatus # # chef-apply script to fix sudoers configuration # # This script can be used to setup a user's sudoers configuration to allow for # using non-interactive sessions. It's main use case is fixing the default # configuration on RHEL and SEL distributions. # # The user name has to be provided in the env varibale "TRAIN_SUDO_USER". # If any configuration for the user is present (user is in /etc/sudoers or # /etc/sudoers.d/user exists), this script will do nothing # (unless you set TRAIN_SUDO_VERY_MUCH=yes) # FIXME user = ENV["TRAIN_SUDO_USER"] || "todo-some-clever-default-maybe-current-user" sudoer = "/etc/sudoers.d/#{user}" log "Warning: a sudoers configuration for user #{user} already exists, "\ "doing nothing (override with TRAIN_SUDO_VERY_MUCH=yes)" do only_if "test -f #{sudoer} || grep #{user} /etc/sudoers" end file sudoer do content "#{user} ALL=(root) NOPASSWD:ALL\n"\ "Defaults:#{user} !requiretty\n" mode 0600 action ENV["TRAIN_SUDO_VERY_MUCH"] == "yes" ? :create : :create_if_missing # Do not add something here if the user is mentioned explicitly in /etc/sudoers not_if "grep #{user} /etc/sudoers" end # /!\ broken files in /etc/sudoers.d/ will break sudo for ALL USERS /!\ execute "revert: delete the file if it's broken" do command "rm #{sudoer}" not_if "visudo -c -f #{sudoer}" # file is ok end train-3.2.28/docs/000077500000000000000000000000001364512547200136645ustar00rootroot00000000000000train-3.2.28/docs/plugins.md000066400000000000000000000151541364512547200156750ustar00rootroot00000000000000# Train Plugins ## Introducing Plugins Train plugins are a way to add new transports and platform detection to Train. If you are familiar with InSpec plugins, be forewarned; the two plugin systems are not similar. ### Why Plugins? #### Benefits of plugins Plugins address two main needs that the Chef InSpec Engineering team (which maintains Train) encountered in 2017-2018: * Passionate contributor parties can develop and release new Train transports at their own pace, without gating by Chef engineers. * Reduction of dependency bloat within Train. For example, since only the AWS transport needs the aws-sdk, we can move the gem dependency into the plugin gem, and out of train itself. #### Future of existing Transports The Chef InSpec Engineering team currently (October 2018) plans to migrate most existing Train transports into plugins. For example, AWS, Azure, and GCP are all excellent candidates for migration to plugin status. The team commits to keeping SSH, WinRM, and Local transports in Train core. All other transports may be migrated. In the near-term, InSpec will carry a gemspec dependency on the migrated plugins. This will continue a smooth experience for users relying on (for example) Azure. ## Managing Plugins ### Installing and Managing Train Plugins as an InSpec User InSpec has a command-line plugin management interface, which is used for managing both InSpec plugins and Train plugins. For example, to install `train-aws`, simply run: ```bash $ inspec plugin install train-aws ``` The management facility can install, update, and remove plugins, including their dependencies. ### Installing Train Plugins outside of InSpec If you need a train plugin installed, and `inspec plugin` is not available to you, you can install a train plugin like any other gem. Just be sure to use the `gem` binary that comes with the application you wish to extend. For example, to add a Train Plugin to a ChefDK installation, use: ```bash $ chef exec gem install train-something ``` ### Finding Train plugins Train plugins can be found by running: ```bash $ inspec plugin search train- ``` If you are not an InSpec user, you may also perform a RubyGems search: ```bash $ gem search train- ``` ## Developing Train Plugins for the Train Plugin API v1 Train plugins are gems. Their names must start with 'train-'. You can use the example plugin at [the Train github project](https://github.com/inspec/train/tree/master/examples/plugins/train-local-rot13) as a starting point. ### The Entry Point As with any Gem library, you should create a file with the name of your plugin, which loads the remaining files you need. Some plugins place them in 1 file, but it is cleaner to place them in 4: a version file, then transport, connection and platform files. ### The Transport File In this file, you should define a class that inherits from `Train.plugin(1)`. The class returned will be `Train::Plugins::Transport` or a descendant. This superclass provides DSL methods, abstract methods, instance variables, and accessors for you to configure your plugin. Feedback about providing a clearer Plugin API for a future Plugin V2 API is welcome. #### `name` DSL method Required. Use the `name` call to register your plugin. Pass a String, which should have the 'train-' portion removed. #### `option` DSL method The option method is used to register new information into your transport options hash. This hash contains all the information your transport will need for its connection and runtime support. These options calls are a good place to pull in defaults or information from environment variables. #### @options Instance Variable This variable includes any options you passed in from the DSL method when defining a transport. It will also merge in any options passed from the URL definition for your transport (schema, host, etc). #### `connection` abstract method Required to be implemented. Called with a single arg which is usually ignored. You must return an instance of a class that is a descendant of `Train::Plugins::Transports::BaseConnection`. Typically you will call the constructor with the `@options`. ### Connection File The your Connection class must inherit from `Train::Plugins::Transports::BaseConnection`. Abstract methods it should implement include: #### initialize Not required but is a good place to set option defaults for options that were passed with the transport URL. Example: ```Ruby def initialize(options) # Override for cli region from url host # aws://region/my-profile options[:region] = options[:host] if options.key?(:host) super(options) end ``` #### run_command_via_connection If your transport is OS based and has the option to read a file you can set this method. It is expected to return a `Train::File::Remote::*` class here to be used upstream in InSpec. Currently the file resource is restricted to Unix and Windows platforms. Caching is enabled by default for this method. #### file_via_connection If your transport is OS based and has the option to run a command you can set this method. It is expected to return a `CommandResult` class here to be used upstream in InSpec. Currently the command resource is restricted to Unix and Windows platforms. Caching is enabled by default for this method. #### API Access Methods When working with API's it's often helpful to create methods to return client information or API objects. These are then accessed upstream in InSpec. Here is an example of a API method you may have: ```Ruby def aws_client(klass) return klass.new unless cache_enabled?(:api_call) @cache[:api_call][klass.to_s.to_sym] ||= klass.new end ``` This will return a class and cache the client object accordingly if caching is enabled. You can call this from a inspec resource by calling `inspec.backend.aws_client(AWS::TEST::CLASS)`. #### platform `platform` is called when InSpec is trying to detect the platform (OS family, etc). We recommend that you implement platform in a separate Module, and include it. ### Platform Detection Platform detection is used if you do not specify a platform method for your transport. Currently it is only used for OS (Unix, Windows) platforms. The detection system will run a series of commands on your target to try and determine what platform it is. This information can be found here [OS Specifications](https://github.com/inspec/train/blob/master/lib/train/platforms/detect/specifications/os.rb). When using an API or a fixed platform for your transport it's suggested you skip the detection process and specify a direct platform. Here is an example: ```Ruby def platform Train::Platforms.name('Aws').in_family('cloud') force_platform!('Aws', release: '1.2', ) end ``` train-3.2.28/examples/000077500000000000000000000000001364512547200145525ustar00rootroot00000000000000train-3.2.28/examples/plugins/000077500000000000000000000000001364512547200162335ustar00rootroot00000000000000train-3.2.28/examples/plugins/train-local-rot13/000077500000000000000000000000001364512547200214065ustar00rootroot00000000000000train-3.2.28/examples/plugins/train-local-rot13/Gemfile000066400000000000000000000012021364512547200226740ustar00rootroot00000000000000# encoding: utf-8 source "https://rubygems.org" # This is Gemfile, which is used by bundler # to ensure a coherent set of gems is installed. # This file lists dependencies needed when outside # of a gem (the gemspec lists deps for gem deployment) # Bundler should refer to the gemspec for any dependencies. gemspec # Remaining group is only used for development. group :development do gem "bundler" gem "byebug" gem "inspec", ">= 2.2.112" # We need InSpec for the test harness while developing. gem "minitest" gem "rake" gem "rubocop", "= 0.49.1" # Need to keep in sync with main InSpec project, so config files will work end train-3.2.28/examples/plugins/train-local-rot13/LICENSE000066400000000000000000000010571364512547200224160ustar00rootroot00000000000000Copyright 2018 Chef Software Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. train-3.2.28/examples/plugins/train-local-rot13/README.md000066400000000000000000000060541364512547200226720ustar00rootroot00000000000000# Example Train Plugin - train-local-rot13 This plugin is provided as a teaching example for building a Train plugin. Train plugins allow you to connect to remote systems or APIs, so that other tools such as InSpec or Chef Workstation can talk over the connection. train-local-rot13's functionality is simple: it acts as a local transport (targeting the local machine), but it applies the [rot13](https://en.wikipedia.org/wiki/ROT13) trivial cypher transformation on the contents of each file it reads, and on the stdout of every command it executes. Please note that ROT13 is an incredibly weak cypher, and can be broken by most elementary school students. Do not use this plugin for security purposes. ## Relationship between InSpec and Train Train itself has no CLI, nor a sophisticated test harness. InSpec does have such facilities, so installing Train plugins will require an InSpec installation. You do not need to use or understand InSpec. Train plugins may be developed without an InSpec installation. ## To Install this as a User You will need InSpec v2.3 or later. If you just want to use this (not learn how to write a plugin), you can so by simply running: ``` $ inspec plugin install train-local-rot13 ``` You can then run: ``` $ inspec detect -t local-rot13:// == Platform Details Name: local-rot13 Families: unix, os, windows, os Release: 0.1.0 Arch: example $ inspec shell -t local-rot13:// -c 'command("echo hello")' uryyb ``` ## Features of This Example Kit This example plugin is a full-fledged plugin example, with everything a real-world, industrial grade plugin would have, including: * an implementation of a Train plugin, using the Train Plugin V1 API, including * a Transport * a Connection * Platform configuration * documentation (you are reading it now) * tests, at the unit and functional level * a .gemspec, for packaging and publishing it as a gem * a Gemfile, for managing its dependencies * a Rakefile, for running development tasks * Rubocop linting support for using the base Train project rubocop.yml (See Rakefile) You are encouraged to use this plugin as a starting point for real plugins. ## Development of a Plugin [Plugin Development](https://github.com/inspec/train/blob/master/docs/plugins.md) is documented on the `train` project on GitHub. Additionally, this example plugin has extensive comments explaining what is happening, and why. ### A Tour of the Plugin One nice circuit of the plugin might be: * look at the gemspec, to see what the plugin thinks it does * look at the functional tests, to see the plugin proving it does what it says * look at the unit tests, to see how the plugin claims it is internally structured * look at the Rakefile, to see how to interact with the project * look at lib/train-local-rot13.rb, the entry point which InSpec will always load if the plugin is installed * look at lib/train-local-rot13/transport.rb, the plugin "backbone" * look at lib/train-local-rot13/connection.rb, the plugin implementation * look at lib/train-local-rot13/platform.rb, OS platform support declaration train-3.2.28/examples/plugins/train-local-rot13/Rakefile000066400000000000000000000026661364512547200230650ustar00rootroot00000000000000# A Rakefile defines tasks to help maintain your project. # Rake provides several task templates that are useful. #------------------------------------------------------------------# # Test Runner Tasks #------------------------------------------------------------------# # This task template will make a task named 'test', and run # the tests that it finds. require "rake/testtask" Rake::TestTask.new do |t| t.libs.push "lib" t.test_files = FileList[ "test/unit/*_test.rb", "test/integration/*_test.rb", "test/functional/*_test.rb", ] t.verbose = true # Ideally, we'd run tests with warnings enabled, # but the dependent gems have many warnings. As this # is an example, let's disable them so the testing # experience is cleaner. t.warning = false end #------------------------------------------------------------------# # Code Style Tasks #------------------------------------------------------------------# require "rubocop/rake_task" RuboCop::RakeTask.new(:lint) do |t| # Choices of rubocop rules to enforce are deeply personal. # Here, we set things up so that your plugin will use the Bundler-installed # train gem's copy of the Train project's rubocop.yml file (which # is indeed packaged with the train gem). require "train/globals" train_rubocop_yml = File.join(Train.src_root, ".rubocop.yml") t.options = ["--display-cop-names", "--config", train_rubocop_yml] end train-3.2.28/examples/plugins/train-local-rot13/lib/000077500000000000000000000000001364512547200221545ustar00rootroot00000000000000train-3.2.28/examples/plugins/train-local-rot13/lib/train-local-rot13.rb000066400000000000000000000015251364512547200256570ustar00rootroot00000000000000# This file is known as the "entry point." # This is the file Train will try to load if it # thinks your plugin is needed. # The *only* thing this file should do is setup the # load path, then load plugin files. # Next two lines simply add the path of the gem to the load path. # This is not needed when being loaded as a gem; but when doing # plugin development, you may need it. Either way, it's harmless. libdir = File.dirname(__FILE__) $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) # It's traditional to keep your gem version in a separate file, so CI can find it easier. require "train-local-rot13/version" # A train plugin has three components: Transport, Connection, and Platform. # Transport acts as the glue. require "train-local-rot13/transport" require "train-local-rot13/platform" require "train-local-rot13/connection" train-3.2.28/examples/plugins/train-local-rot13/lib/train-local-rot13/000077500000000000000000000000001364512547200253275ustar00rootroot00000000000000train-3.2.28/examples/plugins/train-local-rot13/lib/train-local-rot13/connection.rb000066400000000000000000000061241364512547200300160ustar00rootroot00000000000000# Connection definition file for an example Train plugin. # Most of the work of a Train plugin happens in this file. # Connections derive from Train::Plugins::Transport::BaseConnection, # and provide a variety of services. Later generations of the plugin # API will likely separate out these responsibilities, but for now, # some of the responsibilities include: # * authentication to the target # * platform / release /family detection # * caching # * filesystem access # * remote command execution # * API execution # * marshalling to / from JSON # You don't have to worry about most of this. # This allow us to inherit from Train::Plugins::Transport::BaseConnection require "train" # Push platform detection out to a mixin, as it tends # to develop at a different cadence than the rest require "train-local-rot13/platform" # This is a support library for our file content meddling require "train-local-rot13/file_content_rotator" # This is a support library for our command meddling require "mixlib/shellout" require "ostruct" module TrainPlugins module LocalRot13 # You must inherit from BaseConnection. class Connection < Train::Plugins::Transport::BaseConnection # We've placed platform detection in a separate module; pull it in here. include TrainPlugins::LocalRot13::Platform def initialize(options) # 'options' here is a hash, Symbol-keyed, # of what Train.target_config decided to do with the URI that it was # passed by `inspec -t` (or however the application gathered target information) # Some plugins might use this moment to capture credentials from the URI, # and the configure an underlying SDK accordingly. # You might also take a moment to manipulate the options. # Have a look at the Local, SSH, and AWS transports for ideas about what # you can do with the options. # Regardless, let the BaseConnection have a chance to configure itself. super(options) # If you need to attempt a connection to a remote system, or verify your # credentials, now is a good time. end # Filesystem access. # If your plugin is for an API, don't implement this. # If your plugin supports reading files, you'll need to implement this. def file_via_connection(path) train_file = Train::File::Local::Unix.new(self, path) # But then we wrap the return in a class that meddles with the content. FileContentRotator.new(train_file) end # Command execution. # If your plugin is for an API, don't implement this. # If your plugin supports executing commands, you'll need to implement this. def run_command_via_connection(cmd) # Run the command. run_result = Mixlib::ShellOut.new(cmd) run_result.run_command # Wrap the results in a structure that Train expects... OpenStruct.new( # And meddle with the stdout along the way. stdout: Rot13.rotate(run_result.stdout), stderr: run_result.stderr, exit_status: run_result.exitstatus ) end end end end train-3.2.28/examples/plugins/train-local-rot13/lib/train-local-rot13/file_content_rotator.rb000066400000000000000000000013401364512547200320750ustar00rootroot00000000000000# Here's our helper class for the file object. This is just some # silliness specific to the task of applying rot13 to the file content. require "rot13" module TrainPlugins module LocalRot13 class FileContentRotator # The FileContentRotator has-a Train::File def initialize(train_file) @train_file = train_file end # We implement content ourselves, rotating the contents of the file def content Rot13.rotate(@train_file.content) end # Everything else, we delegate to the Train::File object. # This is not a safe or efficient implementation. def method_missing(meth, *args, &block) @train_file.send(meth, *args, &block) end end end end train-3.2.28/examples/plugins/train-local-rot13/lib/train-local-rot13/platform.rb000066400000000000000000000034031364512547200275000ustar00rootroot00000000000000# Platform definition file. This is a good place to separate out any # logic regarding the identification of the OS or API at the far end # of the connection. # Abbreviate the namespace here, if you like. module TrainPlugins::LocalRot13 # Since we're mixing in the platform detection facility into Connection, # this has to come in as a Module. module Platform # The method `platform` is called when platform detection is # about to be performed. Train core defines a sophisticated # system for platform detection, but for most plugins, you'll # only ever run on the special platform for which you are targeting. def platform # If you are declaring a new platform, you will need to tell # Train a bit about it. # If you were defining a cloud API, you should say you are a member # of the cloud family. # This plugin makes up a new platform. Train (or rather InSpec) only # know how to read files on Windows and Un*x (MacOS is a kind of Un*x), # so we'll say we're part of those families. Train::Platforms.name("local-rot13").in_family("unix") Train::Platforms.name("local-rot13").in_family("windows") # When you know you will only ever run on your dedicated platform # (for example, a plugin named train-aws would only run on the AWS # API, which we report as the 'aws' platform). # force_platform! lets you bypass platform detection. # The options to this are not currently documented completely. # Use release to report a version number. You might use the version # of the plugin, or a version of an important underlying SDK, or a # version of a remote API. force_platform!("local-rot13", release: TrainPlugins::LocalRot13::VERSION) end end end train-3.2.28/examples/plugins/train-local-rot13/lib/train-local-rot13/transport.rb000066400000000000000000000020121364512547200277030ustar00rootroot00000000000000# Train Plugins v1 are usually declared under the TrainPlugins namespace. # Each plugin has three components: Transport, Connection, and Platform. # We'll only define the Transport here, but we'll refer to the others. require "train-local-rot13/connection" module TrainPlugins module LocalRot13 class Transport < Train.plugin(1) name "local-rot13" # The only thing you MUST do in a transport is a define a # connection() method that returns a instance that is a # subclass of BaseConnection. # The options passed to this are undocumented and rarely used. def connection(_instance_opts = nil) # Typical practice is to cache the connection as an instance variable. # Do what makes sense for your platform. # @options here is the parsed options that the calling # app handed to us at process invocation. See the Connection class # for more details. @connection ||= TrainPlugins::LocalRot13::Connection.new(@options) end end end end train-3.2.28/examples/plugins/train-local-rot13/lib/train-local-rot13/version.rb000066400000000000000000000005401364512547200273400ustar00rootroot00000000000000# This file exists simply to record the version number of the plugin. # It is kept in a separate file, so that your gemspec can load it and # learn the current version without loading the whole plugin. Also, # many CI servers can update this file when "version bumping". module TrainPlugins module LocalRot13 VERSION = "0.1.0".freeze end end train-3.2.28/examples/plugins/train-local-rot13/test/000077500000000000000000000000001364512547200223655ustar00rootroot00000000000000train-3.2.28/examples/plugins/train-local-rot13/test/fixtures/000077500000000000000000000000001364512547200242365ustar00rootroot00000000000000train-3.2.28/examples/plugins/train-local-rot13/test/fixtures/README.md000066400000000000000000000004121364512547200255120ustar00rootroot00000000000000# Test Fixtures Area In this directory, you would place things that you need during testing. When writing your functional tests, you can point Train at the various test fixture files. To use them, see the helper.rb file included in the example at test/helper.rb .train-3.2.28/examples/plugins/train-local-rot13/test/fixtures/hello000066400000000000000000000000051364512547200252570ustar00rootroot00000000000000hellotrain-3.2.28/examples/plugins/train-local-rot13/test/functional/000077500000000000000000000000001364512547200245275ustar00rootroot00000000000000train-3.2.28/examples/plugins/train-local-rot13/test/functional/local-rot13_test.rb000066400000000000000000000064661364512547200301670ustar00rootroot00000000000000# Functional tests for the Train Local Rot13 Example Plugin. # Functional tests are used to verify the behaviors of the plugin are as # expected, to a user. # For train, a "user" is a developer using your plugin to access things from # their app. Unlike unit tests, we don't assume any knowledge of how anything # works; we just know what it is supposed to do. # Include our test harness require_relative "../helper" # Because InSpec is a Spec-style test suite, and Train has a close relationship # to InSpec, we're going to use MiniTest::Spec here, for familiar look and # feel. However, this isn't InSpec (or RSpec) code. describe "train-local-rot13" do # Our helper.rb locates this library from the Train install that # Bundler installed for us. If we want its methods, we still must # import it. Including it here will make it available in all child # 'describe' blocks. include TrainPluginFunctionalHelper # When thinking up scenarios to test, start with the simplest. # Then think of each major feature, and exercise them. # Running combinations of features makes sense if it is very likely, # or a difficult / dangerous case. You can always add more tests # here as users find subtle problems. In fact, having a user submit # a PR that creates a failing functional test is a great way to # capture the reproduction case. # Some tests through here use minitest Expectations, which attach to all # Objects, and begin with 'must' (positive) or 'wont' (negative) # See https://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest/Expectations.html # LocalRot13 should do at least this: # * Not explode when you run Train with it # * Apply rot13 when you use Train to read a file # * Apply rot13 when you use Train to run a command describe "creating a train instance with this transport" do # This is a bit of an awkward test. There is no 'wont_raise', so # we just execute the risky code; if it breaks, the test will be # registered as an Error. it "should not explode on create" do # This checks for uncaught exceptions. Train.create("local-rot13") # This checks for warnings (or any other output) to stdout/stderr proc { Train.create("local-rot13") }.must_be_silent end it "should not explode on connect" do # This checks for uncaught exceptions. Train.create("local-rot13").connection # This checks for warnings (or any other output) to stdout/stderr proc { Train.create("local-rot13").connection }.must_be_silent end end describe "reading a file" do it "should rotate the text by 13 positions" do conn = Train.create("local-rot13").connection # Here, plugin_fixtures_path is provided by the TrainPluginFunctionalHelper, # and refers to the absolute path to the test fixtures directory. # The file 'hello' simply has the text 'hello' in it. file_obj = conn.file(File.join(plugin_fixtures_path, "hello")) file_obj.content.wont_include("hello") file_obj.content.must_include("uryyb") end end describe "running a command" do it "should rotate the stdout by 13 positions" do conn = Train.create("local-rot13").connection file_obj = conn.run_command("echo hello") file_obj.stdout.wont_include("hello") file_obj.stdout.must_include("uryyb") end end end train-3.2.28/examples/plugins/train-local-rot13/test/helper.rb000066400000000000000000000003301364512547200241650ustar00rootroot00000000000000# Test helper file for example Train plugins # This file's job is to collect any libraries needed for testing, as well as provide # any utilities to make testing a plugin easier. require "train/plugin_test_helper" train-3.2.28/examples/plugins/train-local-rot13/test/unit/000077500000000000000000000000001364512547200233445ustar00rootroot00000000000000train-3.2.28/examples/plugins/train-local-rot13/test/unit/connection_test.rb000066400000000000000000000027521364512547200270750ustar00rootroot00000000000000# This is a unit test for the example Train plugin, LocalRot13. # Its job is to verify that the Connection class is setup correctly. # Include our test harness require_relative "../helper" # Load the class under test, the Connection definition. require "train-local-rot13/connection" # Because InSpec is a Spec-style test suite, we're going to use MiniTest::Spec # here, for familiar look and feel. However, this isn't InSpec (or RSpec) code. describe TrainPlugins::LocalRot13::Connection do # When writing tests, you can use `let` to create variables that you # can reference easily. # This is a long name. Shorten it for clarity. let(:connection_class) { TrainPlugins::LocalRot13::Connection } # Some tests through here use minitest Expectations, which attach to all # Objects, and begin with 'must' (positive) or 'wont' (negative) # See https://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest/Expectations.html it "should inherit from the Train Connection base" do # For Class, '<' means 'is a descendant of' (connection_class < Train::Plugins::Transport::BaseConnection).must_equal(true) end # Since this is a Local-type connection, we MUST implement these three. %i{ file_via_connection run_command_via_connection }.each do |method_name| it "should provide a #{method_name}() method" do # false passed to instance_methods says 'don't use inheritance' connection_class.instance_methods(false).must_include(method_name) end end end train-3.2.28/examples/plugins/train-local-rot13/test/unit/transport_test.rb000066400000000000000000000030301364512547200267600ustar00rootroot00000000000000# This is a unit test for the example Train plugin, LocalRot13. # Its job is to verify that the Transport class is setup correctly. # Include our test harness require_relative "../helper" # Load the class under test, the Plugin definition. require "train-local-rot13/transport" # Because InSpec is a Spec-style test suite, we're going to use MiniTest::Spec # here, for familiar look and feel. However, this isn't InSpec (or RSpec) code. describe TrainPlugins::LocalRot13::Transport do # When writing tests, you can use `let` to create variables that you # can reference easily. # This is a long name. Shorten it for clarity. let(:plugin_class) { TrainPlugins::LocalRot13::Transport } # Some tests through here use minitest Expectations, which attach to all # Objects, and begin with 'must' (positive) or 'wont' (negative) # See https://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest/Expectations.html it "should be registered with the plugin registry without the train- prtefix" do # Note that Train uses String keys here, not Symbols Train::Plugins.registry.keys.wont_include("train-local-rot13") Train::Plugins.registry.keys.must_include("local-rot13") end it "should inherit from the Train plugin base" do # For Class, '<' means 'is a descendant of' (plugin_class < Train.plugin(1)).must_equal(true) end it "should provide a connection() method" do # false passed to instance_methods says 'don't use inheritance' plugin_class.instance_methods(false).must_include(:connection) end end train-3.2.28/examples/plugins/train-local-rot13/train-local-rot13.gemspec000066400000000000000000000041231364512547200261260ustar00rootroot00000000000000# As plugins are usually packaged and distributed as a RubyGem, # we have to provide a .gemspec file, which controls the gembuild # and publish process. This is a fairly generic gemspec. # It is traditional in a gemspec to dynamically load the current version # from a file in the source tree. The next three lines make that happen. lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "train-local-rot13/version" Gem::Specification.new do |spec| # Importantly, all Train plugins must be prefixed with `train-` spec.name = "train-local-rot13" # It is polite to namespace your plugin under TrainPlugins::YourPluginInCamelCase spec.version = TrainPlugins::LocalRot13::VERSION spec.authors = ["Chef InSpec Team"] spec.email = ["inspec@chef.io"] spec.summary = "Train Plugin example, rot13's file content and command output" spec.description = "Example for implementing a Train plugin. This simply performs the ROT13 substitution cipher on file content and command output." spec.homepage = "https://github.com/inspec/train/tree/master/examples/plugin" spec.license = "Apache-2.0" # Though complicated-looking, this is pretty standard for a gemspec. # It just filters what will actually be packaged in the gem (leaving # out tests, etc) spec.files = %w{ README.md train-local-rot13.gemspec Gemfile } + Dir.glob( "lib/**/*", File::FNM_DOTMATCH ).reject { |f| File.directory?(f) } spec.require_paths = ["lib"] # If you rely on any other gems, list them here with any constraints. # This is how `inspec plugin install` is able to manage your dependencies. # For example, perhaps you are writing a thing that talks to AWS, and you # want to ensure you have `aws-sdk` in a certain version. # If you only need certain gems during development or testing, list # them in Gemfile, not here. # Do not list inspec as a dependency of the train plugin. # All plugins should mention train, > 1.4 spec.add_dependency "train", "~> 1.4" spec.add_dependency "rot13", "~> 0.1" end train-3.2.28/lib/000077500000000000000000000000001364512547200135025ustar00rootroot00000000000000train-3.2.28/lib/train.rb000066400000000000000000000160521364512547200151500ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Dominik Richter () require_relative "train/version" require_relative "train/options" require_relative "train/plugins" require_relative "train/errors" require_relative "train/platforms" require "addressable/uri" module Train # Create a new transport instance, with the plugin indicated by the # given name. # # @param [String] name of the plugin # @param [Array] *args list of arguments for the plugin # @return [Transport] instance of the new transport or nil def self.create(name, *args) cls = load_transport(name) cls.new(*args) unless cls.nil? end # Retrieve the configuration options of a transport plugin. # # @param [String] name of the plugin # @return [Hash] map of default options def self.options(name) cls = load_transport(name) cls.default_options unless cls.nil? end # Load the transport plugin indicated by name. If the plugin is not # yet found in the plugin registry, it will be attempted to load from # `train/transports/plugin_name`. # # @param [String] name of the plugin # @return [Train::Transport] the transport plugin def self.load_transport(transport_name) transport_name = transport_name.to_s transport_class = Train::Plugins.registry[transport_name] return transport_class unless transport_class.nil? # Try to load the transport name from the core transports... require "train/transports/" + transport_name Train::Plugins.registry[transport_name] rescue LoadError => _ begin # If it's not in the core transports, try loading from a train plugin gem. gem_name = "train-" + transport_name require gem_name return Train::Plugins.registry[transport_name] # rubocop: disable Lint/HandleExceptions rescue LoadError => _ # rubocop: enable Lint/HandleExceptions # Intentionally empty rescue - we're handling it below anyway end ex = Train::PluginLoadError.new("Can't find train plugin #{transport_name}. Please install it first.") ex.transport_name = transport_name raise ex end # Legacy code to unpack a series of items from an incoming Hash # Inspec::Config.unpack_train_credentials now handles this in most cases that InSpec needs # If you need to unpack a URI, use unpack_target_from_uri # TODO: deprecate; can't issue a warning because train doesn't have a logger until the connection is setup (See base_connection.rb) def self.target_config(config = nil) conf = config.dup # Symbolize keys conf.keys.each do |key| unless key.is_a? Symbol conf[key.to_sym] = conf.delete(key) end end group_keys_and_keyfiles(conf) # TODO: move logic into SSH plugin return conf if conf[:target].to_s.empty? unpack_target_from_uri(conf[:target], conf).merge(conf) end # Given a string that looks like a URI, unpack connection credentials. # The name of the desired transport is always taken from the 'scheme' slot of the URI; # the remaining portion of the URI is parsed as if it were an HTTP URL, and then # the URL components are stored in the credentials hash. It is up to the transport # to interpret the fields in a sensible way for that transport. # New transport authors are encouraged to use transport://credset format (see # inspec/inspec/issues/3661) rather than inventing a new field mapping. def self.unpack_target_from_uri(uri_string, opts = {}) # rubocop: disable Metrics/AbcSize creds = {} return creds if uri_string.empty? # split up the target's host/scheme configuration uri = parse_uri(uri_string) unless uri.host.nil? && uri.scheme.nil? creds[:backend] ||= uri.scheme creds[:host] ||= uri.hostname creds[:port] ||= uri.port creds[:user] ||= uri.user creds[:path] ||= uri.path creds[:password] ||= if opts[:www_form_encoded_password] && !uri.password.nil? Addressable::URI.unencode_component(uri.password) else uri.password end end # ensure path is nil, if its empty; e.g. required to reset defaults for winrm # TODO: move logic into winrm plugin creds[:path] = nil if !creds[:path].nil? && creds[:path].to_s.empty? # compact! is available in ruby 2.4+ # TODO: rewrite next line using compact! once we drop support for ruby 2.3 creds = creds.delete_if { |_, value| value.nil? } # return the updated config creds end # Parse a URI. Supports empty URI's with paths, e.g. `mock://` # # @param string [string] URI string, e.g. `schema://domain.com` # @return [Addressable::URI] parsed URI object def self.parse_uri(string) u = Addressable::URI.parse(string) # A use-case we want to catch is parsing empty URIs with a schema # e.g. mock://. To do this, we match it manually and fake the hostname if u.scheme && (u.host.nil? || u.host.empty?) && u.path.empty? case string when %r{^([a-z]+)://$} string += "dummy" when /^([a-z]+):$/ string += "//dummy" end u = Addressable::URI.parse(string) u.host = nil end u rescue Addressable::URI::InvalidURIError => e raise Train::UserError, e end private_class_method :parse_uri # Examine the given credential information, and if all is well, # return the transport name. # TODO: this actually does no validation of the credential options whatsoever def self.validate_backend(credentials, default_transport_name = "local") return default_transport_name if credentials.nil? transport_name = credentials[:backend] # TODO: Determine if it is ever possible (or supported) for transport_name to be 'localhost' # TODO: After inspec/inspec/pull/3750 is merged, should be able to remove nil from the list if credentials[:sudo] && [nil, "local", "localhost"].include?(transport_name) raise Train::UserError, "Sudo is only valid when running against a remote host. "\ "To run this locally with elevated privileges, run the command with `sudo ...`." end return transport_name unless transport_name.nil? unless credentials[:target].nil? # We should not get here, because if target_uri unpacking was successful, # it would have set credentials[:backend] raise Train::UserError, "Cannot determine backend from target "\ "configuration #{credentials[:target]}. Valid example: ssh://192.168.0.1" end unless credentials[:host].nil? raise Train::UserError, "Host configured, but no backend was provided. Please "\ "specify how you want to connect. Valid example: ssh://192.168.0.1" end credentials[:backend] = default_transport_name end def self.group_keys_and_keyfiles(conf) # in case the user specified a key-file, register it that way # we will clear the list of keys and put keys and key_files separately keys_mixed = conf[:keys] return if keys_mixed.nil? conf[:key_files] = [] conf[:keys] = [] keys_mixed.each do |key| if !key.nil? && File.file?(key) conf[:key_files].push(key) else conf[:keys].push(key) end end end end train-3.2.28/lib/train/000077500000000000000000000000001364512547200146175ustar00rootroot00000000000000train-3.2.28/lib/train/errors.rb000066400000000000000000000023631364512547200164640ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Fletcher Nichol () # Author:: Dominik Richter () # Author:: Christoph Hartmann () # # Copyright (C) 2013, Fletcher Nichol # # Licensed under the Apache License, Version 2.0 (the "License"); module Train # Base exception for any exception explicitly raised by the Train library. class Error < ::StandardError attr_reader :reason def initialize(message = "", reason = :not_provided) super(message) @reason = reason end end # Base exception class for all exceptions that are caused by user input # errors. class UserError < Error; end # We could not load a plugin, because of a user error class PluginLoadError < UserError attr_accessor :transport_name end # Base exception class for all exceptions that are caused by incorrect use # of an API. class ClientError < Error; end # Base exception class for all exceptions that are caused by other failures # in the transport layer. class TransportError < Error; end # Exception for when no platform can be detected. class PlatformDetectionFailed < Error; end # Exception for when a invalid cache type is passed. class UnknownCacheType < Error; end end train-3.2.28/lib/train/extras.rb000066400000000000000000000004441364512547200164540ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Dominik Richter () module Train::Extras require_relative "extras/command_wrapper" require_relative "extras/stat" CommandResult = Struct.new(:stdout, :stderr, :exit_status) LoginCommand = Struct.new(:command, :arguments) end train-3.2.28/lib/train/extras/000077500000000000000000000000001364512547200161255ustar00rootroot00000000000000train-3.2.28/lib/train/extras/command_wrapper.rb000066400000000000000000000130741364512547200216350ustar00rootroot00000000000000# encoding: utf-8 # author: Dominik Richter # author: Christoph Hartmann require "base64" require_relative "../errors" module Train::Extras # Define the interface of all command wrappers. class CommandWrapperBase # Verify that the command wrapper is initialized properly and working. # # @return [Any] verification result, nil if all went well, otherwise a message def verify raise Train::ClientError, "#{self.class} does not implement #verify()" end # Wrap a command and return the augmented command which can be executed. # # @param [Strin] command that will be wrapper # @return [String] result of wrapping the command def run(_command) raise Train::ClientError, "#{self.class} does not implement #run(command)" end end # Wrap linux commands and add functionality like sudo. class LinuxCommand < CommandWrapperBase Train::Options.attach(self) option :shell, default: false option :shell_options, default: nil option :shell_command, default: nil option :sudo, default: false option :sudo_options, default: nil option :sudo_password, default: nil option :sudo_command, default: nil option :user attr_reader :backend def initialize(backend, options) @backend = backend validate_options(options) @shell = options[:shell] @shell_options = options[:shell_options] # e.g. '--login' @shell_command = options[:shell_command] # e.g. '/bin/sh' @sudo = options[:sudo] @sudo_options = options[:sudo_options] @sudo_password = options[:sudo_password] @sudo_command = options[:sudo_command] @user = options[:user] end # (see CommandWrapperBase::verify) def verify # Do nothing, successfully. Don't use "true", that's not available on Windows. cmd = if @sudo # Wrap it up. It needs /dev/null on the outside to disable stdin "sh -c '(#{run("echo")}) < /dev/null'" else run("echo") end # rubocop:disable Style/BlockDelimiters res = @backend.with_sudo_pty { @backend.run_command(cmd) } return nil if res.exit_status == 0 rawerr = "#{res.stdout} #{res.stderr}".strip case rawerr when /Sorry, try again/ ["Wrong sudo password.", :bad_sudo_password] when /sudo: no tty present and no askpass program specified/ ["Sudo requires a password, please configure it.", :sudo_password_required] when /sudo: command not found/ ["Can't find sudo command. Please either install and "\ "configure it on the target or deactivate sudo.", :sudo_command_not_found] when /sudo: sorry, you must have a tty to run sudo/ ["Sudo requires a TTY. Please see the README on how to configure "\ "sudo to allow for non-interactive usage.", :sudo_no_tty] else [rawerr, nil] end end def verify! msg, reason = verify return nil unless msg raise Train::UserError.new("Sudo failed: #{msg}", reason) end # (see CommandWrapperBase::run) def run(command) shell_wrap(sudo_wrap(command)) end def self.active?(options) options.is_a?(Hash) && ( options[:sudo] || options[:shell] ) end private # wrap the cmd in a sudo command def sudo_wrap(cmd) return cmd unless @sudo return cmd if @user == "root" res = (@sudo_command || "sudo") + " " if @sudo_password str = safe_string(@sudo_password + "\n") res = "#{str} | #{res}-S " end res << "#{@sudo_options} " if @sudo_options res + cmd end # wrap the cmd in a subshell allowing for options to # passed to the subshell def shell_wrap(cmd) return cmd unless @shell shell = @shell_command || "$SHELL" options = " #{@shell_options}" if @shell_options "#{safe_string(cmd)} | #{shell}#{options}" end # encapsulates encoding the string into a safe form, and decoding for use. # @return [String] A command line snippet that can be used as part of a pipeline. def safe_string(str) b64str = Base64.strict_encode64(str) "echo #{b64str} | base64 --decode" end end # Wrap windows commands. class WindowsCommand < CommandWrapperBase Train::Options.attach(self) option :shell_command, default: nil def initialize(backend, options) @backend = backend validate_options(options) @shell_command = options[:shell_command] # e.g. 'powershell' end # (see CommandWrapperBase::run) def run(command) powershell_wrap(command) end private # Wrap the cmd in an encoded command to allow pipes and quotes def powershell_wrap(cmd) shell = @shell_command || "powershell" # Prevent progress stream from leaking into stderr script = "$ProgressPreference='SilentlyContinue';" + cmd # Encode script so PowerShell can use it script = script.encode("UTF-16LE", "UTF-8") base64_script = Base64.strict_encode64(script) cmd = "#{shell} -NoProfile -EncodedCommand #{base64_script}" cmd end end class CommandWrapper include_options LinuxCommand include_options WindowsCommand def self.load(transport, options) if transport.platform.unix? return nil unless LinuxCommand.active?(options) res = LinuxCommand.new(transport, options) res.verify! res elsif transport.platform.windows? WindowsCommand.new(transport, options) end end end end train-3.2.28/lib/train/extras/stat.rb000066400000000000000000000106001364512547200174220ustar00rootroot00000000000000# encoding: utf-8 # author: Dominik Richter # author: Christoph Hartmann module Train::Extras class Stat TYPES = { socket: 00140000, symlink: 00120000, file: 00100000, block_device: 00060000, directory: 00040000, character_device: 00020000, pipe: 00010000, }.freeze def self.find_type(mode) res = TYPES.find { |_, mask| mask & mode == mask } res.nil? ? :unknown : res[0] end def self.stat(shell_escaped_path, backend, follow_symlink) # use perl scripts for aix, solaris 10 and hpux if backend.os.aix? || (backend.os.solaris? && backend.os[:release].to_i < 11) || backend.os.hpux? return aix_stat(shell_escaped_path, backend, follow_symlink) end return bsd_stat(shell_escaped_path, backend, follow_symlink) if backend.os.bsd? # linux,solaris 11 and esx will use standard linux stats return linux_stat(shell_escaped_path, backend, follow_symlink) if backend.os.unix? || backend.os.esx? # all other cases we don't handle # TODO: print an error if we get here, as it shouldn't be invoked # on non-unix {} end def self.linux_stat(shell_escaped_path, backend, follow_symlink) lstat = follow_symlink ? " -L" : "" format = (backend.os.esx? || backend.os[:name] == "alpine" || backend.os[:name] == "yocto") ? "-c" : "--printf" res = backend.run_command("stat#{lstat} #{shell_escaped_path} 2>/dev/null #{format} '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'") # ignore the exit_code: it is != 0 if selinux labels are not supported # on the system. fields = res.stdout.split("\n") return {} if fields.length != 9 tmask = fields[1].to_i(16) selinux = fields[8] ## selinux security context string not available on esxi selinux = nil if (selinux == "?") || (selinux == "(null)") || (selinux == "C") { type: find_type(tmask), mode: tmask & 07777, owner: fields[2], uid: fields[3].to_i, group: fields[4], gid: fields[5].to_i, mtime: fields[7].to_i, size: fields[0].to_i, selinux_label: selinux, } end def self.bsd_stat(shell_escaped_path, backend, follow_symlink) # From stat man page on FreeBSD: # z The size of file in bytes (st_size). # p File type and permissions (st_mode). # u, g User ID and group ID of file's owner (st_uid, st_gid). # a, m, c, B # The time file was last accessed or modified, or when the # inode was last changed, or the birth time of the inode # (st_atime, st_mtime, st_ctime, st_birthtime). # # The special output specifier S may be used to indicate that the # output, if applicable, should be in string format. May be used # in combination with: # ... # gu Display group or user name. lstat = follow_symlink ? " -L" : "" res = backend.run_command( "stat#{lstat} -f '%z\n%p\n%Su\n%u\n%Sg\n%g\n%a\n%m' "\ "#{shell_escaped_path}" ) return {} if res.exit_status != 0 fields = res.stdout.split("\n") return {} if fields.length != 8 tmask = fields[1].to_i(8) { type: find_type(tmask), mode: tmask & 07777, owner: fields[2], uid: fields[3].to_i, group: fields[4], gid: fields[5].to_i, mtime: fields[7].to_i, size: fields[0].to_i, selinux_label: fields[8], } end def self.aix_stat(shell_escaped_path, backend, follow_symlink) # Perl here b/c it is default on AIX lstat = follow_symlink ? "lstat" : "stat" stat_cmd = <<-EOP perl -e ' @a = #{lstat}(shift) or exit 2; $u = getpwuid($a[4]); $g = getgrgid($a[5]); printf("0%o\\n%s\\n%d\\n%s\\n%d\\n%d\\n%d\\n", $a[2], $u, $a[4], $g, $a[5], $a[9], $a[7]) ' #{shell_escaped_path} EOP res = backend.run_command(stat_cmd) return {} if res.exit_status != 0 fields = res.stdout.split("\n") return {} if fields.length != 7 tmask = fields[0].to_i(8) { type: find_type(tmask), mode: tmask & 07777, owner: fields[1], uid: fields[2].to_i, group: fields[3], gid: fields[4].to_i, mtime: fields[5].to_i, size: fields[6].to_i, selinux_label: nil, } end end end train-3.2.28/lib/train/file.rb000066400000000000000000000132641364512547200160710ustar00rootroot00000000000000# encoding: utf-8 # # author: Christoph Hartmann # author: Dominik Richter require_relative "file/local" require_relative "file/remote" require_relative "extras/stat" module Train class File # rubocop:disable Metrics/ClassLength def initialize(backend, path, follow_symlink = true) @backend = backend @path = path || "" @follow_symlink = follow_symlink sanitize_filename(path) end # This method gets override by particular os class. def sanitize_filename(_path) nil end # interface methods: these fields should be implemented by every # backend File DATA_FIELDS = %w{ exist? mode owner group uid gid content mtime size selinux_label path }.freeze DATA_FIELDS.each do |m| next if m == "path" define_method m do raise NotImplementedError, "File must implement the #{m}() method." end end def to_json res = Hash[DATA_FIELDS.map { |x| [x, method(x).call] }] # additional fields provided as input res["type"] = type res["follow_symlink"] = @follow_symlink res end def type :unknown end def source if @follow_symlink self.class.new(@backend, @path, false) else self end end def source_path @path end # product_version is primarily used by Windows operating systems only and will be overwritten # in Windows-related classes. Since this field is returned for all file objects, the acceptable # default value is nil def product_version nil end # file_version is primarily used by Windows operating systems only and will be overwritten # in Windows-related classes. Since this field is returned for all file objects, the acceptable # default value is nil def file_version nil end def version?(version) (product_version == version) || (file_version == version) end def block_device? type.to_s == "block_device" end def character_device? type.to_s == "character_device" end def pipe? type.to_s == "pipe" end def file? type.to_s == "file" end def socket? type.to_s == "socket" end def directory? type.to_s == "directory" end def symlink? source.type.to_s == "symlink" end def owned_by?(sth) owner == sth end def path if symlink? && @follow_symlink link_path else @path end end # if the OS-specific file class supports inquirying as to whether the # file/device is mounted, the #mounted method should return a command # object whose stdout will not be nil if indeed the device is mounted. # # if the OS-specific file class does not support checking for mount # status, the method should not be implemented and this method will # return false. def mounted? return false unless respond_to?(:mounted) !mounted.nil? && !mounted.stdout.nil? && !mounted.stdout.empty? end def md5sum # Skip processing rest of method if fallback method is selected return perform_checksum_ruby(:md5) if defined?(@ruby_checksum_fallback) checksum = if @backend.os.family == "windows" perform_checksum_windows(:md5) else @md5_command ||= case @backend.os.family when "darwin" "md5 -r" when "solaris" "digest -a md5" else "md5sum" end perform_checksum_unix(@md5_command) end checksum || perform_checksum_ruby(:md5) end def sha256sum # Skip processing rest of method if fallback method is selected return perform_checksum_ruby(:sha256) if defined?(@ruby_checksum_fallback) checksum = if @backend.os.family == "windows" perform_checksum_windows(:sha256) else @sha256_command ||= case @backend.os.family when "darwin", "hpux", "qnx" "shasum -a 256" when "solaris" "digest -a sha256" else "sha256sum" end perform_checksum_unix(@sha256_command) end checksum || perform_checksum_ruby(:sha256) end private def perform_checksum_unix(cmd) res = @backend.run_command("#{cmd} #{@path}") res.stdout.split(" ").first if res.exit_status == 0 end def perform_checksum_windows(method) cmd = "CertUtil -hashfile #{@path} #{method.to_s.upcase}" res = @backend.run_command(cmd) res.stdout.split("\r\n")[1].tr(" ", "") if res.exit_status == 0 end # This pulls the content of the file to the machine running Train and uses # Digest to perform the checksum. This is less efficient than using remote # system binaries and can lead to incorrect results due to encoding. def perform_checksum_ruby(method) # This is used to skip attempting other checksum methods. If this is set # then we know all other methods have failed. @ruby_checksum_fallback = true case method when :md5 res = Digest::MD5.new when :sha256 res = Digest::SHA256.new end res.update(content) res.hexdigest rescue TypeError => _ nil end end end train-3.2.28/lib/train/file/000077500000000000000000000000001364512547200155365ustar00rootroot00000000000000train-3.2.28/lib/train/file/local.rb000066400000000000000000000032621364512547200171600ustar00rootroot00000000000000# encoding: utf-8 module Train class File class Local < Train::File %w{ exist? file? socket? directory? symlink? pipe? size basename }.each do |m| define_method m.to_sym do ::File.method(m.to_sym).call(@path) end end def content @content ||= ::File.read(@path, encoding: "UTF-8") rescue StandardError => _ nil end def link_path return nil unless symlink? begin @link_path ||= ::File.realpath(@path) rescue Errno::ELOOP => _ # Leave it blank on symbolic loop, same as readlink @link_path = "" end end def shallow_link_path return nil unless symlink? @link_path ||= ::File.readlink(@path) end def block_device? ::File.blockdev?(@path) end def character_device? ::File.chardev?(@path) end def type case ::File.ftype(@path) when "blockSpecial" :block_device when "characterSpecial" :character_device when "link" :symlink when "fifo" :pipe else ::File.ftype(@path).to_sym end end %w{ mode owner group uid gid mtime selinux_label }.each do |field| define_method field.to_sym do stat[field.to_sym] end end def mode?(sth) mode == sth end def linked_to?(dst) link_path == dst end end end end # subclass requires are loaded after Train::File::Local is defined # to avoid superclass mismatch errors require_relative "local/unix" require_relative "local/windows" train-3.2.28/lib/train/file/local/000077500000000000000000000000001364512547200166305ustar00rootroot00000000000000train-3.2.28/lib/train/file/local/unix.rb000066400000000000000000000042521364512547200201430ustar00rootroot00000000000000# encoding: utf-8 require "shellwords" require_relative "../../extras/stat" module Train class File class Local class Unix < Train::File::Local def sanitize_filename(path) @spath = Shellwords.escape(path) || @path end def stat return @stat if defined?(@stat) begin file_stat = if @follow_symlink ::File.stat(@path) else ::File.lstat(@path) end rescue StandardError => _err return @stat = {} end @stat = { type: Train::Extras::Stat.find_type(file_stat.mode), mode: file_stat.mode & 07777, mtime: file_stat.mtime.to_i, size: file_stat.size, owner: pw_username(file_stat.uid), uid: file_stat.uid, group: pw_groupname(file_stat.gid), gid: file_stat.gid, } lstat = @follow_symlink ? " -L" : "" res = @backend.run_command("stat#{lstat} #{@spath} 2>/dev/null --printf '%C'") if res.exit_status == 0 && !res.stdout.empty? && res.stdout != "?" @stat[:selinux_label] = res.stdout.strip end @stat end def mounted @mounted ||= @backend.run_command("mount | grep -- ' on #{@path} '") end def grouped_into?(sth) group == sth end def unix_mode_mask(owner, type) o = UNIX_MODE_OWNERS[owner.to_sym] return nil if o.nil? t = UNIX_MODE_TYPES[type.to_sym] return nil if t.nil? t & o end private def pw_username(uid) Etc.getpwuid(uid).name rescue ArgumentError => _ nil end def pw_groupname(gid) Etc.getgrgid(gid).name rescue ArgumentError => _ nil end UNIX_MODE_OWNERS = { all: 00777, owner: 00700, group: 00070, other: 00007, }.freeze UNIX_MODE_TYPES = { r: 00444, w: 00222, x: 00111, }.freeze end end end end train-3.2.28/lib/train/file/local/windows.rb000066400000000000000000000034351364512547200206540ustar00rootroot00000000000000# encoding: utf-8 module Train class File class Local class Windows < Train::File::Local # Ensures we do not use invalid characters for file names # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions def sanitize_filename(path) return if path.nil? # we do not filter :, backslash and forward slash, since they are part of the path @spath = path.gsub(/[<>"|?*]/, "") end def product_version @product_version ||= @backend.run_command( "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").ProductVersion" ).stdout.chomp end def file_version @file_version ||= @backend.run_command( "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").FileVersion" ).stdout.chomp end def owner owner = @backend.run_command( "Get-Acl \"#{@spath}\" | select -expand Owner" ).stdout.strip return if owner.empty? owner end def stat return @stat if defined?(@stat) begin file_stat = if @follow_symlink ::File.stat(@path) else ::File.lstat(@path) end rescue StandardError => _err return @stat = {} end @stat = { type: type, mode: file_stat.mode, mtime: file_stat.mtime.to_i, size: file_stat.size, owner: owner, uid: file_stat.uid, group: nil, gid: file_stat.gid, selinux_label: nil, } @stat end end end end end train-3.2.28/lib/train/file/remote.rb000066400000000000000000000017321364512547200173610ustar00rootroot00000000000000# encoding: utf-8 module Train class File class Remote < Train::File def basename(suffix = nil, sep = "/") raise "Not yet supported: Suffix in file.basename" unless suffix.nil? @basename ||= detect_filename(path, sep || "/") end def stat return @stat if defined?(@stat) @stat = Train::Extras::Stat.stat(@spath, @backend, @follow_symlink) end # helper methods provided to any implementing class private def detect_filename(path, sep) idx = path.rindex(sep) return path if idx.nil? idx += 1 return detect_filename(path[0..-2], sep) if idx == path.length path[idx..-1] end end end end # subclass requires are loaded after Train::File::Remote is defined # to avoid superclass mismatch errors require_relative "remote/aix" require_relative "remote/linux" require_relative "remote/qnx" require_relative "remote/unix" require_relative "remote/windows" train-3.2.28/lib/train/file/remote/000077500000000000000000000000001364512547200170315ustar00rootroot00000000000000train-3.2.28/lib/train/file/remote/aix.rb000066400000000000000000000012101364512547200201310ustar00rootroot00000000000000# encoding: utf-8 require_relative "unix" module Train class File class Remote class Aix < Train::File::Remote::Unix def link_path return nil unless symlink? @link_path ||= @backend.run_command("perl -e 'print readlink shift' #{@spath}").stdout.chomp end def shallow_link_path return nil unless symlink? @shallow_link_path ||= @backend.run_command("perl -e 'print readlink shift' #{@spath}").stdout.chomp end def mounted @mounted ||= @backend.run_command("lsfs -c #{@spath}") end end end end end train-3.2.28/lib/train/file/remote/linux.rb000066400000000000000000000007061364512547200205200ustar00rootroot00000000000000# encoding: utf-8 require_relative "unix" module Train class File class Remote class Linux < Train::File::Remote::Unix def content return @content if defined?(@content) @content = @backend.run_command("cat #{@spath} || echo -n").stdout return @content unless @content.empty? @content = nil if directory? || size.nil? || (size > 0) @content end end end end end train-3.2.28/lib/train/file/remote/qnx.rb000066400000000000000000000020011364512547200201550ustar00rootroot00000000000000# encoding: utf-8 # # author: Christoph Hartmann # author: Dominik Richter require_relative "unix" module Train class File class Remote class Qnx < Train::File::Remote::Unix def content cat = "cat" cat = "/proc/boot/cat" if @backend.os[:release].to_i >= 7 @content ||= case when !exist? nil else @backend.run_command("#{cat} #{@spath}").stdout || "" end end def type if @backend.run_command("file #{@spath}").stdout.include?("directory") :directory else :file end end %w{ mode owner group uid gid mtime size selinux_label link_path mounted stat }.each do |field| define_method field.to_sym do raise NotImplementedError, "QNX does not implement the #{field}() method yet." end end end end end end train-3.2.28/lib/train/file/remote/unix.rb000066400000000000000000000050551364512547200203460ustar00rootroot00000000000000require "shellwords" module Train class File class Remote class Unix < Train::File::Remote def sanitize_filename(path) @spath = Shellwords.escape(path) || @path end def content @content ||= if !exist? || directory? nil elsif size.nil? || size == 0 "" else @backend.run_command("cat #{@spath}").stdout || "" end end def exist? @exist ||= begin f = @follow_symlink ? "" : " || test -L #{@spath}" @backend.run_command("test -e #{@spath}" + f) .exit_status == 0 end end def mounted @mounted ||= @backend.run_command("mount | grep -- ' on #{@path} '") end %w{ type mode owner group uid gid mtime size selinux_label }.each do |field| define_method field.to_sym do stat[field.to_sym] end end def mode?(sth) mode == sth end def grouped_into?(sth) group == sth end def linked_to?(dst) link_path == dst end def link_path symlink? ? path : nil end def shallow_link_path return nil unless symlink? @shallow_link_path ||= @backend.run_command("readlink #{@spath}").stdout.chomp end def unix_mode_mask(owner, type) o = UNIX_MODE_OWNERS[owner.to_sym] return nil if o.nil? t = UNIX_MODE_TYPES[type.to_sym] return nil if t.nil? t & o end def path return @path unless @follow_symlink && symlink? @link_path ||= read_target_path end private # Returns full path of a symlink target(real dest) or '' on symlink loop def read_target_path full_path = @backend.run_command("readlink -n #{@spath} -f").stdout # Needed for some OSes like OSX that returns relative path # when the link and target are in the same directory if !full_path.start_with?("/") && full_path != "" full_path = ::File.expand_path("../#{full_path}", @spath) end full_path end UNIX_MODE_OWNERS = { all: 00777, owner: 00700, group: 00070, other: 00007, }.freeze UNIX_MODE_TYPES = { r: 00444, w: 00222, x: 00111, }.freeze end end end end train-3.2.28/lib/train/file/remote/windows.rb000066400000000000000000000054571364512547200210630ustar00rootroot00000000000000# encoding: utf-8 module Train class File class Remote class Windows < Train::File::Remote attr_reader :path # Ensures we do not use invalid characters for file names # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions def sanitize_filename(path) return if path.nil? # we do not filter :, backslash and forward slash, since they are part of the path @spath = path.gsub(/[<>"|?*]/, "") end def basename(suffix = nil, sep = '\\') super(suffix, sep) end def content return @content if defined?(@content) @content = @backend.run_command("Get-Content(\"#{@spath}\") | Out-String").stdout return @content unless @content.empty? @content = nil if directory? # or size.nil? or size > 0 @content end def exist? return @exist if defined?(@exist) @exist = @backend.run_command( "(Test-Path -Path \"#{@spath}\").ToString()" ).stdout.chomp == "True" end def owner owner = @backend.run_command( "Get-Acl \"#{@spath}\" | select -expand Owner" ).stdout.strip return if owner.empty? owner end def type if attributes.include?("Archive") && !attributes.include?("Directory") return :file elsif attributes.include?("ReparsePoint") return :symlink elsif attributes.include?("Directory") return :directory end :unknown end def size if file? @backend.run_command("((Get-Item '#{@spath}').Length)").stdout.strip.to_i end end def product_version @product_version ||= @backend.run_command( "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").ProductVersion" ).stdout.chomp end def file_version @file_version ||= @backend.run_command( "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").FileVersion" ).stdout.chomp end %w{ mode group uid gid mtime selinux_label }.each do |field| define_method field.to_sym do nil end end def link_path nil end def shallow_link_path nil end def mounted nil end private def attributes return @attributes if defined?(@attributes) @attributes = @backend.run_command( "(Get-ItemProperty -Path \"#{@spath}\").attributes.ToString()" ).stdout.chomp.split(/\s*,\s*/) end end end end end train-3.2.28/lib/train/globals.rb000066400000000000000000000001471364512547200165710ustar00rootroot00000000000000module Train def self.src_root File.expand_path(File.join(__FILE__, "..", "..", "..")) end end train-3.2.28/lib/train/options.rb000066400000000000000000000043061364512547200166420ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Dominik Richter () # Author:: Christoph Hartmann () module Train module Options def self.attach(target) target.class.method(:include).call(ClassOptions) target.method(:include).call(InstanceOptions) end module ClassOptions def option(name, conf = nil, &block) d = conf || {} unless d.is_a? Hash raise Train::ClientError, "The transport plugin #{self} declared an option #{name} "\ "and didn't provide a valid configuration hash." end if !conf.nil? && !conf[:default].nil? && block_given? raise Train::ClientError, "The transport plugin #{self} declared an option #{name} "\ "with both a default value and block. Only use one of these." end d[:default] = block if block_given? default_options[name] = d end def default_options @default_options = {} unless defined? @default_options @default_options end def include_options(other) unless other.respond_to?(:default_options) raise "Trying to include options from module #{other.inspect}, "\ "which doesn't seem to support options." end default_options.merge!(other.default_options) end end module InstanceOptions # @return [Hash] options, which created this Transport attr_reader :options def default_options self.class.default_options end def merge_options(base, opts) res = base.merge(opts || {}) default_options.each do |field, hm| next unless res[field].nil? && hm.key?(:default) default = hm[:default] if default.is_a? Proc res[field] = default.call(res) else res[field] = default end end res end def validate_options(opts) default_options.each do |field, hm| if opts[field].nil? && hm[:required] raise Train::ClientError, "You must provide a value for #{field.to_s.inspect}." end end opts end end end end train-3.2.28/lib/train/platforms.rb000066400000000000000000000053721364512547200171620ustar00rootroot00000000000000# encoding: utf-8 require_relative "platforms/common" require_relative "platforms/detect" require_relative "platforms/detect/scanner" require_relative "platforms/detect/specifications/os" require_relative "platforms/detect/specifications/api" require_relative "platforms/detect/uuid" require_relative "platforms/family" require_relative "platforms/platform" module Train::Platforms # Retrieve the current platform list # # @return [Hash] map with platform names and their objects def self.list @list ||= {} end # Retrieve the current family list # # @return [Hash] map with family names and their objects def self.families @families ||= {} end # Clear all platform settings. Only used for testing. def self.__reset @list = {} @families = {} end # Create or update a platform # # @return Train::Platform def self.name(name, condition = {}) # TODO: refactor this against family. They're stupidly similar # Check the list to see if one is already created plat = list[name] unless plat.nil? # Pass the condition incase we are adding a family relationship plat.condition = condition unless condition.nil? return plat end Train::Platforms::Platform.new(name, condition) end # Create or update a family # # @return Train::Platforms::Family def self.family(name, condition = {}) # Check the families to see if one is already created family = families[name] unless family.nil? # Pass the condition incase we are adding a family relationship family.condition = condition unless condition.nil? return family end Train::Platforms::Family.new(name, condition) end # Find the families or top level platforms # # @return [Hash] with top level family and platforms def self.top_platforms empty_list = list.select { |_key, value| value.families.empty? } empty_fams = families.select { |_key, value| value.families.empty? } empty_list.merge empty_fams end # List all platforms and families in a readable output def self.list_all top_platforms = self.top_platforms top_platforms.each_value do |platform| puts platform.title print_children(platform) if defined?(platform.children) end end def self.print_children(parent, pad = 2) parent.children.each do |key, value| obj = key puts "#{" " * pad}-> #{obj.title}#{value unless value.empty?}" print_children(obj, pad + 2) if defined?(obj.children) && !obj.children.nil? end end def self.export export = [] list.each do |name, platform| platform.find_family_hierarchy export << { name: name, families: platform.family_hierarchy, } end export.sort_by { |platform| platform[:name] } end end train-3.2.28/lib/train/platforms/000077500000000000000000000000001364512547200166265ustar00rootroot00000000000000train-3.2.28/lib/train/platforms/common.rb000066400000000000000000000016111364512547200204420ustar00rootroot00000000000000# encoding: utf-8 module Train::Platforms module Common # Add a family connection. This will create a family # if it does not exist and add a child relationship. def in_family(family) if self.class == Train::Platforms::Family && @name == family raise "Unable to add family #{@name} to itself: '#{@name}.in_family(#{family})'" end # add family to the family list family = Train::Platforms.family(family) family.children[self] = @condition @families[family] = @condition @condition = nil self end def detect(&block) @detect = block if block # TODO: detect shouldn't be a setter and getter at the same time @detect ||= ->(_) { false } end def to_s be = backend ? backend.backend_type : "unknown" "%s:%s:%s" % [self.class, be, name] end def inspect to_s end end end train-3.2.28/lib/train/platforms/detect.rb000066400000000000000000000004071364512547200204240ustar00rootroot00000000000000# encoding: utf-8 module Train::Platforms module Detect # Main detect method to scan all platforms for a match # # @return Train::Platform instance or error if none found def self.scan(backend) Scanner.new(backend).scan end end end train-3.2.28/lib/train/platforms/detect/000077500000000000000000000000001364512547200200765ustar00rootroot00000000000000train-3.2.28/lib/train/platforms/detect/helpers/000077500000000000000000000000001364512547200215405ustar00rootroot00000000000000train-3.2.28/lib/train/platforms/detect/helpers/os_common.rb000066400000000000000000000122171364512547200240610ustar00rootroot00000000000000require_relative "os_linux" require_relative "os_windows" require "rbconfig" module Train::Platforms::Detect::Helpers module OSCommon include Train::Platforms::Detect::Helpers::Linux include Train::Platforms::Detect::Helpers::Windows def ruby_host_os(regex) ::RbConfig::CONFIG["host_os"] =~ regex end def winrm? backend_name == "TrainPlugins::WinRM::Connection" end def backend_name @backend.class.name end def unix_file_contents(path) # keep a log of files incase multiple checks call the same one return @files[path] if @files.key?(path) res = @backend.run_command("test -f #{path} && cat #{path}") # ignore files that can't be read @files[path] = res.exit_status == 0 ? res.stdout : nil @files[path] end def unix_file_exist?(path) @backend.run_command("test -f #{path}").exit_status == 0 end def command_output(cmd) res = @backend.run_command(cmd).stdout # When you try to execute command using ssh connction as root user and you have provided ssh user identity file # it gives standard output to login as authorised user other than root. To show this standard ouput as an error # to user we are matching the string of stdout and raising the error here so that user gets exact information. if @backend.class.to_s == "Train::Transports::SSH::Connection" && res =~ /Please login as the user/ raise Train::UserError, "SSH failed: #{res}" end res.strip! unless res.nil? res end def unix_uname_s return @uname[:s] if @uname.key?(:s) @uname[:s] = command_output("uname -s") end def unix_uname_r return @uname[:r] if @uname.key?(:r) @uname[:r] = command_output("uname -r") end def unix_uname_m return @uname[:m] if @uname.key?(:m) @uname[:m] = command_output("uname -m") end def brocade_version return @cache[:brocade] if @cache.key?(:brocade) res = command_output("version") m = res.match(/^Fabric OS:\s+v(\S+)$/) @cache[:brocade] = m && { version: m[1], type: "fos" } end def cisco_show_version return @cache[:cisco] if @cache.key?(:cisco) res = command_output("show version") m = res.match(/Cisco IOS Software, [^,]+? \(([^,]+?)\), Version (\d+\.\d+)/) unless m.nil? return @cache[:cisco] = { version: m[2], model: m[1], type: "ios" } end m = res.match(/Cisco IOS Software, IOS-XE Software, [^,]+? \(([^,]+?)\), Version (\d+\.\d+\.\d+[A-Z]*)/) unless m.nil? return @cache[:cisco] = { version: m[2], model: m[1], type: "ios-xe" } end m = res.match(/Cisco Nexus Operating System \(NX-OS\) Software/) unless m.nil? v = res[/^\s*system:\s+version (\d+\.\d+)/, 1] return @cache[:cisco] = { version: v, type: "nexus" } end @cache[:cisco] = nil end def unix_uuid (unix_uuid_from_chef || unix_uuid_from_machine_file || uuid_from_command || raise(Train::TransportError, "Cannot find a UUID for your node.")) end def unix_uuid_from_chef file = @backend.file("/var/chef/cache/data_collector_metadata.json") if file.exist? && file.size != 0 json = ::JSON.parse(file.content) return json["node_uuid"] if json["node_uuid"] end end def unix_uuid_from_machine_file # require 'pry';binding.pry %W{ /etc/chef/chef_guid #{ENV["HOME"]}/.chef/chef_guid /etc/machine-id /var/lib/dbus/machine-id /var/db/dbus/machine-id }.each do |path| file = @backend.file(path) next unless file.exist? && file.size != 0 return file.content.chomp if path =~ /guid/ return uuid_from_string(file.content.chomp) end nil end # This takes a command from the platform detect block to run. # We expect the command to return a unique identifier which # we turn into a UUID. def uuid_from_command return unless @platform[:uuid_command] result = @backend.run_command(@platform[:uuid_command]) uuid_from_string(result.stdout.chomp) if result.exit_status == 0 && !result.stdout.empty? end # This hashes the passed string into SHA1. # Then it downgrades the 160bit SHA1 to a 128bit # then we format it as a valid UUIDv5. def uuid_from_string(string) hash = Digest::SHA1.new hash.update(string) ary = hash.digest.unpack("NnnnnN") ary[2] = (ary[2] & 0x0FFF) | (5 << 12) ary[3] = (ary[3] & 0x3FFF) | 0x8000 # rubocop:disable Style/FormatString "%08x-%04x-%04x-%04x-%04x%08x" % ary end def json_cmd(cmd) cmd = @backend.run_command(cmd) if cmd.exit_status == 0 && !cmd.stdout.empty? require "json" eos_ver = JSON.parse(cmd.stdout) @platform[:release] = eos_ver["version"] @platform[:arch] = eos_ver["architecture"] true end rescue JSON::ParserError nil end def set_from_uname @platform[:name] = unix_uname_s.lines.first.chomp @platform[:release] = unix_uname_r.lines.first.chomp true end end end train-3.2.28/lib/train/platforms/detect/helpers/os_linux.rb000066400000000000000000000045411364512547200237310ustar00rootroot00000000000000# encoding: utf-8 module Train::Platforms::Detect::Helpers module Linux def redhatish_platform(conf) conf =~ /^red hat/i ? "redhat" : /(\w+)/i.match(conf)[1].downcase end def redhatish_version(conf) case conf when /rawhide/i /((\d+) \(Rawhide\))/i.match(conf)[1].downcase when /Amazon Linux/i /([\d\.]+)/.match(conf)[1] when /derived from .*linux|amazon/i /Linux ((\d+|\.)+)/i.match(conf)[1] else /release ([\d\.]+)/.match(conf)[1] end end def redhatish(path) if (raw = unix_file_contents(path)) @platform[:release] = redhatish_version(raw) true end end def linux_os_release data = unix_file_contents("/etc/os-release") return if data.nil? os_info = parse_os_release_info(data) cisco_info_file = os_info["CISCO_RELEASE_INFO"] if cisco_info_file os_info.merge!(parse_os_release_info(unix_file_contents(cisco_info_file))) end os_info end def parse_os_release_info(raw) return {} if raw.nil? raw.lines.each_with_object({}) do |line, memo| line.strip! next if line.nil? || line.empty? next if line.start_with?("#") key, value = line.split("=", 2) memo[key] = value.gsub(/\A"|"\Z/, "") unless value.nil? || value.empty? end end def lsb_config(content) id = /^DISTRIB_ID=["']?(.+?)["']?$/.match(content) release = /^DISTRIB_RELEASE=["']?(.+?)["']?$/.match(content) codename = /^DISTRIB_CODENAME=["']?(.+?)["']?$/.match(content) { id: id.nil? ? nil : id[1], release: release.nil? ? nil : release[1], codename: codename.nil? ? nil : codename[1], } end def lsb_release(content) id = /^Distributor ID:\s+(.+)$/.match(content) release = /^Release:\s+(.+)$/.match(content) codename = /^Codename:\s+(.+)$/.match(content) { id: id.nil? ? nil : id[1], release: release.nil? ? nil : release[1], codename: codename.nil? ? nil : codename[1], } end def read_linux_lsb return @lsb unless @lsb.empty? if !(raw = unix_file_contents("/etc/lsb-release")).nil? @lsb = lsb_config(raw) elsif !(raw = unix_file_contents("/usr/bin/lsb-release")).nil? @lsb = lsb_release(raw) end end end end train-3.2.28/lib/train/platforms/detect/helpers/os_windows.rb000066400000000000000000000113121364512547200242560ustar00rootroot00000000000000module Train::Platforms::Detect::Helpers module Windows def detect_windows check_cmd || check_powershell end def check_cmd # try to detect windows, use cmd.exe to also support Microsoft OpenSSH res = @backend.run_command("cmd.exe /c ver") return false if (res.exit_status != 0) || res.stdout.empty? # if the ver contains `Windows`, we know its a Windows system version = res.stdout.strip return false unless version.downcase =~ /windows/ @platform[:family] = "windows" # try to extract release from eg. `Microsoft Windows [Version 6.3.9600]` release = /\[(?.*)\]/.match(version) if release[:name] # release is 6.3.9600 now @platform[:release] = release[:name].downcase.gsub("version", "").strip # fallback, if we are not able to extract the name from wmic later @platform[:name] = "Windows #{@platform[:release]}" end read_wmic true end def check_powershell command = @backend.run_command( "Get-WmiObject Win32_OperatingSystem | Select Caption,Version | ConvertTo-Json" ) return false if (command.exit_status != 0) || command.stdout.empty? payload = JSON.parse(command.stdout) @platform[:family] = "windows" @platform[:release] = payload["Version"] @platform[:name] = payload["Caption"] read_wmic true end def local_windows? @backend.class.to_s == "Train::Transports::Local::Connection" && ruby_host_os(/mswin|mingw32|windows/) end # reads os name and version from wmic # @see https://msdn.microsoft.com/en-us/library/bb742610.aspx#EEAA # Thanks to Matt Wrock (https://github.com/mwrock) for this hint def read_wmic res = @backend.run_command("wmic os get * /format:list") if res.exit_status == 0 sys_info = {} res.stdout.lines.each do |line| m = /^\s*([^=]*?)\s*=\s*(.*?)\s*$/.match(line) sys_info[m[1].to_sym] = m[2] unless m.nil? || m[1].nil? end @platform[:release] = sys_info[:Version] # additional info on windows @platform[:build] = sys_info[:BuildNumber] @platform[:name] = sys_info[:Caption] @platform[:name] = @platform[:name].gsub("Microsoft", "").strip unless @platform[:name].empty? @platform[:arch] = read_wmic_cpu end end # `OSArchitecture` from `read_wmic` does not match a normal standard # For example, `x86_64` shows as `64-bit` def read_wmic_cpu res = @backend.run_command("wmic cpu get architecture /format:list") if res.exit_status == 0 sys_info = {} res.stdout.lines.each do |line| m = /^\s*([^=]*?)\s*=\s*(.*?)\s*$/.match(line) sys_info[m[1].to_sym] = m[2] unless m.nil? || m[1].nil? end end # This converts `wmic os get architecture` output to a normal standard # https://msdn.microsoft.com/en-us/library/aa394373(VS.85).aspx arch_map = { 0 => "i386", 1 => "mips", 2 => "alpha", 3 => "powerpc", 5 => "arm", 6 => "ia64", 9 => "x86_64", } # The value of `wmic cpu get architecture` is always a number between 0-9 arch_number = sys_info[:Architecture].to_i arch_map[arch_number] end # This method scans the target os for a unique uuid to use def windows_uuid uuid = windows_uuid_from_chef uuid = windows_uuid_from_machine_file if uuid.nil? uuid = windows_uuid_from_wmic if uuid.nil? uuid = windows_uuid_from_registry if uuid.nil? raise Train::TransportError, "Cannot find a UUID for your node." if uuid.nil? uuid end def windows_uuid_from_machine_file %W{ #{ENV["SYSTEMDRIVE"]}\\chef\\chef_guid #{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}\\.chef\\chef_guid }.each do |path| file = @backend.file(path) return file.content.chomp if file.exist? && file.size != 0 end nil end def windows_uuid_from_chef file = @backend.file("#{ENV["SYSTEMDRIVE"]}\\chef\\cache\\data_collector_metadata.json") return if !file.exist? || file.size == 0 json = JSON.parse(file.content) json["node_uuid"] if json["node_uuid"] end def windows_uuid_from_wmic result = @backend.run_command("wmic csproduct get UUID") return unless result.exit_status == 0 result.stdout.split("\r\n")[-1].strip end def windows_uuid_from_registry cmd = '(Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography" -Name "MachineGuid")."MachineGuid"' result = @backend.run_command(cmd) return unless result.exit_status == 0 result.stdout.chomp end end end train-3.2.28/lib/train/platforms/detect/scanner.rb000066400000000000000000000044071364512547200220610ustar00rootroot00000000000000# encoding: utf-8 require_relative "helpers/os_common" module Train::Platforms::Detect class Scanner include Train::Platforms::Detect::Helpers::OSCommon def initialize(backend) @backend = backend @platform = {} @family_hierarchy = [] # detect cache variables @files = {} @uname = {} @lsb = {} @cache = {} end # Main detect method to scan all platforms for a match # # @return Train::Platform instance or error if none found def scan # start with the platform/families who have no families (the top levels) top = Train::Platforms.top_platforms top.each do |_name, plat| # we are doing a instance_eval here to make sure we have the proper # context with all the detect helper methods next unless instance_eval(&plat.detect) # if we have a match start looking at the children plat_result = scan_children(plat) next if plat_result.nil? # return platform to backend @family_hierarchy << plat.name return get_platform(plat_result) end raise Train::PlatformDetectionFailed, "Sorry, we are unable to detect your platform" end def scan_children(parent) parent.children.each do |plat, condition| next unless instance_eval(&plat.detect) if plat.class == Train::Platforms::Platform return plat if condition.empty? || check_condition(condition) elsif plat.class == Train::Platforms::Family plat = scan_family_children(plat) return plat unless plat.nil? end end nil end def scan_family_children(plat) child_result = scan_children(plat) unless plat.children.nil? return if child_result.nil? @family_hierarchy << plat.name child_result end def check_condition(condition) condition.each do |k, v| op, expected = v.strip.split(" ") op = "==" if op == "=" return false if @platform[k].nil? || !instance_eval("'#{@platform[k]}' #{op} '#{expected}'") end true end def get_platform(plat) plat.backend = @backend plat.platform = @platform plat.add_platform_methods plat.family_hierarchy = @family_hierarchy plat end end end train-3.2.28/lib/train/platforms/detect/specifications/000077500000000000000000000000001364512547200231015ustar00rootroot00000000000000train-3.2.28/lib/train/platforms/detect/specifications/api.rb000066400000000000000000000007361364512547200242050ustar00rootroot00000000000000# encoding: utf-8 module Train::Platforms::Detect::Specifications class Api def self.load plat = Train::Platforms plat.family("api") plat.family("cloud").in_family("api") plat.name("aws").in_family("cloud") plat.name("azure").in_family("cloud") plat.name("gcp").in_family("cloud") plat.name("vmware").in_family("cloud") plat.family("iaas").in_family("api") plat.name("oneview").in_family("iaas") end end end train-3.2.28/lib/train/platforms/detect/specifications/os.rb000066400000000000000000000404061364512547200240530ustar00rootroot00000000000000# rubocop:disable Style/ParenthesesAroundCondition module Train::Platforms::Detect::Specifications class OS def self.load load_toplevel load_windows load_unix load_other end def self.load_toplevel plat.family("os").detect { true } end def self.load_windows declare_category("windows", "os") do (winrm? || local_windows? || detect_windows) # ssh end declare_family("windows", "windows") do detect_windows end end def self.load_unix declare_category("unix", "os") do # we want to catch a special case here where cisco commands # don't return an exit status and still print to stdout uname = unix_uname_s unless (uname.empty? || uname.start_with?("Line has invalid autocommand") || uname.start_with?("The command you have entered")) @platform[:arch] = unix_uname_m true end end load_linux load_other_unix load_bsd end def self.load_linux declare_category("linux", "unix") do unix_uname_s =~ /linux/i end declare_category("debian", "linux") do unix_file_exist?("/etc/debian_version") end declare_lsb("ubuntu", "Ubuntu Linux", "debian", /ubuntu/i) declare_lsb("linuxmint", "LinuxMint", "debian", /linuxmint/i) declare_instance("kali", "Kali Linux", "debian") do l_o_r = linux_os_release if l_o_r && l_o_r["ID"] == "kali" @platform[:release] = l_o_r["VERSION"] true end end declare_instance("raspbian", "Raspbian Linux", "debian") do rel = linux_os_release if (rel && rel["NAME"] =~ /raspbian/i) || unix_file_exist?("/usr/bin/raspi-config") @platform[:release] = unix_file_contents("/etc/debian_version").chomp true end end declare_instance("debian", "Debian Linux", "debian") do # if we get this far we have to be some type of debian @platform[:release] = unix_file_contents("/etc/debian_version").chomp true end declare_category("fedora", "linux") do rel = linux_os_release rel && rel["NAME"] =~ /fedora/i end declare_instance("fedora", "Fedora", "fedora") do @platform[:release] = linux_os_release["VERSION_ID"] true end # must come before redhat as it uses fedora under the hood plat.family("arista_eos").title("Arista EOS Family").in_family("linux") .detect do true end declare_instance("arista_eos_bash", "Arista EOS Bash Shell", "arista_eos") do # this checks for the arista bash shell if unix_file_exist?("/usr/bin/FastCli") # TODO: no tests json_cmd('FastCli -p 15 -c "show version | json"') end end declare_category("redhat", "linux") do true end declare_instance("centos", "Centos Linux", "redhat") do lsb = read_linux_lsb if lsb && lsb[:id] =~ /centos/i # TODO: no tests @platform[:release] = lsb[:release] true elsif (rel = linux_os_release) && rel["NAME"] =~ /centos/i @platform[:release] = redhatish_version(unix_file_contents("/etc/redhat-release")) true end end declare_instance("oracle", "Oracle Linux", "redhat") do (redhatish("/etc/oracle-release") || redhatish("/etc/enterprise-release")) end declare_lsb("scientific", "Scientific Linux", "redhat", /scientific/i) declare_lsb("xenserver", "Xenserer Linux", "redhat", /xenserver/i) declare_instance("parallels-release", "Parallels Linux", "redhat") do if (raw = unix_file_contents("/etc/parallels-release")) @platform[:name] = redhatish_platform(raw) @platform[:release] = raw[/(\d\.\d\.\d)/, 1] # TODO: no tests true end end declare_instance("wrlinux", "Wind River Linux", "redhat") do rel = linux_os_release if rel && rel["ID_LIKE"] =~ /wrlinux/i @platform[:release] = rel["VERSION"] true end end declare_lsb_or_content("amazon", "Amazon Linux", "redhat", "/etc/system-release", /amazon/i) declare_lsb_or_content("cloudlinux", "CloudLinux", "redhat", "/etc/redhat-release", /cloudlinux/i) # keep redhat at the end as a fallback for anything with a redhat-release declare_lsb_or_content("redhat", "Red Hat Linux", "redhat", "/etc/redhat-release", /redhat/i, /./) declare_category("suse", "linux") do rel = linux_os_release if rel && rel["ID_LIKE"] =~ /suse/i @platform[:release] = rel["VERSION"] true elsif (suse = unix_file_contents("/etc/SuSE-release")) # https://rubular.com/r/UKaYWolCYFMfp1 version = suse.scan(/VERSION = (\d+)\nPATCHLEVEL = (\d+)/).flatten.join(".") # https://rubular.com/r/b5PN3hZDxa5amV version = suse[/VERSION\s?=\s?"?([\d\.]{2,})"?/, 1] if version == "" @platform[:release] = version true end end declare_os_or_file("opensuse", "OpenSUSE Linux", "suse", "/etc/SuSE-release", /^opensuse/i) declare_os_or_file("suse", "Suse Linux", "suse", "/etc/SuSE-release", /^sles/i, /suse/i) declare_path_and_uname("arch", "Arch Linux", "linux", "/etc/arch-release") declare_file_content("slackware", "Slackware Linux", "linux", "/etc/slackware-version") declare_file_content("gentoo", "Gentoo Linux", "linux", "/etc/gentoo-release") declare_path_and_uname("exherbo", "Exherbo Linux", "linux", "/etc/exherbo-release") # TODO: try using declare_path declare_instance("alpine", "Alpine Linux", "linux") do if (raw = unix_file_contents("/etc/alpine-release")) @platform[:release] = raw.strip # TODO: no tests true end end declare_instance("coreos", "CoreOS Linux", "linux") do if unix_file_exist?("/etc/coreos/update.conf") lsb = read_linux_lsb @platform[:release] = lsb[:release] true end end declare_category("yocto", "linux") do # /etc/issue isn't specific to yocto, but it's the only way to detect # the platform because there are no other identifying files issue = unix_file_contents("/etc/issue") issue && issue.match?(/Poky|balenaOS/) end declare_instance("yocto", "Yocto Project Linux", "yocto") do issue = unix_file_contents("/etc/issue") if issue.match?("Poky") # assuming the Poky version is preferred over the /etc/version build @platform[:release] = issue[/\d+(\.\d+)+/] true end end declare_instance("balenaos", "balenaOS Linux", "yocto") do # balenaOS does have the /etc/os-release file issue = unix_file_contents("/etc/issue") if issue.match?("balenaOS") && linux_os_release["NAME"] =~ /balenaos/i @platform[:release] = linux_os_release["META_BALENA_VERSION"] true end end # brocade family detected here if device responds to 'uname' command, # happens when logging in as root plat.family("brocade").title("Brocade Family").in_family("linux") .detect do # TODO: no tests brocade_version end # this should always be last in the linux family list declare_instance("linux", "Generic Linux", "linux") do true end end def self.load_other_unix declare_instance("openvms", "OpenVMS", "unix") do if unix_uname_s =~ /unrecognized command verb/i # TODO: no tests cmd = @backend.run_command("show system/noprocess") if cmd.exit_status == 0 && !cmd.stdout.empty? # TODO: no tests @platform[:name] = cmd.stdout.downcase.split(" ")[0] cmd = @backend.run_command('write sys$output f$getsyi("VERSION")') @platform[:release] = cmd.stdout.downcase.split("\n")[1][1..-1] cmd = @backend.run_command('write sys$output f$getsyi("ARCH_NAME")') @platform[:arch] = cmd.stdout.downcase.split("\n")[1] true end end end declare_category("aix", "unix") do unix_uname_s =~ /aix/i end declare_instance("aix", "Aix", "aix") do out = @backend.run_command("uname -rvp").stdout if out =~ /(\d+)\s+(\d+)\s+(.*)/ # TODO: no tests @platform[:release] = "#{$2}.#{$1}" @platform[:arch] = "#{$3}" end true end declare_category("solaris", "unix") do if unix_uname_s =~ /sunos/i # TODO: no tests @platform[:release] = $1 if unix_uname_r =~ /^5\.(\d+)$/ arch = @backend.run_command("uname -p") @platform[:arch] = arch.stdout.chomp if arch.exit_status == 0 true end end # TODO: these regexps are probably needlessly wasteful declare_path_regexp("smartos", "SmartOS", "solaris", "/etc/release", /^.*(SmartOS).*$/) declare_path("omnios", "Omnios", "solaris", "/etc/release", /^\s*OmniOS.*r(\d+).*$/) declare_path("openindiana", "Openindiana", "solaris", "/etc/release", /^\s*OpenIndiana.*oi_(\d+).*$/) declare_path("opensolaris", "Open Solaris", "solaris", "/etc/release", /^\s*OpenSolaris.*snv_(\d+).*$/) # TODO: these regexps are probably needlessly wasteful declare_path_regexp("nexentacore", "Nexentacore", "solaris", "/etc/release", /^\s*(NexentaCore)\s.*$/) declare_instance("solaris", "Solaris", "solaris") do rel = unix_file_contents("/etc/release") if rel =~ /Oracle Solaris (\d+)/ @platform[:release] = $1 # TODO: no tests true elsif rel =~ /^\s*(Solaris)\s.*$/ # TODO: no tests true end # TODO: no tests # must be some unknown solaris true end declare_category("hpux", "unix") do unix_uname_s =~ /hp-ux/i end declare_instance("hpux", "Hpux", "hpux") do @platform[:release] = unix_uname_r.lines[0].chomp # TODO: no tests true end declare_category("qnx", "unix") do unix_uname_s =~ /qnx/i end declare_instance("qnx", "QNX", "qnx") do # TODO: refactor these uname patterns @platform[:name] = unix_uname_s.lines[0].chomp.downcase @platform[:release] = unix_uname_r.lines[0].chomp @platform[:arch] = unix_uname_m true end end def self.load_bsd declare_category("bsd", "unix") do # we need a better way to determine this family # for now we are going to just try each platform true end declare_category("darwin", "bsd") do # rubocop:disable Layout/ExtraSpacing # rubocop:disable Layout/SpaceAroundOperators if unix_uname_s =~ /darwin/i @platform[:release] ||= unix_uname_r.lines[0].chomp @platform[:arch] = unix_uname_m cmd = @backend.run_command("sw_vers -buildVersion") @platform[:build] = cmd.stdout.chomp if cmd.exit_status == 0 true end # rubocop:enable Layout/ExtraSpacing # rubocop:enable Layout/SpaceAroundOperators end declare_instance("mac_os_x", "macOS X", "darwin") do cmd = unix_file_contents("/System/Library/CoreServices/SystemVersion.plist") @platform[:uuid_command] = "system_profiler SPHardwareDataType | awk '/UUID/ { print $3; }'" cmd =~ /Mac OS X/i end declare_instance("darwin", "Darwin", "darwin") do # must be some other type of darwin @platform[:name] = unix_uname_s.lines[0].chomp true end declare_bsd("freebsd", "Freebsd", "bsd", /freebsd/i) declare_bsd("openbsd", "Openbsd", "bsd", /openbsd/i) declare_bsd("netbsd", "Netbsd", "bsd", /netbsd/i) end def self.load_other plat.family("arista_eos").title("Arista EOS Family").in_family("os") .detect do true end declare_instance("arista_eos", "Arista EOS", "arista_eos") do json_cmd("show version | json") end plat.family("esx").title("ESXi Family").in_family("os") .detect do unix_uname_s =~ /vmkernel/i end plat.name("vmkernel").in_family("esx") .detect do # TODO: no tests set_from_uname end plat.family("cisco").title("Cisco Family").in_family("os") .detect do cisco_show_version end declare_cisco("cisco_ios", "Cisco IOS", "cisco", :cisco_show_version, "ios") declare_cisco("cisco_ios_xe", "Cisco IOS XE", "cisco", :cisco_show_version, "ios-xe") declare_cisco("cisco_nexus", "Cisco Nexus", "cisco", :cisco_show_version, "nexus", "show version | include Processor") plat.family("brocade").title("Brocade Family").in_family("os") .detect do brocade_version end declare_cisco("brocade_fos", "Brocade FOS", "brocade", :brocade_version, "fos") end ###################################################################### # Helpers (keep these sorted) def self.plat Train::Platforms end def self.declare_category(family, parent, &block) plat.family(family).in_family(parent).detect(&block) end def self.declare_family(name, title = nil, family, &block) thingy = plat.name(name).in_family(family) thingy.title(title) if title thingy.detect(&block) end def self.declare_instance(name, title, family, &block) plat.name(name).title(title).in_family(family).detect(&block) end def self.declare_bsd(name, title, family, regexp) declare_instance(name, title, family) do # TODO: no tests set_from_uname if unix_uname_s =~ regexp end end def self.declare_cisco(name, title, family, detect, type, uuid = nil) declare_instance(name, title, family) do v = send(detect) next unless v[:type] == type @platform[:release] = v[:version] @platform[:arch] = nil @platform[:uuid_command] = uuid if uuid true end end def self.declare_file_content(name, title, family, path) declare_instance(name, title, family) do if (raw = unix_file_contents(path)) # TODO: no tests @platform[:release] = raw.scan(/[\d.]+/).join true end end end def self.declare_lsb(name, title, family, regexp) declare_lsb_or_content(name, title, family, nil, regexp) end def self.declare_lsb_or_content(name, title, family, path, regexp1, regexp2 = regexp1) declare_instance(name, title, family) do lsb = read_linux_lsb if lsb && lsb[:id] =~ regexp1 @platform[:release] = lsb[:release] true elsif path && (raw = unix_file_contents(path)) =~ regexp2 @platform[:name] = redhatish_platform(raw) @platform[:release] = redhatish_version(raw) true end end end def self.declare_os_or_file(name, title, family, path, regexp1, regexp2 = regexp1) declare_instance(name, title, family) do rel = linux_os_release (rel && rel["NAME"] =~ regexp1) || unix_file_contents(path) =~ regexp2 end end def self.declare_path(name, title, family, path, regexp) declare_instance(name, title, family) do rel = unix_file_contents(path) if rel =~ regexp # TODO: no tests @platform[:release] = $1 true end end end def self.declare_path_and_uname(name, title, family, path) declare_instance(name, title, family) do if unix_file_exist?(path) # Because this is a rolling release distribution, # use the kernel release, ex. 4.1.6-1-ARCH # TODO: unix_uname_r.lines[0].chomp ? -- no tests for /etc/exherbo-release or /etc/arch-release @platform[:release] = unix_uname_r true end end end def self.declare_path_regexp(name, title, family, path, regexp) declare_instance(name, title, family) do # TODO: no tests regexp =~ unix_file_contents(path) end end end end train-3.2.28/lib/train/platforms/detect/uuid.rb000066400000000000000000000016051364512547200213730ustar00rootroot00000000000000require "digest/sha1" require "securerandom" require "json" module Train::Platforms::Detect class UUID include Train::Platforms::Detect::Helpers::OSCommon def initialize(platform) @platform = platform @backend = @platform.backend end def find_or_create_uuid # for api transports uuid is defined on the connection if defined?(@backend.unique_identifier) uuid_from_string(@backend.unique_identifier) elsif @platform.unix? unix_uuid elsif @platform.windows? windows_uuid else if @platform[:uuid_command] result = @backend.run_command(@platform[:uuid_command]) return uuid_from_string(result.stdout.chomp) if result.exit_status == 0 && !result.stdout.empty? end raise "Could not find platform uuid! Please set a uuid_command for your platform." end end end end train-3.2.28/lib/train/platforms/family.rb000066400000000000000000000011561364512547200204370ustar00rootroot00000000000000# encoding: utf-8 module Train::Platforms class Family include Train::Platforms::Common attr_accessor :children, :condition, :families, :name def initialize(name, condition) @name = name @condition = condition @families = {} @children = {} @detect = nil @title = "#{name.to_s.capitalize} Family" # add itself to the families list Train::Platforms.families[@name.to_s] = self end def title(title = nil) return @title if title.nil? @title = title self end def inspect "%p[%s]" % [self.class, name] end end end train-3.2.28/lib/train/platforms/platform.rb000066400000000000000000000047201364512547200210020ustar00rootroot00000000000000# encoding: utf-8 module Train::Platforms class Platform include Train::Platforms::Common attr_accessor :backend, :condition, :families, :family_hierarchy, :platform def initialize(name, condition = {}) @name = name @condition = condition @families = {} @family_hierarchy = [] @platform = {} @detect = nil @title = name.to_s.capitalize # add itself to the platform list Train::Platforms.list[name] = self end def direct_families @families.collect { |k, _v| k.name } end def find_family_hierarchy(platform = self) families = platform.families.map { |k, v| [k.name, find_family_hierarchy(k)] } @family_hierarchy = families.flatten end def family @platform[:family] || @family_hierarchy[0] end def name # Override here incase a updated name was set # during the detect logic clean_name end def clean_name(force: false) @cleaned_name = nil if force @cleaned_name ||= (@platform[:name] || @name).downcase.tr(" ", "_") end def uuid @uuid ||= Train::Platforms::Detect::UUID.new(self).find_or_create_uuid.downcase end # This is for backwords compatability with # the current inspec os resource. def[](name) if respond_to?(name) send(name) else "unknown" end end def title(title = nil) return @title if title.nil? @title = title self end def to_hash @platform end # Add generic family? and platform methods to an existing platform # # This is done later to add any custom # families/properties that were created def add_platform_methods # Redo clean name if there is a detect override clean_name(force: true) unless @platform[:name].nil? # Add in family methods family_list = Train::Platforms.families family_list.each_value do |k| name = "#{k.name}?" next if respond_to?(name) define_singleton_method(name) do family_hierarchy.include?(k.name) end end # Helper methods for direct platform info @platform.each_key do |m| next if respond_to?(m) define_singleton_method(m) do @platform[m] end end # Create method for name if its not already true m = name + "?" return if respond_to?(m) define_singleton_method(m) do true end end end end train-3.2.28/lib/train/plugin_test_helper.rb000066400000000000000000000033201364512547200210360ustar00rootroot00000000000000# This file is intended to be 'require'd by plugin authors who are developing a # plugin outside of the Train source tree. # Load Train. We certainly need the plugin system, and also several other parts # that are tightly coupled. Train itself is fairly light, and non-invasive. require_relative "../train" # You can select from a number of test harnesses. Since Train is closely related # to InSpec, and InSpec uses Spec-style controls in profile code, you will # probably want to use something like minitest/spec, which provides Spec-style # tests. require "minitest/spec" require "minitest/autorun" # Data formats commonly used in testing require "json" require "ostruct" # Utilities often needed require "fileutils" require "tmpdir" require "pathname" # You might want to put some debugging tools here. We run tests to find bugs, # after all. require "byebug" # Configure MiniTest to expose things like `let` class Module include Minitest::Spec::DSL end # Finally, let's make some modules that can help us out. module TrainPluginBaseHelper # Sneakily detect the location of the plugin # source code when they include this Module def self.included(base) plugin_test_helper_path = Pathname.new(caller_locations(4, 1).first.absolute_path) plugin_src_root = plugin_test_helper_path.parent.parent base.let(:plugin_src_path) { plugin_src_root } base.let(:plugin_fixtures_path) { File.join(plugin_src_root, "test", "fixtures") } end let(:train_src_path) { File.expand_path(File.join(__FILE__, "..", "..")) } let(:train_fixtures_path) { File.join(train_src_path, "test", "fixtures") } let(:registry) { Train::Plugins.registry } end module TrainPluginFunctionalHelper include TrainPluginBaseHelper end train-3.2.28/lib/train/plugins.rb000066400000000000000000000023271364512547200166310ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Dominik Richter () # Author:: Christoph Hartmann () require_relative "errors" module Train class Plugins require_relative "plugins/transport" class << self # Retrieve the current plugin registry, containing all plugin names # and their transport handlers. # # @return [Hash] map with plugin names and plugins def registry @registry ||= {} end end end # Create a new plugin by inheriting from the class returned by this method. # Create a versioned plugin by providing the transport layer plugin version # to this method. It will then select the correct class to inherit from. # # The plugin version determins what methods will be available to your plugin. # # @param [Int] version = 1 the plugin version to use # @return [Transport] the versioned transport base class def self.plugin(version = 1) if version != 1 raise ClientError, "Only understand train plugin version 1. You are trying to "\ "initialize a train plugin #{version}, which is not supported "\ "in the current release of train." end ::Train::Plugins::Transport end end train-3.2.28/lib/train/plugins/000077500000000000000000000000001364512547200163005ustar00rootroot00000000000000train-3.2.28/lib/train/plugins/base_connection.rb000066400000000000000000000146461364512547200217710ustar00rootroot00000000000000# encoding: utf-8 require_relative "../errors" require_relative "../extras" require_relative "../file" require "logger" class Train::Plugins::Transport # A Connection instance can be generated and re-generated, given new # connection details such as connection port, hostname, credentials, etc. # This object is responsible for carrying out the actions on the remote # host such as executing commands, transferring files, etc. # # @author Fletcher Nichol class BaseConnection include Train::Extras # Create a new Connection instance. # # @param options [Hash] connection options # @yield [self] yields itself for block-style invocation def initialize(options = nil) @options = options || {} @logger = @options.delete(:logger) || Logger.new($stdout, level: :fatal) Train::Platforms::Detect::Specifications::OS.load Train::Platforms::Detect::Specifications::Api.load # default caching options @cache_enabled = { file: true, command: false, api_call: false, } @cache = {} @cache_enabled.each_key do |type| clear_cache(type) end end def with_sudo_pty yield end # Returns cached client if caching enabled. Otherwise returns whatever is # given in the block. # # @example # # def demo_client # cached_client(:api_call, :demo_connection) do # DemoClient.new(args) # end # end # # @param [symbol] type one of [:api_call, :file, :command] # @param [symbol] key for your cached connection def cached_client(type, key) return yield unless cache_enabled?(type) @cache[type][key] ||= yield end def cache_enabled?(type) @cache_enabled[type.to_sym] end # Enable caching types for Train. Currently we support # :api_call, :file and :command types def enable_cache(type) raise Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym) @cache_enabled[type.to_sym] = true end def disable_cache(type) raise Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym) @cache_enabled[type.to_sym] = false clear_cache(type.to_sym) end # Closes the session connection, if it is still active. def close # this method may be left unimplemented if that is applicable end def to_json { "files" => Hash[@cache[:file].map { |x, y| [x, y.to_json] }], } end def load_json(j) require_relative "../transports/mock" j["files"].each do |path, jf| @cache[:file][path] = Train::Transports::Mock::Connection::File.from_json(jf) end end def force_platform!(name, platform_details = nil) plat = Train::Platforms.name(name) plat.backend = self plat.platform = platform_details unless platform_details.nil? plat.find_family_hierarchy plat.add_platform_methods plat end def backend_type @options[:backend] || "unknown" end def inspect "%s[%s]" % [self.class, backend_type] end alias direct_platform force_platform! # Get information on the operating system which this transport connects to. # # @return [Platform] system information def platform @platform ||= Train::Platforms::Detect.scan(self) end # we need to keep os as a method for backwards compatibility with inspec alias os platform # This is the main command call for all connections. This will call the private # run_command_via_connection on the connection with optional caching # # This command accepts an optional data handler block. When provided, # inbound data will be published vi `data_handler.call(data)`. This can allow # callers to receive and render updates from remote command execution. def run_command(cmd, &data_handler) return run_command_via_connection(cmd, &data_handler) unless cache_enabled?(:command) @cache[:command][cmd] ||= run_command_via_connection(cmd, &data_handler) end # This is the main file call for all connections. This will call the private # file_via_connection on the connection with optional caching def file(path, *args) return file_via_connection(path, *args) unless cache_enabled?(:file) @cache[:file][path] ||= file_via_connection(path, *args) end # Builds a LoginCommand which can be used to open an interactive # session on the remote host. # # @return [LoginCommand] array of command line tokens def login_command raise NotImplementedError, "#{self.class} does not implement #login_command()" end # Block and return only when the remote host is prepared and ready to # execute command and upload files. The semantics and details will # vary by implementation, but a round trip through the hosted # service is preferred to simply waiting on a socket to become # available. def wait_until_ready # this method may be left unimplemented if that is applicablelog end private # Execute a command using this connection. # # @param command [String] command string to execute # @param &data_handler(data) [Proc] proc that is called when data arrives from # the connection. This block is optional. Individual transports will need # to explicitly invoke the block in their implementation of run_command_via_connection; # if they do not, the block is ignored and will not be used to report data back to the caller. # # @return [CommandResult] contains the result of running the command def run_command_via_connection(_command, &_data_handler) raise NotImplementedError, "#{self.class} does not implement #run_command_via_connection()" end # Interact with files on the target. Read, write, and get metadata # from files via the transport. # # @param [String] path which is being inspected # @return [FileCommon] file object that allows for interaction def file_via_connection(_path, *_args) raise NotImplementedError, "#{self.class} does not implement #file_via_connection(...)" end def clear_cache(type) @cache[type.to_sym] = {} end # @return [Logger] logger for reporting information # @api private attr_reader :logger # @return [Hash] connection options # @api private attr_reader :options end end train-3.2.28/lib/train/plugins/transport.rb000066400000000000000000000026101364512547200206600ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Dominik Richter () # Author:: Christoph Hartmann () require "logger" require_relative "../errors" require_relative "../extras" require_relative "../options" class Train::Plugins class Transport include Train::Extras Train::Options.attach(self) require_relative "base_connection" # Initialize a new Transport object # # @param [Hash] config = nil the configuration for this transport # @return [Transport] the transport object def initialize(options = {}) @options = merge_options({}, options || {}) @logger = @options[:logger] || Logger.new($stdout, level: :fatal) end # Create a connection to the target. Options may be provided # for additional configuration. # # @param [Hash] _options = nil provide optional configuration params # @return [Connection] the connection for this configuration def connection(_options = nil) raise Train::ClientError, "#{self.class} does not implement #connection()" end # Register the inheriting class with as a train plugin using the # provided name. # # @param [String] name of the plugin, by which it will be found def self.name(name) Train::Plugins.registry[name] = self end private # @return [Logger] logger for reporting information attr_reader :logger end end train-3.2.28/lib/train/transports/000077500000000000000000000000001364512547200170365ustar00rootroot00000000000000train-3.2.28/lib/train/transports/azure.rb000066400000000000000000000146031364512547200205150ustar00rootroot00000000000000# encoding: utf-8 require "train/plugins" require "ms_rest_azure" require "azure_mgmt_resources" require "azure_graph_rbac" require "azure_mgmt_key_vault" require "socket" require "timeout" require "train/transports/helpers/azure/file_credentials" require "train/transports/clients/azure/graph_rbac" require "train/transports/clients/azure/vault" module Train::Transports class Azure < Train.plugin(1) name "azure" option :tenant_id, default: ENV["AZURE_TENANT_ID"] option :client_id, default: ENV["AZURE_CLIENT_ID"] option :client_secret, default: ENV["AZURE_CLIENT_SECRET"] option :subscription_id, default: ENV["AZURE_SUBSCRIPTION_ID"] option :msi_port, default: ENV["AZURE_MSI_PORT"] || "50342" # This can provide the client id and secret option :credentials_file, default: ENV["AZURE_CRED_FILE"] def connection(_ = nil) @connection ||= Connection.new(@options) end class Connection < BaseConnection attr_reader :options DEFAULT_FILE = ::File.join(Dir.home, ".azure", "credentials") def initialize(options) @apis = {} # Override for any cli options # azure://subscription_id options[:subscription_id] = options[:host] || options[:subscription_id] super(options) @cache_enabled[:api_call] = true @cache[:api_call] = {} if @options[:client_secret].nil? && @options[:client_id].nil? options[:credentials_file] = DEFAULT_FILE if options[:credentials_file].nil? @options.merge!(Helpers::Azure::FileCredentials.parse(**@options)) end @options[:msi_port] = @options[:msi_port].to_i unless @options[:msi_port].nil? # additional platform details release = Gem.loaded_specs["azure_mgmt_resources"].version @platform_details = { release: "azure_mgmt_resources-v#{release}" } connect end def platform force_platform!("azure", @platform_details) end def azure_client(klass = ::Azure::Resources::Profiles::Latest::Mgmt::Client, opts = {}) if cache_enabled?(:api_call) return @cache[:api_call][klass.to_s.to_sym] unless @cache[:api_call][klass.to_s.to_sym].nil? end if klass == ::Azure::Resources::Profiles::Latest::Mgmt::Client @credentials[:base_url] = MsRestAzure::AzureEnvironments::AzureCloud.resource_manager_endpoint_url elsif klass == ::Azure::GraphRbac::Profiles::Latest::Client client = GraphRbac.client(@credentials) elsif klass == ::Azure::KeyVault::Profiles::Latest::Mgmt::Client client = Vault.client(opts[:vault_name], @credentials) end client ||= klass.new(@credentials) # Cache if enabled @cache[:api_call][klass.to_s.to_sym] ||= client if cache_enabled?(:api_call) client end def connect if msi_auth? # this needs set for azure cloud to authenticate ENV["MSI_VM"] = "true" provider = ::MsRestAzure::MSITokenProvider.new(@options[:msi_port]) else provider = ::MsRestAzure::ApplicationTokenProvider.new( @options[:tenant_id], @options[:client_id], @options[:client_secret] ) end @credentials = { credentials: ::MsRest::TokenCredentials.new(provider), subscription_id: @options[:subscription_id], tenant_id: @options[:tenant_id], } @credentials[:client_id] = @options[:client_id] unless @options[:client_id].nil? @credentials[:client_secret] = @options[:client_secret] unless @options[:client_secret].nil? end def uri "azure://#{@options[:subscription_id]}" end # Returns the api version for the specified resource type # # If an api version has been specified in the options then the apis version table is updated # with that value and it is returned # # However if it is not specified, or multiple types are being interrogated then this method # will interrogate Azure for each of the types versions and pick the latest one. This is added # to the apis table so that it can be retrieved quickly again of another one of those resources # is encountered again in the resource collection. # # @param string resource_type The resource type for which the API is required # @param hash options Options have that have been passed to the resource during the test. # @option opts [String] :group_name Resource group name # @option opts [String] :type Azure resource type # @option opts [String] :name Name of specific resource to look for # @option opts [String] :apiversion If looking for a specific item or type specify the api version to use # # @return string API Version of the specified resource type def get_api_version(resource_type, options) # if an api version has been set in the options, add to the apis hashtable with # the resource type if options[:apiversion] @apis[resource_type] = options[:apiversion] else # only attempt to get the api version from Azure if the resource type # is not present in the apis hashtable unless @apis.key?(resource_type) # determine the namespace for the resource type namespace, type = resource_type.split(%r{/}) client = azure_client(::Azure::Resources::Profiles::Latest::Mgmt::Client) provider = client.providers.get(namespace) # get the latest API version for the type # assuming that this is the first one in the list api_versions = (provider.resource_types.find { |v| v.resource_type == type }).api_versions @apis[resource_type] = api_versions[0] end end # return the api version for the type @apis[resource_type] end def unique_identifier options[:subscription_id] || options[:tenant_id] end def msi_auth? @options[:client_id].nil? && @options[:client_secret].nil? && port_open?(@options[:msi_port]) end private def port_open?(port, seconds = 3) Timeout.timeout(seconds) do begin TCPSocket.new("localhost", port).close true rescue SystemCallError false end end rescue Timeout::Error false end end end end train-3.2.28/lib/train/transports/cisco_ios_connection.rb000066400000000000000000000075031364512547200235610ustar00rootroot00000000000000# encoding: utf-8 class Train::Transports::SSH class CiscoIOSConnection < BaseConnection class BadEnablePassword < Train::TransportError; end def initialize(options) super(options) # Extract options to avoid passing them in to `Net::SSH.start` later @host = options.delete(:host) @user = options.delete(:user) @port = options.delete(:port) @enable_password = options.delete(:enable_password) # Use all options left that are not `nil` for `Net::SSH.start` later @ssh_options = options.reject { |_key, value| value.nil? } @prompt = /^\S+[>#]\r\n.*$/ end def uri "ssh://#{@user}@#{@host}:#{@port}" end def unique_identifier result = run_command_via_connection("show version | include Processor") result.stdout.split(" ")[-1] end private def establish_connection logger.debug("[SSH] opening connection to #{self}") Net::SSH.start(@host, @user, @ssh_options) end def session return @session unless @session.nil? @session = open_channel(establish_connection) # Escalate privilege to enable mode if password is given if @enable_password # This verifies we are not in privileged exec mode before running the # enable command. Otherwise, the password will be in history. if run_command_via_connection("show privilege").stdout.split[-1] != "15" # Extra newlines to get back to prompt if incorrect password is used run_command_via_connection("enable\n#{@enable_password}\n\n\n") end end # Prevent `--MORE--` by removing terminal length limit run_command_via_connection("terminal length 0") @session end def run_command_via_connection(cmd, &_data_handler) # Ensure buffer is empty before sending data @buf = "" logger.debug("[SSH] Running `#{cmd}` on #{self}") session.send_data(cmd + "\r\n") logger.debug("[SSH] waiting for prompt") until @buf =~ @prompt if @buf =~ /Bad (secrets|password)|Access denied/ raise BadEnablePassword end session.connection.process(0) end # Save the buffer and clear it for the next command output = @buf.dup @buf = "" format_result(format_output(output, cmd)) end ERROR_MATCHERS = [ "Bad IP address", "Incomplete command", "Invalid input detected", "Unrecognized host", ].freeze # IOS commands do not have an exit code so we must compare the command # output with partial segments of known errors. Then, we return a # `CommandResult` with arguments in the correct position based on the # result. def format_result(result) if ERROR_MATCHERS.none? { |e| result.include?(e) } CommandResult.new(result, "", 0) else CommandResult.new("", result, 1) end end # The buffer (@buf) contains all data sent/received on the SSH channel so # we need to format the data to match what we would expect from Train def format_output(output, cmd) leading_prompt = /(\r\n|^)\S+[>#]/ command_string = /#{Regexp.quote(cmd)}\r\n/ trailing_prompt = /\S+[>#](\r\n|$)/ trailing_line_endings = /(\r\n)+$/ output .sub(leading_prompt, "") .sub(command_string, "") .gsub(trailing_prompt, "") .gsub(trailing_line_endings, "") end # Create an SSH channel that writes to @buf when data is received def open_channel(ssh) logger.debug("[SSH] opening SSH channel to #{self}") ssh.open_channel do |ch| ch.on_data do |_, data| @buf += data end ch.send_channel_request("shell") do |_, success| raise "Failed to open SSH shell" unless success logger.debug("[SSH] shell opened") end end end end end train-3.2.28/lib/train/transports/clients/000077500000000000000000000000001364512547200204775ustar00rootroot00000000000000train-3.2.28/lib/train/transports/clients/azure/000077500000000000000000000000001364512547200216255ustar00rootroot00000000000000train-3.2.28/lib/train/transports/clients/azure/graph_rbac.rb000066400000000000000000000022211364512547200242370ustar00rootroot00000000000000# encoding: utf-8 require "azure_graph_rbac" # Wrapper class for ::Azure::GraphRbac::Profiles::Latest::Client allowing custom configuration, # for example, defining additional settings for the ::MsRestAzure::ApplicationTokenProvider. class GraphRbac AUTH_ENDPOINT = MsRestAzure::AzureEnvironments::AzureCloud.active_directory_endpoint_url API_ENDPOINT = MsRestAzure::AzureEnvironments::AzureCloud.active_directory_graph_resource_id def self.client(credentials) credentials[:credentials] = ::MsRest::TokenCredentials.new(provider(credentials)) credentials[:base_url] = API_ENDPOINT ::Azure::GraphRbac::Profiles::Latest::Client.new(credentials) end def self.provider(credentials) ::MsRestAzure::ApplicationTokenProvider.new( credentials[:tenant_id], credentials[:client_id], credentials[:client_secret], settings ) end def self.settings client_settings = MsRestAzure::ActiveDirectoryServiceSettings.get_azure_settings client_settings.authentication_endpoint = AUTH_ENDPOINT client_settings.token_audience = API_ENDPOINT client_settings end private_class_method :provider, :settings end train-3.2.28/lib/train/transports/clients/azure/vault.rb000066400000000000000000000025651364512547200233150ustar00rootroot00000000000000# encoding: utf-8 require "azure_mgmt_key_vault" # Wrapper class for ::Azure::KeyVault::Profiles::Latest::Mgmt::Client allowing custom configuration, # for example, defining additional settings for the ::MsRestAzure::ApplicationTokenProvider. class Vault AUTH_ENDPOINT = MsRestAzure::AzureEnvironments::AzureCloud.active_directory_endpoint_url RESOURCE_ENDPOINT = "https://vault.azure.net".freeze def self.client(vault_name, credentials) raise ::Train::UserError, "Vault Name cannot be nil" if vault_name.nil? credentials[:credentials] = ::MsRest::TokenCredentials.new(provider(credentials)) credentials[:base_url] = api_endpoint(vault_name) ::Azure::KeyVault::Profiles::Latest::Mgmt::Client.new(credentials) end def self.provider(credentials) ::MsRestAzure::ApplicationTokenProvider.new( credentials[:tenant_id], credentials[:client_id], credentials[:client_secret], settings ) end def self.api_endpoint(vault_name) "https://#{vault_name}#{MsRestAzure::AzureEnvironments::AzureCloud.key_vault_dns_suffix}" end def self.settings client_settings = MsRestAzure::ActiveDirectoryServiceSettings.get_azure_settings client_settings.authentication_endpoint = AUTH_ENDPOINT client_settings.token_audience = RESOURCE_ENDPOINT client_settings end private_class_method :provider, :api_endpoint, :settings end train-3.2.28/lib/train/transports/docker.rb000066400000000000000000000047341364512547200206420ustar00rootroot00000000000000require "docker" module Train::Transports class Docker < Train.plugin(1) name "docker" include_options Train::Extras::CommandWrapper option :host, required: true def connection(state = {}, &block) opts = merge_options(options, state || {}) validate_options(opts) if @connection && @connection_options == opts reuse_connection(&block) else create_new_connection(opts, &block) end end private # Creates a new Docker connection instance and save it for potential future # reuse. # # @param options [Hash] connection options # @return [Docker::Connection] a Docker connection instance # @api private def create_new_connection(options, &block) if @connection logger.debug("[Docker] shutting previous connection #{@connection}") @connection.close end @connection_options = options @connection = Connection.new(options, &block) end # Return the last saved Docker connection instance. # # @return [Docker::Connection] a Docker connection instance # @api private def reuse_connection logger.debug("[Docker] reusing existing connection #{@connection}") yield @connection if block_given? @connection end end end class Train::Transports::Docker class Connection < BaseConnection def initialize(conf) super(conf) @id = options[:host] @container = ::Docker::Container.get(@id) || raise("Can't find Docker container #{@id}") @cmd_wrapper = nil @cmd_wrapper = CommandWrapper.load(self, @options) end def close # nothing to do at the moment end def uri if @container.nil? "docker://#{@id}" else "docker://#{@container.id}" end end private def file_via_connection(path) if os.aix? Train::File::Remote::Aix.new(self, path) elsif os.solaris? Train::File::Remote::Unix.new(self, path) else Train::File::Remote::Linux.new(self, path) end end def run_command_via_connection(cmd, &_data_handler) cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil? stdout, stderr, exit_status = @container.exec( [ "/bin/sh", "-c", cmd ] ) CommandResult.new(stdout.join, stderr.join, exit_status) rescue ::Docker::Error::DockerError => _ raise rescue => _ # @TODO: differentiate any other error raise end end end train-3.2.28/lib/train/transports/gcp.rb000066400000000000000000000104271364512547200201400ustar00rootroot00000000000000# encoding: utf-8 require "train/plugins" require "google/apis" require "google/apis/cloudresourcemanager_v1" require "google/apis/compute_v1" require "google/apis/storage_v1" require "google/apis/iam_v1" require "google/apis/admin_directory_v1" require "googleauth" module Train::Transports class Gcp < Train.plugin(1) name "gcp" # GCP will look automatically for the below env var for service accounts etc. : option :google_application_credentials, required: false do ENV["GOOGLE_APPLICATION_CREDENTIALS"] end # see https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application # In the absence of this, the client is expected to have already set up local credentials via: # $ gcloud auth application-default login # $ gcloud config set project # GCP projects can have default regions / zones set, see: # https://cloud.google.com/compute/docs/regions-zones/changing-default-zone-region # can also specify project via env var: option :google_cloud_project, required: false do ENV["GOOGLE_CLOUD_PROJECT"] end option :google_super_admin_email, required: false do ENV["GOOGLE_SUPER_ADMIN_EMAIL"] end def connection(_ = nil) @connection ||= Connection.new(@options) end class Connection < BaseConnection def initialize(options) super(options) # additional GCP platform metadata release = Gem.loaded_specs["google-api-client"].version @platform_details = { release: "google-api-client-v#{release}" } # Initialize the client object cache @cache_enabled[:api_call] = true @cache[:api_call] = {} connect end def platform force_platform!("gcp", @platform_details) end # Instantiate some named classes for ease of use def gcp_compute_client gcp_client(Google::Apis::ComputeV1::ComputeService) end def gcp_iam_client gcp_client(Google::Apis::IamV1::IamService) end def gcp_project_client gcp_client(Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService) end def gcp_storage_client gcp_client(Google::Apis::StorageV1::StorageService) end def gcp_admin_client scopes = ["https://www.googleapis.com/auth/admin.directory.user.readonly"] authorization = Google::Auth.get_application_default(scopes).dup # Use of the Admin API requires delegation (impersonation). An email address of a Super Admin in # the G Suite account may be required. authorization.sub = @options[:google_super_admin_email] if @options[:google_super_admin_email] Google::Apis::RequestOptions.default.authorization = authorization gcp_client(Google::Apis::AdminDirectoryV1::DirectoryService) end # Let's allow for other clients too def gcp_client(klass) return klass.new unless cache_enabled?(:api_call) @cache[:api_call][klass.to_s.to_sym] ||= klass.new end def connect ENV["GOOGLE_APPLICATION_CREDENTIALS"] = @options[:google_application_credentials] if @options[:google_application_credentials] ENV["GOOGLE_CLOUD_PROJECT"] = @options[:google_cloud_project] if @options[:google_cloud_project] # GCP initialization scopes = ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/compute"] authorization = Google::Auth.get_application_default(scopes) Google::Apis::ClientOptions.default.application_name = "chef-inspec-train" Google::Apis::ClientOptions.default.application_version = Train::VERSION Google::Apis::RequestOptions.default.authorization = authorization end def uri "gcp://#{unique_identifier}" end def unique_identifier unique_id = "default" # use auth client_id for users (issuer is nil) unique_id = gcp_iam_client.request_options.authorization.client_id unless gcp_iam_client.request_options.authorization.client_id.nil? # for service account credentials (client_id is nil) unique_id = gcp_iam_client.request_options.authorization.issuer unless gcp_iam_client.request_options.authorization.issuer.nil? unique_id end end end end train-3.2.28/lib/train/transports/helpers/000077500000000000000000000000001364512547200205005ustar00rootroot00000000000000train-3.2.28/lib/train/transports/helpers/azure/000077500000000000000000000000001364512547200216265ustar00rootroot00000000000000train-3.2.28/lib/train/transports/helpers/azure/file_credentials.rb000066400000000000000000000026401364512547200254510ustar00rootroot00000000000000# encoding: utf-8 require "inifile" require_relative "file_parser" require_relative "subscription_number_file_parser" require_relative "subscription_id_file_parser" module Train::Transports module Helpers module Azure class FileCredentials def self.parse(subscription_id: nil, credentials_file: nil, **_) return {} if credentials_file.nil? return {} unless ::File.readable?(credentials_file) credentials = IniFile.load(::File.expand_path(credentials_file)) subscription_id = parser(subscription_id, ENV["AZURE_SUBSCRIPTION_NUMBER"], credentials).subscription_id creds(subscription_id, credentials) end def self.parser(subscription_id, subscription_number, credentials) if subscription_id SubscriptionIdFileParser.new(subscription_id, credentials) elsif !subscription_number.nil? SubscriptionNumberFileParser.new(subscription_number.to_i, credentials) else FileParser.new(credentials) end end def self.creds(subscription_id, credentials) { subscription_id: subscription_id, tenant_id: credentials[subscription_id]["tenant_id"], client_id: credentials[subscription_id]["client_id"], client_secret: credentials[subscription_id]["client_secret"], } end end end end end train-3.2.28/lib/train/transports/helpers/azure/file_parser.rb000066400000000000000000000011121364512547200244410ustar00rootroot00000000000000# encoding: utf-8 module Train::Transports module Helpers module Azure class FileParser def initialize(credentials) @credentials = credentials validate! end def validate! return if @credentials.sections.count == 1 raise "Credentials file must have one entry. Check your credentials file. If you have more than one entry set AZURE_SUBSCRIPTION_ID environment variable." end def subscription_id @subscription_id ||= @credentials.sections[0] end end end end end train-3.2.28/lib/train/transports/helpers/azure/subscription_id_file_parser.rb000066400000000000000000000010701364512547200277240ustar00rootroot00000000000000# encoding: utf-8 module Train::Transports module Helpers module Azure class SubscriptionIdFileParser attr_reader :subscription_id def initialize(subscription_id, credentials) @subscription_id = subscription_id @credentials = credentials validate! end def validate! if @credentials.sections.empty? || @credentials[subscription_id].empty? raise "No credentials found for subscription number #{subscription_id}" end end end end end end train-3.2.28/lib/train/transports/helpers/azure/subscription_number_file_parser.rb000066400000000000000000000013221364512547200306200ustar00rootroot00000000000000# encoding: utf-8 module Train::Transports module Helpers module Azure class SubscriptionNumberFileParser def initialize(index, credentials) @index = index @credentials = credentials validate! end def validate! if @index == 0 raise "Index must be greater than 0." end if @index > @credentials.sections.length raise "Your credentials file only contains #{@credentials.sections.length} subscriptions. You specified number #{@index}." end end def subscription_id @subscription_id ||= @credentials.sections[@index - 1] end end end end end train-3.2.28/lib/train/transports/local.rb000066400000000000000000000170501364512547200204600ustar00rootroot00000000000000# encoding: utf-8 # # author: Dominik Richter # author: Christoph Hartmann require_relative "../plugins" require_relative "../errors" require "mixlib/shellout" module Train::Transports class Local < Train.plugin(1) name "local" class PipeError < Train::TransportError; end def connection(_ = nil) @connection ||= Connection.new(@options) end class Connection < BaseConnection def initialize(options) super(options) @runner = if options[:command_runner] force_runner(options[:command_runner]) else select_runner(options) end end def login_command nil # none, open your shell end def uri "local://" end private def select_runner(options) if os.windows? # Force a 64 bit poweshell if needed if RUBY_PLATFORM == "i386-mingw32" && os.arch == "x86_64" powershell_cmd = "#{ENV["SystemRoot"]}\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe" else powershell_cmd = "powershell" end # Attempt to use a named pipe but fallback to ShellOut if that fails begin WindowsPipeRunner.new(powershell_cmd) rescue PipeError WindowsShellRunner.new(powershell_cmd) end else GenericRunner.new(self, options) end end def force_runner(command_runner) case command_runner when :generic GenericRunner.new(self, options) when :windows_pipe WindowsPipeRunner.new when :windows_shell WindowsShellRunner.new else raise "Runner type `#{command_runner}` not supported" end end def run_command_via_connection(cmd, &_data_handler) # Use the runner if it is available return @runner.run_command(cmd) if defined?(@runner) # If we don't have a runner, such as at the beginning of setting up the # transport and performing the first few steps of OS detection, fall # back to shelling out. res = Mixlib::ShellOut.new(cmd) res.run_command Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus) rescue Errno::ENOENT => _ CommandResult.new("", "", 1) end def file_via_connection(path) if os.windows? Train::File::Local::Windows.new(self, path) else Train::File::Local::Unix.new(self, path) end end class GenericRunner include_options Train::Extras::CommandWrapper def initialize(connection, options) @cmd_wrapper = Local::CommandWrapper.load(connection, options) end def run_command(cmd) if defined?(@cmd_wrapper) && !@cmd_wrapper.nil? cmd = @cmd_wrapper.run(cmd) end res = Mixlib::ShellOut.new(cmd) res.run_command Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus) end end class WindowsShellRunner require "json" require "base64" def initialize(powershell_cmd = "powershell") @powershell_cmd = powershell_cmd end def run_command(script) # Prevent progress stream from leaking into stderr script = "$ProgressPreference='SilentlyContinue';" + script # Encode script so PowerShell can use it script = script.encode("UTF-16LE", "UTF-8") base64_script = Base64.strict_encode64(script) cmd = "#{@powershell_cmd} -NoProfile -EncodedCommand #{base64_script}" res = Mixlib::ShellOut.new(cmd) res.run_command Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus) end end class WindowsPipeRunner require "json" require "base64" require "securerandom" def initialize(powershell_cmd = "powershell") @powershell_cmd = powershell_cmd @pipe = acquire_pipe raise PipeError if @pipe.nil? end # @param cmd The command to execute # @return Local::ComandResult with stdout, stderr and exitstatus # Note that exitstatus ($?) in PowerShell is boolean, but we use a numeric exit code. # A command that succeeds without setting an exit code will have exitstatus 0 # A command that exits with an exit code will have that value as exitstatus # A command that fails (e.g. throws exception) before setting an exit code will have exitstatus 1 def run_command(cmd) script = "$ProgressPreference='SilentlyContinue';" + cmd encoded_script = Base64.strict_encode64(script) @pipe.puts(encoded_script) @pipe.flush res = OpenStruct.new(JSON.parse(Base64.decode64(@pipe.readline))) Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus) end private def acquire_pipe pipe_name = "inspec_#{SecureRandom.hex}" start_pipe_server(pipe_name) pipe = nil # PowerShell needs time to create pipe. 100.times do begin pipe = open("//./pipe/#{pipe_name}", "r+") break rescue sleep 0.1 end end pipe end def start_pipe_server(pipe_name) require "win32/process" script = <<-EOF $ErrorActionPreference = 'Stop' $pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream('#{pipe_name}') $pipeReader = New-Object System.IO.StreamReader($pipeServer) $pipeWriter = New-Object System.IO.StreamWriter($pipeServer) $pipeServer.WaitForConnection() # Create loop to receive and process user commands/scripts $clientConnected = $true while($clientConnected) { $input = $pipeReader.ReadLine() $command = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($input)) # Execute user command/script and convert result to JSON $scriptBlock = $ExecutionContext.InvokeCommand.NewScriptBlock($command) try { $stdout = & $scriptBlock | Out-String $exit_code = $LastExitCode if ($exit_code -eq $null) { $exit_code = 0 } $result = @{ 'stdout' = $stdout ; 'stderr' = ''; 'exitstatus' = $exit_code } } catch { $stderr = $_ | Out-String $exit_code = $LastExitCode $result = @{ 'stdout' = ''; 'stderr' = $stderr; 'exitstatus' = $exit_code } } $resultJSON = $result | ConvertTo-JSON # Encode JSON in Base64 and write to pipe $encodedResult = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($resultJSON)) $pipeWriter.WriteLine($encodedResult) $pipeWriter.Flush() } EOF utf8_script = script.encode("UTF-16LE", "UTF-8") base64_script = Base64.strict_encode64(utf8_script) cmd = "#{@powershell_cmd} -NoProfile -ExecutionPolicy bypass -NonInteractive -EncodedCommand #{base64_script}" server_pid = Process.create(command_line: cmd).process_id # Ensure process is killed when the Train process exits at_exit { Process.kill("KILL", server_pid) } end end end end end train-3.2.28/lib/train/transports/mock.rb000066400000000000000000000105021364512547200203120ustar00rootroot00000000000000require_relative "../plugins" require "digest" module Train::Transports class Mock < Train.plugin(1) name "mock" def initialize(conf = nil) @conf = conf || {} trace_calls if @conf[:trace] end def connection @connection ||= Connection.new(@conf) end def to_s "Mock Transport" end private def trace_calls interface_methods = { "Train::Transports::Mock" => Train::Transports::Mock.instance_methods(false), "Train::Transports::Mock::Connection" => Connection.instance_methods(false), "Train::Transports::Mock::Connection::File" => Connection::FileCommon.instance_methods(false), "Train::Transports::Mock::Connection::OS" => Train::Platform.instance_methods(false), } # rubocop:disable Metrics/ParameterLists # rubocop:disable Security/Eval set_trace_func(proc { |event, _file, _line, id, binding, classname| unless classname.to_s.start_with?("Train::Transports::Mock") && (event == "call") && interface_methods[classname.to_s].include?(id) next end # kindly borrowed from the wonderful simple-tracer by matugm arg_names = eval( "method(__method__).parameters.map { |arg| arg[1].to_s }", binding ) args = eval("#{arg_names}.map { |arg| eval(arg) }", binding).join(", ") prefix = "-" * (classname.to_s.count(":") - 2) + "> " puts("#{prefix}#{id} #{args}") }) # rubocop:enable all end end end class Train::Transports::Mock class Connection < BaseConnection attr_reader :options def initialize(conf = nil) super(conf) mock_os enable_cache(:file) enable_cache(:command) end def uri "mock://" end DEFAULTS = { name: "mock", family: "mock", release: "unknown", arch: "unknown", }.freeze def mock_os(value = {}) value = DEFAULTS .merge(value || {}) .transform_values { |v| v || "unknown" } platform = Train::Platforms.name(value[:name]) platform.find_family_hierarchy platform.platform = value platform.add_platform_methods @platform = platform end def commands=(commands) @cache[:command] = commands end def commands @cache[:command] end def files=(files) @cache[:file] = files end def files @cache[:file] end def mock_command(cmd, stdout = nil, stderr = nil, exit_status = 0) @cache[:command][cmd] = Command.new(stdout || "", stderr || "", exit_status) end def command_not_found(cmd) if @options[:verbose] $stderr.puts("Command not mocked:") $stderr.puts(" " + cmd.to_s.split("\n").join("\n ")) $stderr.puts(" SHA: " + Digest::SHA256.hexdigest(cmd.to_s)) end # return a non-zero exit code mock_command(cmd, nil, nil, 1) end def file_not_found(path) $stderr.puts("File not mocked: " + path.to_s) if @options[:verbose] File.new(self, path) end def to_s "Mock Connection" end private def run_command_via_connection(cmd, &_data_handler) @cache[:command][Digest::SHA256.hexdigest cmd.to_s] || command_not_found(cmd) end def file_via_connection(path) file_not_found(path) end end end class Train::Transports::Mock::Connection Command = Struct.new(:stdout, :stderr, :exit_status) end class Train::Transports::Mock::Connection class File < Train::File def self.from_json(json) res = new(json["backend"], json["path"], json["follow_symlink"]) res.type = json["type"] Train::File::DATA_FIELDS.each do |f| m = (f.tr("?", "") + "=").to_sym res.method(m).call(json[f]) end res end Train::File::DATA_FIELDS.each do |m| attr_accessor m.tr("?", "").to_sym next unless m.include?("?") define_method m.to_sym do method(m.tr("?", "").to_sym).call end end attr_accessor :type def initialize(backend, path, follow_symlink = true) super(backend, path, follow_symlink) @type = :unknown @exist = false end def mounted @mounted ||= @backend.run_command("mount | grep -- ' on #{@path}'") end end end train-3.2.28/lib/train/transports/ssh.rb000066400000000000000000000223161364512547200201640ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Fletcher Nichol () # Author:: Dominik Richter () # Author:: Christoph Hartmann () # # Copyright (C) 2014, Fletcher Nichol # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require "net/ssh" require "net/scp" require_relative "../errors" module Train::Transports # Wrapped exception for any internally raised SSH-related errors. # # @author Fletcher Nichol class SSHFailed < Train::TransportError; end class SSHPTYFailed < Train::TransportError; end # A Transport which uses the SSH protocol to execute commands and transfer # files. # # @author Fletcher Nichol class SSH < Train.plugin(1) # rubocop:disable Metrics/ClassLength name "ssh" require_relative "ssh_connection" require_relative "cisco_ios_connection" # add options for submodules include_options Train::Extras::CommandWrapper # common target configuration option :host, required: true option :port, default: 22, required: true option :user, default: "root", required: true option :key_files, default: nil option :password, default: nil # additional ssh options option :keepalive, default: true option :keepalive_interval, default: 60 option :connection_timeout, default: 15 option :connection_retries, default: 5 option :connection_retry_sleep, default: 1 option :max_wait_until_ready, default: 600 option :compression, default: false option :pty, default: false option :proxy_command, default: nil option :bastion_host, default: nil option :bastion_user, default: "root" option :bastion_port, default: 22 option :non_interactive, default: false option :verify_host_key, default: false option :compression_level do |opts| # on nil or false: set compression level to 0 opts[:compression] ? 6 : 0 end # (see Base#connection) def connection(state = {}, &block) opts = merge_options(options, state || {}) validate_options(opts) conn_opts = connection_options(opts) if defined?(@connection) && @connection_options == conn_opts reuse_connection(&block) else create_new_connection(conn_opts, &block) end end private def validate_options(options) super(options) key_files = Array(options[:key_files]) options[:auth_methods] ||= ["none"] unless key_files.empty? options[:auth_methods].push("publickey") options[:keys_only] = true if options[:password].nil? options[:key_files] = key_files end unless options[:password].nil? options[:auth_methods].push("password", "keyboard-interactive") end if options[:auth_methods] == ["none"] if ssh_known_identities.empty? raise Train::ClientError.new( "Your SSH Agent has no keys added, and you have not specified a password or a key file", :no_ssh_password_or_key_available ) else logger.debug("[SSH] Using Agent keys as no password or key file have been specified") options[:auth_methods].push("publickey") end end if options[:pty] logger.warn("[SSH] PTY requested: stderr will be merged into stdout") end if [options[:proxy_command], options[:bastion_host]].all? { |type| !type.nil? } raise Train::ClientError, "Only one of proxy_command or bastion_host needs to be specified" end super self end # Creates an SSH Authentication KeyManager instance and saves it for # potential future reuse. # # @return [Hash] hash of SSH Known Identities # @api private def ssh_known_identities # Force KeyManager to load the key(s) @manager ||= Net::SSH::Authentication::KeyManager.new(nil).each_identity {} @manager.known_identities end # Builds the hash of options needed by the Connection object on # construction. # # @param opts [Hash] merged configuration and mutable state data # @return [Hash] hash of connection options # @api private def connection_options(opts) connection_options = { logger: logger, user_known_hosts_file: "/dev/null", hostname: opts[:host], port: opts[:port], username: opts[:user], compression: opts[:compression], compression_level: opts[:compression_level], keepalive: opts[:keepalive], keepalive_interval: opts[:keepalive_interval], timeout: opts[:connection_timeout], connection_retries: opts[:connection_retries], connection_retry_sleep: opts[:connection_retry_sleep], max_wait_until_ready: opts[:max_wait_until_ready], auth_methods: opts[:auth_methods], keys_only: opts[:keys_only], keys: opts[:key_files], password: opts[:password], forward_agent: opts[:forward_agent], proxy_command: opts[:proxy_command], bastion_host: opts[:bastion_host], bastion_user: opts[:bastion_user], bastion_port: opts[:bastion_port], non_interactive: opts[:non_interactive], transport_options: opts, } # disable host key verification. The hash key and value to use # depends on the version of net-ssh in use. connection_options[verify_host_key_option] = verify_host_key_value(opts[:verify_host_key]) connection_options end # # Returns the correct host-key-verification option key to use depending # on what version of net-ssh is in use. In net-ssh <= 4.1, the supported # parameter is `paranoid` but in 4.2, it became `verify_host_key` # # `verify_host_key` does not work in <= 4.1, and `paranoid` throws # deprecation warnings in >= 4.2. # # While the "right thing" to do would be to pin train's dependency on # net-ssh to ~> 4.2, this will prevent InSpec from being used in # Chef v12 because of it pinning to a v3 of net-ssh. # def verify_host_key_option current_net_ssh = Net::SSH::Version::CURRENT new_option_version = Net::SSH::Version[4, 2, 0] current_net_ssh >= new_option_version ? :verify_host_key : :paranoid end # Likewise, version <5 accepted false; 5+ requires :never or will # issue a deprecation warning. This method allows a lot of common # things through. def verify_host_key_value(given) current_net_ssh = Net::SSH::Version::CURRENT new_value_version = Net::SSH::Version[5, 0, 0] if current_net_ssh >= new_value_version # 5.0+ style { # It's not a boolean anymore. "true" => :always, "false" => :never, true => :always, false => :never, # May be correct value, but strings from JSON config "always" => :always, "never" => :never, nil => :never, }.fetch(given, given) else # up to 4.2 style { "true" => true, "false" => false, nil => false, }.fetch(given, given) end end # Creates a new SSH Connection instance and save it for potential future # reuse. # # @param options [Hash] conneciton options # @return [Ssh::Connection] an SSH Connection instance # @api private def create_new_connection(options, &block) if defined?(@connection) logger.debug("[SSH] shutting previous connection #{@connection}") @connection.close end @connection_options = options conn = Connection.new(options, &block) # Cisco IOS requires a special implementation of `Net:SSH`. This uses the # SSH transport to identify the platform, but then replaces SSHConnection # with a CiscoIOSConnection in order to behave as expected for the user. if defined?(conn.platform.cisco_ios?) && conn.platform.cisco_ios? ios_options = {} ios_options[:host] = @options[:host] ios_options[:user] = @options[:user] # The enable password is used to elevate privileges on Cisco devices # We will also support the sudo password field for the same purpose # for the interim. # TODO ios_options[:enable_password] = @options[:enable_password] || @options[:sudo_password] ios_options[:logger] = @options[:logger] ios_options.merge!(@connection_options) conn = CiscoIOSConnection.new(ios_options) end @connection = conn unless conn.nil? end # Return the last saved SSH connection instance. # # @return [Ssh::Connection] an SSH Connection instance # @api private def reuse_connection logger.debug("[SSH] reusing existing connection #{@connection}") yield @connection if block_given? @connection end end end train-3.2.28/lib/train/transports/ssh_connection.rb000066400000000000000000000267271364512547200224150ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Fletcher Nichol () # Author:: Dominik Richter () # Author:: Christoph Hartmann () # # Copyright (C) 2014, Fletcher Nichol # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require "net/ssh" require "net/scp" require "timeout" class Train::Transports::SSH # A Connection instance can be generated and re-generated, given new # connection details such as connection port, hostname, credentials, etc. # This object is responsible for carrying out the actions on the remote # host such as executing commands, transferring files, etc. # # @author Fletcher Nichol class Connection < BaseConnection # rubocop:disable Metrics/ClassLength attr_reader :hostname attr_reader :transport_options def initialize(options) # Track IOS command retries to prevent infinite loop on IOError. This must # be done before `super()` because the parent runs detection commands. @ios_cmd_retries = 0 super(options) @session = nil @username = @options.delete(:username) @hostname = @options.delete(:hostname) @port = @options[:port] # don't delete from options @connection_retries = @options.delete(:connection_retries) @connection_retry_sleep = @options.delete(:connection_retry_sleep) @max_wait_until_ready = @options.delete(:max_wait_until_ready) @max_ssh_sessions = @options.delete(:max_ssh_connections) { 9 } @transport_options = @options.delete(:transport_options) @proxy_command = @options.delete(:proxy_command) @bastion_host = @options.delete(:bastion_host) @bastion_user = @options.delete(:bastion_user) @bastion_port = @options.delete(:bastion_port) @cmd_wrapper = CommandWrapper.load(self, @transport_options) end # (see Base::Connection#close) def close return if @session.nil? logger.debug("[SSH] closing connection to #{self}") session.close ensure @session = nil end def ssh_opts level = logger.debug? ? "VERBOSE" : "ERROR" fwd_agent = options[:forward_agent] ? "yes" : "no" args = %w{ -o UserKnownHostsFile=/dev/null } args += %w{ -o StrictHostKeyChecking=no } args += %w{ -o IdentitiesOnly=yes } if options[:keys] args += %w{ -o BatchMode=yes } if options[:non_interactive] args += %W{ -o LogLevel=#{level} } args += %W{ -o ForwardAgent=#{fwd_agent} } if options.key?(:forward_agent) Array(options[:keys]).each do |ssh_key| args += %W{ -i #{ssh_key} } end args end def check_proxy [@proxy_command, @bastion_host].any? { |type| !type.nil? } end def generate_proxy_command return @proxy_command unless @proxy_command.nil? args = %w{ ssh } args += ssh_opts args += %W{ #{@bastion_user}@#{@bastion_host} } args += %W{ -p #{@bastion_port} } args += %w{ -W %h:%p } args.join(" ") end # (see Base::Connection#login_command) def login_command args = ssh_opts args += %W{ -o ProxyCommand='#{generate_proxy_command}' } if check_proxy args += %W{ -p #{@port} } args += %W{ #{@username}@#{@hostname} } LoginCommand.new("ssh", args) end # (see Base::Connection#upload) def upload(locals, remote) waits = [] Array(locals).each do |local| opts = File.directory?(local) ? { recursive: true } : {} waits.push session.scp.upload(local, remote, opts) do |_ch, name, sent, total| logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total end waits.shift.wait while waits.length >= @max_ssh_sessions end waits.each(&:wait) rescue Net::SSH::Exception => ex raise Train::Transports::SSHFailed, "SCP upload failed (#{ex.message})" end def download(remotes, local) waits = [] Array(remotes).map do |remote| opts = file(remote).directory? ? { recursive: true } : {} waits.push session.scp.download(remote, local, opts) do |_ch, name, recv, total| logger.debug("Downloaded #{name} (#{total} bytes)") if recv == total end waits.shift.wait while waits.length >= @max_ssh_sessions end waits.each(&:wait) rescue Net::SSH::Exception => ex raise Train::Transports::SSHFailed, "SCP download failed (#{ex.message})" end # (see Base::Connection#wait_until_ready) def wait_until_ready delay = 3 session( retries: @max_wait_until_ready / delay, delay: delay, message: "Waiting for SSH service on #{@hostname}:#{@port}, " \ "retrying in #{delay} seconds" ) run_command(PING_COMMAND.dup) end def uri "ssh://#{@username}@#{@hostname}:#{@port}" end # remote_port_forwarding def forward_remote(port, host, remote_port, remote_host = "127.0.0.1") @session.forward.remote(port, host, remote_port, remote_host) end def obscured_options options_to_print = @options.clone options_to_print[:password] = "" if options_to_print.key?(:password) options_to_print end def with_sudo_pty old_pty = transport_options[:pty] transport_options[:pty] = true if @sudo yield ensure transport_options[:pty] = old_pty end private PING_COMMAND = "echo '[SSH] Established'".freeze RESCUE_EXCEPTIONS_ON_ESTABLISH = [ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH, Errno::EPIPE, Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout, Timeout::Error ].freeze # Establish an SSH session on the remote host. # # @param opts [Hash] retry options # @option opts [Integer] :retries the number of times to retry before # failing # @option opts [Float] :delay the number of seconds to wait until # attempting a retry # @option opts [String] :message an optional message to be logged on # debug (overriding the default) when a rescuable exception is raised # @return [Net::SSH::Connection::Session] the SSH connection session # @api private def establish_connection(opts) logger.debug("[SSH] opening connection to #{self}") logger.debug("[SSH] using options %p" % [obscured_options]) if check_proxy require "net/ssh/proxy/command" @options[:proxy] = Net::SSH::Proxy::Command.new(generate_proxy_command) end Net::SSH.start(@hostname, @username, @options.clone.delete_if { |_key, value| value.nil? }) rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e if (opts[:retries] -= 1) <= 0 logger.warn("[SSH] connection failed, terminating (#{e.inspect})") raise Train::Transports::SSHFailed, "SSH session could not be established" end if opts[:message] logger.debug("[SSH] connection failed (#{e.inspect})") message = opts[:message] else message = "[SSH] connection failed, retrying in #{opts[:delay]}"\ " seconds (#{e.inspect})" end logger.info(message) sleep(opts[:delay]) retry end def file_via_connection(path, *args) if os.aix? Train::File::Remote::Aix.new(self, path, *args) elsif os.solaris? Train::File::Remote::Unix.new(self, path, *args) elsif os[:name] == "qnx" Train::File::Remote::Qnx.new(self, path, *args) elsif os.windows? Train::File::Remote::Windows.new(self, path, *args) else Train::File::Remote::Linux.new(self, path, *args) end end def run_command_via_connection(cmd, &data_handler) cmd.dup.force_encoding("binary") if cmd.respond_to?(:force_encoding) reset_session if session.closed? exit_status, stdout, stderr = execute_on_channel(cmd, &data_handler) # Since `@session.loop` succeeded, reset the IOS command retry counter @ios_cmd_retries = 0 CommandResult.new(stdout, stderr, exit_status) rescue Net::SSH::Exception => ex raise Train::Transports::SSHFailed, "SSH command failed (#{ex.message})" rescue IOError # Cisco IOS occasionally closes the stream prematurely while we are # running commands to detect if we need to switch to the Cisco IOS # transport. This retries the command if this is the case. # See: # https://github.com/inspec/train/pull/271 logger.debug("[SSH] Possible Cisco IOS race condition, retrying command") # Only attempt retry up to 5 times to avoid infinite loop @ios_cmd_retries += 1 raise if @ios_cmd_retries >= 5 retry end # Returns a connection session, or establishes one when invoked the # first time. # # @param retry_options [Hash] retry options for the initial connection # @return [Net::SSH::Connection::Session] the SSH connection session # @api private def session(retry_options = {}) @session ||= establish_connection({ retries: @connection_retries.to_i, delay: @connection_retry_sleep.to_i, }.merge(retry_options)) end def reset_session @session = nil end # String representation of object, reporting its connection details and # configuration. # # @api private def to_s "#{@username}@#{@hostname}" end # Given a channel and a command string, it will execute the command on the channel # and accumulate results in @stdout/@stderr. # # @param channel [Net::SSH::Connection::Channel] an open ssh channel # @param cmd [String] the command to execute # @return [Integer] exit status or nil if exit-status/exit-signal requests # not received. # # @api private def execute_on_channel(cmd) stdout = "" stderr = "" exit_status = nil session.open_channel do |channel| # wrap commands if that is configured cmd = @cmd_wrapper.run(cmd) if @cmd_wrapper logger.debug("[SSH] #{self} cmd = #{cmd}") if @transport_options[:pty] channel.request_pty do |_ch, success| raise Train::Transports::SSHPTYFailed, "Requesting PTY failed" unless success end end channel.exec(cmd) do |_, success| abort "Couldn't execute command on SSH." unless success channel.on_data do |_, data| yield(data) if block_given? stdout += data end channel.on_extended_data do |_, _type, data| yield(data) if block_given? stderr += data end channel.on_request("exit-status") do |_, data| exit_status = data.read_long end channel.on_request("exit-signal") do |_, data| exit_status = data.read_long end end end session.loop [exit_status, stdout, stderr] end end end train-3.2.28/lib/train/transports/vmware.rb000066400000000000000000000125531364512547200206720ustar00rootroot00000000000000# encoding: utf-8 require "train/plugins" require "open3" require "ostruct" require "json" require "mkmf" module Train::Transports class VMware < Train.plugin(1) name "vmware" option :viserver, default: proc { ENV["VISERVER"] } option :username, default: proc { ENV["VISERVER_USERNAME"] } option :password, default: proc { ENV["VISERVER_PASSWORD"] } option :insecure, default: false def connection(_ = nil) @connection ||= Connection.new(@options) end class Connection < BaseConnection # rubocop:disable ClassLength POWERSHELL_PROMPT_REGEX = /PS\s.*> $/.freeze def initialize(options) super(options) options[:viserver] = options[:viserver] || options[:host] options[:username] = options[:username] || options[:user] @username = options[:username] @viserver = options[:viserver] @session = nil @stdout_buffer = "" @stderr_buffer = "" @powershell_binary = detect_powershell_binary if @powershell_binary == :powershell require_relative "local" @powershell = Train::Transports::Local::Connection.new(options) end if options[:insecure] == true run_command_via_connection("Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$False") end @platform_details = { release: "vmware-powercli-#{powercli_version}", } connect end def connect login_command = "Connect-VIServer #{options[:viserver]} -User #{options[:username]} -Password #{options[:password]} | Out-Null" result = run_command_via_connection(login_command) if result.exit_status != 0 message = "Unable to connect to VIServer at #{options[:viserver]}. " case result.stderr when /Invalid server certificate/ message += "Certification verification failed. Please use `--insecure` or set `Set-PowerCLIConfiguration -InvalidCertificateAction Ignore` in PowerShell" when /incorrect user name or password/ message += "Incorrect username or password" else message += result.stderr.gsub(/-Password .*\s/, "-Password REDACTED") end raise message end end def platform force_platform!("vmware", @platform_details) end def run_command_via_connection(cmd, &_data_handler) if @powershell_binary == :pwsh result = parse_pwsh_output(cmd) # Attach exit status to result exit_status = parse_pwsh_output("echo $?").stdout.chomp result.exit_status = exit_status == "True" ? 0 : 1 result else @powershell.run_command(cmd) end end def unique_identifier uuid_command = "(Get-VMHost | Get-View).hardware.systeminfo.uuid" run_command_via_connection(uuid_command).stdout.chomp end def uri "vmware://#{@username}@#{@viserver}" end private def detect_powershell_binary if find_executable0("pwsh") :pwsh elsif find_executable0("powershell") :powershell else raise "Cannot find PowerShell binary, is `pwsh` installed?" end end # Read from stdout pipe until prompt is received def flush_stdout(pipe) @stdout_buffer += pipe.read_nonblock(1) while @stdout_buffer !~ POWERSHELL_PROMPT_REGEX @stdout_buffer rescue IO::EAGAINWaitReadable # We cannot know when the stdout pipe is finished so we keep reading retry ensure @stdout_buffer = "" end # This must be called after `flush_stdout` to ensure buffer is full def flush_stderr(pipe) loop do @stderr_buffer += pipe.read_nonblock(1) end rescue IO::EAGAINWaitReadable # If `flush_stderr` is ran after reading stdout we know that all of # stderr is in the pipe. Thus, we can return the buffer once the pipe # is unreadable. @stderr_buffer ensure @stderr_buffer = "" end def parse_pwsh_output(cmd) session.stdin.puts(cmd) stdout = flush_stdout(session.stdout) # Remove stdin from stdout (including trailing newline) stdout.slice!(0, cmd.length + 1) # Remove prompt from stdout stdout.gsub!(POWERSHELL_PROMPT_REGEX, "") # Grab stderr stderr = flush_stderr(session.stderr) CommandResult.new( stdout, stderr, nil # exit_status is attached in `run_command_via_connection` ) end def powercli_version version_command = "[string](Get-Module -Name VMware.PowerCLI -ListAvailable | Select -ExpandProperty Version)" result = run_command_via_connection(version_command) if result.stdout.empty? || result.exit_status != 0 raise "Unable to determine PowerCLI Module version, is it installed?" end result.stdout.chomp end def session return @session unless @session.nil? stdin, stdout, stderr = Open3.popen3("pwsh") # Remove leading prompt and intro text flush_stdout(stdout) @session = OpenStruct.new @session.stdin = stdin @session.stdout = stdout @session.stderr = stderr @session end end end end train-3.2.28/lib/train/version.rb000066400000000000000000000001731364512547200166320ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Dominik Richter () module Train VERSION = "3.2.28".freeze end train-3.2.28/test/000077500000000000000000000000001364512547200137135ustar00rootroot00000000000000train-3.2.28/test/fixtures/000077500000000000000000000000001364512547200155645ustar00rootroot00000000000000train-3.2.28/test/fixtures/PowerShell/000077500000000000000000000000001364512547200176505ustar00rootroot00000000000000train-3.2.28/test/fixtures/PowerShell/exit_fortytwo.ps1000066400000000000000000000000261364512547200232210ustar00rootroot00000000000000echo 'Goodbye' exit 42train-3.2.28/test/fixtures/PowerShell/exit_zero.ps1000066400000000000000000000000141364512547200223000ustar00rootroot00000000000000echo 'Hello'train-3.2.28/test/fixtures/PowerShell/throws.ps1000066400000000000000000000000501364512547200216160ustar00rootroot00000000000000echo 'Next line throws' Throw 'Oh dear.'train-3.2.28/test/fixtures/plugins/000077500000000000000000000000001364512547200172455ustar00rootroot00000000000000train-3.2.28/test/fixtures/plugins/train-test-fixture/000077500000000000000000000000001364512547200230235ustar00rootroot00000000000000train-3.2.28/test/fixtures/plugins/train-test-fixture/LICENSE000066400000000000000000000251421364512547200240340ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. train-3.2.28/test/fixtures/plugins/train-test-fixture/README.md000066400000000000000000000005321364512547200243020ustar00rootroot00000000000000This is a very simple train transport plugin. It provided fixed responses to file.content and command.stdout/stderr/exit_status. It is not a good example to use for learning, nor a good base for starting your own plugin - it's intended for for use during the testing of Train. For good examples of plugin development, see train/examples/plugin.train-3.2.28/test/fixtures/plugins/train-test-fixture/lib/000077500000000000000000000000001364512547200235715ustar00rootroot00000000000000train-3.2.28/test/fixtures/plugins/train-test-fixture/lib/train-test-fixture.rb000066400000000000000000000002161364512547200276730ustar00rootroot00000000000000lib = File.expand_path("../../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "train-test-fixture/transport" train-3.2.28/test/fixtures/plugins/train-test-fixture/lib/train-test-fixture/000077500000000000000000000000001364512547200273475ustar00rootroot00000000000000train-3.2.28/test/fixtures/plugins/train-test-fixture/lib/train-test-fixture/connection.rb000066400000000000000000000014131364512547200320320ustar00rootroot00000000000000require "train-test-fixture/platform" require "train/transports/local" module TrainPlugins module TestFixture class Connection < Train::Plugins::Transport::BaseConnection include TrainPlugins::TestFixture::Platform def initialize(options) super(options) end private def run_command_via_connection(cmd) Train::Transports::Local::CommandResult.new( "Mock Command Result stdout", "Mock Command Result stderr", 17 ) end def file_via_connection(path, *args) MockFile.new(self, path) end class MockFile < Train::File def content # Remarkably, the content is always the same. "Lorem Ipsum" end end end end end train-3.2.28/test/fixtures/plugins/train-test-fixture/lib/train-test-fixture/platform.rb000066400000000000000000000010231364512547200315140ustar00rootroot00000000000000require "train-test-fixture/version" module TrainPlugins module TestFixture module Platform def platform # Build this platform's family declarations. # You'll need at least unix and windows to make the file() resource work. Train::Platforms.name("test-fixture").in_family("unix") Train::Platforms.name("test-fixture").in_family("windows") force_platform!("test-fixture", release: TrainPlugins::TestFixture::VERSION, arch: "mock") end end end end train-3.2.28/test/fixtures/plugins/train-test-fixture/lib/train-test-fixture/transport.rb000066400000000000000000000004321364512547200317270ustar00rootroot00000000000000require "train-test-fixture/connection" module TrainPlugins module TestFixture class Transport < Train.plugin(1) name "test-fixture" def connection(_ = nil) @connection ||= TrainPlugins::TestFixture::Connection.new(@options) end end end end train-3.2.28/test/fixtures/plugins/train-test-fixture/lib/train-test-fixture/version.rb000066400000000000000000000001201364512547200313520ustar00rootroot00000000000000module TrainPlugins module TestFixture VERSION = "0.1.0".freeze end end train-3.2.28/test/fixtures/plugins/train-test-fixture/pkg/000077500000000000000000000000001364512547200236045ustar00rootroot00000000000000train-3.2.28/test/fixtures/plugins/train-test-fixture/pkg/train-test-fixture-0.1.0.gem000066400000000000000000000230001364512547200305010ustar00rootroot00000000000000metadata.gz0000444000000000000000000000124013352730225013435 0ustar00wheelwheel00000000000000[TMo0W B> "*Am"3b;ت;mQr$JdY. *ޢ^k%I;+4XymF([ w ؁(/KZ;o*ks>T"38-(ж"zm[ &xۨ="V6WOS:.OE#YV>PU|GA4i@ҟ\U. ȅ~ǒR0=H&v!#l瑱ӻa+>$ˢ~΍}Ó8UfhL̇i4O N[y&skv2Ĝ99"uDCԍ\9St"E:y#FB_lSbJx{:\ ܠIʸ! W/k8UFLrɣenzUܷbWY{a3xEq0TۢʊտV$ Yld$;^VpK .PͫH}m0,.Y{RxG Kyc[{:bĮoLƗ}x%eL\I/^hY1%.܄WfX!>a4Fz\/&l5sujRO]?|.4o+zdata.tar.gz0000444000000000000000000001246013352730225013361 0ustar00wheelwheel00000000000000[\{s7XD]JRŒɒKחJT Hb=3`3~0=D"F? t?󵻷ɓGZtvGbgx5)3CWy{_GsΔ8թ*5/ڔbe#}yƬ;;b(i;9obwzWX8?;]. $'/Ώ߼u<}?XMtk{HD/L(,E GUUX!LxJ4V%RdM'̴+=nC!p3R\W؃+LgOLq&m ULFRjJOg0RUY^ 3SfȦLVfSVSZtmsJȔa,a੣K+˛䉐or5CM)q7J,t=Exxi*`TsjѲ1Ȗ$wKV 63 U% dODmD*A8ԕ(d) ;&9.N M%-Ш2@ $ ;s\f'R\wtmKӴJS32 JY7V%< N?/0۱Fg .TXp"EB[KL:NB%쓂m4DU̥b{\0I.Ӽu"ׅ}AdLꐥ@kěDOr ueπMN?Lә,XR~}iqZCDH,p=5Z!Ѧ q >2!8 ;^E*Ry8[S_3|HGA]j[zVn;J!3 7Rr{{W-XInǪ7% *i\}ݡq>w,-ʩ$plP: |17ȥ VMRC%v8 Xj*;f+aPRԁx-a=%l.uJg1Dq IvRluj.+Q_JKP=1k ZPBm{j*DA(3q~^E]U`~1gI> pHK3h̠).Hktf 9 H`"i:B^x-@):cnF+`8p,4}\r0C`Yo 2'}YT8$pДq?ΐ^B/ A$oW5Z r-|HVaQJNd \Cf1v!5rt <6K'9S|Qi.503h`JM*&>PVl ) c4PMsV'98ytf%˭@5 s >U6S\N%>&wRAhV!>5 "~B" O)p'v"$[ºEVKp# ng|#~\};ʊtBZ2kX`',Xd(.T\PҦ\>jեG{E@;5H_qOѳr'%mALpS(Ҝr 85KS$bp,t?1 _kW!hq;Z0I \ PO347FOLxJWHgY&5ʈ -g[Z'h7m0K&2p&_G#rD~%(:Pݟ_ J x:QUP#z^=\ĥlOˬPы>mMEcr#;6Xc%Iǯ: epZ|D"p̜'ʓa_c3?724 ȕ (=u-s[cw4)j\#ަmZ_T [G^QA! V@R, Vel XbWR';V;秡  >D8!߳0/)v\EO8Bi VLYSx (8x'⨯7 w ;P]5%cfl,oI ;RcJ3WpǔbJ#t 6tBx b&HIL([V0Z vL'\ċ%UB/mA#+eSJ+\s١xSBij$"u [sJZ.jp}`梺OT5Z`5+8CF?fy!5Aqal8AcR 8s2-r/GB1z uRs&Tj*+nb}~?wсEa̐F 1z,VTulqZʃZzZWq_~jk` $=Ϧ*lh¹crX-K9 |( p bt<9^rbH,f2βI`)բ3g9@*1~P,u不% pK1o.Gͷ\GGgWKq~w_ΎhXbкhrYT6l;%dJ=7 ܻ]$ǣN^]%ŋoGHU^N.} >ټ9=\>< @Td\Z*34Bc:T^3rZ,xJڒ_&!d캃TۃI"( !8rs0J &eMQN?rW_P:NK55Tm'#t\-:8c!;cYDzj[jnVv PiKZe!5Nim O +.%X^[q_#.Ulq&ľ&?ԞEǐzzza-tj!\b #wNΛ'Mق\T j EdhjM%Ppwpkwki(Rxr=qϣ6F:vȹk׽# ә1\_ $`"Nhe9W[rī p|=ŒsW!\n(wh.Ѷe,0\-0RTt?l!QJH8VW"q(&(z>͘1a~djxm˪ n3Ҧޏ_TSA.*&Aw%ebҋH"=9;0BUw5<!J2oKP/~3"b- #G' 0 61hgVoe=&]|Zz!2(=)B[_ۂ_- 8@QD4o|:x#`0+tѮnH>r!rN@mщw[v[X[ dΰ Bo{a?.NHy+wӄ,N:w+z_(CY[5`9wRr#w~tw\x B!Vh{OW\_hV粞 D\_\_oæ3=0dWXz/u] nf!8$}W{Iw=xgcbڝܤ2 5)~ )AWB~FMXu6G>?G%>LQptJqPɢÿhG$؟<7ߤ35j|oA)"OGzF]#Qwk*8b|3W=9?,7)g6s#ˣ%>?YPqv|ʼn9= ]8p_ܭC!Mk> ,uis<7=!b5ޱ;;S]Ϛ05Ŏ&QbR;V{{ۙ)^ A]VkXh +蝻HDwinx{\+f㠩 eْ鸜%COG0s)^?dgwVchecksums.yaml.gz0000444000000000000000000000041513352730225014606 0ustar00wheelwheel00000000000000[e9@ D9\`32rNज़Mi~WU~^߾9/?~< SP#N09`%+U/ɼ' C J崰U;!9Bb_ѽVنpC 3͇Zm "N%a}t{P@TW  Ӂ9.,e(jx+LӝS^O&Ȉqֶ f>Hءx6h6tfn>h yuITs?Ptrain-3.2.28/test/fixtures/plugins/train-test-fixture/train-test-fixture.gemspec000066400000000000000000000022351364512547200301500ustar00rootroot00000000000000 lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) Gem::Specification.new do |spec| spec.name = "train-test-fixture" spec.version = "0.1.0" spec.authors = ["Inspec core engineering team"] spec.email = ["hello@chef.io"] spec.license = "Apache-2.0" spec.summary = %q{Test train plugin. Not intended for use as an example.} spec.description = <<-EOD Train plugin used in testing Train's plugin loader and InSpec's plugin manager. This plugin does things that a normal plugin should not. Do not use it as an example or as a starting point for plugin of your own. For that, please see https://github.com/inspec/train/tree/master/examples/plugins EOD spec.homepage = "https://github.com/inspec/train" spec.files = %w{ README.md LICENSE lib/train-test-fixture.rb lib/train-test-fixture/version.rb lib/train-test-fixture/transport.rb lib/train-test-fixture/connection.rb lib/train-test-fixture/platform.rb train-test-fixture.gemspec } spec.executables = [] spec.require_paths = ["lib"] # No deps end train-3.2.28/test/helper.rb000066400000000000000000000010301364512547200155110ustar00rootroot00000000000000# encoding: utf-8 if ENV["CI_ENABLE_COVERAGE"] require "simplecov" require "coveralls" SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter, ]) SimpleCov.start do add_filter "/test/" end end require "minitest/autorun" require "minitest/spec" require "mocha/minitest" require "byebug" require "train" class Minitest::Spec before do Train::Platforms.__reset Train::Platforms::Detect::Specifications::OS.load end end train-3.2.28/test/integration/000077500000000000000000000000001364512547200162365ustar00rootroot00000000000000train-3.2.28/test/integration/.kitchen.yml000066400000000000000000000015201364512547200204620ustar00rootroot00000000000000--- driver: name: vagrant provisioner: name: chef_solo data_path: ../../. platforms: - name: centos-7.1 - name: centos-6.7 - name: centos-6.7-i386 - name: centos-5.11 - name: centos-5.11-i386 - name: debian-6.0.10 - name: debian-6.0.10-i386 - name: debian-7.8 - name: debian-7.8-i386 - name: debian-8.1 - name: debian-8.1-i386 - name: fedora-21 - name: fedora-21-i386 - name: fedora-22 - name: freebsd-9.3 - name: freebsd-10.2 - name: opensuse-13.2-x86_64 - name: opensuse-13.2-i386 - name: ubuntu-14.04 - name: ubuntu-14.04-i386 - name: ubuntu-12.04 - name: ubuntu-12.04-i386 - name: ubuntu-10.04 - name: ubuntu-10.04-i386 suites: - name: default run_list: - recipe[sudo] - recipe[test] attributes: authorization: sudo: include_sudoers_d: true train-3.2.28/test/integration/Berksfile000066400000000000000000000001511364512547200200640ustar00rootroot00000000000000source "https://supermarket.chef.io" cookbook "sudo", "~> 2.7.2" cookbook "test", path: "cookbooks/test" train-3.2.28/test/integration/bootstrap.sh000066400000000000000000000010401364512547200206020ustar00rootroot00000000000000#!/bin/sh tmpdir=${TMPDIR:-/tmp} test ! -e $tmpdir/folder && \ mkdir $tmpdir/folder chmod 0567 $tmpdir/folder echo -n 'hello world' > $tmpdir/file test ! -e $tmpdir/symlink && \ ln -s $tmpdir/file /tmp/symlink chmod 0777 $tmpdir/symlink chmod 0765 $tmpdir/file echo -n 'hello suid/sgid/sticky' > $tmpdir/sfile chmod 7765 $tmpdir/sfile echo -n 'hello space' > $tmpdir/spaced\ file test ! -e $tmpdir/pipe && \ mkfifo $tmpdir/pipe test ! -e $tmpdir/block_device && \ mknod $tmpdir/block_device b 7 7 chmod 0666 $tmpdir/block_device train-3.2.28/test/integration/chefignore000066400000000000000000000000111364512547200202620ustar00rootroot00000000000000.kitchen train-3.2.28/test/integration/cookbooks/000077500000000000000000000000001364512547200202275ustar00rootroot00000000000000train-3.2.28/test/integration/cookbooks/test/000077500000000000000000000000001364512547200212065ustar00rootroot00000000000000train-3.2.28/test/integration/cookbooks/test/metadata.rb000066400000000000000000000000141364512547200233060ustar00rootroot00000000000000name "test" train-3.2.28/test/integration/cookbooks/test/recipes/000077500000000000000000000000001364512547200226405ustar00rootroot00000000000000train-3.2.28/test/integration/cookbooks/test/recipes/default.rb000066400000000000000000000050021364512547200246060ustar00rootroot00000000000000# encoding: utf-8 # author: Dominik Richter # # Helper recipe to create create a few files in the operating # systems, which the runner will test against. # It also initializes the runner inside the machines # and makes sure all dependencies are ready to go. # # Finally (for now), it actually executes the all tests with # the local execution backend include_recipe("test::prep_files") # prepare ssh for backend execute "create ssh key" do command 'ssh-keygen -t rsa -b 2048 -f /root/.ssh/id_rsa -N ""' not_if "test -e /root/.ssh/id_rsa" end execute "add ssh key to vagrant user" do command "cat /root/.ssh/id_rsa.pub >> /home/vagrant/.ssh/authorized_keys" end execute "test ssh connection" do command 'ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa vagrant@localhost "echo 1"' end # prepare a few users %w{ nopasswd passwd nosudo reqtty customcommand }.each do |name| user name do password "$1$7MCNTXPI$r./jqCEoVlLlByYKSL3sZ." manage_home true end end %w{nopasswd vagrant}.each do |name| sudo name do user "%" + name nopasswd true defaults ["!requiretty"] end end sudo "passwd" do user "passwd" nopasswd false defaults ["!requiretty"] end sudo "reqtty" do user "reqtty" nopasswd true defaults ["requiretty"] end sudo "customcommand" do user "customcommand" nopasswd true defaults ["!requiretty"] end # execute tests execute "bundle install" do command "/opt/chef/embedded/bin/bundle install --without integration tools" cwd "/tmp/kitchen/data" end execute "run local tests" do command "/opt/chef/embedded/bin/ruby -I lib test/integration/test_local.rb test/integration/tests/*_test.rb" cwd "/tmp/kitchen/data" end execute "run ssh tests" do command "/opt/chef/embedded/bin/ruby -I lib test/integration/test_ssh.rb test/integration/tests/*_test.rb" cwd "/tmp/kitchen/data" end %w{passwd nopasswd reqtty customcommand}.each do |name| execute "run local sudo tests as #{name}" do command "/opt/chef/embedded/bin/ruby -I lib test/integration/sudo/#{name}.rb" cwd "/tmp/kitchen/data" user name end end execute "fix sudoers for reqtty" do command "chef-apply contrib/fixup_requiretty.rb" cwd "/tmp/kitchen/data" environment( "TRAIN_SUDO_USER" => "reqtty", "TRAIN_SUDO_VERY_MUCH" => "yes" ) end # if it's fixed, it should behave like user 'nopasswd' execute "run local sudo tests as reqtty, no longer requiring a tty" do command "/opt/chef/embedded/bin/ruby -I lib test/integration/sudo/nopasswd.rb" cwd "/tmp/kitchen/data" user "reqtty" end train-3.2.28/test/integration/cookbooks/test/recipes/prep_files.rb000066400000000000000000000020221364512547200253110ustar00rootroot00000000000000# encoding: utf-8 # author: Dominik Richter # author: Christoph Hartmann # # Helper recipe to create create a few files in the operating # systems, which the runner will test against. gid = node["platform_family"] == "aix" ? "system" : node["root_group"] file "/tmp/file" do mode "0765" owner "root" group gid content "hello world" end file "/tmp/sfile" do mode "7765" owner "root" group gid content "hello suid/sgid/sticky" end file "/tmp/spaced file" do content "hello space" end directory "/tmp/folder" do mode "0567" owner "root" group gid end link "/tmp/symlink" do to "/tmp/file" owner "root" group gid mode "0777" end link "/usr/bin/allyourbase" do to "/usr/bin/sudo" owner "root" group gid mode "0777" end execute "create pipe/fifo" do command "mkfifo /tmp/pipe" not_if "test -e /tmp/pipe" end execute "create block_device" do command "mknod /tmp/block_device b 7 7 && chmod 0666 /tmp/block_device && chown root:#{gid} /tmp/block_device" not_if "test -e /tmp/block_device" end train-3.2.28/test/integration/docker_run.rb000066400000000000000000000073361364512547200207270ustar00rootroot00000000000000require "docker" require "yaml" require "concurrent" class DockerRunner def initialize(conf_path = nil) @conf_path = conf_path || ENV["config"] unless File.file?(@conf_path) raise "Can't find configuration in #{@conf_path}" end @conf = YAML.load_file(@conf_path) if @conf.nil? || @conf.empty? raise "Can't read coniguration in #{@conf_path}" end if @conf["images"].nil? raise "You must configure test images in your #{@conf_path}" end @images = docker_images_by_tag @image_pull_tickets = Concurrent::Semaphore.new(2) @docker_run_tickets = Concurrent::Semaphore.new(5) end def run_all(&block) raise "You must provide a block for run_all" unless block_given? promises = @conf["images"].map do |id| run_on_target(id, &block) end # wait for all tests to be finished sleep(0.1) until promises.all?(&:fulfilled?) # return resulting values promises.map(&:value) end def run_on_target(name, &block) pr = Concurrent::Promise.new do begin container = start_container(name) res = yield(name, container) # special rescue block to handle not implemented error rescue NotImplementedError => err raise err.message end # always stop the container stop_container(container) res end.execute # failure handling pr.rescue do |err| msg = "\033[31;1m#{err.message}\033[0m" puts msg msg + "\n" + err.backtrace.join("\n") end end def provision_image(image, prov, files) tries ||= 3 return image if prov["script"].nil? path = File.join(File.dirname(@conf_path), prov["script"]) unless File.file?(path) puts "Can't find script file #{path}" return image end puts " script #{path}" dst = "/bootstrap#{files.length}.sh" files.push(dst) image.insert_local("localPath" => path, "outputPath" => dst) rescue StandardError => _ retry unless (tries -= 1) == 0 end def bootstrap_image(name, image) files = [] provisions = Array(@conf["provision"]) puts "--> provision docker #{name}" unless provisions.empty? provisions.each do |prov| image = provision_image(image, prov, files) end [image, files] end def start_container(name, version = nil) unless name.include?(":") version ||= "latest" name = "#{name}:#{version}" end puts "--> schedule docker #{name}" image = @images[name] if image.nil? puts "\033[35;1m--> pull docker images #{name} "\ "(this may take a while)\033[0m" @image_pull_tickets.acquire(1) puts "... start pull image #{name}" image = Docker::Image.create("fromImage" => name) @image_pull_tickets.release(1) unless image.nil? puts "\033[35;1m--> pull docker images finished for #{name}\033[0m" end end raise "Can't find nor pull docker image #{name}" if image.nil? @docker_run_tickets.acquire(1) image, scripts = bootstrap_image(name, image) puts "--> start docker #{name}" container = Docker::Container.create( "Cmd" => %w{sleep 3600}, "Image" => image.id, "OpenStdin" => true ) container.start scripts.each do |script| container.exec(%w{chmod +x}.push(script)) container.exec(%w{sh -c}.push(script)) end container end def stop_container(container) @docker_run_tickets.release(1) puts "--> killrm docker #{container.id}" container.kill container.delete(force: true) end private # get all docker image tags def docker_images_by_tag images = {} Docker::Image.all.map do |img| Array(img.info["RepoTags"]).each do |tag| images[tag] = img end end images end end train-3.2.28/test/integration/docker_test.rb000066400000000000000000000011601364512547200210670ustar00rootroot00000000000000# encoding: utf-8 require_relative "docker_run" tests = ARGV def test_container(container, tests) puts "--> run test on docker #{container.id}" pid = Process.fork do ENV["CONTAINER"] = container.id require_relative "docker_test_container.rb" Process.exit end _, status = Process.waitpid2(pid) status.exitstatus == 0 end results = DockerRunner.new.run_all do |name, container| status = test_container(container, tests) status ? nil : "Failed to run tests on #{name}" end failures = results.compact failures.each { |f| puts "\033[31;1m#{f}\033[0m\n\n" } failures.empty? || raise("Test failures") train-3.2.28/test/integration/docker_test_container.rb000066400000000000000000000010211364512547200231250ustar00rootroot00000000000000# encoding: utf-8 # author: Dominik Richter require "train" require_relative "helper" (container_id = ENV["CONTAINER"]) || raise("You must provide a container ID via CONTAINER env") tests = ARGV puts ["Running tests:", tests].flatten.join("\n- ") puts "" backends = {} backends[:docker] = proc { |*args| opt = Train.target_config({ host: container_id }) Train.create("docker", opt).connection(args[0]) } backends.each do |type, get_backend| tests.each do |test| instance_eval(File.read(test), test, 1) end end train-3.2.28/test/integration/helper.rb000066400000000000000000000027241364512547200200470ustar00rootroot00000000000000# encoding: utf-8 require "coveralls" Coveralls.wear! require "minitest/autorun" require "minitest/spec" # Tests configuration: module Test class << self # MTime tracks the maximum range of modification time in seconds. # i.e. MTime == 60*60*1 is 1 hour of modification time range, # which translates to a modification time range of: # [ now-1hour, now ] def mtime 60 * 60 * 24 * 1 end def dup(o) Marshal.load(Marshal.dump(o)) end def root_group(os) case os[:family] when "freebsd" "wheel" when "aix" "system" else "root" end end def selinux_label(backend, path = nil) return nil if backend.class.to_s =~ /docker/i os = backend.os labels = {} h = {} h.default = Hash.new(nil) h["redhat"] = {} h["redhat"].default = "unconfined_u:object_r:user_tmp_t:s0" h["redhat"]["5.11"] = "user_u:object_r:tmp_t" h["centos"] = h["fedora"] = h["redhat"] labels.default = dup(h) h["redhat"].default = "unconfined_u:object_r:tmp_t:s0" labels["/tmp/block_device"] = dup(h) h = {} h.default = Hash.new(nil) h["redhat"] = {} h["redhat"].default = "system_u:object_r:null_device_t:s0" h["redhat"]["5.11"] = "system_u:object_r:null_device_t" h["centos"] = h["fedora"] = h["redhat"] labels["/dev/null"] = dup(h) labels[path][os[:family]][os[:release]] end end end train-3.2.28/test/integration/sudo/000077500000000000000000000000001364512547200172105ustar00rootroot00000000000000train-3.2.28/test/integration/sudo/customcommand.rb000066400000000000000000000005611364512547200224100ustar00rootroot00000000000000# encoding: utf-8 # author: Jeremy Miller require_relative "run_as" describe "run custom sudo command" do it "is running as non-root without sudo" do run_as("whoami").stdout.wont_match(/root/i) end it "is running nopasswd custom sudo command" do run_as("whoami", { sudo: true, sudo_command: "allyourbase" }) .stdout.must_match(/root/i) end end train-3.2.28/test/integration/sudo/nopasswd.rb000066400000000000000000000004161364512547200213740ustar00rootroot00000000000000require_relative "run_as" describe "run_command" do it "is running as non-root without sudo" do run_as("whoami").stdout.wont_match(/root/i) end it "is running nopasswd sudo" do run_as("whoami", { sudo: true }) .stdout.must_match(/root/i) end end train-3.2.28/test/integration/sudo/passwd.rb000066400000000000000000000007621364512547200210430ustar00rootroot00000000000000require_relative "run_as" describe "run_command" do it "is running as non-root without sudo" do run_as("whoami").stdout.wont_match(/root/i) end it "is not running sudo without password" do err = -> { Train.create("local", { sudo: true }).connection }.must_raise Train::UserError err.message.must_match(/Sudo requires a password/) end it "is running passwd sudo" do run_as("whoami", { sudo: true, sudo_password: "password" }) .stdout.must_match(/root/i) end end train-3.2.28/test/integration/sudo/reqtty.rb000066400000000000000000000005731364512547200210720ustar00rootroot00000000000000require_relative "run_as" describe "run_command" do it "is running as non-root without sudo" do run_as("whoami").stdout.wont_match(/root/i) end it "is throwing an error trying to use sudo" do err = -> { run_as("whoami", { sudo: true }) }.must_raise Train::UserError err.message.must_match(/Sudo failed: Sudo requires a TTY. Please see the README/i) end end train-3.2.28/test/integration/sudo/run_as.rb000066400000000000000000000003331364512547200210230ustar00rootroot00000000000000# encoding: utf-8 # author: Dominik Richter # author: Christoph Hartmann require_relative "../helper" require "train" def run_as(cmd, opts = {}) Train.create("local", opts) .connection .run_command(cmd) end train-3.2.28/test/integration/test-travis-centos.yml000066400000000000000000000001271364512547200225370ustar00rootroot00000000000000images: - centos:5.11 - centos:6.8 - centos:7.2.1511 provision: - script: bootstrap.sh train-3.2.28/test/integration/test-travis-debian.yml000066400000000000000000000001251364512547200224640ustar00rootroot00000000000000images: - debian:6.0.10 - debian:7.11 - debian:8.5 provision: - script: bootstrap.sh train-3.2.28/test/integration/test-travis-fedora.yml000066400000000000000000000001461364512547200225050ustar00rootroot00000000000000images: - fedora:20 - fedora:21 - fedora:22 - fedora:23 - fedora:24 provision: - script: bootstrap.sh train-3.2.28/test/integration/test-travis-oel.yml000066400000000000000000000001411364512547200220170ustar00rootroot00000000000000images: - oraclelinux:5.11 - oraclelinux:6.8 - oraclelinux:7.2 provision: - script: bootstrap.sh train-3.2.28/test/integration/test-travis-ubuntu.yml000066400000000000000000000001651364512547200225700ustar00rootroot00000000000000images: - ubuntu:10.04 - ubuntu:12.04 - ubuntu:14.04 - ubuntu:16.04 - ubuntu:16.10 provision: - script: bootstrap.sh train-3.2.28/test/integration/test_local.rb000066400000000000000000000004741364512547200207210ustar00rootroot00000000000000# encoding: utf-8 # author: Dominik Richter require_relative "helper" require "train" backends = {} backends[:local] = proc { |*opts| Train.create("local", {}).connection(opts[0]) } tests = ARGV backends.each do |type, get_backend| tests.each do |test| instance_eval(File.read(test), test, 1) end end train-3.2.28/test/integration/test_ssh.rb000066400000000000000000000014331364512547200204200ustar00rootroot00000000000000# encoding: utf-8 # author: Dominik Richter require_relative "helper" require "train" require "logger" backends = {} backend_conf = { "target" => ENV["target"] || "vagrant@localhost", "key_files" => ENV["key_files"] || "/root/.ssh/id_rsa", "logger" => Logger.new($stdout), } backend_conf["target"] = "ssh://" + backend_conf["target"] backend_conf["logger"].level = \ if ENV.key?("debug") case ENV["debug"].to_s when /^false$/i, /^0$/i Logger::INFO else Logger::DEBUG end else Logger::INFO end backends[:ssh] = proc { |*args| conf = Train.target_config(backend_conf) Train.create("ssh", conf).connection(args[0]) } tests = ARGV backends.each do |type, get_backend| tests.each do |test| instance_eval(File.read(test), test, 1) end end train-3.2.28/test/integration/tests/000077500000000000000000000000001364512547200174005ustar00rootroot00000000000000train-3.2.28/test/integration/tests/path_block_device_test.rb000066400000000000000000000031761364512547200244200ustar00rootroot00000000000000# encoding: utf-8 describe "file interface" do let(:backend) { get_backend.call } describe "block device" do let(:file) { backend.file("/tmp/block_device") } it "exists" do _(file.exist?).must_equal(true) end it "is a block device" do _(file.block_device?).must_equal(true) end it "has type :block_device" do _(file.type).must_equal(:block_device) end it "has no content" do _(file.content).must_equal("") end it "has owner name root" do _(file.owner).must_equal("root") end it "has group name" do _(file.group).must_equal(Test.root_group(backend.os)) end it "has mode 0666" do _(file.mode).must_equal(00666) end it "checks mode? 0666" do _(file.mode?(00666)).must_equal(true) end it "has no link_path" do _(file.link_path).must_be_nil end it "has the correct md5sum" do _(file.md5sum).must_equal("d41d8cd98f00b204e9800998ecf8427e") end it "has the correct sha256sum" do _(file.sha256sum).must_equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") end it "has a modified time" do _(file.mtime).must_be_close_to(Time.now.to_i - Test.mtime / 2, Test.mtime) end it "has inode size of 0" do _(file.size).must_equal(0) end it "has selinux label handling" do res = Test.selinux_label(backend, file.path) _(file.selinux_label).must_equal(res) end it "has no product_version" do _(file.product_version).must_be_nil end it "has no file_version" do _(file.file_version).must_be_nil end end end train-3.2.28/test/integration/tests/path_character_device_test.rb000066400000000000000000000031731364512547200252570ustar00rootroot00000000000000# encoding: utf-8 describe "file interface" do let(:backend) { get_backend.call } describe "character device" do let(:file) { backend.file("/dev/null") } it "exists" do _(file.exist?).must_equal(true) end it "is a character device" do _(file.character_device?).must_equal(true) end it "has type :character_device" do _(file.type).must_equal(:character_device) end it "has empty content" do _(file.content).must_equal("") end it "has owner name root" do _(file.owner).must_equal("root") end it "has group name" do _(file.group).must_equal(Test.root_group(backend.os)) end it "has mode 0666" do _(file.mode).must_equal(00666) end it "checks mode? 0666" do _(file.mode?(00666)).must_equal(true) end it "has no link_path" do _(file.link_path).must_be_nil end it "has an md5sum" do _(file.md5sum).must_equal("d41d8cd98f00b204e9800998ecf8427e") end it "has an sha256sum" do _(file.sha256sum).must_equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") end it "has a modified time" do _(file.mtime).must_be_close_to(Time.now.to_i - Test.mtime / 2, Test.mtime) end it "has inode size of 0" do _(file.size).must_equal(0) end it "has selinux label handling" do res = Test.selinux_label(backend, file.path) _(file.selinux_label).must_equal(res) end it "has no product_version" do _(file.product_version).must_be_nil end it "has no file_version" do _(file.file_version).must_be_nil end end end train-3.2.28/test/integration/tests/path_file_test.rb000066400000000000000000000042331364512547200227210ustar00rootroot00000000000000# encoding: utf-8 describe "file interface" do let(:backend) { get_backend.call } describe "regular file" do let(:file) { backend.file("/tmp/file") } it "exists" do _(file.exist?).must_equal(true) end it "is a file" do _(file.file?).must_equal(true) end it "has type :file" do _(file.type).must_equal(:file) end it "has content" do _(file.content).must_equal("hello world") end it "has owner name root" do _(file.owner).must_equal("root") end it "has group name" do _(file.group).must_equal(Test.root_group(backend.os)) end it "has mode 0765" do _(file.mode).must_equal(00765) end it "checks mode? 0765" do _(file.mode?(00765)).must_equal(true) end it "doesnt check mode? 0764" do _(file.mode?(00764)).must_equal(false) end it "has no link_path" do _(file.link_path).must_be_nil end it "has an md5sum" do _(file.md5sum).must_equal("5eb63bbbe01eeed093cb22bb8f5acdc3") end it "has an sha256sum" do _(file.sha256sum).must_equal("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") end it "has a modified time" do _(file.mtime).must_be_close_to(Time.now.to_i - Test.mtime / 2, Test.mtime) end it "has size" do # Must be around 11 Bytes, +- 4 _(file.size).must_be_close_to(11, 4) end it "has selinux label handling" do res = Test.selinux_label(backend, file.path) _(file.selinux_label).must_equal(res) end it "has no product_version" do _(file.product_version).must_be_nil end it "has no file_version" do _(file.file_version).must_be_nil end it "provides a json representation" do j = file.to_json _(j).must_be_kind_of Hash _(j["type"]).must_equal :file end end describe "regular file" do let(:file) { backend.file("/tmp/sfile") } it "has mode 7765" do _(file.mode).must_equal(07765) end end describe "regular file" do let(:file) { backend.file("/tmp/spaced file") } it "has content" do _(file.content).must_equal("hello space") end end end train-3.2.28/test/integration/tests/path_folder_test.rb000066400000000000000000000040231364512547200232520ustar00rootroot00000000000000# encoding: utf-8 describe "file interface" do let(:backend) { get_backend.call } describe "a folder" do let(:file) { backend.file("/tmp/folder") } it "exists" do _(file.exist?).must_equal(true) end it "is a directory" do _(file.directory?).must_equal(true) end it "has type :directory" do _(file.type).must_equal(:directory) end case get_backend.call.os[:family] when "freebsd" it "has freebsd folder content behavior" do _(file.content).must_equal("\u0003\u0000") end it "has an md5sum" do _(file.md5sum).must_equal("598f4fe64aefab8f00bcbea4c9239abf") end it "has an sha256sum" do _(file.sha256sum).must_equal("9b4fb24edd6d1d8830e272398263cdbf026b97392cc35387b991dc0248a628f9") end else it "has no content" do _(file.content).must_be_nil end it "raises an error if md5sum is attempted" do _ { file.md5sum }.must_raise RuntimeError end it "raises an error if sha256sum is attempted" do _ { file.sha256sum }.must_raise RuntimeError end end it "has owner name root" do _(file.owner).must_equal("root") end it "has group name" do _(file.group).must_equal(Test.root_group(backend.os)) end it "has mode 0567" do _(file.mode).must_equal(00567) end it "checks mode? 0567" do _(file.mode?(00567)).must_equal(true) end it "has no link_path" do _(file.link_path).must_be_nil end it "has a modified time" do _(file.mtime).must_be_close_to(Time.now.to_i - Test.mtime / 2, Test.mtime) end it "has inode size" do _(file.size).must_be_close_to(4096, 4096) end it "has selinux label handling" do res = Test.selinux_label(backend, file.path) _(file.selinux_label).must_equal(res) end it "has no product_version" do _(file.product_version).must_be_nil end it "has no file_version" do _(file.file_version).must_be_nil end end end train-3.2.28/test/integration/tests/path_missing_test.rb000066400000000000000000000027611364512547200234570ustar00rootroot00000000000000# encoding: utf-8 describe "file interface" do let(:backend) { get_backend.call } describe "a path that doesnt exist" do let(:file) do backend.file("/do_not_create_this_path_please_or_my_tests_will_fail") end it "does not exist" do _(file.exist?).must_equal(false) end it "is not a file" do _(file.file?).must_equal(false) end it "has type nil" do _(file.type).must_be_nil end it "has no content" do _(file.content).must_be_nil end it "has no owner" do _(file.owner).must_be_nil end it "has no group" do _(file.group).must_be_nil end it "has mode nil" do _(file.mode).must_be_nil end it "checks mode? nil" do _(file.mode?(nil)).must_equal(true) end it "has no link_path" do _(file.link_path).must_be_nil end it "raises an error if md5sum is attempted" do _ { file.md5sum }.must_raise RuntimeError end it "raises an error if sha256sum is attempted" do _ { file.sha256sum }.must_raise RuntimeError end it "has a modified time" do _(file.mtime).must_be_nil end it "has inode size" do # Must be around 11 Bytes, +- 4 _(file.size).must_be_nil end it "has no selinux_label" do _(file.selinux_label).must_be_nil end it "has no product_version" do _(file.product_version).must_be_nil end it "has no file_version" do _(file.file_version).must_be_nil end end end train-3.2.28/test/integration/tests/path_pipe_test.rb000066400000000000000000000024061364512547200227370ustar00rootroot00000000000000# encoding: utf-8 describe "file interface" do let(:backend) { get_backend.call } describe "pipe / fifo" do let(:file) { backend.file("/tmp/pipe") } it "exists" do _(file.exist?).must_equal(true) end it "is a pipe" do _(file.pipe?).must_equal(true) end it "has type :pipe" do _(file.type).must_equal(:pipe) end it "has owner name root" do _(file.owner).must_equal("root") end it "has group name" do _(file.group).must_equal(Test.root_group(backend.os)) end it "has mode 0644" do _(file.mode).must_equal(00644) end it "checks mode? 0644" do _(file.mode?(00644)).must_equal(true) end it "has no link_path" do _(file.link_path).must_be_nil end it "has a modified time" do _(file.mtime).must_be_close_to(Time.now.to_i - Test.mtime / 2, Test.mtime) end it "has inode size of 0" do _(file.size).must_equal(0) end it "has selinux label handling" do res = Test.selinux_label(backend, file.path) _(file.selinux_label).must_equal(res) end it "has no product_version" do _(file.product_version).must_be_nil end it "has no file_version" do _(file.file_version).must_be_nil end end end train-3.2.28/test/integration/tests/path_symlink_test.rb000066400000000000000000000037741364512547200235010ustar00rootroot00000000000000# encoding: utf-8 describe "file interface" do let(:backend) { get_backend.call } describe "symlink file" do let(:file) { backend.file("/tmp/symlink") } it "exists" do _(file.exist?).must_equal(true) end it "is a symlink" do _(file.symlink?).must_equal(true) end it "is pointing to a file" do _(file.file?).must_equal(true) end it "is not pointing to a folder" do _(file.directory?).must_equal(false) end it "has type :file" do _(file.type).must_equal(:file) end it "has content" do _(file.content).must_equal("hello world") end it "has owner name root" do _(file.owner).must_equal("root") end it "has uid 0" do _(file.uid).must_equal(0) end it "has group name" do _(file.group).must_equal(Test.root_group(backend.os)) end it "has gid 0" do _(file.gid).must_equal(0) end it "has mode 0777" do _(file.source.mode).must_equal(00777) end it "has mode 0765" do _(file.mode).must_equal(00765) end it "checks mode? 0765" do _(file.mode?(00765)).must_equal(true) end it "has link_path" do _(file.link_path).must_equal("/tmp/file") end it "has an md5sum" do _(file.md5sum).must_equal("5eb63bbbe01eeed093cb22bb8f5acdc3") end it "has an sha256sum" do _(file.sha256sum).must_equal("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") end it "has a modified time" do _(file.mtime).must_be_close_to(Time.now.to_i - Test.mtime / 2, Test.mtime) end it "has size" do # Must be around 11 Bytes, +- 4 _(file.size).must_be_close_to(11, 4) end it "has selinux label handling" do res = Test.selinux_label(backend, file.path) _(file.selinux_label).must_equal(res) end it "has no product_version" do _(file.product_version).must_be_nil end it "has no file_version" do _(file.file_version).must_be_nil end end end train-3.2.28/test/integration/tests/run_command_test.rb000066400000000000000000000017311364512547200232700ustar00rootroot00000000000000# encoding: utf-8 describe "run_command" do let(:backend) { get_backend.call } it "can echo commands" do res = backend.run_command("echo hello world") _(res.stdout).must_equal("hello world\n") _(res.stderr).must_equal("") _(res.exit_status).must_equal(0) end it "can run frozen commands" do res = backend.run_command("echo hello world".freeze) _(res.stdout).must_equal("hello world\n") _(res.stderr).must_equal("") _(res.exit_status).must_equal(0) end it "can echo commands to stderr" do # TODO: Specinfra often fails on this test. # Fix and re-enable it. res = backend.run_command(">&2 echo hello world") _(res.stdout).must_equal("") _(res.stderr).must_equal("hello world\n") _(res.exit_status).must_equal(0) end it "prints a correct exit status" do res = backend.run_command("exit 123") _(res.stdout).must_equal("") _(res.stderr).must_equal("") _(res.exit_status).must_equal(123) end end train-3.2.28/test/unit/000077500000000000000000000000001364512547200146725ustar00rootroot00000000000000train-3.2.28/test/unit/extras/000077500000000000000000000000001364512547200162005ustar00rootroot00000000000000train-3.2.28/test/unit/extras/command_wrapper_test.rb000066400000000000000000000150561364512547200227510ustar00rootroot00000000000000# encoding: utf-8 # author: Dominik Richter # author: Christoph Hartmann require "helper" require "train/transports/mock" require "train/extras" require "base64" describe "linux command" do let(:cls) { Train::Extras::LinuxCommand } let(:cmd) { rand.to_s } let(:backend) do backend = Train::Transports::Mock.new.connection backend.mock_os({ family: "linux" }) backend end describe "sudo wrapping" do it "wraps commands in sudo" do lc = cls.new(backend, { sudo: true }) _(lc.run(cmd)).must_equal "sudo #{cmd}" end it "doesnt wrap commands in sudo if user == root" do lc = cls.new(backend, { sudo: true, user: "root" }) _(lc.run(cmd)).must_equal cmd end it "wraps commands in sudo with all options" do opts = rand.to_s lc = cls.new(backend, { sudo: true, sudo_options: opts }) _(lc.run(cmd)).must_equal "sudo #{opts} #{cmd}" end it "runs commands in sudo with password" do pw = rand.to_s lc = cls.new(backend, { sudo: true, sudo_password: pw }) bpw = Base64.strict_encode64(pw + "\n") _(lc.run(cmd)).must_equal "echo #{bpw} | base64 --decode | sudo -S #{cmd}" end it "wraps commands in sudo_command instead of sudo" do sudo_command = rand.to_s lc = cls.new(backend, { sudo: true, sudo_command: sudo_command }) _(lc.run(cmd)).must_equal "#{sudo_command} #{cmd}" end it "wraps commands in sudo_command with all options" do opts = rand.to_s sudo_command = rand.to_s lc = cls.new(backend, { sudo: true, sudo_command: sudo_command, sudo_options: opts }) _(lc.run(cmd)).must_equal "#{sudo_command} #{opts} #{cmd}" end it "runs commands in sudo_command with password" do pw = rand.to_s sudo_command = rand.to_s lc = cls.new(backend, { sudo: true, sudo_command: sudo_command, sudo_password: pw }) bpw = Base64.strict_encode64(pw + "\n") _(lc.run(cmd)).must_equal "echo #{bpw} | base64 --decode | #{sudo_command} -S #{cmd}" end end describe "shell wrapping" do it "wraps commands in a default shell with login" do lc = cls.new(backend, { shell: true, shell_options: "--login" }) bcmd = Base64.strict_encode64(cmd) _(lc.run(cmd)).must_equal "echo #{bcmd} | base64 --decode | $SHELL --login" end it "wraps sudo commands in a default shell with login" do lc = cls.new(backend, { sudo: true, shell: true, shell_options: "--login" }) bcmd = Base64.strict_encode64("sudo #{cmd}") _(lc.run(cmd)).must_equal "echo #{bcmd} | base64 --decode | $SHELL --login" end it "wraps sudo commands and sudo passwords in a default shell with login" do pw = rand.to_s lc = cls.new(backend, { sudo: true, sudo_password: pw, shell: true, shell_options: "--login" }) bpw = Base64.strict_encode64(pw + "\n") bcmd = Base64.strict_encode64("echo #{bpw} | base64 --decode | sudo -S #{cmd}") _(lc.run(cmd)).must_equal "echo #{bcmd} | base64 --decode | $SHELL --login" end it "wraps commands in a default shell when shell is true" do lc = cls.new(backend, { shell: true }) bcmd = Base64.strict_encode64(cmd) _(lc.run(cmd)).must_equal "echo #{bcmd} | base64 --decode | $SHELL" end it "doesnt wrap commands in a shell when shell is false" do lc = cls.new(backend, { shell: false }) _(lc.run(cmd)).must_equal cmd end it "wraps commands in a `shell` instead of default shell" do lc = cls.new(backend, { shell: true, shell_command: "/bin/bash" }) bcmd = Base64.strict_encode64(cmd) _(lc.run(cmd)).must_equal "echo #{bcmd} | base64 --decode | /bin/bash" end it "wraps commands in a default shell with login" do lc = cls.new(backend, { shell: true, shell_command: "/bin/bash", shell_options: "--login" }) bcmd = Base64.strict_encode64(cmd) _(lc.run(cmd)).must_equal "echo #{bcmd} | base64 --decode | /bin/bash --login" end end describe "#verify" do def mock_connect_result(stderr, exit_status) OpenStruct.new(stdout: "", stderr: stderr, exit_status: exit_status) end it "returns nil on success" do backend.stubs(:run_command).returns(mock_connect_result(nil, 0)) lc = cls.new(backend, { sudo: true }) _(lc.verify).must_be_nil end it "error message for bad sudo password" do backend.stubs(:run_command).returns(mock_connect_result("Sorry, try again", 1)) lc = cls.new(backend, { sudo: true }) err = _ { lc.verify! }.must_raise Train::UserError _(err.message).must_match(/Sudo failed: Wrong sudo password./) end it "error message for sudo password required" do backend.stubs(:run_command).returns(mock_connect_result("sudo: no tty present and no askpass program specified", 1)) lc = cls.new(backend, { sudo: true }) err = _ { lc.verify! }.must_raise Train::UserError _(err.message).must_match(/Sudo requires a password, please configure it./) end it "error message for sudo: command not found" do backend.stubs(:run_command).returns(mock_connect_result("sudo: command not found", 1)) lc = cls.new(backend, { sudo: true }) err = _ { lc.verify! }.must_raise Train::UserError _(err.message).must_match(/Can't find sudo command. Please either install and configure it on the target or deactivate sudo./) end it "error message for requires tty" do backend.stubs(:run_command).returns(mock_connect_result("sudo: sorry, you must have a tty to run sudo", 1)) lc = cls.new(backend, { sudo: true }) err = _ { lc.verify! }.must_raise Train::UserError _(err.message).must_match(/Sudo failed: Sudo requires a TTY. Please see the README/) end it "error message for other sudo related errors" do backend.stubs(:run_command).returns(mock_connect_result("Other sudo related error", 1)) lc = cls.new(backend, { sudo: true }) err = _ { lc.verify! }.must_raise Train::UserError _(err.message).must_match(/Other sudo related error/) end end end describe "windows command" do let(:cls) { Train::Extras::WindowsCommand } let(:cmd) { rand.to_s } let(:backend) do backend = Train::Transports::Mock.new.connection backend.mock_os({ family: "windows" }) backend end describe "shell wrapping" do it "wraps commands in a default powershell" do lc = cls.new(backend, { shell: true }) wcmd = "$ProgressPreference='SilentlyContinue';" + cmd bcmd = Base64.strict_encode64(wcmd.encode("UTF-16LE", "UTF-8")) _(lc.run(cmd)).must_equal "powershell -NoProfile -EncodedCommand #{bcmd}" end end end train-3.2.28/test/unit/extras/stat_test.rb000066400000000000000000000167121364512547200205460ustar00rootroot00000000000000require "helper" require "train/extras" require "train/transports/mock" describe "stat" do let(:cls) { Train::Extras::Stat } describe "find_type" do let(:random_mode) { (rand * 1000).to_i } it "detects :unknown types" do _(cls.find_type(random_mode)).must_equal :unknown end it "detects sockets" do _(cls.find_type(00140755)).must_equal :socket end it "detects symlinks" do _(cls.find_type(00120755)).must_equal :symlink end it "detects files" do _(cls.find_type(00100755)).must_equal :file end it "detects block devices" do _(cls.find_type(00060755)).must_equal :block_device end it "detects directories" do _(cls.find_type(00040755)).must_equal :directory end it "detects character devices" do _(cls.find_type(00020755)).must_equal :character_device end it "detects pipes" do _(cls.find_type(00010755)).must_equal :pipe end end describe "linux stat" do let(:backend) do mock = Train::Transports::Mock.new(verbose: true).connection mock.mock_os({ name: "ubuntu", family: "debian", release: "15.04", arch: "x86_64" }) mock.commands = { "stat /path 2>/dev/null --printf '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'" => mock.mock_command("", "", "", 0), "stat /path-stat 2>/dev/null --printf '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'" => mock.mock_command("", "360\n43ff\nroot\n0\nrootz\n1\n1444520846\n1444522445\n?", "", 0), } mock end it "ignores wrong stat results" do _(cls.linux_stat("/path", backend, false)).must_equal({}) end it "reads correct stat results" do _(cls.linux_stat("/path-stat", backend, false)).must_equal({ type: :directory, mode: 01777, owner: "root", uid: 0, group: "rootz", gid: 1, mtime: 1444522445, size: 360, selinux_label: nil, }) end end describe "esx stat" do let(:backend) do mock = Train::Transports::Mock.new(verbose: true).connection mock.mock_os({ name: "vmkernel", family: "esx", release: "5" }) mock.commands = { "stat /path 2>/dev/null -c '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'" => mock.mock_command("", "", "", 0), "stat /path-stat 2>/dev/null -c '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'" => mock.mock_command("", "360\n43ff\nroot\n0\nrootz\n1\n1444520846\n1444522445\n?", "", 0), } mock end it "ignores wrong stat results" do _(cls.linux_stat("/path", backend, false)).must_equal({}) end it "reads correct stat results" do _(cls.linux_stat("/path-stat", backend, false)).must_equal({ type: :directory, mode: 01777, owner: "root", uid: 0, group: "rootz", gid: 1, mtime: 1444522445, size: 360, selinux_label: nil, }) end end describe "alpine stat" do let(:backend) do mock = Train::Transports::Mock.new(verbose: true).connection mock.mock_os({ name: "alpine", family: "alpine", release: nil }) mock.commands = { "stat /path 2>/dev/null -c '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'" => mock.mock_command("", "", "", 0), "stat /path-stat 2>/dev/null -c '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'" => mock.mock_command("", "360\n43ff\nroot\n0\nrootz\n1\n1444520846\n1444522445\n?", "", 0), } mock end it "ignores wrong stat results" do _(cls.linux_stat("/path", backend, false)).must_equal({}) end it "reads correct stat results" do _(cls.linux_stat("/path-stat", backend, false)).must_equal({ type: :directory, mode: 01777, owner: "root", uid: 0, group: "rootz", gid: 1, mtime: 1444522445, size: 360, selinux_label: nil, }) end end describe "yocto stat" do let(:backend) do mock = Train::Transports::Mock.new(verbose: true).connection mock.mock_os({ name: "yocto", family: "yocto", release: nil }) mock.commands = { "stat /path 2>/dev/null -c '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'" => mock.mock_command("", "", "", 0), "stat /path-stat 2>/dev/null -c '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'" => mock.mock_command("", "360\n43ff\nroot\n0\nrootz\n1\n1444520846\n1444522445\n?", "", 0), } mock end it "ignores wrong stat results" do _(cls.linux_stat("/path", backend, false)).must_equal({}) end it "reads correct stat results" do _(cls.linux_stat("/path-stat", backend, false)).must_equal({ type: :directory, mode: 01777, owner: "root", uid: 0, group: "rootz", gid: 1, mtime: 1444522445, size: 360, selinux_label: nil, }) end end describe "bsd stat" do let(:backend) { Minitest::Mock.new } it "ignores failed stat results" do res = Minitest::Mock.new res.expect :stdout, "....." res.expect :exit_status, 1 backend.expect :run_command, res, [String] _(cls.bsd_stat("/path", backend, false)).must_equal({}) end it "ignores wrong stat results" do res = Minitest::Mock.new res.expect :stdout, "" res.expect :exit_status, 0 backend.expect :run_command, res, [String] _(cls.bsd_stat("/path", backend, false)).must_equal({}) end it "reads correct stat results" do res = Minitest::Mock.new res.expect :stdout, "360\n41777\nroot\n0\nrootz\n1\n1444520846\n1444522445" res.expect :exit_status, 0 backend.expect :run_command, res, [String] _(cls.bsd_stat("/path", backend, false)).must_equal({ type: :directory, mode: 01777, owner: "root", uid: 0, group: "rootz", gid: 1, mtime: 1444522445, size: 360, selinux_label: nil, }) end end describe "aix stat" do let(:backend) { Minitest::Mock.new } it "ignores failed stat results" do res = Minitest::Mock.new res.expect :stdout, "....." res.expect :exit_status, 1 backend.expect :run_command, res, [String] _(cls.aix_stat("/path", backend, false)).must_equal({}) end it "ignores wrong stat results" do res = Minitest::Mock.new res.expect :stdout, "" res.expect :exit_status, 0 backend.expect :run_command, res, [String] _(cls.aix_stat("/path", backend, false)).must_equal({}) end it "reads correct stat results" do res = Minitest::Mock.new res.expect :stdout, "41777\nroot\n0\nrootz\n1\n1444522445\n360\n" res.expect :exit_status, 0 backend.expect :run_command, res, [String] _(cls.aix_stat("/path", backend, false)).must_equal({ type: :directory, mode: 01777, owner: "root", uid: 0, group: "rootz", gid: 1, mtime: 1444522445, size: 360, selinux_label: nil, }) end end end train-3.2.28/test/unit/file/000077500000000000000000000000001364512547200156115ustar00rootroot00000000000000train-3.2.28/test/unit/file/local/000077500000000000000000000000001364512547200167035ustar00rootroot00000000000000train-3.2.28/test/unit/file/local/unix_test.rb000066400000000000000000000165531364512547200212640ustar00rootroot00000000000000require "helper" require "train/file/local/unix" require "train/transports/mock" require "train/transports/local" class FileTester < Train::File::Local::Unix def type :file end end describe Train::File::Local::Unix do let(:cls) { Train::File::Local::Unix } let(:backend) do backend = Train::Transports::Mock.new.connection backend.mock_os({ family: "linux" }) backend end it "checks a mounted path" do backend.mock_command("mount | grep -- ' on /mount/path '", rand.to_s) _(cls.new(backend, "/mount/path").mounted?).must_equal true end describe "file metadata" do before do # something about the way meta_stub works blows out on windows skip "not on windows" if windows? end # there is zero need to instantiate this OVER and over, so just do it once. transport = Train::Transports::Local.new connection = transport.connection let(:stat) { Struct.new(:mode, :size, :mtime, :uid, :gid) } let(:uid) { rand } let(:gid) { rand } let(:statres) { stat.new(00140755, rand, (rand * 100).to_i, uid, gid) } def meta_stub(method, param, &block) pwres = Struct.new(:name) Etc.stub :getpwuid, pwres.new("owner") do Etc.stub :getgrgid, pwres.new("group") do File.stub method, param do yield end end end end it "recognizes type" do meta_stub :stat, statres do _(connection.file(rand.to_s).stat[:type]).must_equal :socket end end it "recognizes mode" do meta_stub :stat, statres do _(connection.file(rand.to_s).stat[:mode]).must_equal 00755 end end it "recognizes mtime" do meta_stub :stat, statres do _(connection.file(rand.to_s).stat[:mtime]).must_equal statres.mtime end end it "recognizes size" do meta_stub :stat, statres do _(connection.file(rand.to_s).stat[:size]).must_equal statres.size end end it "recognizes uid" do meta_stub :stat, statres do _(connection.file(rand.to_s).stat[:uid]).must_equal uid end end it "recognizes gid" do meta_stub :stat, statres do _(connection.file(rand.to_s).stat[:gid]).must_equal gid end end it "recognizes owner" do meta_stub :stat, statres do _(connection.file(rand.to_s).owner).must_equal "owner" end end it "recognizes group" do meta_stub :stat, statres do _(connection.file(rand.to_s).group).must_equal "group" end end it "grouped_into" do meta_stub :stat, statres do _(connection.file(rand.to_s).grouped_into?("group")).must_equal true end end it "recognizes selinux label" do meta_stub :stat, statres do label = rand.to_s res = Train::Extras::CommandResult.new(label, nil, 0) connection.stub :run_command, res do _(connection.file(rand.to_s).selinux_label).must_equal label end end end it "recognizes source selinux label" do meta_stub :lstat, statres do label = rand.to_s res = Train::Extras::CommandResult.new(label, nil, 0) connection.stub :run_command, res do _(connection.file(rand.to_s).source.selinux_label).must_equal label end end end end describe "#unix_mode_mask" do let(:file_tester) do FileTester.new(nil, nil, false) end it "check owner mode calculation" do _(file_tester.unix_mode_mask("owner", "x")).must_equal 0100 _(file_tester.unix_mode_mask("owner", "w")).must_equal 0200 _(file_tester.unix_mode_mask("owner", "r")).must_equal 0400 end it "check group mode calculation" do _(file_tester.unix_mode_mask("group", "x")).must_equal 0010 _(file_tester.unix_mode_mask("group", "w")).must_equal 0020 _(file_tester.unix_mode_mask("group", "r")).must_equal 0040 end it "check other mode calculation" do _(file_tester.unix_mode_mask("other", "x")).must_equal 0001 _(file_tester.unix_mode_mask("other", "w")).must_equal 0002 _(file_tester.unix_mode_mask("other", "r")).must_equal 0004 end it "check all mode calculation" do _(file_tester.unix_mode_mask("all", "x")).must_equal 0111 _(file_tester.unix_mode_mask("all", "w")).must_equal 0222 _(file_tester.unix_mode_mask("all", "r")).must_equal 0444 end end describe "#md5sum" do let(:md5_checksum) { "57d4c6f9d15313fd5651317e588c035d" } let(:ruby_md5_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(md5_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("md5sum /tmp/testfile", "", "", 1) Digest::MD5.expects(:new).returns(ruby_md5_mock) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end it "calculates the correct md5sum on the `linux` platform family" do output = "#{md5_checksum} /tmp/testfile" backend.mock_command("md5sum /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end it "calculates the correct md5sum on the `darwin` platform family" do output = "#{md5_checksum} /tmp/testfile" backend.mock_os(family: "darwin") backend.mock_command("md5 -r /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end it "calculates the correct md5sum on the `solaris` platform family" do # The `digest` command doesn't output the filename by default output = "#{md5_checksum}" backend.mock_os(family: "solaris") backend.mock_command("digest -a md5 /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end end describe "#sha256sum" do let(:sha256_checksum) do "491260aaa6638d4a64c714a17828c3d82bad6ca600c9149b3b3350e91bcd283d" end let(:ruby_sha256_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(sha256_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("sha256sum /tmp/testfile", "", "", 1) Digest::SHA256.expects(:new).returns(ruby_sha256_mock) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end it "calculates the correct sha256sum on the `linux` platform family" do output = "#{sha256_checksum} /tmp/testfile" backend.mock_command("sha256sum /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end it "calculates the correct sha256sum on the `darwin` platform family" do output = "#{sha256_checksum} /tmp/testfile" backend.mock_os(family: "darwin") backend.mock_command("shasum -a 256 /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end it "calculates the correct sha256sum on the `solaris` platform family" do # The `digest` command doesn't output the filename by default output = "#{sha256_checksum}" backend.mock_os(family: "solaris") backend.mock_command("digest -a sha256 /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end end end train-3.2.28/test/unit/file/local/windows_test.rb000066400000000000000000000067161364512547200217730ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/mock" require "train/file/local/windows" describe "file common" do let(:cls) { Train::File::Local::Windows } let(:backend) do backend = Train::Transports::Mock.new.connection backend.mock_os({ family: "windows" }) backend end it "check escaping of invalid chars in path" do wf = cls.new(nil, nil) _(wf.sanitize_filename("c:/test") ).must_equal "c:/test" _(wf.sanitize_filename("c:/test directory") ).must_equal "c:/test directory" %w{ < > " * ?}.each do |char| _(wf.sanitize_filename("c:/test#{char}directory") ).must_equal "c:/testdirectory" end end it "returns file version" do out = rand.to_s backend.mock_command('[System.Diagnostics.FileVersionInfo]::GetVersionInfo("path").FileVersion', out) _(cls.new(backend, "path").file_version).must_equal out end it "returns product version" do out = rand.to_s backend.mock_command('[System.Diagnostics.FileVersionInfo]::GetVersionInfo("path").FileVersion', out) _(cls.new(backend, "path").file_version).must_equal out end it "returns owner of file" do out = rand.to_s backend.mock_command('Get-Acl "path" | select -expand Owner', out) _(cls.new(backend, "path").owner).must_equal out end describe "#md5sum" do let(:md5_checksum) { "4ce0c733cdcf1d2f78532bbd9ce3441d" } let(:filepath) { 'C:\Windows\explorer.exe' } let(:ruby_md5_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(md5_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("CertUtil -hashfile #{filepath} MD5", "", "", 1) Digest::MD5.expects(:new).returns(ruby_md5_mock) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end it "calculates the correct md5sum on the `windows` platform family" do output = <<-EOC MD5 hash of file C:\\Windows\\explorer.exe:\r 4c e0 c7 33 cd cf 1d 2f 78 53 2b bd 9c e3 44 1d\r CertUtil: -hashfile command completed successfully.\r EOC backend.mock_command("CertUtil -hashfile #{filepath} MD5", output) _(cls.new(backend, filepath).md5sum).must_equal md5_checksum end end describe "#sha256sum" do let(:sha256_checksum) do "85270240a5fd51934f0627c92b2282749d071fdc9ac351b81039ced5b10f798b" end let(:filepath) { 'C:\Windows\explorer.exe' } let(:ruby_sha256_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(sha256_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command('CertUtil -hashfile #{filepath} SHA256', "", "", 1) Digest::SHA256.expects(:new).returns(ruby_sha256_mock) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end it "calculates the correct sha256sum on the `windows` platform family" do output = <<-EOC SHA256 hash of file C:\\Windows\\explorer.exe:\r 85 27 02 40 a5 fd 51 93 4f 06 27 c9 2b 22 82 74 9d 07 1f dc 9a c3 51 b8 10 39 ce d5 b1 0f 79 8b\r CertUtil: -hashfile command completed successfully.\r EOC backend.mock_command("CertUtil -hashfile #{filepath} SHA256", output) _(cls.new(backend, filepath).sha256sum).must_equal sha256_checksum end end end train-3.2.28/test/unit/file/local_test.rb000066400000000000000000000065761364512547200203050ustar00rootroot00000000000000require "helper" require "train/transports/local" describe Train::File::Local do # there is zero need to instantiate this OVER and over, so just do it once. transport = Train::Transports::Local.new connection = transport.connection it "gets file contents" do res = rand.to_s File.stub :read, res do _(connection.file(rand.to_s).content).must_equal(res) end end { exist?: :exist?, file?: :file?, socket?: :socket?, directory?: :directory?, symlink?: :symlink?, pipe?: :pipe?, character_device?: :chardev?, block_device?: :blockdev?, }.each do |method, file_method| it "checks if file is a #{method}" do File.stub file_method.to_sym, true do _(connection.file(rand.to_s).method(method.to_sym).call).must_equal(true) end end end it "has a friendly inspect" do _(connection.inspect).must_equal "Train::Transports::Local::Connection[unknown]" end describe "#type" do it "returns the type block_device if it is block device" do File.stub :ftype, "blockSpecial" do _(connection.file(rand.to_s).type).must_equal :block_device end end it "returns the type character_device if it is character device" do File.stub :ftype, "characterSpecial" do _(connection.file(rand.to_s).type).must_equal :character_device end end it "returns the type symlink if it is symlink" do File.stub :ftype, "link" do _(connection.file(rand.to_s).type).must_equal :symlink end end it "returns the type file if it is file" do File.stub :ftype, "file" do _(connection.file(rand.to_s).type).must_equal :file end end it "returns the type directory if it is block directory" do File.stub :ftype, "directory" do _(connection.file(rand.to_s).type).must_equal :directory end end it "returns the type pipe if it is pipe" do File.stub :ftype, "fifo" do _(connection.file(rand.to_s).type).must_equal :pipe end end it "returns the type socket if it is socket" do File.stub :ftype, "socket" do _(connection.file(rand.to_s).type).must_equal :socket end end it "returns the unknown if not known" do File.stub :ftype, "unknown" do _(connection.file(rand.to_s).type).must_equal :unknown end end end describe "#path" do it "returns the path if it is not a symlink" do File.stub :symlink?, false do filename = rand.to_s _(connection.file(filename).path).must_equal filename end end it "returns the link_path if it is a symlink" do File.stub :symlink?, true do file_obj = connection.file(rand.to_s) file_obj.stub :link_path, "/path/to/resolved_link" do _(file_obj.path).must_equal "/path/to/resolved_link" end end end end describe "#link_path" do it "returns file's link path" do out = rand.to_s File.stub :realpath, out do File.stub :symlink?, true do _(connection.file(rand.to_s).link_path).must_equal out end end end end describe "#shallow_shlink_path" do it "returns file's direct link path" do out = rand.to_s File.stub :readlink, out do File.stub :symlink?, true do _(connection.file(rand.to_s).shallow_link_path).must_equal out end end end end end train-3.2.28/test/unit/file/remote/000077500000000000000000000000001364512547200171045ustar00rootroot00000000000000train-3.2.28/test/unit/file/remote/aix_test.rb000066400000000000000000000052761364512547200212630ustar00rootroot00000000000000require "helper" require "train/transports/local" require "train/file/remote/aix" require "train/transports/mock" describe Train::File::Remote::Aix do let(:cls) { Train::File::Remote::Aix } let(:backend) do backend = Train::Transports::Mock.new.connection backend.mock_os({ name: "aix", family: "unix" }) backend end it "returns a nil link_path if the object is not a symlink" do file = cls.new(backend, "path") file.stubs(:symlink?).returns(false) _(file.link_path).must_be_nil end it "returns a correct link_path" do file = cls.new(backend, "path") file.stubs(:symlink?).returns(true) backend.mock_command("perl -e 'print readlink shift' path", "our_link_path") _(file.link_path).must_equal "our_link_path" end it "returns a correct shallow_link_path" do file = cls.new(backend, "path") file.stubs(:symlink?).returns(true) backend.mock_command("perl -e 'print readlink shift' path", "our_link_path") _(file.link_path).must_equal "our_link_path" end describe "#md5sum" do let(:md5_checksum) { "57d4c6f9d15313fd5651317e588c035d" } let(:ruby_md5_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(md5_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("md5sum /tmp/testfile", "", "", 1) Digest::MD5.expects(:new).returns(ruby_md5_mock) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end it "calculates the correct md5sum on the `aix` platform family" do output = "#{md5_checksum} /tmp/testfile" backend.mock_command("md5sum /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end end describe "#sha256sum" do let(:sha256_checksum) do "491260aaa6638d4a64c714a17828c3d82bad6ca600c9149b3b3350e91bcd283d" end let(:ruby_sha256_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(sha256_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("sha256sum /tmp/testfile", "", "", 1) Digest::SHA256.expects(:new).returns(ruby_sha256_mock) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end it "calculates the correct sha256sum on the `aix` platform family" do output = "#{sha256_checksum} /tmp/testfile" backend.mock_command("sha256sum /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end end end train-3.2.28/test/unit/file/remote/linux_test.rb000066400000000000000000000141401364512547200216270ustar00rootroot00000000000000require "helper" require "train/transports/local" require "train/file/remote/linux" require "train/transports/mock" describe Train::File::Remote::Linux do let(:cls) { Train::File::Remote::Linux } let(:backend) do backend = Train::Transports::Mock.new.connection backend.mock_os({ name: "linux", family: "unix" }) backend end def mock_stat(args, out, err = "", code = 0) backend.mock_command( "stat #{args} 2>/dev/null --printf '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'", out, err, code ) end it "works on nil path" do _(cls.new(backend, nil).path).must_equal "" end it "provides the full path" do _(cls.new(backend, "/dir/file").path).must_equal "/dir/file" end it "provides the basename to a unix path" do _(cls.new(backend, "/dir/file").basename).must_equal "file" end it "reads file contents" do out = rand.to_s backend.mock_command("cat path || echo -n", out) _(cls.new(backend, "path").content).must_equal out end it "reads file contents" do backend.mock_command("cat path || echo -n", "") mock_stat("-L path", "", "some error...", 1) _(cls.new(backend, "path").content).must_be_nil end it "reads file contents" do out = rand.to_s backend.mock_command('cat /spaced\\ path || echo -n', out) _(cls.new(backend, "/spaced path").content).must_equal out end it "checks for file existance" do backend.mock_command("test -e path", true) _(cls.new(backend, "path").exist?).must_equal true end it "checks for file existance" do backend.mock_command("test -e path", nil, nil, 1) _(cls.new(backend, "path").exist?).must_equal false end it "retrieves the link path via #path()" do out = rand.to_s mock_stat("path", "13\na1ff\nz\n1001\nz\n1001\n1444573475\n1444573475\n?") backend.mock_command("readlink -n path -f", out) _(cls.new(backend, "path").path).must_equal File.join(Dir.pwd, out) end it "retrieves the link path" do out = rand.to_s mock_stat("path", "13\na1ff\nz\n1001\nz\n1001\n1444573475\n1444573475\n?") backend.mock_command("readlink -n path -f", out) _(cls.new(backend, "path").link_path).must_equal File.join(Dir.pwd, out) end it "provide the source path" do _(cls.new(backend, "path").source_path).must_equal "path" end it "checks a mounted path" do backend.mock_command("mount | grep -- ' on /mount/path '", rand.to_s) _(cls.new(backend, "/mount/path").mounted?).must_equal true end it "has nil product version" do _(cls.new(backend, "path").product_version).must_be_nil end it "has nil file version" do _(cls.new(backend, "path").file_version).must_be_nil end describe "stat on a file" do before { mock_stat("-L path", "13\na1ff\nz\n1001\nz2\n1002\n1444573475\n1444573475\nlabels") } let(:f) { cls.new(backend, "path") } it "retrieves the file type" do _(f.type).must_equal :symlink end it "retrieves the file mode" do _(f.mode).must_equal 00777 end it "retrieves the file owner" do _(f.owner).must_equal "z" end it "retrieves the file uid" do _(f.uid).must_equal 1001 end it "retrieves the file group" do _(f.group).must_equal "z2" end it "retrieves the file gid" do _(f.gid).must_equal 1002 end it "retrieves the file mtime" do _(f.mtime).must_equal 1444573475 end it "retrieves the file size" do _(f.size).must_equal 13 end it "retrieves the file selinux_label" do _(f.selinux_label).must_equal "labels" end end describe "stat on the source file" do before { mock_stat("path", "13\na1ff\nz\n1001\nz2\n1002\n1444573475\n1444573475\nlabels") } let(:f) { cls.new(backend, "path").source } it "retrieves the file type" do _(f.type).must_equal :symlink end it "retrieves the file mode" do _(f.mode).must_equal 00777 end it "retrieves the file owner" do _(f.owner).must_equal "z" end it "retrieves the file uid" do _(f.uid).must_equal 1001 end it "retrieves the file group" do _(f.group).must_equal "z2" end it "retrieves the file gid" do _(f.gid).must_equal 1002 end it "retrieves the file mtime" do _(f.mtime).must_equal 1444573475 end it "retrieves the file size" do _(f.size).must_equal 13 end it "retrieves the file selinux_label" do _(f.selinux_label).must_equal "labels" end end describe "#md5sum" do let(:md5_checksum) { "57d4c6f9d15313fd5651317e588c035d" } let(:ruby_md5_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(md5_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("md5sum /tmp/testfile", "", "", 1) Digest::MD5.expects(:new).returns(ruby_md5_mock) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end it "calculates the correct md5sum on the `linux` platform family" do output = "#{md5_checksum} /tmp/testfile" backend.mock_command("md5sum /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end end describe "#sha256sum" do let(:sha256_checksum) do "491260aaa6638d4a64c714a17828c3d82bad6ca600c9149b3b3350e91bcd283d" end let(:ruby_sha256_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(sha256_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("sha256sum /tmp/testfile", "", "", 1) Digest::SHA256.expects(:new).returns(ruby_sha256_mock) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end it "calculates the correct sha256sum on the `linux` platform family" do output = "#{sha256_checksum} /tmp/testfile" backend.mock_command("sha256sum /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end end end train-3.2.28/test/unit/file/remote/qnx_test.rb000066400000000000000000000046631364512547200213070ustar00rootroot00000000000000require "helper" require "train/transports/local" require "train/file/remote/qnx" require "train/transports/mock" describe Train::File::Remote::Qnx do let(:cls) { Train::File::Remote::Qnx } let(:backend) do backend = Train::Transports::Mock.new.connection backend.mock_os({ family: "qnx" }) backend end it "returns file contents when the file exists" do out = rand.to_s backend.mock_command("cat path", out) file = cls.new(backend, "path") file.stubs(:exist?).returns(true) _(file.content).must_equal out end it "returns nil contents when the file does not exist" do file = cls.new(backend, "path") file.stubs(:exist?).returns(false) _(file.content).must_be_nil end it "returns a file type" do backend.mock_command("file path", "blah directory blah") _(cls.new(backend, "path").type).must_equal :directory end it "returns a directory type" do backend.mock_command("file path", "blah regular file blah") _(cls.new(backend, "path").type).must_equal :file end it "raises exception for unimplemented methods" do file = cls.new(backend, "path") %w{mode owner group uid gid mtime size selinux_label link_path mounted stat}.each do |m| _ { file.send(m) }.must_raise NotImplementedError end end describe "#md5sum" do let(:md5_checksum) { "17404a596cbd0d1e6c7d23fcd845ab82" } let(:ruby_md5_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(md5_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("md5sum /tmp/testfile", "", "", 1) Digest::MD5.expects(:new).returns(ruby_md5_mock) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end end describe "#sha256sum" do let(:sha256_checksum) do "ec864fe99b539704b8872ac591067ef22d836a8d942087f2dba274b301ebe6e5" end let(:ruby_sha256_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(sha256_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("sha256sum /tmp/testfile", "", "", 1) Digest::SHA256.expects(:new).returns(ruby_sha256_mock) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end end end train-3.2.28/test/unit/file/remote/unix_test.rb000066400000000000000000000076151364512547200214640ustar00rootroot00000000000000require "helper" require "train/transports/mock" require "train/file/remote/unix" describe Train::File::Remote::Unix do let(:cls) { Train::File::Remote::Unix } let(:backend) do backend = Train::Transports::Mock.new.connection backend.mock_os({ family: "linux" }) backend end def mockup(stubs) Class.new(cls) do stubs.each do |k, v| define_method k.to_sym do v end end end.new(nil, nil, false) end describe "unix_mode_mask" do let(:fc) { mockup(type: :file) } it "check owner mode calculation" do _(fc.unix_mode_mask("owner", "x")).must_equal 0100 _(fc.unix_mode_mask("owner", "w")).must_equal 0200 _(fc.unix_mode_mask("owner", "r")).must_equal 0400 end it "check group mode calculation" do _(fc.unix_mode_mask("group", "x")).must_equal 0010 _(fc.unix_mode_mask("group", "w")).must_equal 0020 _(fc.unix_mode_mask("group", "r")).must_equal 0040 end it "check other mode calculation" do _(fc.unix_mode_mask("other", "x")).must_equal 0001 _(fc.unix_mode_mask("other", "w")).must_equal 0002 _(fc.unix_mode_mask("other", "r")).must_equal 0004 end it "check all mode calculation" do _(fc.unix_mode_mask("all", "x")).must_equal 0111 _(fc.unix_mode_mask("all", "w")).must_equal 0222 _(fc.unix_mode_mask("all", "r")).must_equal 0444 end end describe "#md5sum" do let(:md5_checksum) { "57d4c6f9d15313fd5651317e588c035d" } let(:ruby_md5_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(md5_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("md5 -r /tmp/testfile", "", "", 1) Digest::MD5.expects(:new).returns(ruby_md5_mock) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end it "calculates the correct md5sum on the `darwin` platform family" do output = "#{md5_checksum} /tmp/testfile" backend.mock_os(family: "darwin") backend.mock_command("md5 -r /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end it "calculates the correct md5sum on the `solaris` platform family" do # The `digest` command doesn't output the filename by default output = "#{md5_checksum}" backend.mock_os(family: "solaris") backend.mock_command("digest -a md5 /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end end describe "#sha256sum" do let(:sha256_checksum) do "491260aaa6638d4a64c714a17828c3d82bad6ca600c9149b3b3350e91bcd283d" end let(:ruby_sha256_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(sha256_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("shasum -a 256 /tmp/testfile", "", "", 1) Digest::SHA256.expects(:new).returns(ruby_sha256_mock) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end it "calculates the correct sha256sum on the `darwin` platform family" do output = "#{sha256_checksum} /tmp/testfile" backend.mock_os(family: "darwin") backend.mock_command("shasum -a 256 /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end it "calculates the correct sha256sum on the `solaris` platform family" do # The `digest` command doesn't output the filename by default output = "#{sha256_checksum}" backend.mock_os(family: "solaris") backend.mock_command("digest -a sha256 /tmp/testfile", output) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end end end train-3.2.28/test/unit/file/remote/windows_test.rb000066400000000000000000000047561364512547200221760ustar00rootroot00000000000000require "helper" require "train/transports/mock" require "train/file/remote/windows" describe Train::File::Remote::Windows do let(:cls) { Train::File::Remote::Windows } let(:backend) do backend = Train::Transports::Mock.new.connection backend.mock_os({ family: "windows" }) backend end describe "#md5sum" do let(:md5_checksum) { "4ce0c733cdcf1d2f78532bbd9ce3441d" } let(:filepath) { 'C:\Windows\explorer.exe' } let(:ruby_md5_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(md5_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("CertUtil -hashfile #{filepath} MD5", "", "", 1) Digest::MD5.expects(:new).returns(ruby_md5_mock) _(cls.new(backend, "/tmp/testfile").md5sum).must_equal md5_checksum end it "calculates the correct md5sum on the `windows` platform family" do output = <<-EOC MD5 hash of file C:\\Windows\\explorer.exe:\r 4c e0 c7 33 cd cf 1d 2f 78 53 2b bd 9c e3 44 1d\r CertUtil: -hashfile command completed successfully.\r EOC backend.mock_command("CertUtil -hashfile #{filepath} MD5", output) _(cls.new(backend, filepath).md5sum).must_equal md5_checksum end end describe "#sha256sum" do let(:sha256_checksum) do "85270240a5fd51934f0627c92b2282749d071fdc9ac351b81039ced5b10f798b" end let(:filepath) { 'C:\Windows\explorer.exe' } let(:ruby_sha256_mock) do checksum_mock = mock checksum_mock.expects(:update).returns("") checksum_mock.expects(:hexdigest).returns(sha256_checksum) checksum_mock end it "defaults to a Ruby based checksum if other methods fail" do backend.mock_command("CertUtil -hashfile #{filepath} SHA256", "", "", 1) Digest::SHA256.expects(:new).returns(ruby_sha256_mock) _(cls.new(backend, "/tmp/testfile").sha256sum).must_equal sha256_checksum end it "calculates the correct sha256sum on the `windows` platform family" do output = <<-EOC SHA256 hash of file C:\\Windows\\explorer.exe:\r 85 27 02 40 a5 fd 51 93 4f 06 27 c9 2b 22 82 74 9d 07 1f dc 9a c3 51 b8 10 39 ce d5 b1 0f 79 8b\r CertUtil: -hashfile command completed successfully.\r EOC backend.mock_command("CertUtil -hashfile #{filepath} SHA256", output) _(cls.new(backend, filepath).sha256sum).must_equal sha256_checksum end end end train-3.2.28/test/unit/file/remote_test.rb000066400000000000000000000031331364512547200204700ustar00rootroot00000000000000require "helper" require "train/file/remote" describe Train::File::Remote do let(:cls) { Train::File::Remote } def mockup(stubs) Class.new(cls) do stubs.each do |k, v| define_method k.to_sym do v end end end.new(nil, nil, false) end describe "basename helper" do def fc(path) mockup(type: :file, path: path) end it "works with an empty path" do _(fc("").basename).must_equal "" end it "separates a simple path (defaults to unix mode)" do _(fc("/dir/file").basename).must_equal "file" end it "separates a simple path (Unix mode)" do _(fc("/dir/file").basename(nil, "/")).must_equal "file" end it "separates a simple path (Windows mode)" do _(fc('C:\dir\file').basename(nil, '\\')).must_equal "file" end it "identifies a folder name (Unix mode)" do _(fc("/dir/file/").basename(nil, "/")).must_equal "file" end it "identifies a folder name (Windows mode)" do _(fc('C:\dir\file\\').basename(nil, '\\')).must_equal "file" end it "ignores tailing separators (Unix mode)" do _(fc("/dir/file///").basename(nil, "/")).must_equal "file" end it "ignores tailing separators (Windows mode)" do _(fc('C:\dir\file\\\\\\').basename(nil, '\\')).must_equal "file" end it "doesnt work with backward slashes (Unix mode)" do _(fc('C:\dir\file').basename(nil, "/")).must_equal 'C:\\dir\file' end it "doesnt work with forward slashes (Windows mode)" do _(fc("/dir/file").basename(nil, '\\')).must_equal "/dir/file" end end end train-3.2.28/test/unit/file_test.rb000066400000000000000000000062401364512547200171770ustar00rootroot00000000000000require "helper" describe Train::File do let(:cls) { Train::File } let(:new_cls) { cls.new(nil, "/temp/file", false) } def mockup(stubs) Class.new(cls) do stubs.each do |k, v| define_method k.to_sym do v end end end.new(nil, nil, false) end it "has the default type of unknown" do _(new_cls.type).must_equal :unknown end it "throws Not implemented error for exist?" do # proc { Train.validate_backend({ host: rand }) }.must_raise Train::UserError _ { new_cls.exist? }.must_raise NotImplementedError end it "throws Not implemented error for mode" do _ { new_cls.mode }.must_raise NotImplementedError end it "throws Not implemented error for owner" do _ { new_cls.owner }.must_raise NotImplementedError end it "throws Not implemented error for group" do _ { new_cls.group }.must_raise NotImplementedError end it "throws Not implemented error for uid" do _ { new_cls.uid }.must_raise NotImplementedError end it "throws Not implemented error for gid" do _ { new_cls.gid }.must_raise NotImplementedError end it "throws Not implemented error for content" do _ { new_cls.content }.must_raise NotImplementedError end it "throws Not implemented error for mtime" do _ { new_cls.mtime }.must_raise NotImplementedError end it "throws Not implemented error for size" do _ { new_cls.size }.must_raise NotImplementedError end it "throws Not implemented error for selinux_label" do _ { new_cls.selinux_label }.must_raise NotImplementedError end it "return path of file" do _(new_cls.path).must_equal("/temp/file") end it "set product_version to nil" do _(new_cls.product_version).must_be_nil end it "set product_version to nil" do _(new_cls.file_version).must_be_nil end describe "type" do it "recognized type == file" do fc = mockup(type: :file) _(fc.file?).must_equal true end it "recognized type == block_device" do fc = mockup(type: :block_device) _(fc.block_device?).must_equal true end it "recognized type == character_device" do fc = mockup(type: :character_device) _(fc.character_device?).must_equal true end it "recognized type == socket" do fc = mockup(type: :socket) _(fc.socket?).must_equal true end it "recognized type == directory" do fc = mockup(type: :directory) _(fc.directory?).must_equal true end it "recognized type == pipe" do fc = mockup(type: :pipe) _(fc.pipe?).must_equal true end it "recognized type == symlink" do fc = mockup(type: :symlink) _(fc.symlink?).must_equal true end end describe "version" do it "recognized wrong version" do fc = mockup(product_version: rand, file_version: rand) _(fc.version?(rand)).must_equal false end it "recognized product_version" do x = rand fc = mockup(product_version: x, file_version: rand) _(fc.version?(x)).must_equal true end it "recognized file_version" do x = rand fc = mockup(product_version: rand, file_version: x) _(fc.version?(x)).must_equal true end end end train-3.2.28/test/unit/platforms/000077500000000000000000000000001364512547200167015ustar00rootroot00000000000000train-3.2.28/test/unit/platforms/detect/000077500000000000000000000000001364512547200201515ustar00rootroot00000000000000train-3.2.28/test/unit/platforms/detect/os_common_test.rb000066400000000000000000000052051364512547200235300ustar00rootroot00000000000000# encoding: utf-8 require "helper" class OsDetectLinuxTester attr_reader :platform include Train::Platforms::Detect::Helpers::OSCommon def initialize @platform = {} end end describe "os_common" do let(:detector) { OsDetectLinuxTester.new } describe "winrm? check" do it "return winrm? true" do OsDetectLinuxTester.any_instance.stubs(:backend_name).returns("TrainPlugins::WinRM::Connection") _(detector.winrm?).must_equal(true) end it "return winrm? false when winrm is not loaded" do OsDetectLinuxTester.any_instance.stubs(:backend_name).returns("Something::Else") _(detector.winrm?).must_equal(false) end end describe "unix file contents" do it "return new file contents" do be = mock("Backend") output = mock("Output", exit_status: 0) output.expects(:stdout).returns("test") be.stubs(:run_command).with("test -f /etc/fstab && cat /etc/fstab").returns(output) detector.instance_variable_set(:@backend, be) detector.instance_variable_set(:@files, {}) _(detector.unix_file_contents("/etc/fstab")).must_equal("test") end it "return new file contents cached" do be = mock("Backend") detector.instance_variable_set(:@backend, be) detector.instance_variable_set(:@files, { "/etc/profile" => "test" }) _(detector.unix_file_contents("/etc/profile")).must_equal("test") end end describe "unix file exist?" do it "file does exist" do be = mock("Backend") be.stubs(:run_command).with("test -f /etc/test").returns(mock("Output", exit_status: 0)) detector.instance_variable_set(:@backend, be) _(detector.unix_file_exist?("/etc/test")).must_equal(true) end end describe "#detect_linux_arch" do it "uname m call" do be = mock("Backend") be.stubs(:run_command).with("uname -m").returns(mock("Output", stdout: "x86_64\n")) detector.instance_variable_set(:@backend, be) detector.instance_variable_set(:@uname, {}) _(detector.unix_uname_m).must_equal("x86_64") end it "uname s call" do be = mock("Backend") be.stubs(:run_command).with("uname -s").returns(mock("Output", stdout: "linux")) detector.instance_variable_set(:@backend, be) detector.instance_variable_set(:@uname, {}) _(detector.unix_uname_s).must_equal("linux") end it "uname r call" do be = mock("Backend") be.stubs(:run_command).with("uname -r").returns(mock("Output", stdout: "17.0.0\n")) detector.instance_variable_set(:@backend, be) detector.instance_variable_set(:@uname, {}) _(detector.unix_uname_r).must_equal("17.0.0") end end end train-3.2.28/test/unit/platforms/detect/os_linux_test.rb000066400000000000000000000101141364512547200233720ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/mock" class OsDetectLinuxTester include Train::Platforms::Detect::Helpers::OSCommon end describe "os_linux" do let(:detector) { OsDetectLinuxTester.new } describe "redhatish_platform cleaner" do it "normal redhat" do _(detector.redhatish_platform("Red Hattter")).must_equal("redhat") end it "custom redhat" do _(detector.redhatish_platform("Centos Pro 11")).must_equal("centos") end end describe "redhatish_version cleaner" do it "normal rawhide" do _(detector.redhatish_version("18 (Rawhide) Pro")).must_equal("18 (rawhide)") end it "normal linux" do _(detector.redhatish_version("derived from Ubuntu Linux 11")).must_equal("11") end it "amazon linux 2 new release naming schema" do _(detector.redhatish_version("Amazon Linux release 2 (Karoo)")).must_equal("2") end it "amazon linux 2 old release naming schema" do _(detector.redhatish_version("Amazon Linux 2")).must_equal("2") end end describe "lsb parse" do it "lsb config" do lsb = "DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=14.06\nDISTRIB_CODENAME=xenial" expect = { id: "Ubuntu", release: "14.06", codename: "xenial" } _(detector.lsb_config(lsb)).must_equal(expect) end it "lsb releasel" do lsb = "Distributor ID: Ubuntu\nRelease: 14.06\nCodename: xenial" expect = { id: "Ubuntu", release: "14.06", codename: "xenial" } _(detector.lsb_release(lsb)).must_equal(expect) end end describe "#linux_os_release" do describe "when no os-release data is available" do it "returns nil" do detector.expects(:unix_file_contents).with("/etc/os-release").returns(nil) _(detector.linux_os_release).must_be_nil end end end describe "when os-release data exists with no CISCO_RELEASE_INFO" do let(:os_release) { { "KEY1" => "VALUE1" } } it "returns a correct hash" do detector.expects(:unix_file_contents).with("/etc/os-release").returns("os-release data") detector.expects(:parse_os_release_info).with("os-release data").returns(os_release) _(detector.linux_os_release["KEY1"]).must_equal("VALUE1") end end describe "when os-release data exists with CISCO_RELEASE_INFO" do let(:os_release) { { "KEY1" => "VALUE1", "CISCO_RELEASE_INFO" => "cisco_file" } } let(:cisco_release) { { "KEY1" => "NEWVALUE1", "KEY2" => "VALUE2" } } it "returns a correct hash" do detector.expects(:unix_file_contents).with("/etc/os-release").returns("os-release data") detector.expects(:unix_file_contents).with("cisco_file").returns("cisco data") detector.expects(:parse_os_release_info).with("os-release data").returns(os_release) detector.expects(:parse_os_release_info).with("cisco data").returns(cisco_release) os_info = detector.linux_os_release _(os_info["KEY1"]).must_equal("NEWVALUE1") _(os_info["KEY2"]).must_equal("VALUE2") end end describe "#parse_os_release_info" do describe "when nil is supplied" do it "returns an empty hash" do _(detector.parse_os_release_info(nil)).must_equal({}) end end describe "when unexpectedly-formatted data is supplied" do let(:data) do <<~EOL blah blah no good data here EOL end it "returns an empty hash" do _(detector.parse_os_release_info(nil)).must_equal({}) end end describe "when properly-formatted data is supplied" do let(:data) do <<~EOL KEY1=value1 KEY2= KEY3=value3 KEY4="value4 with spaces" KEY5="value5 with a = sign" EOL end it "parses the data correctly" do parsed_data = detector.parse_os_release_info(data) _(parsed_data["KEY1"]).must_equal("value1") _(parsed_data.key?("KEY2")).must_equal(false) _(parsed_data["KEY3"]).must_equal("value3") _(parsed_data["KEY4"]).must_equal("value4 with spaces") _(parsed_data["KEY5"]).must_equal("value5 with a = sign") end end end end train-3.2.28/test/unit/platforms/detect/os_windows_test.rb000066400000000000000000000130521364512547200237310ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/mock" class OsDetectWindowsTester attr_reader :platform, :backend include Train::Platforms::Detect::Helpers::Windows def initialize @platform = {} @backend = Train::Transports::Mock.new.connection @backend.mock_os({ family: "windows" }) end end describe "os_detect_windows" do describe "windows 2012" do let(:detector) do detector = OsDetectWindowsTester.new detector.backend.mock_command("cmd.exe /c ver", "\r\nMicrosoft Windows [Version 6.3.9600]\r\n", "", 0) detector.backend.mock_command("wmic os get * /format:list", "\r\r\nBuildNumber=9600\r\r\nCaption=Microsoft Windows Server 2012 R2 Standard\r\r\nOSArchitecture=64-bit\r\r\nVersion=6.3.9600\r\r\n" , "", 0) detector.backend.mock_command("wmic cpu get architecture /format:list", "\r\r\nArchitecture=9\r\r\n" , "", 0) detector end it "sets the correct family/release for windows" do detector.detect_windows _(detector.platform[:family]).must_equal("windows") _(detector.platform[:name]).must_equal("Windows Server 2012 R2 Standard") _(detector.platform[:arch]).must_equal("x86_64") _(detector.platform[:release]).must_equal("6.3.9600") end end describe "windows 2008" do let(:detector) do detector = OsDetectWindowsTester.new detector.backend.mock_command("cmd.exe /c ver", "\r\nMicrosoft Windows [Version 6.1.7601]\r\n", "", 0) detector.backend.mock_command("wmic os get * /format:list", "\r\r\nBuildNumber=7601\r\r\nCaption=Microsoft Windows Server 2008 R2 Standard \r\r\nOSArchitecture=64-bit\r\r\nVersion=6.1.7601\r\r\n" , "", 0) detector.backend.mock_command("wmic cpu get architecture /format:list", "\r\r\nArchitecture=9\r\r\n" , "", 0) detector end it "sets the correct family/release for windows" do detector.detect_windows _(detector.platform[:family]).must_equal("windows") _(detector.platform[:name]).must_equal("Windows Server 2008 R2 Standard") _(detector.platform[:arch]).must_equal("x86_64") _(detector.platform[:release]).must_equal("6.1.7601") end end describe "windows 7" do let(:detector) do detector = OsDetectWindowsTester.new detector.backend.mock_command("cmd.exe /c ver", "\r\nMicrosoft Windows [Version 6.1.7601]\r\n", "", 0) detector.backend.mock_command("wmic os get * /format:list", "\r\r\nBuildNumber=7601\r\r\nCaption=Microsoft Windows 7 Enterprise \r\r\nOSArchitecture=32-bit\r\r\nVersion=6.1.7601\r\r\n\r\r\n" , "", 0) detector.backend.mock_command("wmic cpu get architecture /format:list", "\r\r\nArchitecture=0\r\r\n" , "", 0) detector end it "sets the correct family/release for windows" do detector.detect_windows _(detector.platform[:family]).must_equal("windows") _(detector.platform[:name]).must_equal("Windows 7 Enterprise") _(detector.platform[:arch]).must_equal("i386") _(detector.platform[:release]).must_equal("6.1.7601") end end describe "windows 10" do let(:detector) do detector = OsDetectWindowsTester.new detector.backend.mock_command("cmd.exe /c ver", "\r\nMicrosoft Windows [Version 10.0.10240]\r\n", "", 0) detector.backend.mock_command("wmic os get * /format:list", "\r\r\nBuildNumber=10240\r\r\nCaption=Microsoft Windows 10 Pro\r\r\nOSArchitecture=64-bit\r\r\nVersion=10.0.10240\r\r\n\r\r\n" , "", 0) detector.backend.mock_command("wmic cpu get architecture /format:list", "\r\r\nArchitecture=9\r\r\n" , "", 0) detector end it "sets the correct family/release for windows" do detector.detect_windows _(detector.platform[:family]).must_equal("windows") _(detector.platform[:name]).must_equal("Windows 10 Pro") _(detector.platform[:arch]).must_equal("x86_64") _(detector.platform[:release]).must_equal("10.0.10240") end end describe "Windows 10 with Powershell" do let(:detector) do detector = OsDetectWindowsTester.new detector.backend.mock_command("Get-WmiObject Win32_OperatingSystem | Select Caption,Version | ConvertTo-Json", "{\"Caption\":\"Microsoft Windows 10 Pro\", \"Version\": \"10.0.18362\"}", "", 0) detector.backend.mock_command("wmic os get * /format:list", "\r\r\nBuildNumber=10240\r\r\nCaption=Microsoft Windows 10 Pro\r\r\nOSArchitecture=64-bit\r\r\nVersion=10.0.18362\r\r\n\r\r\n" , "", 0) detector.backend.mock_command("wmic cpu get architecture /format:list", "\r\r\nArchitecture=9\r\r\n" , "", 0) detector end it "sets the correct family/release for windows" do detector.detect_windows _(detector.platform[:family]).must_equal("windows") _(detector.platform[:name]).must_equal("Windows 10 Pro") _(detector.platform[:arch]).must_equal("x86_64") _(detector.platform[:release]).must_equal("10.0.18362") end end describe "windows 98" do let(:detector) do detector = OsDetectWindowsTester.new detector.backend.mock_command("cmd.exe /c ver", "\r\nMicrosoft Windows [Version 4.10.1998]\r\n", "", 0) detector.backend.mock_command("wmic os get * /format:list", nil, "", 1) detector.backend.mock_command("wmic cpu get architecture /format:list", nil, "", 1) detector end it "fallback to version number if wmic is not available" do detector.detect_windows _(detector.platform[:family]).must_equal("windows") _(detector.platform[:name]).must_equal("Windows 4.10.1998") _(detector.platform[:arch]).must_be_nil _(detector.platform[:release]).must_equal("4.10.1998") end end end train-3.2.28/test/unit/platforms/detect/scanner_test.rb000066400000000000000000000040041364512547200231640ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/platforms/detect/scanner" require "train/transports/mock" describe "scanner" do let(:backend) { Train::Transports::Mock::Connection.new } let(:scanner) { Train::Platforms::Detect::Scanner.new(backend) } describe "scan family children" do it "return child" do family = Train::Platforms.family("linux") _(scanner.scan_family_children(family).name).must_equal("linux") _(scanner.instance_variable_get(:@family_hierarchy)).must_equal(["linux"]) end it "return nil" do family = Train::Platforms.family("fake-fam") _(scanner.scan_family_children(family)).must_be_nil _(scanner.instance_variable_get(:@family_hierarchy)).must_be_empty end end describe "check condition" do it "return true equal" do scanner.instance_variable_set(:@platform, { arch: "x86_64" }) _(scanner.check_condition({ arch: "= x86_64" })).must_equal(true) end it "return true greater then" do scanner.instance_variable_set(:@platform, { release: "8.2" }) _(scanner.check_condition({ release: ">= 7" })).must_equal(true) end it "return false greater then" do scanner.instance_variable_set(:@platform, { release: "2.2" }) _(scanner.check_condition({ release: "> 7" })).must_equal(false) end end describe "get platform" do it "return empty platform" do plat = Train::Platforms.name("linux") plat = scanner.get_platform(plat) _(plat.platform).must_equal({}) _(plat.backend).must_equal(backend) _(plat.family_hierarchy).must_equal([]) end it "return full platform" do scanner.instance_variable_set(:@platform, { family: "linux" }) scanner.instance_variable_set(:@family_hierarchy, %w{linux unix}) plat = Train::Platforms.name("linux") plat = scanner.get_platform(plat) _(plat.platform).must_equal({ family: "linux" }) _(plat.backend).must_equal(backend) _(plat.family_hierarchy).must_equal(%w{linux unix}) end end end train-3.2.28/test/unit/platforms/detect/uuid_test.rb000066400000000000000000000106461364512547200225120ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/mock" require "securerandom" class TestFile def initialize(string) @string = string end def exist? true end def size @string.length end def content @string end end describe "uuid" do def mock_platform(name, commands = {}, files = {}, plat_options = {}) Train::Platforms.list[name] = nil mock = Train::Transports::Mock::Connection.new commands.each do |command, data| mock.mock_command(command, data) end file_objects = {} files.each do |path, content| file_objects[path] = TestFile.new(content) end mock.files = file_objects mock.force_platform!(name, plat_options) end it "finds a linux uuid from chef entity_uuid" do files = { "/var/chef/cache/data_collector_metadata.json" => '{"node_uuid":"d400073f-0920-41aa-8dd3-2ea59b18f5ce"}' } plat = mock_platform("linux", {}, files) _(plat.uuid).must_equal "d400073f-0920-41aa-8dd3-2ea59b18f5ce" end it "finds a windows uuid from chef entity_uuid" do ENV["SYSTEMDRIVE"] = "C:" files = { 'C:\chef\cache\data_collector_metadata.json' => '{"node_uuid":"d400073f-0920-41aa-8dd3-2ea59b18f5ce"}' } plat = mock_platform("windows", {}, files) _(plat.uuid).must_equal "d400073f-0920-41aa-8dd3-2ea59b18f5ce" end it "finds a linux uuid from /etc/chef/chef_guid" do files = { "/etc/chef/chef_guid" => "5e430326-b5aa-56f8-975f-c3ca1c21df91" } plat = mock_platform("linux", {}, files) _(plat.uuid).must_equal "5e430326-b5aa-56f8-975f-c3ca1c21df91" end it "finds a linux uuid from /home/testuser/.chef/chef_guid" do ENV["HOME"] = "/home/testuser" files = { "/home/testuser/.chef/chef_guid" => "5e430326-b5aa-56f8-975f-c3ca1c21df91" } plat = mock_platform("linux", {}, files) _(plat.uuid).must_equal "5e430326-b5aa-56f8-975f-c3ca1c21df91" end it "finds a linux uuid from /etc/machine-id" do files = { "/etc/machine-id" => "123141dsfadf" } plat = mock_platform("linux", {}, files) _(plat.uuid).must_equal "5e430326-b5aa-56f8-975f-c3ca1c21df91" end it "finds a linux uuid from /var/lib/dbus/machine-id" do files = { "/etc/machine-id" => "", "/var/lib/dbus/machine-id" => "123141dsfadf", } plat = mock_platform("linux", {}, files) _(plat.uuid).must_equal "5e430326-b5aa-56f8-975f-c3ca1c21df91" end it "finds a linux uuid from /etc/machine-id" do files = { "/etc/machine-id" => "123141dsfadf" } plat = mock_platform("linux", {}, files) _(plat.uuid).must_equal "5e430326-b5aa-56f8-975f-c3ca1c21df91" end it "finds a windows uuid from wmic" do commands = { "wmic csproduct get UUID" => "UUID\r\nd400073f-0920-41aa-8dd3-2ea59b18f5ce\r\n" } plat = mock_platform("windows", commands) _(plat.uuid).must_equal "d400073f-0920-41aa-8dd3-2ea59b18f5ce" end it "finds a windows uuid from registry" do commands = { '(Get-ItemProperty "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography" -Name "MachineGuid")."MachineGuid"' => "d400073f-0920-41aa-8dd3-2ea59b18f5ce\r\n" } plat = mock_platform("windows", commands) _(plat.uuid).must_equal "d400073f-0920-41aa-8dd3-2ea59b18f5ce" end it 'finds a windows uuid from C:\chef\chef_guid' do ENV["SYSTEMDRIVE"] = "C:" files = { 'C:\chef\chef_guid' => "5e430326-b5aa-56f8-975f-c3ca1c21df91" } plat = mock_platform("windows", {}, files) _(plat.uuid).must_equal "5e430326-b5aa-56f8-975f-c3ca1c21df91" end it 'finds a windows uuid from C:\Users\test\.chef\chef_guid' do ENV["HOMEDRIVE"] = 'C:\\' ENV["HOMEPATH"] = 'Users\test' files = { 'C:\Users\test\.chef\chef_guid' => "5e430326-b5aa-56f8-975f-c3ca1c21df91" } plat = mock_platform("windows", {}, files) _(plat.uuid).must_equal "5e430326-b5aa-56f8-975f-c3ca1c21df91" end it "generates a uuid from a string" do plat = mock_platform("linux") uuid = Train::Platforms::Detect::UUID.new(plat) _(uuid.uuid_from_string("123141dsfadf")).must_equal "5e430326-b5aa-56f8-975f-c3ca1c21df91" end it "finds a aws uuid" do plat = mock_platform("aws") plat.backend.stubs(:unique_identifier).returns("158551926027") _(plat.uuid).must_equal "1d74ce61-ac15-5c48-9ee3-5aa8207ac37f" end it "finds an azure uuid" do plat = mock_platform("azure") plat.backend.stubs(:unique_identifier).returns("1d74ce61-ac15-5c48-9ee3-5aa8207ac37f") _(plat.uuid).must_equal "2c2e4fa9-7287-5dee-85a3-6527face7b7b" end end train-3.2.28/test/unit/platforms/family_test.rb000066400000000000000000000015771364512547200215600ustar00rootroot00000000000000# encoding: utf-8 require "helper" describe "platform family" do def mock_family(x) Train::Platforms.families[x] = nil if x == "mock" Train::Platforms.family(x) end it "set family title" do plat = mock_family("mock") _(plat.title).must_equal("Mock Family") plat.title("The Best Mock Family") _(plat.title).must_equal("The Best Mock Family") end it "set family in a family" do plat = mock_family("family1") plat.in_family("family2") _(plat.families.keys[0].name).must_equal("family2") plat = mock_family("family2") _(plat.children.keys[0].name).must_equal("family1") end it "set family in a family with condition" do plat = Train::Platforms.family("family4", arch: "= x68_64").in_family("family5") _(plat.families.keys[0].name).must_equal("family5") _(plat.families.values[0]).must_equal({ arch: "= x68_64" }) end end train-3.2.28/test/unit/platforms/os_detect_test.rb000066400000000000000000000357201364512547200222450ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/mock" class OsDetectTester include Train::Platforms::Detect::Helpers::OSCommon end describe "os_detect" do let(:detector) { OsDetectTester.new } def scan_with_files(uname, files) mock = Train::Transports::Mock::Connection.new mock.mock_command("uname -s", uname) mock.mock_command("uname -r", "test-release") yield mock if block_given? files.each do |path, data| mock.mock_command("test -f #{path}") mock.mock_command("test -f #{path} && cat #{path}", data) end Train::Platforms::Detect.scan(mock) end def scan_with_windows mock = Train::Transports::Mock::Connection.new mock.mock_command("cmd.exe /c ver", "Microsoft Windows [Version 6.3.9600]") Train::Platforms::Detect.scan(mock) end ## Detect all linux distros describe "/etc/enterprise-release" do it "sets the correct family/release for oracle" do path = "/etc/enterprise-release" platform = scan_with_files("linux", { path => "release 7" }) _(platform[:name]).must_equal("oracle") _(platform[:family]).must_equal("redhat") _(platform[:release]).must_equal("7") end end describe "/etc/redhat-release" do describe "and /etc/os-release" do it "sets the correct family, name, and release on centos" do files = { "/etc/redhat-release" => "CentOS Linux release 7.2.1511 (Core) \n", "/etc/os-release" => "NAME=\"CentOS Linux\"\nVERSION=\"7 (Core)\"\nID=\"centos\"\nID_LIKE=\"rhel fedora\"\n", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("centos") _(platform[:family]).must_equal("redhat") _(platform[:release]).must_equal("7.2.1511") end it "sets the correct family, name, and release on scientific linux" do files = { "/etc/redhat-release" => "Scientific Linux release 7.4 (Nitrogen)\n", "/etc/os-release" => "NAME=\"Scientific Linux\"\nVERSION=\"7.4 (Nitrogen)\"\nID=\"rhel\"\nID_LIKE=\"scientific centos fedora\"\nVERSION_ID=\"7.4\"\nPRETTY_NAME=\"Scientific Linux 7.4 (Nitrogen)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:scientificlinux:scientificlinux:7.4:GA\"\nHOME_URL=\"http://www.scientificlinux.org//\"\nBUG_REPORT_URL=\"mailto:scientific-linux-devel@listserv.fnal.gov\"\n\nREDHAT_BUGZILLA_PRODUCT=\"Scientific Linux 7\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=7.4\nREDHAT_SUPPORT_PRODUCT=\"Scientific Linux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"7.4\"\n", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("scientific") _(platform[:family]).must_equal("redhat") _(platform[:release]).must_equal("7.4") end it "sets the correct family, name, and release on CloudLinux" do files = { "/etc/redhat-release" => "CloudLinux release 7.4 (Georgy Grechko)\n", "/etc/os-release" => "NAME=\"CloudLinux\"\nVERSION=\"7.4 (Georgy Grechko)\"\nID=\"cloudlinux\"\nID_LIKE=\"rhel fedora centos\"\nVERSION_ID=\"7.4\"\nPRETTY_NAME=\"CloudLinux 7.4 (Georgy Grechko)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:cloudlinux:cloudlinux:7.4:GA:server\"\nHOME_URL=\"https://www.cloudlinux.com//\"\nBUG_REPORT_URL=\"https://www.cloudlinux.com/support\"\n", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("cloudlinux") _(platform[:family]).must_equal("redhat") _(platform[:release]).must_equal("7.4") end it "sets the correct family, name, and release on SLES ESR RHEL" do files = { "/etc/redhat-release" => "Red Hat Enterprise Linux Server release 7.4 (Maipo)\n# This is a \"SLES Expanded Support platform release 7.4\"\n# The above \"Red Hat Enterprise Linux Server\" string is only used to \n# keep software compatibility.\n", "/etc/os-release" => "NAME=\"Red Hat Enterprise Linux Server\"\nVERSION=\"7.4 (Maipo)\"\nID=\"rhel\"\nID_LIKE=\"fedora\"\nVERSION_ID=\"7.4\"\nPRETTY_NAME=\"Red Hat Enterprise Linux Server 7.4\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:redhat:enterprise_linux:7.4:GA:server\"\nHOME_URL=\"https://www.redhat.com/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\n\nREDHAT_BUGZILLA_PRODUCT=\"Red Hat Enterprise Linux 7\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=7.4\nREDHAT_SUPPORT_PRODUCT=\"Red Hat Enterprise Linux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=7.4\n# This is a \"SLES Expanded Support platform release 7.4\"\n# The above \"Red Hat Enterprise Linux Server\" string is only used to\n# keep software compatibility.\n", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("redhat") _(platform[:family]).must_equal("redhat") _(platform[:release]).must_equal("7.4") end end end describe "darwin" do describe "mac_os_x" do it "sets the correct family, name, and release on os_x" do files = { "/System/Library/CoreServices/SystemVersion.plist" => "Mac OS X", } platform = scan_with_files("darwin", files) _(platform[:name]).must_equal("mac_os_x") _(platform[:family]).must_equal("darwin") _(platform[:release]).must_equal("test-release") end end describe "generic darwin" do it "sets the correct family, name, and release on darwin" do files = { "/usr/bin/sw_vers" => "ProductVersion: 17.0.1\nBuildVersion: alpha.x1", } platform = scan_with_files("darwin", files) do |mock| mock.mock_command("sw_vers -buildVersion", "12345\n") end _(platform[:name]).must_equal("darwin") _(platform[:family]).must_equal("darwin") _(platform[:release]).must_equal("test-release") _(platform[:build]).must_equal("12345") end end end describe "/etc/debian_version" do def debian_scan(id, version) lsb_release = "DISTRIB_ID=#{id}\nDISTRIB_RELEASE=#{version}" files = { "/etc/lsb-release" => lsb_release, "/etc/debian_version" => "11", } scan_with_files("linux", files) end describe "ubuntu" do it "sets the correct family/release for ubuntu" do platform = debian_scan("ubuntu", "16.04") _(platform[:name]).must_equal("ubuntu") _(platform[:family]).must_equal("debian") _(platform[:release]).must_equal("16.04") end end describe "linuxmint" do it "sets the correct family/release for linuxmint" do platform = debian_scan("linuxmint", "12") _(platform[:name]).must_equal("linuxmint") _(platform[:family]).must_equal("debian") _(platform[:release]).must_equal("12") end end describe "kali" do it "sets the correct family/release for kali" do os_release = "PRETTY_NAME=\"Kali GNU/Linux Rolling\"\nNAME=\"Kali GNU/Linux\"\nID=kali\nVERSION=\"2019.4\"\nVERSION_ID=\"2019.4\"\nVERSION_CODENAME=\"kali-rolling\"\nID_LIKE=debian\nANSI_COLOR=\"1;31\"\nHOME_URL=\"https://www.kali.org/\"\nSUPPORT_URL=\"https://forums.kali.org/\"\nBUG_REPORT_URL=\"https://bugs.kali.org/\"\n" files = { "/etc/os-release" => os_release, "/etc/debian_version" => "kali-rolling", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("kali") _(platform[:family]).must_equal("debian") _(platform[:release]).must_equal("2019.4") end end describe "raspbian" do it "sets the correct family/release for raspbian " do files = { "/usr/bin/raspi-config" => "data", "/etc/debian_version" => "13.6", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("raspbian") _(platform[:family]).must_equal("debian") _(platform[:release]).must_equal("13.6") end end describe "windows" do it "sets the correct family/release for windows " do platform = scan_with_windows _(platform[:name]).must_equal("windows_6.3.9600") _(platform[:family]).must_equal("windows") _(platform[:release]).must_equal("6.3.9600") end end describe "everything else" do it "sets the correct family/release for debian " do platform = debian_scan("some_debian", "12.99") _(platform[:name]).must_equal("debian") _(platform[:family]).must_equal("debian") _(platform[:release]).must_equal("11") end end end describe "windows" do it "sets the correct family/release for windows " do platform = scan_with_windows _(platform[:name]).must_equal("windows_6.3.9600") _(platform[:family]).must_equal("windows") _(platform[:release]).must_equal("6.3.9600") end end describe "/etc/coreos/update.conf" do it "sets the correct family/release for coreos" do lsb_release = "DISTRIB_ID=Container Linux by CoreOS\nDISTRIB_RELEASE=27.9" files = { "/etc/lsb-release" => lsb_release, "/etc/coreos/update.conf" => "data", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("coreos") _(platform[:family]).must_equal("linux") _(platform[:release]).must_equal("27.9") end end describe "/etc/os-release" do describe "when not on a wrlinux build" do it "fail back to generic linux" do os_release = "ID_LIKE=cisco-unkwown\nVERSION=unknown" files = { "/etc/os-release" => os_release, } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("linux") _(platform[:family]).must_equal("linux") end end describe "when on a wrlinux build" do it "sets the correct family/release for wrlinux" do os_release = "ID_LIKE=cisco-wrlinux\nVERSION=cisco123" files = { "/etc/os-release" => os_release, } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("wrlinux") _(platform[:family]).must_equal("redhat") _(platform[:release]).must_equal("cisco123") end end describe "when on a suse build" do describe "when /etc/os-release is present" do it "sets the correct family/release for SLES" do files = { "/etc/os-release" => "NAME=\"SLES\"\nVERSION=\"15.1\"\nID=\"sles\"\nID_LIKE=\"suse\"\n", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("suse") _(platform[:family]).must_equal("suse") _(platform[:release]).must_equal("15.1") end it "sets the correct family/release for openSUSE" do files = { "/etc/os-release" => "NAME=\"openSUSE Leap\"\nVERSION=\"15.1\"\nID=\"opensuse-leap\"\nID_LIKE=\"suse opensuse\"\n", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("opensuse") _(platform[:family]).must_equal("suse") _(platform[:release]).must_equal("15.1") end end describe "when /etc/os-release is not present" do it "sets the correct family/release for SLES" do files = { "/etc/SuSE-release" => "SUSE Linux Enterprise Server 11 (x86_64)\nVERSION = 11\nPATCHLEVEL = 2", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("suse") _(platform[:family]).must_equal("suse") _(platform[:release]).must_equal("11.2") end it "sets the correct family/release for openSUSE" do files = { "/etc/SuSE-release" => "openSUSE 10.2 (x86_64)\nVERSION = 10.2", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("opensuse") _(platform[:family]).must_equal("suse") _(platform[:release]).must_equal("10.2") end end end end describe "yocto" do it "sets the correct family, name, and release on yocto" do files = { "/etc/issue" => "Poky (Yocto Project Reference Distro) 2.7 \\n \\l", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("yocto") _(platform[:family]).must_equal("yocto") _(platform[:release]).must_equal("2.7") end end describe "balenaos" do it "sets the correct family, name, and release on balenaos" do files = { "/etc/issue" => "balenaOS 2.46.1 \n \l", "/etc/os-release" => "ID=\"balena-os\"\nNAME=\"balenaOS\"\nVERSION=\"2.46.1+rev1\"\nVERSION_ID=\"2.46.1+rev1\"\nPRETTY_NAME=\"balenaOS 2.46.1+rev1\"\nMACHINE=\"raspberrypi3\"\nVARIANT=\"Development\"\nVARIANT_ID=\"dev\"\nMETA_BALENA_VERSION=\"2.46.1\"\nRESIN_BOARD_REV=\"e194600\"\nMETA_RESIN_REV=\"f2295d2\"\nSLUG=\"raspberrypi3\"\n", } platform = scan_with_files("linux", files) _(platform[:name]).must_equal("balenaos") _(platform[:family]).must_equal("yocto") _(platform[:release]).must_equal("2.46.1") end end describe "qnx" do it "sets the correct info for qnx platform" do platform = scan_with_files("qnx", {}) _(platform[:name]).must_equal("qnx") _(platform[:family]).must_equal("qnx") _(platform[:release]).must_equal("test-release") end end describe "cisco" do it "recognizes Cisco IOS12" do mock = Train::Transports::Mock::Connection.new mock.mock_command("show version", "Cisco IOS Software, C3750E Software (C3750E-UNIVERSALK9-M), Version 12.2(58)SE") platform = Train::Platforms::Detect.scan(mock) _(platform[:name]).must_equal("cisco_ios") _(platform[:family]).must_equal("cisco") _(platform[:release]).must_equal("12.2") end it "recognizes Cisco IOS XE" do mock = Train::Transports::Mock::Connection.new mock.mock_command("show version", "Cisco IOS Software, IOS-XE Software, Catalyst L3 Switch Software (CAT3K_CAA-UNIVERSALK9-M), Version 03.03.03SE RELEASE SOFTWARE (fc2)") platform = Train::Platforms::Detect.scan(mock) _(platform[:name]).must_equal("cisco_ios_xe") _(platform[:family]).must_equal("cisco") _(platform[:release]).must_equal("03.03.03SE") end it "recognizes Cisco Nexus" do mock = Train::Transports::Mock::Connection.new mock.mock_command("show version", "Cisco Nexus Operating System (NX-OS) Software\n system: version 5.2(1)N1(8b)\n") platform = Train::Platforms::Detect.scan(mock) _(platform[:name]).must_equal("cisco_nexus") _(platform[:family]).must_equal("cisco") _(platform[:release]).must_equal("5.2") end end describe "brocade" do it "recognizes Brocade FOS-based SAN switches" do mock = Train::Transports::Mock::Connection.new mock.mock_command("version", "Kernel: 2.6.14.2\nFabric OS: v7.4.2a\nMade on: Thu Jun 29 19:22:14 2017\nFlash: Sat Sep 9 17:30:42 2017\nBootProm: 1.0.11") platform = Train::Platforms::Detect.scan(mock) _(platform[:name]).must_equal("brocade_fos") _(platform[:family]).must_equal("brocade") _(platform[:release]).must_equal("7.4.2a") end end end train-3.2.28/test/unit/platforms/platform_test.rb000066400000000000000000000313671364512547200221230ustar00rootroot00000000000000# encoding: utf-8 require "helper" describe "platform" do def mock_platform(x) plat = Train::Platforms.name(x) plat.family_hierarchy = mock_os_hierarchy(plat).flatten plat.platform[:family] = plat.family_hierarchy[0] plat.add_platform_methods plat end def mock_platform_family(x) Train::Platforms.list[x] = nil if x == "mock" plat = Train::Platforms.name(x).in_family(x) plat.family_hierarchy = mock_os_hierarchy(plat).flatten plat.platform[:family] = plat.family_hierarchy[0] plat.add_platform_methods plat end def mock_os_hierarchy(plat) plat.families.each_with_object([]) do |(k, _v), memo| memo << k.name memo << mock_os_hierarchy(k) unless k.families.empty? end end it "set platform title" do plat = mock_platform_family("mock") _(plat.title).must_equal("Mock") plat.title("The Best Mock") _(plat.title).must_equal("The Best Mock") end it "clean init name" do plat = mock_platform_family("Mo ck") _(plat.name).must_equal("mo_ck") end it "set name and name override" do plat = mock_platform_family("mock") _(plat.name).must_equal("mock") _(plat[:name]).must_equal("mock") plat.platform[:name] = "Mock 2020" plat.add_platform_methods _(plat.name).must_equal("mock_2020") _(plat[:name]).must_equal("mock_2020") end it "check families" do plat = mock_platform_family("mock") _(plat.families.keys[0].name).must_equal("mock") end it "check families with condition" do Train::Platforms.list["mock"] = nil plat = Train::Platforms.name("mock", arch: "= x86_64").in_family("linux") _(plat.families.keys[0].name).must_equal("linux") _(plat.families.values[0]).must_equal({ arch: "= x86_64" }) end it "finds family hierarchy" do plat = Train::Platforms.name("linux") plat.find_family_hierarchy _(plat.family_hierarchy).must_equal %w{linux unix os} end it "return direct families" do plat = mock_platform_family("mock") plat.in_family("mock2") plat.in_family("mock3") _(plat.direct_families).must_equal(%w{mock mock2 mock3}) end it "return to_hash" do plat = mock_platform_family("mock") _(plat.to_hash).must_equal({ family: "mock" }) end it "return unknown release" do plat = mock_platform_family("mock") _(plat[:release]).must_equal("unknown") end it "return name?" do plat = Train::Platforms.name("windows_rc1") _(defined?(plat.windows_rc1?)).must_be_nil plat.add_platform_methods _(plat.windows_rc1?).must_equal(true) end it "add platform methods" do Train::Platforms.list["mock"] = nil plat = Train::Platforms.name("mock").in_family("linux") _(defined?(plat.linux?)).must_be_nil plat.family_hierarchy = mock_os_hierarchy(plat).flatten plat.add_platform_methods _(plat.linux?).must_equal(true) end it "provides a method to access platform data" do family = "test-os" os = mock_platform_family(family) _(os[:family]).must_equal family end it "provides an accessor for the full hash" do x = "test-os" os = mock_platform_family(x) _(os.to_hash).must_equal({ family: x }) end it "has a friendly #to_s and #inspect" do plat = mock_platform("redhat") _(plat.to_s).must_equal "Train::Platforms::Platform:unknown:redhat" _(plat.inspect).must_equal "Train::Platforms::Platform:unknown:redhat" end describe "with platform set to redhat" do let(:os) { mock_platform("redhat") } it { _(os.redhat?).must_equal(true) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to oracle" do let(:os) { mock_platform("oracle") } it { _(os.redhat?).must_equal(true) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to centos" do let(:os) { mock_platform("centos") } it { _(os.redhat?).must_equal(true) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to cloudlinux" do let(:os) { mock_platform("cloudlinux") } it { _(os.redhat?).must_equal(true) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to fedora" do let(:os) { mock_platform("fedora") } it { _(os.fedora?).must_equal(true) } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to amazon" do let(:os) { mock_platform("amazon") } it { _(os.fedora?).must_equal(false) } it { _(os.redhat?).must_equal(true) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to debian" do let(:os) { mock_platform("debian") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(true) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to ubuntu" do let(:os) { mock_platform("ubuntu") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(true) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to linuxmint" do let(:os) { mock_platform("linuxmint") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(true) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to raspbian" do let(:os) { mock_platform("raspbian") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(true) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to suse" do let(:os) { mock_platform("suse") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(true) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to opensuse" do let(:os) { mock_platform("opensuse") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(true) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to alpine" do let(:os) { mock_platform("alpine") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to arch" do let(:os) { mock_platform("arch") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to coreos" do let(:os) { mock_platform("coreos") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to exherbo" do let(:os) { mock_platform("exherbo") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to gentoo" do let(:os) { mock_platform("gentoo") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to slackware" do let(:os) { mock_platform("slackware") } it { _(os.redhat?).must_equal(false) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to wrlinux" do let(:os) { mock_platform("wrlinux") } it { _(os.redhat?).must_equal(true) } it { _(os.debian?).must_equal(false) } it { _(os.suse?).must_equal(false) } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to linux" do let(:os) { mock_platform("linux") } it { _(os.linux?).must_equal(true) } it { _(os.unix?).must_equal(true) } end describe "with platform set to freebsd" do let(:os) { mock_platform("freebsd") } it { _(os.bsd?).must_equal(true) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } end describe "with platform set to netbsd" do let(:os) { mock_platform("netbsd") } it { _(os.bsd?).must_equal(true) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } end describe "with platform set to openbsd" do let(:os) { mock_platform("openbsd") } it { _(os.bsd?).must_equal(true) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } end describe "with platform set to darwin" do let(:os) { mock_platform("darwin") } it { _(os.bsd?).must_equal(true) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } end describe "with platform set to solaris" do let(:os) { mock_platform("solaris") } it { _(os.solaris?).must_equal(true) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } end describe "with platform set to smartos" do let(:os) { mock_platform("smartos") } it { _(os.solaris?).must_equal(true) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } end describe "with platform set to openindiana" do let(:os) { mock_platform("openindiana") } it { _(os.solaris?).must_equal(true) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } end describe "with platform set to opensolaris" do let(:os) { mock_platform("opensolaris") } it { _(os.solaris?).must_equal(true) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } end describe "with platform set to nexentacore" do let(:os) { mock_platform("nexentacore") } it { _(os.solaris?).must_equal(true) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } end describe "with platform set to windows" do let(:os) { mock_platform("windows") } it { _(os.solaris?).must_equal(false) } it { _(os.bsd?).must_equal(false) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(false) } end describe "with platform set to hpux" do let(:os) { mock_platform("hpux") } it { _(os.solaris?).must_equal(false) } it { _(os.linux?).must_equal(false) } it { _(os.unix?).must_equal(true) } it { _(os.hpux?).must_equal(true) } end describe "with platform set to esx" do let(:os) { mock_platform("vmkernel") } it { _(os.solaris?).must_equal(false) } it { _(os.linux?).must_equal(false) } it { _(os[:family]).must_equal("esx") } it { _(os.unix?).must_equal(false) } it { _(os.esx?).must_equal(true) } end describe "with platform set to darwin" do let(:os) { mock_platform("darwin") } it { _(os.solaris?).must_equal(false) } it { _(os.linux?).must_equal(false) } it { _(os[:family]).must_equal("darwin") } it { _(os.bsd?).must_equal(true) } it { _(os.darwin?).must_equal(true) } it { _(os.unix?).must_equal(true) } it { _(os.bsd?).must_equal(true) } it { _(os.esx?).must_equal(false) } end describe "with platform set to mac_os_x" do let(:os) { mock_platform("mac_os_x") } it { _(os.solaris?).must_equal(false) } it { _(os.linux?).must_equal(false) } it { _(os[:family]).must_equal("darwin") } it { _(os.bsd?).must_equal(true) } it { _(os.darwin?).must_equal(true) } it { _(os.unix?).must_equal(true) } it { _(os.bsd?).must_equal(true) } it { _(os.esx?).must_equal(false) } end end train-3.2.28/test/unit/platforms/platforms_test.rb000066400000000000000000000030231364512547200222720ustar00rootroot00000000000000# encoding: utf-8 require "helper" describe "platforms" do it "create platform" do Train::Platforms.list["mock"] = nil plat = Train::Platforms.name("mock") Train::Platforms.name("mock").in_family("test") Train::Platforms.name("mock").detect { true } _(plat.title).must_equal("Mock") _(plat.detect.call).must_equal(true) _(plat.families.keys[0].name).must_equal("test") end it "create family" do Train::Platforms.families["mock"] = nil fam = Train::Platforms.family("mock") Train::Platforms.family("mock").in_family("test") Train::Platforms.family("mock").detect { true } _(fam.title).must_equal("Mock Family") _(fam.detect.call).must_equal(true) _(fam.families.keys[0].name).must_equal("test") end it "return top platforms empty" do Train::Platforms.stubs(:list).returns({}) Train::Platforms.stubs(:families).returns({}) top = Train::Platforms.top_platforms _(top.count).must_equal(0) end it "return top platforms with data" do plat = Train::Platforms.name("linux") plat.stubs(:families).returns({}) Train::Platforms.stubs(:list).returns({ "linux" => plat }) Train::Platforms.stubs(:families).returns({}) top = Train::Platforms.top_platforms _(top.count).must_equal(1) end it "return platforms export with data" do export = Train::Platforms.export _(export.size).must_be :>, 10 _(export[0][:name]).must_equal "aix" expected_families = %w{aix unix os} _(export[0][:families]).must_equal expected_families end end train-3.2.28/test/unit/plugins/000077500000000000000000000000001364512547200163535ustar00rootroot00000000000000train-3.2.28/test/unit/plugins/connection_test.rb000066400000000000000000000146131364512547200221030ustar00rootroot00000000000000# encoding: utf-8 require "helper" describe "v1 Connection Plugin" do describe "empty v1 connection plugin" do let(:cls) { Train::Plugins::Transport::BaseConnection } let(:connection) { cls.new({}) } it "provides a close method" do connection.close # wont raise end it "raises an exception for run_command" do _ { connection.run_command("") }.must_raise NotImplementedError end it "raises an exception for run_command_via_connection" do _ { connection.send(:run_command_via_connection, "") }.must_raise NotImplementedError end it "raises an exception for os method" do _ { connection.os }.must_raise NotImplementedError end it "raises an exception for file method" do _ { connection.file("") }.must_raise NotImplementedError end it "raises an exception for file_via_connection method" do _ { connection.send(:file_via_connection, "") }.must_raise NotImplementedError end it "raises an exception for login command method" do _ { connection.login_command }.must_raise NotImplementedError end it "can wait until ready" do connection.wait_until_ready # wont raise end it "provides a default logger" do _(connection.method(:logger).call) .must_be_instance_of(Logger) end it "provides direct platform" do plat = connection.force_platform!("mac_os_x") _(plat.name).must_equal "mac_os_x" _(plat.linux?).must_equal false _(plat.cloud?).must_equal false _(plat.unix?).must_equal true _(plat.family).must_equal "darwin" _(plat.family_hierarchy).must_equal %w{darwin bsd unix os} end it "must use the user-provided logger" do l = rand _(cls.new({ logger: l }) .method(:logger).call).must_equal(l) end describe "cached_client helper" do class DemoConnection < Train::Plugins::Transport::BaseConnection def initialize(options = {}) super(options) @cache_enabled[:api_call] = true @cache[:api_call] = {} end def demo_client cached_client(:api_call, :demo_client) do DemoClient.new end end class DemoClient end end it "returns a new connection when cached disabled" do conn = DemoConnection.new conn.disable_cache(:api_call) client1 = conn.demo_client client2 = conn.demo_client _(client1).wont_be_same_as client2 end it "returns a new connection when cache enabled and not hydrated" do conn = DemoConnection.new conn.enable_cache(:api_call) client1 = conn.demo_client _(client1).must_be_instance_of DemoConnection::DemoClient end it "returns a cached connection when cache enabled and hydrated" do conn = DemoConnection.new conn.enable_cache(:api_call) client1 = conn.demo_client client2 = conn.demo_client _(client1).must_be_same_as client2 end end describe "create cache connection" do it "default connection cache settings" do _(connection.cache_enabled?(:file)).must_equal true _(connection.cache_enabled?(:command)).must_equal false _(connection.cache_enabled?(:api_call)).must_equal false end end describe "disable/enable caching" do it "disable file cache via connection" do connection.disable_cache(:file) _(connection.cache_enabled?(:file)).must_equal false end it "enable command cache via cache_connection" do connection.enable_cache(:command) _(connection.cache_enabled?(:command)).must_equal true end it "raises an exception for unknown cache type" do _ { connection.enable_cache(:fake) }.must_raise Train::UnknownCacheType _ { connection.disable_cache(:fake) }.must_raise Train::UnknownCacheType end end describe "cache enable check" do it "returns true when cache is enabled" do cache_enabled = connection.instance_variable_get(:@cache_enabled) cache_enabled[:test] = true _(connection.cache_enabled?(:test)).must_equal true end it "returns false when cache is disabled" do cache_enabled = connection.instance_variable_get(:@cache_enabled) cache_enabled[:test] = false _(connection.cache_enabled?(:test)).must_equal false end end describe "clear cache" do it "clear file cache" do cache = connection.instance_variable_get(:@cache) cache[:file]["/tmp"] = "test" connection.send(:clear_cache, :file) cache = connection.instance_variable_get(:@cache) _(cache[:file]).must_equal({}) end end describe "load file" do it "with caching" do connection.enable_cache(:file) connection.expects(:file_via_connection).once.returns("test_file") _(connection.file("/tmp/test")).must_equal("test_file") _(connection.file("/tmp/test")).must_equal("test_file") assert = { "/tmp/test" => "test_file" } cache = connection.instance_variable_get(:@cache) _(cache[:file]).must_equal(assert) end it "without caching" do connection.disable_cache(:file) connection.expects(:file_via_connection).twice.returns("test_file") _(connection.file("/tmp/test")).must_equal("test_file") _(connection.file("/tmp/test")).must_equal("test_file") cache = connection.instance_variable_get(:@cache) _(cache[:file]).must_equal({}) end end describe "run command" do it "with caching" do connection.enable_cache(:command) connection.expects(:run_command_via_connection).once.returns("test_user") _(connection.run_command("whoami")).must_equal("test_user") _(connection.run_command("whoami")).must_equal("test_user") assert = { "whoami" => "test_user" } cache = connection.instance_variable_get(:@cache) _(cache[:command]).must_equal(assert) end it "without caching" do connection.disable_cache(:command) connection.expects(:run_command_via_connection).twice.returns("test_user") _(connection.run_command("whoami")).must_equal("test_user") _(connection.run_command("whoami")).must_equal("test_user") cache = connection.instance_variable_get(:@cache) _(cache[:command]).must_equal({}) end end end end train-3.2.28/test/unit/plugins/transport_test.rb000066400000000000000000000055171364512547200220030ustar00rootroot00000000000000require "helper" describe "v1 Transport Plugin" do describe "empty v1 transport plugin" do let(:plugin) { Class.new(Train.plugin(1)) } it "initializes an empty configuration" do _(plugin.new.options).must_equal({}) end it "saves the provided configuration" do conf = { a: rand } _(plugin.new(conf).options).must_equal(conf) end it "saves the provided configuration" do conf = { a: rand } _(plugin.new(conf).options).must_equal(conf) end it "provides a default logger" do conf = { a: rand } _(plugin.new(conf) .method(:logger).call) .must_be_instance_of(Logger) end it "can configure custom loggers" do l = rand _(plugin.new({ logger: l }) .method(:logger).call) .must_equal(l) end it "provides a connection method" do _ { plugin.new.connection }.must_raise Train::ClientError end end describe "registered with a name" do before do Train::Plugins.registry.clear end it "doesnt have any plugins in the registry if none were configured" do _(Train::Plugins.registry.empty?).must_equal true end it "is is added to the plugins registry" do plugin_name = rand _(Train::Plugins.registry).wont_include(plugin_name) plugin = Class.new(Train.plugin(1)) do name plugin_name end _(Train::Plugins.registry[plugin_name]).must_equal(plugin) end end describe "with options" do def train_class(opts = {}) name = rand.to_s plugin = Class.new(Train.plugin(1)) do option name, opts end [name, plugin] end it "exposes the parameters via api" do name, plugin = train_class _(plugin.default_options.keys).must_equal [name] end it "exposes the parameters via api" do default = rand.to_s name, plugin = train_class({ default: default }) _(plugin.default_options[name][:default]).must_equal default end it "option must be required" do name, plugin = train_class(required: true) _(plugin.default_options[name][:required]).must_equal true end it "default option must not be required" do name, plugin = train_class _(plugin.default_options[name][:required]).must_be_nil end it "can include options from another module" do name_a, plugin_a = train_class b = Class.new(Train.plugin(1)) do include_options(plugin_a) end _(b.default_options[name_a]).wont_be_nil end it "overwrites existing options when including" do old = rand.to_s nu = rand.to_s name_a, plugin_a = train_class({ default: nu }) b = Class.new(Train.plugin(1)) do option name_a, default: old include_options(plugin_a) end _(b.default_options[name_a][:default]).must_equal nu end end end train-3.2.28/test/unit/plugins_test.rb000066400000000000000000000010771364512547200177440ustar00rootroot00000000000000# encoding: utf-8 require "helper" # rubocop:disable Style/BlockDelimiters describe Train::Plugins do it "provides a method to create new v1 transport plugins" do _(Train.plugin(1)).must_equal Train::Plugins::Transport end it "fails when called with an unsupported plugin version" do _ { Train.plugin(2) }.must_raise Train::ClientError end it "defaults to v1 plugins" do _(Train.plugin).must_equal Train::Plugins::Transport end it "provides a registry of plugins" do _(Train::Plugins.registry).must_be_instance_of(Hash) end end train-3.2.28/test/unit/train_test.rb000066400000000000000000000204031364512547200173720ustar00rootroot00000000000000# encoding: utf-8 # # Author:: Dominik Richter () require "helper" $:.concat Dir["test/fixtures/plugins/*/lib"] # HACK? I honestly can't tell describe Train do before do Train::Plugins.registry.clear end describe "#create" do it "raises an error if the plugin isnt found" do _ { Train.create("missing") }.must_raise Train::UserError _ { Train.create("missing") }.must_raise Train::PluginLoadError end it "loads a core plugin if it isnt in the registry yet via symbol" do Kernel.stub :require, true do ex = Class.new(Train.plugin 1) { name "existing" } train = Train.create(:existing) _(train.class).must_equal ex end end it "loads a core plugin if it isnt in the registry yet via string" do Kernel.stub :require, true do ex = Class.new(Train.plugin 1) { name "existing" } train = Train.create("existing") _(train.class).must_equal ex end end it "loads a gem plugin if it isnt in the registry yet via string" do # The 'train-test-fixture' gem is located in test/fixtures/plugins/train-test-fixture and is # lib/train/trainsports, and Train will need to pre-pend 'train-' to the # transport name to get the gem name. transport = Train.create("test-fixture") # Normally one would call transport.class.name, but that's been overridden to be a write-only DSL method # So use to_s _(transport.class.to_s).must_equal "TrainPlugins::TestFixture::Transport" end end describe "#options" do it "raises exception if a given transport plugin isnt found" do _ { Train.options("missing") }.must_raise Train::UserError _ { Train.options("missing") }.must_raise Train::PluginLoadError end it "provides empty options of a transport plugin" do Class.new(Train.plugin 1) { name "none" } _(Train.options("none")).must_equal({}) end it "provides all options of a transport plugin" do Class.new(Train.plugin 1) do name "one" option :one, required: true, default: 123 end _(Train.options("one")).must_equal({ one: { required: true, default: 123, }, }) end end describe "#target_config - URI parsing" do it "configures resolves target" do org = { target: "ssh://user:pass@host.com:123/path", } res = Train.target_config(org) _(res[:backend]).must_equal "ssh" _(res[:host]).must_equal "host.com" _(res[:user]).must_equal "user" _(res[:password]).must_equal "pass" _(res[:port]).must_equal 123 _(res[:target]).must_equal org[:target] _(res[:path]).must_equal "/path" _(org.keys).must_equal [:target] end it "resolves a target while keeping existing fields" do org = { target: "ssh://user:pass@host.com:123/path", backend: rand, host: rand, user: rand, password: rand, port: rand, path: rand, } res = Train.target_config(org) _(res).must_equal org end it "resolves a winrm target" do org = { target: "winrm://Administrator@192.168.10.140", backend: "winrm", host: "192.168.10.140", user: "Administrator", password: nil, port: nil, path: nil, } res = Train.target_config(org) _(res).must_equal org end it "keeps the configuration when incorrect target is supplied" do org = { target: "wrong", } res = Train.target_config(org) _(res[:backend]).must_be_nil _(res[:host]).must_be_nil _(res[:user]).must_be_nil _(res[:password]).must_be_nil _(res[:port]).must_be_nil _(res[:path]).must_be_nil _(res[:target]).must_equal org[:target] end it "always transforms config fields into ruby symbols" do org = { "target" => "ssh://user:pass@host.com:123/path", "backend" => rand, "host" => rand, "user" => rand, "password" => rand, "port" => rand, "path" => rand, } nu = org.each_with_object({}) do |(x, y), acc| acc[x.to_sym] = y; acc end res = Train.target_config(org) _(res).must_equal nu end it "supports IPv4 URIs" do org = { target: "mock://1.2.3.4:123" } res = Train.target_config(org) _(res[:backend]).must_equal "mock" _(res[:host]).must_equal "1.2.3.4" _(res[:user]).must_be_nil _(res[:password]).must_be_nil _(res[:port]).must_equal 123 _(res[:path]).must_be_nil _(res[:target]).must_equal org[:target] end it "supports IPv6 URIs (with brackets)" do org = { target: "mock://[abc::def]:123" } res = Train.target_config(org) _(res[:backend]).must_equal "mock" _(res[:host]).must_equal "abc::def" _(res[:user]).must_be_nil _(res[:password]).must_be_nil _(res[:port]).must_equal 123 _(res[:path]).must_be_nil _(res[:target]).must_equal org[:target] end it "supports IPv6 URIs (without brackets)" do org = { target: "mock://FEDC:BA98:7654:3210:FEDC:BA98:7654:3210:123" } res = Train.target_config(org) _(res[:backend]).must_equal "mock" _(res[:host]).must_equal "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210" _(res[:user]).must_be_nil _(res[:password]).must_be_nil _(res[:port]).must_equal 123 _(res[:path]).must_be_nil _(res[:target]).must_equal org[:target] end it "supports empty URIs with schema://" do org = { target: "mock://" } res = Train.target_config(org) _(res[:backend]).must_equal "mock" _(res[:host]).must_be_nil _(res[:user]).must_be_nil _(res[:password]).must_be_nil _(res[:port]).must_be_nil _(res[:path]).must_be_nil _(res[:target]).must_equal org[:target] end it "supports empty URIs with schema:" do org = { target: "mock:" } res = Train.target_config(org) _(res[:backend]).must_equal "mock" _(res[:host]).must_be_nil _(res[:user]).must_be_nil _(res[:password]).must_be_nil _(res[:port]).must_be_nil _(res[:path]).must_be_nil _(res[:target]).must_equal org[:target] end it "supports www-form encoded passwords when the option is set" do raw_password = '+!@#$%^&*()_-\';:"\\|/?.>,<][}{=`~' encoded_password = Addressable::URI.normalize_component(raw_password, Addressable::URI::CharacterClasses::UNRESERVED) org = { target: "mock://username:#{encoded_password}@1.2.3.4:100", www_form_encoded_password: true } res = Train.target_config(org) _(res[:backend]).must_equal "mock" _(res[:host]).must_equal "1.2.3.4" _(res[:user]).must_equal "username" _(res[:password]).must_equal raw_password _(res[:port]).must_equal 100 _(res[:target]).must_equal org[:target] end it "ignores www-form-encoded password value when there is no password" do org = { target: "mock://username@1.2.3.4:100", www_form_encoded_password: true } res = Train.target_config(org) _(res[:backend]).must_equal "mock" _(res[:host]).must_equal "1.2.3.4" _(res[:user]).must_equal "username" _(res[:password]).must_be_nil _(res[:port]).must_equal 100 _(res[:target]).must_equal org[:target] end it "it raises UserError on invalid URIs (invalid scheme)" do org = { target: "123://invalid_scheme.example.com/" } _ { Train.target_config(org) }.must_raise Train::UserError end end describe "#validate_backend" do it "just returns the backend if it is provided" do x = rand _(Train.validate_backend({ backend: x })).must_equal x end it "returns the local backend if nothing was provided" do _(Train.validate_backend({})).must_equal "local" end it "returns the default backend if nothing was provided" do x = rand _(Train.validate_backend({}, x)).must_equal x end it "fails if no backend was given but a target is provided" do _ { Train.validate_backend({ target: rand }) }.must_raise Train::UserError end it "fails if no backend was given but a host is provided" do _ { Train.validate_backend({ host: rand }) }.must_raise Train::UserError end end end train-3.2.28/test/unit/transports/000077500000000000000000000000001364512547200171115ustar00rootroot00000000000000train-3.2.28/test/unit/transports/azure_test.rb000066400000000000000000000124721364512547200216310ustar00rootroot00000000000000# encoding: utf-8 require "helper" # Required because this test file acesses classes under Azure:: require "azure_mgmt_resources" require "azure_graph_rbac" require "azure_mgmt_key_vault" describe "azure transport" do def transport(options = nil) ENV["AZURE_TENANT_ID"] = "test_tenant_id" ENV["AZURE_CLIENT_ID"] = "test_client_id" ENV["AZURE_CLIENT_SECRET"] = "test_client_secret" ENV["AZURE_SUBSCRIPTION_ID"] = "test_subscription_id" # need to require this at here as it captures the envs on load require "train/transports/azure" Train::Transports::Azure.new(options) end let(:connection) { transport.connection } let(:options) { connection.instance_variable_get(:@options) } let(:cache) { connection.instance_variable_get(:@cache) } let(:credentials) { connection.instance_variable_get(:@credentials) } describe "options" do it "defaults to env options" do _(options[:tenant_id]).must_equal "test_tenant_id" _(options[:client_id]).must_equal "test_client_id" _(options[:client_secret]).must_equal "test_client_secret" _(options[:subscription_id]).must_equal "test_subscription_id" end it "allows for options override" do transport = transport(subscription_id: "102", client_id: "717") options = transport.connection.instance_variable_get(:@options) _(options[:tenant_id]).must_equal "test_tenant_id" _(options[:client_id]).must_equal "717" _(options[:client_secret]).must_equal "test_client_secret" _(options[:subscription_id]).must_equal "102" end it "allows uri parse override" do transport = transport(host: "999") options = transport.connection.instance_variable_get(:@options) _(options[:tenant_id]).must_equal "test_tenant_id" _(options[:subscription_id]).must_equal "999" end end describe "platform" do it "returns platform" do plat = connection.platform _(plat.name).must_equal "azure" _(plat.family_hierarchy).must_equal %w{cloud api} end end describe "azure_client" do class AzureResource attr_reader :hash def initialize(hash) @hash = hash end end it "can use azure_client with caching" do connection.instance_variable_set(:@credentials, {}) client = connection.azure_client(AzureResource) _(client.is_a?(AzureResource)).must_equal true _(cache[:api_call].count).must_equal 1 end it "can use azure_client without caching" do connection.instance_variable_set(:@credentials, {}) connection.disable_cache(:api_call) client = connection.azure_client(AzureResource) _(client.is_a?(AzureResource)).must_equal true _(cache[:api_call].count).must_equal 0 end it "can use azure_client default client" do management_api_client = Azure::Resources::Profiles::Latest::Mgmt::Client client = connection.azure_client _(client.class).must_equal management_api_client end it "can use azure_client graph client" do graph_api_client = Azure::GraphRbac::Profiles::Latest::Client client = connection.azure_client(graph_api_client) _(client.class).must_equal graph_api_client end it "can use azure_client vault client" do vault_api_client = ::Azure::KeyVault::Profiles::Latest::Mgmt::Client client = connection.azure_client(vault_api_client, vault_name: "Test Vault") _(client.class).must_equal vault_api_client end it "cannot instantiate azure_client vault client without a vault name" do vault_api_client = ::Azure::KeyVault::Profiles::Latest::Mgmt::Client assert_raises(Train::UserError) do connection.azure_client(vault_api_client) end end end describe "connect" do it "validate credentials" do connection.connect token = credentials[:credentials].instance_variable_get(:@token_provider) _(token.class).must_equal MsRestAzure::ApplicationTokenProvider _(credentials[:credentials].class).must_equal MsRest::TokenCredentials _(credentials[:tenant_id]).must_equal "test_tenant_id" _(credentials[:client_id]).must_equal "test_client_id" _(credentials[:client_secret]).must_equal "test_client_secret" _(credentials[:subscription_id]).must_equal "test_subscription_id" end it "validate msi credentials" do options[:client_id] = nil options[:client_secret] = nil Train::Transports::Azure::Connection.any_instance.stubs(:port_open?).returns(true) connection.connect token = credentials[:credentials].instance_variable_get(:@token_provider) _(token.class).must_equal MsRestAzure::MSITokenProvider _(credentials[:credentials].class).must_equal MsRest::TokenCredentials _(credentials[:tenant_id]).must_equal "test_tenant_id" _(credentials[:subscription_id]).must_equal "test_subscription_id" _(credentials[:client_id]).must_be_nil _(credentials[:client_secret]).must_be_nil _(options[:msi_port]).must_equal 50342 end end describe "unique_identifier" do it "returns a subscription id" do _(connection.unique_identifier).must_equal "test_subscription_id" end it "returns a tenant id" do options = connection.instance_variable_get(:@options) options[:subscription_id] = nil _(connection.unique_identifier).must_equal "test_tenant_id" end end end train-3.2.28/test/unit/transports/cisco_ios_connection_test.rb000066400000000000000000000063551364512547200246770ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/ssh" describe "CiscoIOSConnection" do let(:cls) do plat = Train::Platforms.name("mock").in_family("cisco_ios") plat.add_platform_methods def plat.cisco_ios? true end Train::Platforms::Detect.stubs(:scan).returns(plat) Train::Transports::SSH end let(:opts) do { host: "fakehost", user: "fakeuser", password: "fakepassword", } end let(:connection) do cls.new(opts).connection end describe "#initialize" do it "provides a uri" do _(connection.uri).must_equal "ssh://fakeuser@fakehost:22" end end describe "#unique_identifier" do it "returns the correct identifier" do output = "\r\nProcessor board ID 1111111111\r\n" Train::Transports::SSH::CiscoIOSConnection.any_instance .expects(:run_command_via_connection) .with("show version | include Processor") .returns(OpenStruct.new(stdout: output)) _(connection.unique_identifier).must_equal("1111111111") end end describe "#format_result" do it "returns correctly when result is 'good'" do exp = Train::Extras::CommandResult.new("good", "", 0) assert_equal exp, connection.send(:format_result, "good") end it "returns correctly when result matches /Bad IP address/" do output = "Translating \"nope\"\r\n\r\nTranslating \"nope\"\r\n\r\n% Bad IP address or host name\r\n% Unknown command or computer name, or unable to find computer address\r\n" Train::Extras::CommandResult.expects(:new).with("", output, 1) connection.send(:format_result, output) end it "returns correctly when result matches /Incomplete command/" do output = "% Incomplete command.\r\n\r\n" Train::Extras::CommandResult.expects(:new).with("", output, 1) connection.send(:format_result, output) end it "returns correctly when result matches /Invalid input detected/" do output = " ^\r\n% Invalid input detected at '^' marker.\r\n\r\n" Train::Extras::CommandResult.expects(:new).with("", output, 1) connection.send(:format_result, output) end it "returns correctly when result matches /Unrecognized host/" do output = "Translating \"nope\"\r\n% Unrecognized host or address, or protocol not running.\r\n\r\n" Train::Extras::CommandResult.expects(:new).with("", output, 1) connection.send(:format_result, output) end end describe "#format_output" do it "returns the correct output" do cmd = "show calendar" output = "show calendar\r\n10:35:50 UTC Fri Mar 23 2018\r\n7200_ios_12#\r\n7200_ios_12#" result = connection.send(:format_output, output, cmd) _(result).must_equal "10:35:50 UTC Fri Mar 23 2018" end it "returns the correct output when a pipe is used" do cmd = "show running-config | section line con 0" output = "show running-config | section line con 0\r\nline con 0\r\n exec-timeout 0 0\r\n privilege level 15\r\n logging synchronous\r\n stopbits 1\r\n7200_ios_12#\r\n7200_ios_12#" result = connection.send(:format_output, output, cmd) _(result).must_equal "line con 0\r\n exec-timeout 0 0\r\n privilege level 15\r\n logging synchronous\r\n stopbits 1" end end end train-3.2.28/test/unit/transports/gcp_test.rb000066400000000000000000000220351364512547200212500ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/gcp" describe "gcp transport" do let(:credentials_file) do require "tempfile" file = Tempfile.new("application_default_credentials.json") info = <<~INFO { "client_id": "asdfasf-asdfasdf.apps.googleusercontent.com", "client_secret": "d-asdfasdf", "refresh_token": "1/adsfasdf-lCkju3-yQmjr20xVZonrfkE48L", "type": "authorized_user" } INFO file.write(info) file.close file end let(:credentials_file_override) do require "tempfile" file = Tempfile.new("application_default_credentials.json") info = <<~INFO { "client_id": "asdfasf-asdfasdf.apps.googleusercontent.com", "client_secret": "d-asdfasdf", "refresh_token": "1/adsfasdf-lCkju3-yQmjr20xVZonrfkE48L", "type": "authorized_user" } INFO file.write(info) file.close file end def transport(options = nil) ENV["GOOGLE_APPLICATION_CREDENTIALS"] = credentials_file.path ENV["GOOGLE_CLOUD_PROJECT"] = "test_project" Train::Transports::Gcp.new(options) end let(:connection) { transport.connection } let(:options) { connection.instance_variable_get(:@options) } let(:cache) { connection.instance_variable_get(:@cache) } describe "options" do it "defaults to env options" do options[:google_application_credentials] = credentials_file.path _(options[:google_cloud_project]).must_equal "test_project" end end it "allows for options override" do transport = transport(google_application_credentials: credentials_file_override.path, google_cloud_project: "override_project") options = transport.connection.instance_variable_get(:@options) _(options[:google_application_credentials]).must_equal credentials_file_override.path _(options[:google_cloud_project]).must_equal "override_project" end describe "platform" do it "returns platform" do platform = connection.platform _(platform.name).must_equal "gcp" _(platform.family_hierarchy).must_equal %w{cloud api} end end describe "gcp_client" do it "test gcp_client with caching" do client = connection.gcp_client(Object) _(client.is_a?(Object)).must_equal true _(cache[:api_call].count).must_equal 1 end it "test gcp_client without caching" do connection.disable_cache(:api_call) client = connection.gcp_client(Object) _(client.is_a?(Object)).must_equal true _(cache[:api_call].count).must_equal 0 end end describe "gcp_compute_client" do it "test gcp_compute_client with caching" do client = connection.gcp_compute_client _(client.is_a?(Google::Apis::ComputeV1::ComputeService)).must_equal true _(cache[:api_call].count).must_equal 1 end it "test gcp_client without caching" do connection.disable_cache(:api_call) client = connection.gcp_compute_client _(client.is_a?(Google::Apis::ComputeV1::ComputeService)).must_equal true _(cache[:api_call].count).must_equal 0 end it "test gcp_compute_client application name" do client = connection.gcp_compute_client _(client.is_a?(Google::Apis::ComputeV1::ComputeService)).must_equal true _(client.client_options.application_name).must_equal "chef-inspec-train" end it "test gcp_compute_client application version" do client = connection.gcp_compute_client _(client.is_a?(Google::Apis::ComputeV1::ComputeService)).must_equal true _(client.client_options.application_version).must_equal Train::VERSION end end describe "gcp_iam_client" do it "test gcp_iam_client with caching" do client = connection.gcp_iam_client _(client.is_a?(Google::Apis::IamV1::IamService)).must_equal true _(cache[:api_call].count).must_equal 1 end it "test gcp_iam_client without caching" do connection.disable_cache(:api_call) client = connection.gcp_iam_client _(client.is_a?(Google::Apis::IamV1::IamService)).must_equal true _(cache[:api_call].count).must_equal 0 end it "test gcp_iam_client application name" do client = connection.gcp_iam_client _(client.is_a?(Google::Apis::IamV1::IamService)).must_equal true _(client.client_options.application_name).must_equal "chef-inspec-train" end it "test gcp_iam_client application version" do client = connection.gcp_iam_client _(client.is_a?(Google::Apis::IamV1::IamService)).must_equal true _(client.client_options.application_version).must_equal Train::VERSION end end describe "gcp_project_client" do it "test gcp_project_client with caching" do client = connection.gcp_project_client _(client.is_a?(Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService)).must_equal true _(cache[:api_call].count).must_equal 1 end it "test gcp_project_client without caching" do connection.disable_cache(:api_call) client = connection.gcp_project_client _(client.is_a?(Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService)).must_equal true _(cache[:api_call].count).must_equal 0 end it "test gcp_project_client application name" do client = connection.gcp_project_client _(client.is_a?(Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService)).must_equal true _(client.client_options.application_name).must_equal "chef-inspec-train" end it "test gcp_project_client application version" do client = connection.gcp_project_client _(client.is_a?(Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService)).must_equal true _(client.client_options.application_version).must_equal Train::VERSION end end describe "gcp_storage_client" do it "test gcp_storage_client with caching" do client = connection.gcp_storage_client _(client.is_a?(Google::Apis::StorageV1::StorageService)).must_equal true _(cache[:api_call].count).must_equal 1 end it "test gcp_storage_client without caching" do connection.disable_cache(:api_call) client = connection.gcp_storage_client _(client.is_a?(Google::Apis::StorageV1::StorageService)).must_equal true _(cache[:api_call].count).must_equal 0 end it "test gcp_storage_client application name" do client = connection.gcp_storage_client _(client.is_a?(Google::Apis::StorageV1::StorageService)).must_equal true _(client.client_options.application_name).must_equal "chef-inspec-train" end it "test gcp_storage_client application version" do client = connection.gcp_storage_client _(client.is_a?(Google::Apis::StorageV1::StorageService)).must_equal true _(client.client_options.application_version).must_equal Train::VERSION end end describe "gcp_admin_client" do it "test gcp_admin_client with caching" do client = connection.gcp_admin_client _(client.is_a?(Google::Apis::AdminDirectoryV1::DirectoryService)).must_equal true _(cache[:api_call].count).must_equal 1 end it "test gcp_admin_client without caching" do connection.disable_cache(:api_call) client = connection.gcp_admin_client _(client.is_a?(Google::Apis::AdminDirectoryV1::DirectoryService)).must_equal true _(cache[:api_call].count).must_equal 0 end it "test gcp_admin_client application name" do client = connection.gcp_admin_client _(client.is_a?(Google::Apis::AdminDirectoryV1::DirectoryService)).must_equal true _(client.client_options.application_name).must_equal "chef-inspec-train" end it "test gcp_admin_client application version" do client = connection.gcp_admin_client _(client.is_a?(Google::Apis::AdminDirectoryV1::DirectoryService)).must_equal true _(client.client_options.application_version).must_equal Train::VERSION end end # test options override of env vars in connect describe "connect" do let(:creds) do require "tempfile" file = Tempfile.new("creds") info = <<~INFO { "client_id": "asdfasf-asdfasdf.apps.googleusercontent.com", "client_secret": "d-asdfasdf", "refresh_token": "1/adsfasdf-lCkju3-yQmjr20xVZonrfkE48L", "type": "authorized_user" } INFO file.write(info) file.close file end it "validate gcp connection with credentials" do options[:google_application_credentials] = creds.path connection.connect _(ENV["GOOGLE_APPLICATION_CREDENTIALS"]).must_equal creds.path end it "validate gcp connection with project" do options[:google_cloud_project] = "project" connection.connect _(ENV["GOOGLE_CLOUD_PROJECT"]).must_equal "project" end end describe "unique_identifier" do it "test connection unique identifier" do client = connection _(client.unique_identifier).must_equal "asdfasf-asdfasdf.apps.googleusercontent.com" end end describe "uri" do it "test uri" do client = connection _(client.uri).must_equal "gcp://asdfasf-asdfasdf.apps.googleusercontent.com" end end end train-3.2.28/test/unit/transports/helpers/000077500000000000000000000000001364512547200205535ustar00rootroot00000000000000train-3.2.28/test/unit/transports/helpers/azure/000077500000000000000000000000001364512547200217015ustar00rootroot00000000000000train-3.2.28/test/unit/transports/helpers/azure/file_credentials_test.rb000066400000000000000000000100371364512547200265620ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "tempfile" require "train/transports/helpers/azure/file_credentials" describe "parse_credentials_file" do let(:cred_file_single_entry) do file = Tempfile.new("cred_file") info = <<-INFO [my_subscription_id] client_id = "my_client_id" client_secret = "my_client_secret" tenant_id = "my_tenant_id" INFO file.write(info) file.close file end let(:cred_file_multiple_entries) do file = Tempfile.new("cred_file") info = <<-INFO [my_subscription_id] client_id = "my_client_id" client_secret = "my_client_secret" tenant_id = "my_tenant_id" [my_subscription_id2] client_id = "my_client_id2" client_secret = "my_client_secret2" tenant_id = "my_tenant_id2" INFO file.write(info) file.close file end let(:options) { { credentials_file: cred_file_multiple_entries.path } } it "handles a nil file" do options[:credentials_file] = nil result = Train::Transports::Helpers::Azure::FileCredentials.parse(**options) assert_empty(result) end it "returns empty hash when no credentials file detected" do result = Train::Transports::Helpers::Azure::FileCredentials.parse(**{}) assert_empty(result) end it "loads only entry from file when no subscription id given" do options[:credentials_file] = cred_file_single_entry.path result = Train::Transports::Helpers::Azure::FileCredentials.parse(**options) assert_equal("my_tenant_id", result[:tenant_id]) assert_equal("my_client_id", result[:client_id]) assert_equal("my_client_secret", result[:client_secret]) assert_equal("my_subscription_id", result[:subscription_id]) end it "raises an error when no subscription id given and multiple entries" do error = assert_raises RuntimeError do Train::Transports::Helpers::Azure::FileCredentials.parse(**options) end assert_equal("Credentials file must have one entry. Check your credentials file. If you have more than one entry set AZURE_SUBSCRIPTION_ID environment variable.", error.message) end it "loads entry when subscription id is given" do options[:subscription_id] = "my_subscription_id" result = Train::Transports::Helpers::Azure::FileCredentials.parse(**options) assert_equal("my_tenant_id", result[:tenant_id]) assert_equal("my_client_id", result[:client_id]) assert_equal("my_client_secret", result[:client_secret]) assert_equal("my_subscription_id", result[:subscription_id]) end it "raises an error when subscription id not found" do options[:subscription_id] = "missing_subscription_id" error = assert_raises RuntimeError do Train::Transports::Helpers::Azure::FileCredentials.parse(**options) end assert_equal("No credentials found for subscription number missing_subscription_id", error.message) end it "loads entry based on index" do ENV["AZURE_SUBSCRIPTION_NUMBER"] = "2" result = Train::Transports::Helpers::Azure::FileCredentials.parse(**options) ENV.delete("AZURE_SUBSCRIPTION_NUMBER") assert_equal("my_tenant_id2", result[:tenant_id]) assert_equal("my_client_id2", result[:client_id]) assert_equal("my_client_secret2", result[:client_secret]) assert_equal("my_subscription_id2", result[:subscription_id]) end it "raises an error when index is out of bounds" do ENV["AZURE_SUBSCRIPTION_NUMBER"] = "3" error = assert_raises RuntimeError do Train::Transports::Helpers::Azure::FileCredentials.parse(**options) end ENV.delete("AZURE_SUBSCRIPTION_NUMBER") assert_equal("Your credentials file only contains 2 subscriptions. You specified number 3.", error.message) end it "raises an error when index 0 is given" do ENV["AZURE_SUBSCRIPTION_NUMBER"] = "0" error = assert_raises RuntimeError do Train::Transports::Helpers::Azure::FileCredentials.parse(**options) end ENV.delete("AZURE_SUBSCRIPTION_NUMBER") assert_equal("Index must be greater than 0.", error.message) end end train-3.2.28/test/unit/transports/local_test.rb000066400000000000000000000121771364512547200215770ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/local" class TransportHelper attr_accessor :transport def initialize(user_opts = {}) opts = { platform_name: "mock", family_hierarchy: ["mock"] }.merge(user_opts) plat = Train::Platforms.name(opts[:platform_name]) plat.family_hierarchy = opts[:family_hierarchy] plat.add_platform_methods Train::Platforms::Detect.stubs(:scan).returns(plat) @transport = Train::Transports::Local.new(user_opts) end end describe "local transport" do let(:transport) { TransportHelper.new.transport } let(:connection) { transport.connection } it "can be instantiated" do _(transport).wont_be_nil end it "gets the connection" do _(connection).must_be_kind_of Train::Transports::Local::Connection end it "provides a uri" do _(connection.uri).must_equal "local://" end it "doesnt wait to be read" do _(connection.wait_until_ready).must_be_nil end it "inspects with readable output" do _(connection.inspect).must_match(/Train::Transports::Local::Connection\[\w+\]/) end it "can be closed" do _(connection.close).must_be_nil end it "has no login command" do _(connection.login_command).must_be_nil end it "provides a run_command_via_connection method" do methods = connection.class.private_instance_methods(false) _(methods.include?(:run_command_via_connection)).must_equal true end it "provides a file_via_connection method" do methods = connection.class.private_instance_methods(false) _(methods.include?(:file_via_connection)).must_equal true end describe "when overriding runner selection" do it "can select the `GenericRunner`" do Train::Transports::Local::Connection::GenericRunner .expects(:new) Train::Transports::Local::Connection::WindowsPipeRunner .expects(:new) .never Train::Transports::Local::Connection::WindowsShellRunner .expects(:new) .never Train::Transports::Local::Connection.new(command_runner: :generic) end it "can select the `WindowsPipeRunner`" do Train::Transports::Local::Connection::GenericRunner .expects(:new) .never Train::Transports::Local::Connection::WindowsPipeRunner .expects(:new) Train::Transports::Local::Connection::WindowsShellRunner .expects(:new) .never Train::Transports::Local::Connection.new(command_runner: :windows_pipe) end it "can select the `WindowsShellRunner`" do Train::Transports::Local::Connection::GenericRunner .expects(:new) .never Train::Transports::Local::Connection::WindowsPipeRunner .expects(:new) .never Train::Transports::Local::Connection::WindowsShellRunner .expects(:new) Train::Transports::Local::Connection.new(command_runner: :windows_shell) end it "throws a RuntimeError when an invalid runner type is passed" do _ { Train::Transports::Local::Connection.new(command_runner: :nope ) } .must_raise(RuntimeError, "Runner type `:nope` not supported") end end describe "when running a local command" do let(:cmd_runner) { Minitest::Mock.new } def mock_run_cmd(cmd, &block) cmd_runner.expect :run_command, nil Mixlib::ShellOut.stub :new, cmd_runner do |*args| yield end end it "gets stdout" do mock_run_cmd(rand) do x = rand cmd_runner.expect :stdout, x cmd_runner.expect :stderr, nil cmd_runner.expect :exitstatus, nil _(connection.run_command(rand).stdout).must_equal x end end it "gets stderr" do mock_run_cmd(rand) do x = rand cmd_runner.expect :stdout, nil cmd_runner.expect :stderr, x cmd_runner.expect :exitstatus, nil _(connection.run_command(rand).stderr).must_equal x end end it "gets exit_status" do mock_run_cmd(rand) do x = rand cmd_runner.expect :stdout, nil cmd_runner.expect :stderr, nil cmd_runner.expect :exitstatus, x _(connection.run_command(rand).exit_status).must_equal x end end end describe "when running on Windows" do let(:connection) do TransportHelper.new(family_hierarchy: ["windows"]).transport.connection end let(:runner) { mock } it "uses `WindowsPipeRunner` by default" do Train::Transports::Local::Connection::WindowsPipeRunner .expects(:new) .returns(runner) Train::Transports::Local::Connection::WindowsShellRunner .expects(:new) .never runner.expects(:run_command).with("not actually executed") connection.run_command("not actually executed") end it "uses `WindowsShellRunner` when a named pipe is not available" do Train::Transports::Local::Connection::WindowsPipeRunner .expects(:new) .raises(Train::Transports::Local::PipeError) Train::Transports::Local::Connection::WindowsShellRunner .expects(:new) .returns(runner) runner.expects(:run_command).with("not actually executed") connection.run_command("not actually executed") end end end train-3.2.28/test/unit/transports/mock_test.rb000066400000000000000000000112501364512547200214250ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/mock" require "digest/sha2" describe "mock transport" do let(:transport) { Train::Transports::Mock.new(verbose: true) } let(:connection) { transport.connection } it "can be instantiated" do _(transport).wont_be_nil end it "can create a connection" do _(connection).wont_be_nil end it "provides a uri" do _(connection.uri).must_equal "mock://" end it "provides a run_command_via_connection method" do methods = connection.class.private_instance_methods(false) _(methods.include?(:run_command_via_connection)).must_equal true end it "provides a file_via_connection method" do methods = connection.class.private_instance_methods(false) _(methods.include?(:file_via_connection)).must_equal true end describe "when running a mocked command" do let(:mock_cmd) {} it "has a simple mock command creator" do out = rand cls = Train::Transports::Mock::Connection::Command res = cls.new(out, "", 0) _(connection.mock_command("test", out)).must_equal res end it "handles nil commands" do assert_output "", /Command not mocked/ do _(connection.run_command(nil).stdout).must_equal("") end end it "can mock up nil commands" do out = rand connection.mock_command("", rand) # don't pull this result! always mock the input connection.mock_command(nil, out) # pull this result _(connection.run_command(nil).stdout).must_equal(out) end it "gets results for stdout" do out = rand cmd = rand connection.mock_command(cmd, out) _(connection.run_command(cmd).stdout).must_equal(out) end it "gets results for stderr" do err = rand cmd = rand connection.mock_command(cmd, nil, err) _(connection.run_command(cmd).stderr).must_equal(err) end it "gets results for exit_status" do code = rand cmd = rand connection.mock_command(cmd, nil, nil, code) _(connection.run_command(cmd).exit_status).must_equal(code) end it "can mock a command via its SHA2 sum" do out = rand.to_s cmd = rand.to_s shacmd = Digest::SHA256.hexdigest cmd connection.mock_command(shacmd, out) _(connection.run_command(cmd).stdout).must_equal(out) end end describe "when accessing a mocked os" do it "has the default mock os faily set to mock" do _(connection.os[:name]).must_equal "mock" _(connection.platform[:name]).must_equal "mock" end it "sets the OS to the mocked value" do connection.mock_os({ name: "centos", family: "redhat" }) _(connection.os.linux?).must_equal true _(connection.os.redhat?).must_equal true _(connection.os[:family]).must_equal "redhat" end it "allows the setting of the name" do connection.mock_os({ name: "foo" }) _(connection.os[:name]).must_equal "foo" end it "allows setting of the family" do connection.mock_os({ family: "foo" }) _(connection.os[:family]).must_equal "foo" end it "allows setting of the release" do connection.mock_os({ release: "1.2.3" }) _(connection.os[:release]).must_equal "1.2.3" end it "allows setting of the arch" do connection.mock_os({ arch: "amd123" }) _(connection.os[:arch]).must_equal "amd123" end it "allow setting of multiple values" do connection.mock_os({ name: "foo", family: "bar" }) _(connection.os[:name]).must_equal "foo" _(connection.os[:family]).must_equal "bar" _(connection.os[:arch]).must_equal "unknown" _(connection.os[:release]).must_equal "unknown" end it "properly handles a nil value" do connection.mock_os(nil) _(connection.os[:name]).must_equal "mock" _(connection.os[:family]).must_equal "mock" end end describe "when accessing a mocked file" do it "handles a non-existing file" do x = rand.to_s assert_output "", /File not mocked/ do f = connection.file(x) _(f).must_be_kind_of Train::Transports::Mock::Connection::File _(f.exist?).must_equal false _(f.path).must_equal x end end # tests if all fields between the local json and resulting mock file # are equal JSON_DATA = Train.create("local").connection.file(__FILE__).to_json RES = Train::Transports::Mock::Connection::File.from_json(JSON_DATA) %w{ content mode owner group }.each do |f| it "can be initialized from json (field #{f})" do r = RES.send(f) d = JSON_DATA[f] if d _(r).must_equal d else _(r).must_be_nil # I think just group on windows end end end end end train-3.2.28/test/unit/transports/ssh_connection_test.rb000066400000000000000000000040531364512547200235130ustar00rootroot00000000000000require "helper" require "train/transports/ssh" require "train/transports/ssh_connection" # Mocha limitations don't let us mock a function # such that it can receive a block, so here's a minimal # class that simulates Net::SSH::Connection::Channel to the # extent required by Transports::SSH::Connection class MockChannel def exec(cmd) @cmd = cmd yield("ignored", true) end def data_handler @handler end def on_data(&block) @handler = block end def mock_inbound_data(data) # trigger the 'on-data' event off of the channel. @handler.call("ignored", data) end def on_extended_data; end def on_request(any); end end describe "ssh connection" do let(:cls) do plat = Train::Platforms.name("mock").in_family("linux") plat.add_platform_methods Train::Platforms::Detect.stubs(:scan).returns(plat) Train::Transports::SSH::Connection end let(:conf) do { host: rand.to_s, password: rand.to_s, transport_options: {}, } end describe "#run_command_via_connection through BaseConnection::run_command" do let(:ssh) { cls.new(conf) } # A bit more mocking than I'd like to see, but there's no sane way around # it if we want to test output handling behavior. let(:inbound_data) { "testdata" } let(:channel_mock) { MockChannel.new } let(:session_mock) do session_mock = mock session_mock.stubs(:closed?).returns false session_mock.stubs(:open_channel).yields(channel_mock) # Simulate the way that Net::SSH::Session processes request(s) on invoking #loop. session_mock.stubs(:loop).with do channel_mock.mock_inbound_data(inbound_data) end session_mock end it "invokes the provided block when a block is provided and data is received" do ssh.stubs(:session).returns(session_mock) called = false # run_command b/c run_command_via_connection is private. ssh.run_command("test") do |data| called = true _(data).must_equal inbound_data end _(called).must_equal true end end end train-3.2.28/test/unit/transports/ssh_test.rb000066400000000000000000000271301364512547200212750ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/ssh" describe "ssh transport" do let(:cls) do plat = Train::Platforms.name("mock").in_family("linux") plat.add_platform_methods Train::Platforms::Detect.stubs(:scan).returns(plat) Train::Transports::SSH end let(:conf) do { host: rand.to_s, password: rand.to_s, key_files: rand.to_s, proxy_command: "ssh root@127.0.0.1 -W %h:%p", } end let(:cls_agent) { cls.new({ host: rand.to_s }) } describe "default options" do let(:ssh) { cls.new({ host: "dummy" }) } it "can be instantiated (with valid config)" do _(ssh).wont_be_nil end it "configures the host" do _(ssh.options[:host]).must_equal "dummy" end it "has default port" do _(ssh.options[:port]).must_equal 22 end it "has default user" do _(ssh.options[:user]).must_equal "root" end it "by default does not request a pty" do _(ssh.options[:pty]).must_equal false end end describe "connection options" do let(:ssh) { cls.new({ host: "dummy" }) } let(:opts) { {} } let(:connection_options) { ssh.send(:connection_options, opts) } it "does not set a paranoid option - deprecated in net-ssh 4.2" do _(connection_options.key?(:paranoid)).must_equal false end describe "various values are mapped appropriately for verify_host_key" do # This would be better: # Net::SSH::Version.stub_const(:CURRENT, Net::SSH::Version[5,0,1]) current_version = Net::SSH::Version::CURRENT threshold_version = Net::SSH::Version[5, 0, 0] if current_version < threshold_version it "maps correctly when net-ssh < 5.0" do { "true" => true, "false" => false, nil => false, }.each do |given, expected| opts = { verify_host_key: given } seen_opts = ssh.send(:connection_options, opts) _(seen_opts[:verify_host_key]).must_equal expected end end it "defaults verify_host_key option to false" do _(connection_options[:verify_host_key]).must_equal false end else it "maps correctly when net-ssh > 5.0" do { "true" => :always, "false" => :never, true => :always, false => :never, "always" => :always, "never" => :never, nil => :never, }.each do |given, expected| opts = { verify_host_key: given } seen_opts = ssh.send(:connection_options, opts) _(seen_opts[:verify_host_key]).must_equal expected end end it "defaults verify_host_key option to :never" do _(connection_options[:verify_host_key]).must_equal :never end end end end describe "ssh options" do let(:ssh) { cls.new(conf) } let(:connection) { ssh.connection } it "includes BatchMode when :non_interactive is set" do conf[:non_interactive] = true _(connection.ssh_opts.include?("BatchMode=yes")).must_equal true end it "excludes BatchMode when :non_interactive is not set" do _(connection.ssh_opts.include?("BatchMode=yes")).must_equal false end end describe "opening a connection" do let(:ssh) { cls.new(conf) } let(:connection) { ssh.connection } it "provides a run_command_via_connection method" do methods = connection.class.private_instance_methods(false) _(methods.include?(:run_command_via_connection)).must_equal true end it "provides a file_via_connection method" do methods = connection.class.private_instance_methods(false) _(methods.include?(:file_via_connection)).must_equal true end it "gets the connection" do _(connection).must_be_kind_of Train::Transports::SSH::Connection end it "provides a uri" do _(connection.uri).must_equal "ssh://root@#{conf[:host]}:22" end it "must respond to wait_until_ready" do _(connection).must_respond_to :wait_until_ready end it "can be closed" do _(connection.close).must_be_nil end it "has a login command == ssh" do _(connection.login_command.command).must_equal "ssh" end it "has login command arguments" do _(connection.login_command.arguments).must_equal([ "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "IdentitiesOnly=yes", "-o", "LogLevel=ERROR", "-o", "ForwardAgent=no", "-i", conf[:key_files], "-o", "ProxyCommand='ssh root@127.0.0.1 -W %h:%p'", "-p", "22", "root@#{conf[:host]}" ]) end it "sets the right auth_methods when password is specified" do conf[:key_files] = nil _(cls.new(conf).connection.method(:options).call[:auth_methods]).must_equal %w{none password keyboard-interactive} end it "sets the right auth_methods when keys are specified" do conf[:password] = nil _(cls.new(conf).connection.method(:options).call[:auth_methods]).must_equal %w{none publickey} end it "sets the right auth_methods for agent auth" do cls_agent.stubs(:ssh_known_identities).returns({ some: "rsa_key" }) _(cls_agent.connection.method(:options).call[:auth_methods]).must_equal %w{none publickey} end it "works with ssh agent auth" do cls_agent.stubs(:ssh_known_identities).returns({ some: "rsa_key" }) cls_agent.connection end it "sets up a proxy when ssh proxy command is specified" do mock = MiniTest::Mock.new mock.expect(:call, true) do |hostname, username, options| options[:proxy].is_a?(Net::SSH::Proxy::Command) && "ssh root@127.0.0.1 -W %h:%p" == options[:proxy].command_line_template end connection.stubs(:run_command) Net::SSH.stub(:start, mock) do connection.wait_until_ready end mock.verify end end describe "obscured_options" do it "masks passwords" do connection = cls.new(conf).connection assert_equal "", connection.obscured_options[:password] end end describe "failed configuration" do it "works with a minimum valid config" do cls.new(conf).connection end it "does not like host == nil" do conf.delete(:host) _ { cls.new(conf).connection }.must_raise Train::ClientError end it "reverts to root on user == nil" do conf[:user] = nil cls.new(conf).connection.method(:options).call[:user] == "root" end it "does not like key and password == nil" do cls_agent.stubs(:ssh_known_identities).returns({}) _ { cls_agent.connection }.must_raise Train::ClientError end it "wont connect if it is not possible" do conf[:connection_timeout] = 1 conf[:connection_retries] = 1 conf[:host] = "localhost" conf[:port] = 1 conf.delete :proxy_command conn = cls.new(conf).connection _ { conn.run_command("uname") }.must_raise Train::Transports::SSHFailed end end end describe "ssh transport with bastion" do let(:cls) do plat = Train::Platforms.name("mock").in_family("linux") plat.add_platform_methods Train::Platforms::Detect.stubs(:scan).returns(plat) Train::Transports::SSH end let(:conf) do { host: rand.to_s, password: rand.to_s, key_files: rand.to_s, bastion_host: "bastion_dummy", } end let(:cls_agent) { cls.new({ host: rand.to_s }) } describe "bastion" do describe "default options" do let(:ssh) { cls.new({ bastion_host: "bastion_dummy" }) } it "configures the host" do _(ssh.options[:bastion_host]).must_equal "bastion_dummy" end it "has default port" do _(ssh.options[:bastion_port]).must_equal 22 end it "has default user" do _(ssh.options[:bastion_user]).must_equal "root" end end describe "opening a connection" do let(:ssh) { cls.new(conf) } let(:connection) { ssh.connection } it "provides a run_command_via_connection method" do methods = connection.class.private_instance_methods(false) _(methods.include?(:run_command_via_connection)).must_equal true end it "provides a file_via_connection method" do methods = connection.class.private_instance_methods(false) _(methods.include?(:file_via_connection)).must_equal true end it "gets the connection" do _(connection).must_be_kind_of Train::Transports::SSH::Connection end it "provides a uri" do _(connection.uri).must_equal "ssh://root@#{conf[:host]}:22" end it "must respond to wait_until_ready" do _(connection).must_respond_to :wait_until_ready end it "can be closed" do _(connection.close).must_be_nil end it "has a login command == ssh" do _(connection.login_command.command).must_equal "ssh" end make_my_diffs_pretty! it "has login command arguments" do _(connection.login_command.arguments).must_equal([ "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "IdentitiesOnly=yes", "-o", "LogLevel=ERROR", "-o", "ForwardAgent=no", "-i", conf[:key_files], "-o", "ProxyCommand='ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o LogLevel=ERROR -o ForwardAgent=no -i #{conf[:key_files]} root@bastion_dummy -p 22 -W %h:%p'", "-p", "22", "root@#{conf[:host]}" ]) end it "sets the right auth_methods when password is specified" do conf[:key_files] = nil _(cls.new(conf).connection.method(:options).call[:auth_methods]).must_equal %w{none password keyboard-interactive} end it "sets the right auth_methods when keys are specified" do conf[:password] = nil _(cls.new(conf).connection.method(:options).call[:auth_methods]).must_equal %w{none publickey} end it "sets the right auth_methods for agent auth" do cls_agent.stubs(:ssh_known_identities).returns({ some: "rsa_key" }) _(cls_agent.connection.method(:options).call[:auth_methods]).must_equal %w{none publickey} end it "works with ssh agent auth" do cls_agent.stubs(:ssh_known_identities).returns({ some: "rsa_key" }) cls_agent.connection end it "sets up a proxy when ssh proxy command is specified" do mock = MiniTest::Mock.new mock.expect(:call, true) do |hostname, username, options| options[:proxy].is_a?(Net::SSH::Proxy::Command) && "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o LogLevel=ERROR -o ForwardAgent=no -i #{conf[:key_files]} root@bastion_dummy -p 22 -W %h:%p" == options[:proxy].command_line_template end connection.stubs(:run_command) Net::SSH.stub(:start, mock) do connection.wait_until_ready end mock.verify end end end end describe "ssh transport with bastion and proxy" do let(:cls) do plat = Train::Platforms.name("mock").in_family("linux") plat.add_platform_methods Train::Platforms::Detect.stubs(:scan).returns(plat) Train::Transports::SSH end let(:conf) do { host: rand.to_s, password: rand.to_s, key_files: rand.to_s, bastion_host: "bastion_dummy", proxy_command: "dummy", } end let(:cls_agent) { cls.new({ host: rand.to_s }) } describe "bastion and proxy" do it "will throw an exception when both proxy_command and bastion_host is specified" do _ { cls.new(conf).connection }.must_raise Train::ClientError end end end train-3.2.28/test/unit/transports/vmware_test.rb000066400000000000000000000124151364512547200220010ustar00rootroot00000000000000# encoding: utf-8 require "helper" require "train/transports/vmware" describe "Train::Transports::VMware::Connection" do def add_stubs(stub_options) Train::Transports::VMware::Connection.any_instance .stubs(:detect_powershell_binary) .returns(stub_options[:powershell_binary] || :pwsh) Train::Transports::VMware::Connection.any_instance .stubs(:powercli_version) .returns("10.1.1.8827525") Train::Transports::VMware::Connection.any_instance .stubs(:run_command_via_connection) .with("(Get-VMHost | Get-View).hardware.systeminfo.uuid") .returns(stub_options[:mock_uuid_result] || nil) if stub_options[:mock_connect_result] Train::Transports::VMware::Connection.any_instance .expects(:run_command_via_connection) .with("Connect-VIServer 10.0.0.10 -User testuser -Password supersecurepassword | Out-Null") .returns(stub_options[:mock_connect_result]) else Train::Transports::VMware::Connection.any_instance .stubs(:connect) .returns(nil) end end def create_transport(options = {}) ENV["VISERVER"] = "10.0.0.10" ENV["VISERVER_USERNAME"] = "testuser" ENV["VISERVER_PASSWORD"] = "supersecurepassword" add_stubs(options[:stub_options] || {}) Train::Transports::VMware.new(options[:transport_options]) end describe "#initialize" do it "defaults to ENV options" do options = create_transport.connection.instance_variable_get(:@options) _(options[:viserver]).must_equal "10.0.0.10" _(options[:username]).must_equal "testuser" _(options[:password]).must_equal "supersecurepassword" _(options[:insecure]).must_equal false end it "allows for overriding options" do transport = create_transport( transport_options: { viserver: "10.1.1.1", username: "anotheruser", password: "notsecurepassword", insecure: false, } ) options = transport.connection.instance_variable_get(:@options) _(options[:viserver]).must_equal "10.1.1.1" _(options[:username]).must_equal "anotheruser" _(options[:password]).must_equal "notsecurepassword" _(options[:insecure]).must_equal false end it "ignores certificate validation if --insecure is used" do Train::Transports::VMware::Connection.any_instance .expects(:run_command_via_connection) .with("Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$False") .returns(nil) transport = create_transport(transport_options: { insecure: true }) options = transport.connection.instance_variable_get(:@options) _(options[:insecure]).must_equal true end it "uses the Local connection when Windows PowerShell is found" do require "train/transports/local" Train::Transports::Local::Connection.expects(:new) create_transport( stub_options: { powershell_binary: :powershell, } ).connection end end # rubocop:disable Style/BlockDelimiters describe "#connect" do def mock_connect_result(stderr, exit_status) OpenStruct.new(stderr: stderr, exit_status: exit_status) end it "raises certificate error when stderr matches regular expression" do e = _ { create_transport( stub_options: { mock_connect_result: mock_connect_result( "Invalid server certificate", 1 ), } ).connection }.must_raise(RuntimeError) _(e.message).must_match(/Unable to connect.*Please use `--insecure`/) end it "raises auth error when stderr matches regular expression" do e = _ { create_transport( stub_options: { mock_connect_result: mock_connect_result( "incorrect user name or password", 1 ), } ).connection }.must_raise(RuntimeError) _(e.message).must_match(/Unable to connect.*Incorrect username/) end it "redacts the password when an unspecified error is raised" do e = _ { create_transport( stub_options: { mock_connect_result: mock_connect_result( "something unexpected -Password supersecret -AnotherOption", 1 ), } ).connection }.must_raise(RuntimeError) _(e.message).must_match(/-Password REDACTED/) end end describe "#platform" do it "returns correct platform details" do platform = create_transport.connection.platform _(platform.clean_name).must_equal "vmware" _(platform.family_hierarchy).must_equal %w{cloud api} _(platform.platform).must_equal(release: "vmware-powercli-10.1.1.8827525") _(platform.vmware?).must_equal true end end describe "#unique_identifier" do it "returns the correct unique identifier" do uuid = "1f261432-e23e-6911-841c-94c6911a02dd" mock_uuid_result = OpenStruct.new( stdout: uuid + "\n" ) connection = create_transport( stub_options: { mock_uuid_result: mock_uuid_result } ).connection _(connection.unique_identifier).must_equal(uuid) end end describe "#uri" do it "returns the correct URI" do _(create_transport.connection.uri).must_equal "vmware://testuser@10.0.0.10" end end end train-3.2.28/test/unit/version_test.rb000066400000000000000000000002151364512547200177410ustar00rootroot00000000000000# encoding: utf-8 require "helper" describe Train do it "defines a version" do _(Train::VERSION).must_be_instance_of String end end train-3.2.28/test/windows/000077500000000000000000000000001364512547200154055ustar00rootroot00000000000000train-3.2.28/test/windows/local_test.rb000066400000000000000000000162231364512547200200670ustar00rootroot00000000000000# encoding: utf-8 # author: Christoph Hartmann # author: Dominik Richter require "minitest/autorun" require "minitest/spec" require "mocha/setup" require "train" require "tempfile" require "logger" # Loading here to ensure methods exist to be stubbed require "train/transports/local" describe "windows local command" do let(:backend) do # get final config target_config = Train.target_config({ logger: Logger.new(STDERR, level: :info) }) # initialize train Train.create("local", target_config) end let(:conn) { backend.connection } it "verify os" do os = conn.os _(os[:name]).must_match(/windows_server.*/) _(os[:family]).must_equal "windows" _(os[:release]).must_match(/\d+(\.\d+)+/) _(os[:arch]).must_equal "x86_64" end it "run echo test" do cmd = conn.run_command('Write-Output "test"') _(cmd.stdout).must_equal "test\r\n" _(cmd.stderr).must_equal "" _(cmd.exit_status).must_equal 0 end it "run script without exit code" do cmd = conn.run_command("powershell -file test/fixtures/PowerShell/exit_zero.ps1") _(cmd.stdout).must_equal "Hello\r\n" _(cmd.stderr).must_equal "" _(cmd.exit_status).must_equal 0 end it "run script without exit code" do cmd = conn.run_command("powershell -file test/fixtures/PowerShell/exit_fortytwo.ps1") _(cmd.stdout).must_equal "Goodbye\r\n" _(cmd.stderr).must_equal "" _(cmd.exit_status).must_equal 42 end it "returns exit code 1 for a script that throws" do cmd = conn.run_command("powershell -file test/fixtures/PowerShell/throws.ps1") _(cmd.stdout).must_match(/Next line throws/) _(cmd.stderr).must_equal "" _(cmd.exit_status).must_equal 1 end describe "force 64 bit powershell command" do let(:runner) { conn.instance_variable_get(:@runner) } let(:powershell) { runner.instance_variable_get(:@powershell_cmd) } RUBY_PLATFORM_DUP = RUBY_PLATFORM.dup def override_platform(platform) ::Object.send(:remove_const, :RUBY_PLATFORM) ::Object.const_set(:RUBY_PLATFORM, platform) end after do backend.instance_variable_set(:@connection, nil) ::Object.send(:remove_const, :RUBY_PLATFORM) ::Object.const_set(:RUBY_PLATFORM, RUBY_PLATFORM_DUP) end it "use normal powershell with PipeRunner" do Train::Transports::Local::Connection::WindowsPipeRunner .any_instance .expects(:acquire_pipe) .returns("acquired") override_platform("x64-mingw32") _(powershell).must_equal "powershell" end it "use 64bit powershell with PipeRunner" do Train::Transports::Local::Connection::WindowsPipeRunner .any_instance .expects(:acquire_pipe) .returns("acquired") override_platform("i386-mingw32") _(powershell).must_equal "#{ENV["SystemRoot"]}\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe" end it "use normal powershell with ShellRunner" do Train::Transports::Local::Connection::WindowsPipeRunner .any_instance .expects(:acquire_pipe) .returns(nil) override_platform("x64-mingw32") _(runner.class).must_equal Train::Transports::Local::Connection::WindowsShellRunner _(powershell).must_equal "powershell" end it "use 64bit powershell with ShellRunner" do Train::Transports::Local::Connection::WindowsPipeRunner .any_instance .expects(:acquire_pipe) .returns(nil) override_platform("i386-mingw32") _(runner.class).must_equal Train::Transports::Local::Connection::WindowsShellRunner _(powershell).must_equal "#{ENV["SystemRoot"]}\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe" end end it "use powershell piping" do cmd = conn.run_command("New-Object -Type PSObject | Add-Member -MemberType NoteProperty -Name A -Value (Write-Output 'PropertyA') -PassThru | Add-Member -MemberType NoteProperty -Name B -Value (Write-Output 'PropertyB') -PassThru | ConvertTo-Json") _(cmd.stdout).must_equal "{\r\n \"A\": \"PropertyA\",\r\n \"B\": \"PropertyB\"\r\n}\r\n" _(cmd.stderr).must_equal "" end it "can execute a command using a named pipe" do SecureRandom.expects(:hex).returns("via_pipe") Train::Transports::Local::Connection::WindowsShellRunner .any_instance .expects(:new) .never cmd = conn.run_command('Write-Output "Create pipe"') _(File.exist?("//./pipe/inspec_via_pipe")).must_equal true _(cmd.stdout).must_equal "Create pipe\r\n" _(cmd.stderr).must_equal "" end it "can execute a command via ShellRunner if pipe creation fails" do # By forcing `acquire_pipe` to fail to return a pipe, any attempts to create # a `WindowsPipeRunner` object should fail. If we can still run a command, # then we know that it was successfully executed by `Mixlib::ShellOut`. Train::Transports::Local::Connection::WindowsPipeRunner .any_instance .expects(:acquire_pipe) .at_least_once .returns(nil) proc { Train::Transports::Local::Connection::WindowsPipeRunner.new } .must_raise(Train::Transports::Local::PipeError) cmd = conn.run_command('Write-Output "test"') _(cmd.stdout).must_equal "test\r\n" _(cmd.stderr).must_equal "" end describe "file" do before do @temp = Tempfile.new("foo") @temp.write("hello world") @temp.rewind end let(:file) { conn.file(@temp.path) } it "exists" do _(file.exist?).must_equal(true) end it "is a file" do _(file.file?).must_equal(true) end it "has type :file" do _(file.type).must_equal(:file) end it "has content" do _(file.content).must_equal("hello world") end it "returns basename of file" do file_name = ::File.basename(@temp) _(file.basename).must_equal(file_name) end it "has owner name" do _(file.owner).wont_be_nil end it "has no group name" do _(file.group).must_be_nil end it "has no mode" do _(file.mode).wont_be_nil end it "has an md5sum" do _(file.md5sum).wont_be_nil end it "has an sha256sum" do _(file.sha256sum).wont_be_nil end it "has no modified time" do _(file.mtime).wont_be_nil end it "has no size" do _(file.size).wont_be_nil end it "has size 11" do size = ::File.size(@temp) _(file.size).must_equal size end it "has no selinux_label handling" do _(file.selinux_label).must_be_nil end it "has product_version" do _(file.product_version).wont_be_nil end it "has file_version" do _(file.file_version).wont_be_nil end it "provides a json representation" do j = file.to_json _(j).must_be_kind_of Hash _(j["type"]).must_equal :file end after do @temp.close @temp.unlink end end describe "file" do before do @temp = Tempfile.new("foo bar") @temp.rewind end let(:file) { conn.file(@temp.path) } it 'provides the full path with whitespace for path #{@temp.path}' do _(file.path).must_equal @temp.path end after do @temp.close @temp.unlink end end after do # close the connection conn.close end end train-3.2.28/train-core.gemspec000066400000000000000000000024401364512547200163440ustar00rootroot00000000000000# encoding: utf-8 lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "train/version" Gem::Specification.new do |spec| spec.name = "train-core" spec.version = Train::VERSION spec.authors = ["Chef InSpec Team"] spec.email = ["inspec@chef.io"] spec.summary = "Transport interface to talk to a selected set of backends." spec.description = "A minimal Train with a backends for ssh and winrm." spec.license = "Apache-2.0" spec.metadata = { "homepage_uri" => "https://github.com/inspec/train", "changelog_uri" => "https://github.com/inspec/train/blob/master/CHANGELOG.md", "source_code_uri" => "https://github.com/inspec/train", "bug_tracker_uri" => "https://github.com/inspec/train/issues", } spec.required_ruby_version = ">= 2.4" spec.files = Dir.glob("{LICENSE,lib/**/*}") .grep_v(%r{transports/(azure|clients|docker|gcp|helpers|vmware)}) .reject { |f| File.directory?(f) } spec.require_paths = ["lib"] spec.add_dependency "addressable", "~> 2.5" spec.add_dependency "json", ">= 1.8", "< 3.0" spec.add_dependency "mixlib-shellout", ">= 2.0", "< 4.0" spec.add_dependency "net-scp", ">= 1.2", "< 3.0" spec.add_dependency "net-ssh", ">= 2.9", "< 6.0" end train-3.2.28/train.gemspec000066400000000000000000000034071364512547200154220ustar00rootroot00000000000000# encoding: utf-8 lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "train/version" Gem::Specification.new do |spec| spec.name = "train" spec.version = Train::VERSION spec.authors = ["Chef InSpec Team"] spec.email = ["inspec@chef.io"] spec.summary = "Transport interface to talk to different backends." spec.description = "Transport interface to talk to different backends." spec.license = "Apache-2.0" spec.metadata = { "homepage_uri" => "https://github.com/inspec/train", "changelog_uri" => "https://github.com/inspec/train/blob/master/CHANGELOG.md", "source_code_uri" => "https://github.com/inspec/train", "bug_tracker_uri" => "https://github.com/inspec/train/issues", } spec.required_ruby_version = ">= 2.4" spec.files = %w{LICENSE} + Dir.glob("lib/**/*") .grep(%r{transports/(azure|clients|docker|gcp|helpers|vmware)}) .reject { |f| File.directory?(f) } spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_dependency "train-core", "= #{Train::VERSION}" spec.add_dependency "train-winrm", "~> 0.2" # azure, docker, gcp dependencies spec.add_dependency "activesupport", "~> 5.2.3" spec.add_dependency "inifile", "~> 3.0" spec.add_dependency "azure_graph_rbac", "~> 0.16" spec.add_dependency "azure_mgmt_key_vault", "~> 0.17" spec.add_dependency "azure_mgmt_resources", "~> 0.15" spec.add_dependency "azure_mgmt_security", "~> 0.18" spec.add_dependency "azure_mgmt_storage", "~> 0.18" spec.add_dependency "docker-api", "~> 1.26" spec.add_dependency "google-api-client", ">= 0.23.9", "< 0.35.0" spec.add_dependency "googleauth", ">= 0.6.6", "< 0.11.0" end