├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.template.yml ├── dependabot.yml └── workflows │ ├── add-dependabot-pr-to-project.yml │ ├── generate-dependabot.yml │ ├── integration-test.yml │ ├── lint.yaml │ └── spec-tests.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── .version ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin └── octofacts-updater ├── contrib └── plugins │ └── .gitkeep ├── doc ├── manipulators.md ├── more-examples.md ├── octofacts-updater.md ├── plugin-reference.md └── tutorial.md ├── examples ├── code │ └── .gitkeep └── config │ └── quickstart.yaml ├── lib ├── octofacts.rb ├── octofacts │ ├── backends │ │ ├── base.rb │ │ ├── index.rb │ │ └── yaml_file.rb │ ├── constructors │ │ ├── from_file.rb │ │ └── from_index.rb │ ├── errors.rb │ ├── facts.rb │ ├── manipulators.rb │ ├── manipulators │ │ ├── base.rb │ │ └── replace.rb │ ├── util │ │ ├── config.rb │ │ └── keys.rb │ └── version.rb ├── octofacts_updater.rb └── octofacts_updater │ ├── cli.rb │ ├── fact.rb │ ├── fact_index.rb │ ├── fixture.rb │ ├── plugin.rb │ ├── plugins │ ├── ip.rb │ ├── ssh.rb │ └── static.rb │ ├── service │ ├── base.rb │ ├── enc.rb │ ├── github.rb │ ├── local_file.rb │ ├── puppetdb.rb │ └── ssh.rb │ └── version.rb ├── octofacts-updater.gemspec ├── octofacts.gemspec ├── rake └── gem.rb ├── script ├── bootstrap ├── cibuild ├── console └── git-pre-commit ├── spec ├── fixtures │ ├── facts │ │ ├── basic.yaml │ │ ├── ops-consul-12345.dc2.example.com.yaml │ │ ├── ops-consul-67890.dc1.example.com.yaml │ │ ├── puppet-puppetserver-00decaf.dc1.example.com.yaml │ │ └── puppet-puppetserver-12345.dc1.example.com.yaml │ ├── index-no-nodes.yaml │ ├── index.yaml │ └── sorted-index.yaml ├── integration │ ├── hiera.yaml │ ├── manifests │ │ └── defaults.pp │ ├── modules │ │ └── test │ │ │ ├── manifests │ │ │ ├── init.pp │ │ │ └── one.pp │ │ │ ├── spec │ │ │ └── classes │ │ │ │ └── test_one_spec.rb │ │ │ └── templates │ │ │ └── one │ │ │ └── system-info.txt │ └── spec │ │ └── spec_helper.rb ├── octofacts │ ├── backends │ │ ├── index_spec.rb │ │ └── yaml_file_spec.rb │ ├── constructors │ │ └── from_file_spec.rb │ ├── examples_spec.rb │ ├── facts_spec.rb │ ├── manipulators │ │ ├── base_spec.rb │ │ └── replace_spec.rb │ ├── manipulators_spec.rb │ ├── octofacts_spec.rb │ ├── octofacts_spec_helper.rb │ └── util │ │ ├── config_spec.rb │ │ └── keys_spec.rb ├── octofacts_updater │ ├── fact_index_spec.rb │ ├── fact_spec.rb │ ├── fixture_spec.rb │ ├── octofacts_updater_spec.rb │ ├── plugin_spec.rb │ ├── plugins │ │ ├── ip_spec.rb │ │ ├── ssh_spec.rb │ │ └── static_spec.rb │ └── service │ │ ├── base_spec.rb │ │ ├── enc_spec.rb │ │ ├── github_spec.rb │ │ ├── local_file_spec.rb │ │ ├── puppetdb_spec.rb │ │ └── ssh_spec.rb └── spec_helper.rb └── vendor └── cache ├── activesupport-7.1.3.4.gem ├── addressable-2.8.7.gem ├── ast-2.4.2.gem ├── base64-0.2.0.gem ├── bigdecimal-3.1.8.gem ├── coderay-1.1.3.gem ├── concurrent-ruby-1.3.4.gem ├── connection_pool-2.4.1.gem ├── csv-3.3.0.gem ├── deep_merge-1.2.2.gem ├── diff-lcs-1.5.1.gem ├── diffy-3.4.2.gem ├── docile-1.4.1.gem ├── drb-2.2.1.gem ├── facter-4.6.1.gem ├── faraday-2.0.0.gem ├── fast_gettext-2.4.0.gem ├── forwardable-1.3.3.gem ├── hashdiff-1.1.1.gem ├── hiera-3.12.0.gem ├── hocon-1.4.0.gem ├── httparty-0.22.0.gem ├── i18n-1.14.5.gem ├── json-2.7.2.gem ├── language_server-protocol-3.17.0.3.gem ├── locale-2.1.4.gem ├── method_source-1.1.0.gem ├── mini_mime-1.1.5.gem ├── minitest-5.24.1.gem ├── multi_json-1.15.0.gem ├── multi_xml-0.6.0.gem ├── mutex_m-0.2.0.gem ├── net-ssh-7.2.3.gem ├── octocatalog-diff-2.1.0.gem ├── octokit-9.1.0.gem ├── parallel-1.26.3.gem ├── parser-3.3.4.2.gem ├── prime-0.1.2.gem ├── pry-0.14.2.gem ├── public_suffix-5.1.1.gem ├── puppet-7.30.0.gem ├── puppet-resource_api-1.9.0.gem ├── racc-1.8.1.gem ├── rack-3.1.12.gem ├── rainbow-3.1.1.gem ├── rake-13.2.1.gem ├── regexp_parser-2.9.2.gem ├── rexml-3.3.9.gem ├── rspec-3.13.0.gem ├── rspec-core-3.13.0.gem ├── rspec-expectations-3.13.1.gem ├── rspec-mocks-3.13.1.gem ├── rspec-puppet-3.0.0.gem ├── rspec-support-3.13.1.gem ├── rubocop-1.65.1.gem ├── rubocop-ast-1.32.0.gem ├── rubocop-github-0.20.0.gem ├── rubocop-performance-1.21.1.gem ├── rubocop-rails-2.25.1.gem ├── ruby-progressbar-1.13.0.gem ├── ruby2_keywords-0.0.5.gem ├── rugged-1.7.2.gem ├── sawyer-0.9.2.gem ├── scanf-1.0.0.gem ├── semantic_puppet-1.1.0.gem ├── simplecov-0.22.0.gem ├── simplecov-html-0.12.3.gem ├── simplecov-json-0.2.3.gem ├── simplecov_json_formatter-0.1.4.gem ├── singleton-0.2.0.gem ├── thor-1.3.1.gem ├── tzinfo-2.0.6.gem └── unicode-display_width-2.5.0.gem /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | > Description of problem 8 | 9 | - What did you do? 10 | - What happened? 11 | - What did you expect to happen? 12 | - How can someone reproduce the problem? 13 | 14 | > Command/code used 15 | 16 | ``` 17 | Paste the exact command or code here. 18 | ``` 19 | 20 | > Platform and version information 21 | 22 | - Your OS: 23 | - Your Ruby version: 24 | - Your version of Puppet: 25 | - Your version of octofacts: 26 | 27 | > Do the tests pass from a clean checkout? 28 | 29 | 30 | 31 | > Anything else to add that you think will be helpful? 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | ## Overview 14 | 15 | This pull request [introduces/changes/removes] [functionality/feature]. 16 | 17 | (Please write a summary of your pull request here. This paragraph should go into detail about what is changing, the motivation behind this change, and the approach you took.) 18 | 19 | ## Checklist 20 | 21 | - [ ] Make sure that all of the tests pass, and fix any that don't. Just run `bundle exec rake` in your checkout directory, or review the CI job triggered whenever you push to a pull request. 22 | - [ ] Make sure that there is 100% test coverage (the CI job will test for this). You can ignore untestable sections of code with `# :nocov:` comments. If you need help getting to 100% coverage please ask; however, don't just submit code with no tests. 23 | - [ ] If you have added any new gem dependencies, make sure those gems are licensed under the MIT or Apache 2.0 license. We cannot add any dependencies on gems licensed under GPL. 24 | - [ ] If you have added any new gem dependencies, make sure you've checked in a copy of the `.gem` file into the [vendor/cache](/vendor/cache) directory. 25 | 26 | /cc [related issues] [teams and individuals, making sure to mention why you're CC-ing them] 27 | -------------------------------------------------------------------------------- /.github/dependabot.template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "bundler" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | bundler-dev: 10 | patterns: 11 | - "parallel" 12 | - "pry" 13 | - "rake" 14 | - "rubocop-github" 15 | - "simplecov*" 16 | update-types: 17 | - "patch" 18 | - "minor" 19 | bundler-prod: 20 | patterns: 21 | - "*" 22 | exclude-patterns: 23 | - "parallel" 24 | - "pry" 25 | - "rake" 26 | - "rubocop-github" 27 | - "simplecov*" 28 | update-types: 29 | - "patch" 30 | - "minor" 31 | open-pull-requests-limit: 20 32 | vendor: true 33 | - package-ecosystem: "github-actions" 34 | directory: "/" 35 | schedule: 36 | interval: "weekly" 37 | groups: 38 | github-actions: 39 | patterns: 40 | - "*" # Group all GitHub Actions 41 | open-pull-requests-limit: 20 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file was generated by the "Generate Dependabot Glob" action. Do not edit it directly. 2 | # Make changes to `.github/dependabot.template.yml` and a PR will be automatically created. 3 | version: 2 4 | updates: 5 | - package-ecosystem: bundler 6 | directory: / 7 | schedule: 8 | interval: weekly 9 | groups: 10 | bundler-dev: 11 | patterns: 12 | - parallel 13 | - pry 14 | - rake 15 | - rubocop-github 16 | - simplecov* 17 | update-types: 18 | - patch 19 | - minor 20 | bundler-prod: 21 | patterns: 22 | - '*' 23 | exclude-patterns: 24 | - parallel 25 | - pry 26 | - rake 27 | - rubocop-github 28 | - simplecov* 29 | update-types: 30 | - patch 31 | - minor 32 | open-pull-requests-limit: 20 33 | vendor: true 34 | - package-ecosystem: github-actions 35 | directory: / 36 | schedule: 37 | interval: weekly 38 | groups: 39 | github-actions: 40 | patterns: 41 | - '*' 42 | open-pull-requests-limit: 20 43 | -------------------------------------------------------------------------------- /.github/workflows/add-dependabot-pr-to-project.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Assign Dependabot PR to Compute Foundation Project 3 | 4 | on: 5 | workflow_dispatch: 6 | pull_request: 7 | types: [opened, reopened, labeled] 8 | 9 | permissions: 10 | contents: read 11 | pull-requests: write 12 | 13 | jobs: 14 | add-to-project: 15 | name: Add to Compute Foundation Project Board 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/add-to-project@v1.0.2 19 | with: 20 | project-url: https://github.com/orgs/github/projects/5753/ # Compute Foundation Project Board 21 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 22 | labeled: dependencies,external-dependency 23 | -------------------------------------------------------------------------------- /.github/workflows/generate-dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generate dependabot.yml 3 | 4 | on: 5 | push: 6 | repository_dispatch: 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | generate: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Generate dependabot.yml 20 | uses: Makeshift/generate-dependabot-glob-action@5cd45385ce6519f68d574aab9699832b3a5e5031 # v1.3.4 21 | 22 | - name: Create Pull Request 23 | uses: peter-evans/create-pull-request@8867c4aba1b742c39f8d0ba35429c2dfa4b6cb20 # v7.0.1 24 | with: 25 | title: '[Automated] Update dependabot.yml' 26 | body: | 27 | This PR was automatically generated by the generate-dependabot.yml workflow. 28 | -------------------------------------------------------------------------------- /.github/workflows/integration-test.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | on: [pull_request, workflow_dispatch] 3 | permissions: 4 | contents: read 5 | jobs: 6 | integration-4_10_4: 7 | name: Integration Tests (Puppet 4.10.4) 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Set up Ruby 12 | uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # v1.191.0 13 | - name: Install dependencies 14 | run: bundle install --jobs 4 --retry 3 15 | - name: Run Integration Tests 16 | run: | 17 | bundle exec rake octofacts:spec:octofacts_integration 18 | local_integration_rspec=$? 19 | if [ "$local_integration_rspec" -ne 0 ]; then 20 | exit 1 21 | else 22 | exit 0 23 | fi 24 | environment: 25 | RSPEC_PUPPET_VERSION="2.6.15" 26 | PUPPET_VERSION="4.10.4" 27 | integration-7_30_0: 28 | name: Integration Tests (Puppet 7.30.0) 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # v1.191.0 34 | - name: Install dependencies 35 | run: bundle install --jobs 4 --retry 3 36 | - name: Run Integration Tests 37 | run: | 38 | bundle exec rake octofacts:spec:octofacts_integration 39 | local_integration_rspec=$? 40 | if [ "$local_integration_rspec" -ne 0 ]; then 41 | exit 1 42 | else 43 | exit 0 44 | fi 45 | environment: 46 | RSPEC_PUPPET_VERSION="3.0.0" 47 | PUPPET_VERSION="7.30.0" 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint (Rubocop) 2 | on: [pull_request, workflow_dispatch] 3 | permissions: 4 | contents: read 5 | jobs: 6 | rubocop: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Setup Ruby 11 | uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # v1.191.0 12 | - name: Install dependencies 13 | run: | 14 | bundle install --jobs 4 --retry 3 15 | - name: Lint with Rubocop 16 | run: | 17 | bundle exec rubocop --parallel 18 | -------------------------------------------------------------------------------- /.github/workflows/spec-tests.yml: -------------------------------------------------------------------------------- 1 | name: Spec Tests (Rspec) 2 | on: [pull_request, workflow_dispatch] 3 | permissions: 4 | contents: read 5 | jobs: 6 | octofacts-rspec: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # v1.191.0 11 | - name: Install dependencies 12 | run: | 13 | bundle install --jobs 4 --retry 3 14 | - name: Test octofacts 15 | run: | 16 | bundle exec rake octofacts:spec:octofacts 17 | - name: Test octofacts_updater 18 | run: | 19 | bundle exec rake octofacts:spec:octofacts_updater 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | /.config 3 | /coverage/ 4 | /InstalledFiles 5 | /pkg/ 6 | /lib/octofacts/coverage 7 | /lib/octofacts_updater/coverage 8 | /spec/reports/ 9 | /spec/examples.txt 10 | /test/tmp/ 11 | /test/version_tmp/ 12 | /tmp/ 13 | 14 | # Used by dotenv library to load environment variables. 15 | # .env 16 | 17 | ## Specific to RubyMotion: 18 | .dat* 19 | .repl_history 20 | build/ 21 | *.bridgesupport 22 | build-iPhoneOS/ 23 | build-iPhoneSimulator/ 24 | 25 | ## Specific to RubyMotion (use of CocoaPods): 26 | # 27 | # We recommend against adding the Pods directory to your .gitignore. However 28 | # you should judge for yourself, the pros and cons are mentioned at: 29 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 30 | # 31 | # vendor/Pods/ 32 | 33 | # Binstubs - if this gem ships any binstubs we'll need to whitelist them. 34 | /bin/* 35 | !/bin/octofacts-updater 36 | 37 | ## Documentation cache and generated files: 38 | /.yardoc/ 39 | /_yardoc/ 40 | /rdoc/ 41 | 42 | ## Environment normalization: 43 | /.bundle/ 44 | /vendor/bundle 45 | /lib/bundler/man/ 46 | 47 | # for a library or gem, you might want to ignore these files since the code is 48 | # intended to run in multiple environments; otherwise, check them in: 49 | # Gemfile.lock 50 | # .ruby-version 51 | # .ruby-gemset 52 | 53 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 54 | .rvmrc 55 | 56 | # JetBrains 57 | .idea/** 58 | 59 | # VS Code 60 | .vscode/** -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | rubocop-github: 3 | - config/default.yml 4 | 5 | AllCops: 6 | NewCops: enable 7 | DisplayCopNames: true 8 | TargetRubyVersion: 2.7 9 | 10 | Style/HashSyntax: 11 | Exclude: 12 | - spec/octofacts/util/keys_spec.rb 13 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.8 2 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | 0.6.1 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project 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, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers 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. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both 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. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems 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. 38 | 39 | Project maintainers 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. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: https://github.com/github/octofacts/fork 4 | [pr]: https://github.com/github/octofacts/compare 5 | [style]: https://github.com/styleguide/ruby 6 | [code-of-conduct]: CODE_OF_CONDUCT.md 7 | 8 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 9 | 10 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 11 | 12 | ## Submitting a pull request 13 | 14 | 0. [Fork][fork] and clone the repository 15 | 0. Configure and install the dependencies: `script/bootstrap` 16 | 0. Make sure the tests pass on your machine: `bundle exec rake` 17 | 0. Create a new branch: `git checkout -b my-branch-name` 18 | 0. Make your change, add tests, and make sure the tests still pass 19 | 0. Push to your fork and [submit a pull request][pr] 20 | 0. Pat your self on the back and wait for your pull request to be reviewed and merged. 21 | 22 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 23 | 24 | - Follow the [style guide][style]. 25 | - Write tests. We require 100% rspec test coverage in this project. 26 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 27 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 28 | 29 | ## Resources 30 | 31 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 32 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 33 | - [GitHub Help](https://help.github.com) 34 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source "https://rubygems.org" 3 | 4 | 5 | gemspec name: "octofacts" 6 | gemspec name: "octofacts-updater" 7 | 8 | group :development do 9 | gem "parallel", "1.26.3" 10 | gem "pry", "~> 0.14" 11 | gem "rake", "~> 13.2" 12 | gem "rubocop-github", "~> 0.20.0" 13 | gem "simplecov", ">= 0.14.1" 14 | gem "simplecov-json", "~> 0.2" 15 | 16 | # Integration test 17 | gem "puppet", "~> #{ENV['PUPPET_VERSION'] || '7.30.0'}" 18 | gem "rspec-puppet", "~> #{ENV['RSPEC_PUPPET_VERSION'] || '3.0.0'}" 19 | end 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | octofacts (0.6.1) 5 | octofacts-updater (0.6.1) 6 | diffy (>= 3.1.0) 7 | net-ssh (>= 2.9) 8 | octocatalog-diff (>= 2.1.0) 9 | octokit (>= 4.2.0) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | activesupport (7.1.3.4) 15 | base64 16 | bigdecimal 17 | concurrent-ruby (~> 1.0, >= 1.0.2) 18 | connection_pool (>= 2.2.5) 19 | drb 20 | i18n (>= 1.6, < 2) 21 | minitest (>= 5.1) 22 | mutex_m 23 | tzinfo (~> 2.0) 24 | addressable (2.8.7) 25 | public_suffix (>= 2.0.2, < 7.0) 26 | ast (2.4.2) 27 | base64 (0.2.0) 28 | bigdecimal (3.1.8) 29 | coderay (1.1.3) 30 | concurrent-ruby (1.3.4) 31 | connection_pool (2.4.1) 32 | csv (3.3.0) 33 | deep_merge (1.2.2) 34 | diff-lcs (1.5.1) 35 | diffy (3.4.2) 36 | docile (1.4.1) 37 | drb (2.2.1) 38 | facter (4.6.1) 39 | hocon (~> 1.3) 40 | thor (>= 1.0.1, < 2.0) 41 | faraday (2.0.0) 42 | ruby2_keywords (>= 0.0.4) 43 | fast_gettext (2.4.0) 44 | prime 45 | forwardable (1.3.3) 46 | hashdiff (1.1.1) 47 | hiera (3.12.0) 48 | hocon (1.4.0) 49 | httparty (0.22.0) 50 | csv 51 | mini_mime (>= 1.0.0) 52 | multi_xml (>= 0.5.2) 53 | i18n (1.14.5) 54 | concurrent-ruby (~> 1.0) 55 | json (2.7.2) 56 | language_server-protocol (3.17.0.3) 57 | locale (2.1.4) 58 | method_source (1.1.0) 59 | mini_mime (1.1.5) 60 | minitest (5.24.1) 61 | multi_json (1.15.0) 62 | multi_xml (0.6.0) 63 | mutex_m (0.2.0) 64 | net-ssh (7.2.3) 65 | octocatalog-diff (2.1.0) 66 | diffy (>= 3.1.0) 67 | hashdiff (>= 0.3.0) 68 | httparty (>= 0.11.0) 69 | parallel (>= 1.12.0) 70 | rugged (>= 0.25.0b2) 71 | octokit (9.1.0) 72 | faraday (>= 1, < 3) 73 | sawyer (~> 0.9) 74 | parallel (1.26.3) 75 | parser (3.3.4.2) 76 | ast (~> 2.4.1) 77 | racc 78 | prime (0.1.2) 79 | forwardable 80 | singleton 81 | pry (0.14.2) 82 | coderay (~> 1.1) 83 | method_source (~> 1.0) 84 | public_suffix (5.1.1) 85 | puppet (7.30.0) 86 | concurrent-ruby (~> 1.0) 87 | deep_merge (~> 1.0) 88 | facter (> 2.0.1, < 5) 89 | fast_gettext (>= 1.1, < 3) 90 | hiera (>= 3.2.1, < 4) 91 | locale (~> 2.1) 92 | multi_json (~> 1.10) 93 | puppet-resource_api (~> 1.5) 94 | scanf (~> 1.0) 95 | semantic_puppet (~> 1.0) 96 | puppet-resource_api (1.9.0) 97 | hocon (>= 1.0) 98 | racc (1.8.1) 99 | rack (3.1.12) 100 | rainbow (3.1.1) 101 | rake (13.2.1) 102 | regexp_parser (2.9.2) 103 | rexml (3.3.9) 104 | rspec (3.13.0) 105 | rspec-core (~> 3.13.0) 106 | rspec-expectations (~> 3.13.0) 107 | rspec-mocks (~> 3.13.0) 108 | rspec-core (3.13.0) 109 | rspec-support (~> 3.13.0) 110 | rspec-expectations (3.13.1) 111 | diff-lcs (>= 1.2.0, < 2.0) 112 | rspec-support (~> 3.13.0) 113 | rspec-mocks (3.13.1) 114 | diff-lcs (>= 1.2.0, < 2.0) 115 | rspec-support (~> 3.13.0) 116 | rspec-puppet (3.0.0) 117 | rspec 118 | rspec-support (3.13.1) 119 | rubocop (1.65.1) 120 | json (~> 2.3) 121 | language_server-protocol (>= 3.17.0) 122 | parallel (~> 1.10) 123 | parser (>= 3.3.0.2) 124 | rainbow (>= 2.2.2, < 4.0) 125 | regexp_parser (>= 2.4, < 3.0) 126 | rexml (>= 3.2.5, < 4.0) 127 | rubocop-ast (>= 1.31.1, < 2.0) 128 | ruby-progressbar (~> 1.7) 129 | unicode-display_width (>= 2.4.0, < 3.0) 130 | rubocop-ast (1.32.0) 131 | parser (>= 3.3.1.0) 132 | rubocop-github (0.20.0) 133 | rubocop (>= 1.37) 134 | rubocop-performance (>= 1.15) 135 | rubocop-rails (>= 2.17) 136 | rubocop-performance (1.21.1) 137 | rubocop (>= 1.48.1, < 2.0) 138 | rubocop-ast (>= 1.31.1, < 2.0) 139 | rubocop-rails (2.25.1) 140 | activesupport (>= 4.2.0) 141 | rack (>= 1.1) 142 | rubocop (>= 1.33.0, < 2.0) 143 | rubocop-ast (>= 1.31.1, < 2.0) 144 | ruby-progressbar (1.13.0) 145 | ruby2_keywords (0.0.5) 146 | rugged (1.7.2) 147 | sawyer (0.9.2) 148 | addressable (>= 2.3.5) 149 | faraday (>= 0.17.3, < 3) 150 | scanf (1.0.0) 151 | semantic_puppet (1.1.0) 152 | simplecov (0.22.0) 153 | docile (~> 1.1) 154 | simplecov-html (~> 0.11) 155 | simplecov_json_formatter (~> 0.1) 156 | simplecov-html (0.12.3) 157 | simplecov-json (0.2.3) 158 | json 159 | simplecov 160 | simplecov_json_formatter (0.1.4) 161 | singleton (0.2.0) 162 | thor (1.3.1) 163 | tzinfo (2.0.6) 164 | concurrent-ruby (~> 1.0) 165 | unicode-display_width (2.5.0) 166 | 167 | PLATFORMS 168 | ruby 169 | 170 | DEPENDENCIES 171 | octofacts! 172 | octofacts-updater! 173 | parallel (= 1.26.3) 174 | pry (~> 0.14) 175 | puppet (~> 7.30.0) 176 | rake (~> 13.2) 177 | rspec-puppet (~> 3.0.0) 178 | rubocop-github (~> 0.20.0) 179 | simplecov (>= 0.14.1) 180 | simplecov-json (~> 0.2) 181 | 182 | BUNDLED WITH 183 | 2.4.19 184 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 GitHub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # octofacts 2 | 3 | `octofacts` is a tool that enables Puppet developers to provide complete sets of facts for rspec-puppet tests. It works by saving facts from actual hosts as fixture files, and then presenting a straightforward programming interface to select and manipulate those facts within tests. Using nearly real-life facts is a good way to ensure that rspec-puppet tests match production as closely as possible. 4 | 5 | `octofacts` is actively used in production at [GitHub](https://github.com). This project is actively maintained by the original authors and the rest of the Site Reliability Engineering team at GitHub. 6 | 7 | ## Overview 8 | 9 | The `octofacts` project is distributed with two components: 10 | 11 | - The `octofacts` gem is called within your rspec-puppet tests, to provide facts from indexed fact fixture files in your repository. This allows you to replace a hard-coded `let (:facts) { ... }` hash with more realistic facts from recent production runs. 12 | 13 | - The `octofacts-updater` gem is a utility to maintain the indexed fact fixture files consumed by `octofacts`. It pulls facts from a data source (e.g. PuppetDB, fact caches, or SSH), indexes your facts, and can even create Pull Requests on GitHub to update those fixture files for you. 14 | 15 | ## Requirements 16 | 17 | To use `octofacts` in your rspec-puppet tests, those tests must be executed with Ruby 2.1 or higher and rspec-puppet 2.3.2 or higher, and executed on a Unix-like operating system. We explicitly test `octofacts` with Linux and Mac OS, but do not test under Windows. 18 | 19 | To use `octofacts-updater`, we recommend using PuppetDB, and if you do you'll need version 3.0 or higher. 20 | 21 | ## Example 22 | 23 | Once you complete the initial setup and generate fact fixtures, you'll be able to use code like this in your rspec-puppet tests: 24 | 25 | ``` 26 | describe "modulename::classname" do 27 | let(:node) { "fake-node.example.net" } 28 | let(:facts) { Octofacts.from_index(app: "my_app_name", role: "my_role_name") } 29 | 30 | it "should do whatever..." 31 | ... 32 | end 33 | end 34 | ``` 35 | 36 | ## Installation and use 37 | 38 | The basics: 39 | 40 | - [Quick start tutorial - covers installation and basic configuration](/doc/tutorial.md) <-- **New users start here** 41 | - [Automating fixture generation with octofacts-updater](/doc/octofacts-updater.md) 42 | 43 | More advanced usage: 44 | 45 | - [Plugin reference for octofacts-updater](/doc/plugin-reference.md) 46 | - [Using manipulators to adjust fact values](/doc/manipulators.md) 47 | - [Additional examples of octofacts capabilities](/doc/more-examples.md) 48 | 49 | ## Contributing 50 | 51 | Please see our [contributing document](CONTRIBUTING.md) if you would like to participate! 52 | 53 | We would specifically appreciate contributions in these areas: 54 | 55 | - Any updates you make to make octofacts compatible with your site -- there are probably assumptions made from the original environment that need to be more flexible. 56 | - Any interesting anonymization plugins you write for octofacts-updater -- you may place these in the [contrib/plugins](/contrib/plugins) directory. 57 | 58 | ## Getting help 59 | 60 | If you have a problem or suggestion, please [open an issue](https://github.com/github/octofacts/issues/new) in this repository, and we will do our best to help. Please note that this project adheres to its [Code of Conduct](/CODE_OF_CONDUCT.md). 61 | 62 | ## License 63 | 64 | `octofacts` is licensed under the [MIT license](/LICENSE). 65 | 66 | ## Authors 67 | 68 | - [@antonio - Antonio Santos](https://github.com/antonio) 69 | - [@kpaulisse - Kevin Paulisse](https://github.com/kpaulisse) 70 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #frozen_string_literal: true 2 | 3 | require "rake" 4 | require "rspec/core/rake_task" 5 | require_relative "rake/gem" 6 | 7 | namespace :octofacts do 8 | task default: [":octofacts:spec:octofacts", ":octofacts:spec:octofacts_updater", ":octofacts:spec:octofacts_integration"] do 9 | end 10 | end 11 | 12 | RSpec::Core::RakeTask.new(:"octofacts:spec:octofacts") do |t| 13 | t.pattern = File.join(File.dirname(__FILE__), "spec/octofacts/**/*_spec.rb") 14 | t.name = "octofacts" 15 | ENV["SPEC_NAME"] = "octofacts" 16 | end 17 | 18 | RSpec::Core::RakeTask.new(:"octofacts:spec:octofacts_updater") do |t| 19 | t.pattern = File.join(File.dirname(__FILE__), "spec/octofacts_updater/**/*_spec.rb") 20 | t.name = "octofacts-updater" 21 | ENV["SPEC_NAME"] = "octofacts_updater" 22 | end 23 | 24 | RSpec::Core::RakeTask.new(:"octofacts:spec:octofacts_integration") do |t| 25 | t.pattern = File.join(File.dirname(__FILE__), "spec/integration/**/*_spec.rb") 26 | t.name = "octofacts-integration" 27 | ENV.delete("SPEC_NAME") 28 | end 29 | 30 | task default: :"octofacts:default" 31 | -------------------------------------------------------------------------------- /bin/octofacts-updater: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | require "bundler/setup" 5 | require "octofacts_updater" 6 | cli = OctofactsUpdater::CLI.new(ARGV) 7 | cli.run 8 | -------------------------------------------------------------------------------- /contrib/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/contrib/plugins/.gitkeep -------------------------------------------------------------------------------- /doc/manipulators.md: -------------------------------------------------------------------------------- 1 | # Manipulators - Modifying facts before use 2 | 3 | Octofacts provides the capability to modify facts before they are passed to `rspec-puppet`. We provide certain methods to make this easier and more human-readable, but it is also possible to use regular ruby if you prefer. 4 | 5 | ## Available manipulators 6 | 7 | ### `.replace` - Replace or set facts 8 | 9 | For example, this replaces two facts with string values: 10 | 11 | ``` 12 | Octofacts.from_index(environment: "test").replace(operatingsystem: "Debian", lsbdistcodename: "jessie") 13 | ``` 14 | 15 | It is also possible to perform replacements in structured facts, using `::` as the delimiter. 16 | 17 | ``` 18 | Octofacts.from_index(environment: "test").replace("os::name" => "Debian", "os::lsb::distcodename" => "jessie") 19 | ``` 20 | 21 | *Note*: It doesn't matter if the fact you're trying to "replace" currently exists. The "replace" method will set the fact to your new value regardless of whether that fact existed before. 22 | 23 | *Note*: If you attempt to set a structured fact and the intermediate hash structure does not exist, that intermediate hash structure will be auto-created as necessary so that the fact you defined can be created. Example: 24 | 25 | ``` 26 | # Current fact value: foo = { "existing_level" => { "foo" => "bar" } } 27 | Octofacts.from_index(...).replace("foo::new_level::test" => "value") 28 | #=> foo = { "existing_level" => { "foo" => "bar" }, "new_level" => { "test" => "value" } } 29 | ``` 30 | 31 | *Note*: The "replace" method accepts keys (fact names) both as strings and as symbols. `.replace(foo: "bar")` and `.replace("foo" => "bar")` are equivalent. 32 | 33 | ## Advanced 34 | 35 | ### Using regular ruby 36 | 37 | If you prefer to use regular ruby without using (or after using) our manipulators, you are free to do so. For example: 38 | 39 | ``` 40 | let(:facts) do 41 | f = Octofacts.from_index(environment: "test") 42 | f.merge!(foo: "FOO", bar: "BAR") 43 | f.delete(:baz) 44 | f 45 | end 46 | ``` 47 | 48 | ### Using lambdas as new values 49 | 50 | It is possible to use lambda methods to assign new values using the "replace" method, to perform a programmatic replacement based on the existing values. For example: 51 | 52 | ``` 53 | Octofacts.from_index(environment: "test").replace(operatingsystem: lambda { |old_value| old_value.upcase }) 54 | #=> operatingsystem = "DEBIAN" 55 | ``` 56 | 57 | The lambda method can be defined with one parameter or three parameters as follows. 58 | 59 | ``` 60 | # One parameter - operates on the old value of the fact 61 | lambda { |old_value| ... } 62 | 63 | # Three parameters - takes into account the entire fact set 64 | # 1. fact_set - The Hash of all of the current facts 65 | # 2. fact_name - The name of the fact being operated upon 66 | # 3. old_value - The current (old) value of the fact 67 | lambda { |fact_set, fact_name, old_value| ... } 68 | ``` 69 | 70 | *Note*: If a lambda function returns `nil`, the key is deleted. 71 | 72 | ## Limitations 73 | 74 | ### Order is important 75 | 76 | #### Left to right evaluation 77 | 78 | Evaluation is from left to right. Operations performed later in the chain may be influenced by, and/or take precedence over, earlier operations. For example: 79 | 80 | ``` 81 | Octofacts.from_index(environment: "test").replace(foo: "bar").replace(foo: "baz") 82 | #=> foo = "baz" 83 | ``` 84 | 85 | #### Select before manipulating 86 | 87 | It is *not* possible to use fact selector methods (e.g. `.select`, `.reject`, `.prefer`) after performing manipulations. This is because backends may be tracking multiple possible sets of facts, but manipulating the facts will internally select a set of facts before proceeding. An error message is raised if, for example, you try this: 88 | 89 | ``` 90 | Octofacts.from_index(environment: "test").replace(foo: "bar").select(operatingsystem: "Debian") 91 | #=> Error! 92 | ``` 93 | 94 | You can instead do this, which works fine: 95 | 96 | ``` 97 | Octofacts.from_index(environment: "production").select(operatingsystem: "Debian").replace(foo: "bar") 98 | #=> Works 99 | ``` 100 | -------------------------------------------------------------------------------- /doc/more-examples.md: -------------------------------------------------------------------------------- 1 | # Octofacts examples 2 | 3 | ## Manipulating facts with built-in functions 4 | 5 | We provide some helper functions to manipulate facts easily, since we take care of symbolizing and lower-casing keys for you: 6 | 7 | ``` 8 | describe modulename::classname do 9 | let(:node) { "fake-node.example.net" } 10 | let(:facts) { Octofacts.from_index(app: "my_app_name", role: "my_role_name").replace("fact-name", "new-value") } 11 | 12 | it "should do whatever..." 13 | ... 14 | end 15 | end 16 | ``` 17 | 18 | ## Manipulating facts with pure ruby 19 | 20 | If you don't want to use our helper functions, you can use the object as a normal ruby hash: 21 | 22 | ``` 23 | describe modulename::classname do 24 | let(:node) { "fake-node.example.net" } 25 | let(:facts) do 26 | f = Octofacts.from_index(app: "my_app_name", role: "my_role_name") 27 | f.merge!(:some_fact, "new-value") 28 | f.delete(:some_other_fact) 29 | f 30 | end 31 | 32 | it "should do whatever..." 33 | ... 34 | end 35 | end 36 | ``` 37 | 38 | ## Defining your own helper functions 39 | 40 | You can also define your own helper functions by adding them to your `spec_helper` with no need to modify our code: 41 | 42 | ``` 43 | # spec/spec_helper.rb 44 | # -- 45 | 46 | module Octofacts 47 | class Manipulators 48 | class AddFakeDrive 49 | def self.execute(fact_set, args, _) 50 | fact_set[:blockdevices] = (fact_set[:blockdevices] || "").split(",").concat(args[0]).join(",") 51 | fact_set[:"blockdevice_#{args[0]}_size"] = args[1] 52 | end 53 | end 54 | end 55 | end 56 | 57 | # modules/modulename/spec/classes/classname_spec.rb 58 | # -- 59 | describe modulename::classname do 60 | let(:node) { "fake-node.example.net" } 61 | let(:facts) { Octofacts.from_index(app: "my_app_name", role: "my_role_name").add_fake_drive("sdz", 21474836480) } 62 | 63 | it "should do whatever..." 64 | ... 65 | end 66 | end 67 | ``` 68 | -------------------------------------------------------------------------------- /doc/plugin-reference.md: -------------------------------------------------------------------------------- 1 | # Plugin refeerence for octofacts-updater 2 | 3 | Please refer to the [octofacts-updater documentation](/doc/octofacts-updater.md) for general instructions to configure the system. 4 | 5 | This document is a reference to all available plugins for fact manipulation. All of the distributed plugins are found in the [/lib/octofacts_updater/plugins](/lib/octofacts_updater/plugins) directory. 6 | 7 | ## delete 8 | 9 | Source: [static.rb](/lib/octofacts_updater/plugins/static.rb) 10 | 11 | Description: Deletes a fact or component of a structured fact. 12 | 13 | Parameters: (None) 14 | 15 | Supports structured facts: Yes 16 | 17 | Example usage: 18 | 19 | ``` 20 | facts: 21 | some_fact_to_delete: 22 | plugin: delete 23 | some_structured_fact: 24 | structure: 25 | - regexp: .+ 26 | - regexp: _key$ 27 | plugin: delete 28 | ``` 29 | 30 | ## ipv4_anonymize 31 | 32 | Source: [ip.rb](/lib/octofacts_updater/plugins/ip.rb) 33 | 34 | Description: Choose a random IP address from the specified IPv4 subnet. The original IP address is used to seed the random number generator, so as long as that IP address does not change, the randomized IP address will remain constant. 35 | 36 | Parameters: 37 | 38 | | Parameter | Required? | Description | 39 | | --------- | --------- | ----------- | 40 | | `subnet` | Yes | CIDR notation of subnet from which random IP is to be chosen | 41 | 42 | Supports structured facts: Yes 43 | 44 | Example usage: 45 | 46 | ``` 47 | ipaddress: 48 | plugin: ipv4_randomize 49 | subnet: 10.1.0.0/24 50 | ``` 51 | 52 | ## ipv6_anonymize 53 | 54 | Source: [ip.rb](/lib/octofacts_updater/plugins/ip.rb) 55 | 56 | Description: Choose a random IP address from the specified IPv6 subnet. The original IP address is used to seed the random number generator, so as long as that IP address does not change, the randomized IP address will remain constant. 57 | 58 | Parameters: 59 | 60 | | Parameter | Required? | Description | 61 | | --------- | --------- | ----------- | 62 | | `subnet` | Yes | CIDR notation of subnet from which random IP is to be chosen | 63 | 64 | Supports structured facts: Yes 65 | 66 | Example usage: 67 | 68 | ``` 69 | ipaddress: 70 | plugin: ipv6_randomize 71 | subnet: "fd00::/8" 72 | ``` 73 | 74 | ## noop 75 | 76 | Source: [static.rb](/lib/octofacts_updater/plugins/static.rb) 77 | 78 | Description: Does nothing at all. 79 | 80 | Parameters: (None) 81 | 82 | Supports structured facts: Yes 83 | 84 | Example usage: 85 | 86 | ``` 87 | facts: 88 | ec2_userdata: 89 | plugin: noop 90 | ``` 91 | 92 | ## randomize_long_string 93 | 94 | Source: [static.rb](/lib/octofacts_updater/plugins/static.rb) 95 | 96 | Description: Given a string of length N, this generates a random string of length N using the original string to seed the random number generator. This ensures that the random string is consistent between runs of octofacts-updater. It is not possible to use the random string to reconstruct the original string (although for sufficiently short strings, it may be possible to brute-force guess the original string, much like brute-force password cracking). 97 | 98 | Parameters: (None) 99 | 100 | Supports structured facts: Yes 101 | 102 | Example usage: 103 | 104 | ``` 105 | facts: 106 | some_fact_to_modify: 107 | plugin: randomize_long_string 108 | some_structured_fact: 109 | structure: 110 | - regexp: .+ 111 | - regexp: _key$ 112 | plugin: randomize_long_string 113 | ``` 114 | 115 | Example result: 116 | 117 | ``` 118 | some_fact_to_modify: randomrandomrandom 119 | some_structured_fact: 120 | foo: 121 | ssl_cert: ABCDEF... 122 | ssl_key: randomrandomrandom 123 | bar: 124 | ssl_cert: 012345... 125 | ssl_key: randomrandomrandom 126 | ``` 127 | 128 | ## remove_from_delimited_string 129 | 130 | Source: [static.rb](/lib/octofacts_updater/plugins/static.rb) 131 | 132 | Description: Given a string that is delimited, remove all elements from that string that match the provided regular expression. 133 | 134 | Parameters: 135 | 136 | | Parameter | Required? | Description | 137 | | --------- | --------- | ----------- | 138 | | `delimiter` | Yes | Character that is the delimiter | 139 | | `regexp` | Yes | Remove all items from the string matching this regexp | 140 | 141 | Supports structured facts: Yes 142 | 143 | Example usage: 144 | 145 | ``` 146 | facts: 147 | interfaces: 148 | plugin: remove_from_delimited_string 149 | delimiter: , 150 | regexp: ^tun\d+ 151 | ``` 152 | 153 | Example result: 154 | 155 | ``` 156 | # Before 157 | interfaces: eth0,eth1,bond0,tun0,tun1,tun2,lo 158 | 159 | # After 160 | interfaces: eth0,eth1,bond0,lo 161 | ``` 162 | 163 | ## set 164 | 165 | Source: [static.rb](/lib/octofacts_updater/plugins/static.rb) 166 | 167 | Description: Sets the value of a fact or component of a structured fact to a pre-determined value. 168 | 169 | Parameters: 170 | 171 | | Parameter | Required? | Description | 172 | | --------- | --------- | ----------- | 173 | | `value` | Yes | Static value to set | 174 | 175 | Supports structured facts: Yes 176 | 177 | Example usage: 178 | 179 | ``` 180 | facts: 181 | some_fact_to_modify: 182 | plugin: set 183 | value: new_value_of_fact 184 | some_structured_fact: 185 | structure: 186 | - regexp: .+ 187 | - regexp: _key$ 188 | plugin: set 189 | value: we_dont_include_keys 190 | ``` 191 | 192 | Example result: 193 | 194 | ``` 195 | some_fact_to_modify: new_value_of_fact 196 | some_structured_fact: 197 | foo: 198 | ssl_cert: ABCDEF... 199 | ssl_key: we_dont_include_keys 200 | bar: 201 | ssl_cert: 012345... 202 | ssl_key: we_dont_include_keys 203 | ``` 204 | 205 | ## sshfp_randomize 206 | 207 | Source: [ssh.rb](/lib/octofacts_updater/plugins/ssh.rb) 208 | 209 | Description: Sets the SSH fingerprint portion of a fact to a random string, while preserving the numeric portion and other structure. 210 | 211 | Parameters: (None) 212 | 213 | Supports structured facts: Yes 214 | 215 | Example usage: 216 | 217 | ``` 218 | facts: 219 | ssh: 220 | plugin: sshfp_randomize 221 | structure: 222 | - regexp: .* 223 | - regexp: ^fingerprints$ 224 | - regexp: ^sha\d+ 225 | ``` 226 | 227 | Example result: 228 | 229 | ``` 230 | # Before 231 | ssh: 232 | rsa: 233 | fingerprints: 234 | sha1: SSHFP 1 1 abcdefabcdefabcdefabcdefabcdefabcdefabcd 235 | sha256: SSHFP 1 2 abcdefabcdefabcdefabcdefabcdefabcdefabcdabcdefabcdefabcdefabcdef 236 | key: AAAA0123456012345601234560123456 237 | 238 | # After 239 | ssh: 240 | rsa: 241 | fingerprints: 242 | sha1: SSHFP 1 1 randomrandomrandomrandomrandomrandomrand 243 | sha256: SSHFP 1 2 randomrandomrandomrandomrandomrandomrandomrandomrandomrandomrand 244 | key: AAAA0123456012345601234560123456 245 | ``` 246 | -------------------------------------------------------------------------------- /doc/tutorial.md: -------------------------------------------------------------------------------- 1 | # Octofacts quick-start tutorial 2 | 3 | Hello there! This tutorial is intended to get you up and running quickly with octofacts, so that you can see its capabilities. 4 | 5 | ## Prerequisites 6 | 7 | Before you get started with this tutorial, please make sure that the following prerequisites are in place. 8 | 9 | - You should already have [rspec-puppet](http://rspec-puppet.com/) up and running for your Puppet repository. 10 | - You should have a `spec/spec_helper.rb` file that's included in your rspec-puppet tests, as generally described in the [rspec-puppet tutorial](http://rspec-puppet.com/tutorial/). 11 | - You should have at least one rspec-puppet test that is passing. 12 | 13 | Additionally, we recommend that you are able to run this rspec-puppet test from your local machine. However, if you must push your changes to a source code repository (e.g. GitHub) to run the test through your CI system, that's OK too -- you'll need to commit changes and push the branches as needed. 14 | 15 | ## Installing octofacts and octofacts-updater 16 | 17 | If you are using `bundler` to manage the gem dependencies of your Puppet repository, you can add these two gems to your Gemfile. The exact strings to add to your Gemfile can be found on rubygems: 18 | 19 | - https://rubygems.org/gems/octofacts 20 | - https://rubygems.org/gems/octofacts-updater 21 | 22 | Alternatively, you can directly install octofacts and octofacts-updater into your current ruby environment using: 23 | 24 | ``` 25 | gem install octofacts octofacts-updater 26 | ``` 27 | 28 | ## Creating the directory structure 29 | 30 | The remainder of this tutorial assumes you will be using the following layout for octofacts fixture files: 31 | 32 | ``` 33 | - / 34 | - spec/ 35 | - spec_helper.rb 36 | - fixtures/ 37 | - facts/ 38 | - octofacts/ 39 | - node-1.example.net.yaml 40 | - node-2.example.net.yaml 41 | - node-3.example.net.yaml 42 | - octofacts-index.yaml 43 | ``` 44 | 45 | To create the necessary directory structure, `cd` to the "spec" directory of your checkout, and then make the directories. 46 | 47 | ``` 48 | cd 49 | cd spec 50 | mkdir -p fixtures/facts/octofacts 51 | ``` 52 | 53 | ## Get your first set of facts 54 | 55 | To obtain facts from a node in the environment, we will instruct you to log in to a node and run Puppet's `facter` command, and save the resulting output in a file. Please note that the resulting file may have sensitive information (e.g. the private SSH key for the host) so you should treat it carefully. 56 | 57 | Here is an example procedure to obtain the facts for the node, but do note that the exact procedure to do this may vary based on your own environment's setup. 58 | 59 | ``` 60 | your-workstation$ export TARGET_HOSTNAME="some-host.yourdomain.com" #<-- change as needed for your situation 61 | 62 | your-workstation$ ssh "$TARGET_HOSTNAME" 63 | 64 | some-host$ sudo facter -p --yaml > facts.yaml 65 | some-host$ exit 66 | 67 | your-workstation$ scp "$TARGET_HOSTNAME":~/facts.yaml /tmp/facts.yaml 68 | ``` 69 | 70 | Now you can run `octofacts-updater` to import this set of facts into your code repository. 71 | 72 | ``` 73 | cd 74 | 75 | # If you installed with `gem install` 76 | octofacts-updater --action facts --hostname "$TARGET_HOSTNAME" \ 77 | --datasource localfile --config-override localfile:path=/tmp/facts.yaml \ 78 | --output-file "spec/fixtures/facts/octofacts/${TARGET_HOSTNAME}.yaml" 79 | 80 | # If you installed with `bundler` 81 | bundle exec bin/octofacts-updater --action facts --hostname "$TARGET_HOSTNAME" \ 82 | --datasource localfile --config-override localfile:path=/tmp/facts.yaml \ 83 | --output-file "spec/fixtures/facts/octofacts/${TARGET_HOSTNAME}.yaml" 84 | 85 | # Once you've done either of those commands, you should be able to see your file 86 | cat "spec/fixtures/facts/octofacts/${TARGET_HOSTNAME}.yaml" 87 | ``` 88 | 89 | :warning: Until you set up anonymizers by configuring [octofacts-updater](/doc/octofacts-updater.md), the facts as you have copied may contain sensitive information (e.g. the private SSH keys for the node). Please keep this in mind before committing the newly generated file to your source code repository. 90 | 91 | ## Update your rspec-puppet spec helper to use octofacts 92 | 93 | Add the following lines to your `spec/spec_helper.rb` file: 94 | 95 | ```title=spec_helper.rb 96 | require "octofacts" 97 | ENV["OCTOFACTS_FIXTURE_PATH"] ||= File.expand_path("fixtures/facts/octofacts", File.dirname(__FILE__)) 98 | ENV["OCTOFACTS_INDEX_PATH"] ||= File.expand_path("fixtures/facts/octofacts-index.yaml", File.dirname(__FILE__)) 99 | ``` 100 | 101 | Once you've done this, run one of your `rspec-puppet` tests to make sure it still passes. If you get a failure about not being able to load octofacts, this means you have not set up your gem configuration correctly. 102 | 103 | ## Update your rspec-puppet test to use the facts you just installed 104 | 105 | Thus far you've obtained a fact fixture and configured rspec-puppet to use octofacts. You're finally ready to update one of your rspec-puppet tests to use that octofacts fixture. 106 | 107 | Your existing test might look something like this: 108 | 109 | ```title=example_spec.rb 110 | require 'spec_helper' 111 | 112 | describe 'module::class' do 113 | let(:node) { 'some-host.yourdomain.com' } 114 | 115 | let(:facts) do 116 | { 117 | ... 118 | } 119 | end 120 | 121 | it 'should do something' do 122 | is_expected.to ... 123 | end 124 | end 125 | ``` 126 | 127 | Change *only* the facts section to: 128 | 129 | ``` 130 | let(:facts) { Octofacts.from_file('some-host.yourdomain.com.yaml') } 131 | ``` 132 | 133 | If there was no `:facts` section, it's possible that default facts were being set from your `spec_helper.rb`. In this case, you can simply add the line above to your test. 134 | 135 | Now, run your test. If it passes, then congratulations -- you have successfully set up octofacts! 136 | 137 | ## Next steps 138 | 139 | Now that you have octofacts running, you'll want to configure `octofacts-updater` to anonymize facts, create an index, and automate the maintenance of fact fixtures. 140 | 141 | - [Configuring octofacts-updater](/doc/octofacts-updater.md) 142 | -------------------------------------------------------------------------------- /examples/code/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/examples/code/.gitkeep -------------------------------------------------------------------------------- /examples/config/quickstart.yaml: -------------------------------------------------------------------------------- 1 | # Configuration file for the octofacts updater. 2 | --- 3 | # This section configures a connection to PuppetDB, so you can retrieve facts from there. 4 | # Your PuppetDB must support query API v4 (which is supported in PuppetDB 3.0 and higher.) 5 | # puppetdb: 6 | # url: https://puppetdb.example.net:8081 7 | 8 | # This section configures an SSH connection to a Puppet Server, so you can retrieve facts 9 | # from its cache. The 'server' and 'user' parameters are required. 10 | # 11 | # 'command' defaults to running "cat /opt/puppetlabs/server/data/puppetserver/yaml/facts/%%NODE%%.yaml" 12 | # on the target system. Please note that the user you log in as must be sufficiently privileged, as normally 13 | # this directory is locked down only to the puppet user. 14 | # 15 | # Any other parameters you provide will be symbolized and passed to net-ssh (https://github.com/net-ssh/net-ssh). 16 | # ssh: 17 | # server: puppetserver.example.net 18 | # user: puppet 19 | # command: cat /opt/puppetlabs/server/data/puppetserver/yaml/facts/%%NODE%%.yaml 20 | # password: secret001 21 | 22 | # You can also configure "ssh" to log in to a specific server. This is useful if you want to run "facter" on 23 | # that system and capture the results. Similar to the previous section, "%%NODE%%" is replaced with the FQDN 24 | # of the node whose facts you are gathering. 25 | # ssh: 26 | # server: %%NODE%% 27 | # user: puppet 28 | # command: /opt/puppetlabs/puppet/bin/facter -p --yaml 29 | 30 | # This section controls your index file. If you'd like to select appropriate fact fixtures 31 | # without explicitly naming a node, list the facts here that you would like to have indexed. 32 | # You can also specify the path to the index file. Be sure to adjust it for your site. 33 | index: 34 | file: ../spec/fixtures/facts/octofacts-index.yaml 35 | node_path: ../spec/fixtures/facts/octofacts 36 | indexed_facts: 37 | - app 38 | - role 39 | - virtual 40 | 41 | # If you are using an external node classifier (ENC) you can import its results into octofacts 42 | # by having the octofacts-updater run the ENC as the node's facts are obtained. Anything in the 43 | # ENC's "parameters" hash will be configured as a fact and override the facts from the data source. 44 | # Provide the path to your ENC script here. 45 | # enc: 46 | # path: ./config/enc.sh 47 | 48 | # This section cleans up and anonymizes facts. We have added a number of facts to this list 49 | # that contain sensitive information or change frequently. You should add any facts from your 50 | # site as needed. 51 | facts: 52 | ec2_userdata: 53 | plugin: delete 54 | trusted: 55 | plugin: delete 56 | ec2_metadata: 57 | structure: iam::info 58 | plugin: delete 59 | ec2_iam_info_NNN: 60 | regexp: ^ec2_iam_info_\d+ 61 | plugin: delete 62 | uptime: 63 | regexp: ^uptime 64 | plugin: delete 65 | memoryfree: 66 | regexp: ^memoryfree 67 | plugin: delete 68 | swapfree: 69 | regexp: ^swapfree 70 | plugin: delete 71 | load_averages: 72 | plugin: delete 73 | memory_stats: 74 | fact: memory 75 | structure: 76 | - regexp: .+ 77 | - regexp: ^(available|available_bytes|capacity|used|used_bytes)$ 78 | plugin: delete 79 | system_uptime: 80 | value: 81 | days: 17 82 | hours: 415 83 | seconds: 1495197 84 | uptime: 17 days 85 | plugin: set 86 | ssh_keys: 87 | regexp: ^ssh\w+key$ 88 | plugin: randomize_long_string 89 | sshfp_keys: 90 | regexp: ^sshfp_\w+$ 91 | plugin: sshfp_randomize 92 | ssh_structured_keys: 93 | fact: ssh 94 | structure: 95 | - regexp: .+ 96 | - regexp: ^key$ 97 | plugin: randomize_long_string 98 | ssh_structured_fingerprints: 99 | fact: ssh 100 | structure: 101 | - regexp: .+ 102 | - regexp: ^fingerprints$ 103 | - regexp: ^sha\d+$ 104 | plugin: sshfp_randomize 105 | -------------------------------------------------------------------------------- /lib/octofacts.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "octofacts/constructors/from_file" 3 | require "octofacts/constructors/from_index" 4 | require "octofacts/manipulators" 5 | require "octofacts/errors" 6 | require "octofacts/facts" 7 | require "octofacts/backends/base" 8 | require "octofacts/backends/index" 9 | require "octofacts/backends/yaml_file" 10 | require "octofacts/util/config" 11 | require "octofacts/util/keys" 12 | require "octofacts/version" 13 | 14 | module Octofacts 15 | # 16 | end 17 | -------------------------------------------------------------------------------- /lib/octofacts/backends/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Octofacts 3 | module Backends 4 | # This is a template class to define the minimum API to be implemented 5 | class Base 6 | # Returns a hash of the facts selected based on current criteria. Once this is done, 7 | # it is no longer possible to select, reject, or prefer. 8 | def facts 9 | # :nocov: 10 | raise NotImplementedError, "This method needs to be implemented in the subclass" 11 | # :nocov: 12 | end 13 | 14 | # Filters the possible fact sets based on the criteria. 15 | def select(*) 16 | # :nocov: 17 | raise NotImplementedError, "This method needs to be implemented in the subclass" 18 | # :nocov: 19 | end 20 | 21 | # Removes possible fact sets based on the criteria. 22 | def reject(*) 23 | # :nocov: 24 | raise NotImplementedError, "This method needs to be implemented in the subclass" 25 | # :nocov: 26 | end 27 | 28 | # Reorders possible fact sets based on the criteria. 29 | def prefer(*) 30 | # :nocov: 31 | raise NotImplementedError, "This method needs to be implemented in the subclass" 32 | # :nocov: 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/octofacts/backends/index.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "yaml" 4 | require "set" 5 | 6 | module Octofacts 7 | module Backends 8 | class Index < Base 9 | attr_reader :index_path, :fixture_path, :options 10 | attr_writer :facts 11 | 12 | def initialize(args = {}) 13 | index_path = Octofacts::Util::Config.fetch(:octofacts_index_path, args) 14 | fixture_path = Octofacts::Util::Config.fetch(:octofacts_fixture_path, args) 15 | strict_index = Octofacts::Util::Config.fetch(:octofacts_strict_index, args, false) 16 | 17 | raise(ArgumentError, "No index passed and ENV['OCTOFACTS_INDEX_PATH'] is not defined") if index_path.nil? 18 | raise(ArgumentError, "No fixture path passed and ENV['OCTOFACTS_FIXTURE_PATH'] is not defined") if fixture_path.nil? 19 | raise(Errno::ENOENT, "The index file #{index_path} does not exist") unless File.file?(index_path) 20 | raise(Errno::ENOENT, "The fixture path #{fixture_path} does not exist") unless File.directory?(fixture_path) 21 | 22 | @index_path = index_path 23 | @fixture_path = fixture_path 24 | @strict_index = strict_index == true || strict_index == "true" 25 | @facts = nil 26 | @options = args 27 | 28 | @node_facts = {} 29 | 30 | # If there are any other arguments treat them as `select` conditions. 31 | remaining_args = args.dup 32 | remaining_args.delete(:octofacts_index_path) 33 | remaining_args.delete(:octofacts_fixture_path) 34 | remaining_args.delete(:octofacts_strict_index) 35 | select(remaining_args) if remaining_args 36 | end 37 | 38 | def facts 39 | @facts ||= node_facts(nodes.first) 40 | end 41 | 42 | def select(conditions) 43 | Octofacts::Util::Keys.desymbolize_keys!(conditions) 44 | conditions.each do |key, value| 45 | add_fact_to_index(key) unless indexed_fact?(key) 46 | matching_nodes = index[key][value.to_s] 47 | raise Octofacts::Errors::NoFactsError if matching_nodes.nil? 48 | @nodes = nodes & matching_nodes 49 | end 50 | 51 | self 52 | end 53 | 54 | def reject(conditions) 55 | matching_nodes = nodes 56 | Octofacts::Util::Keys.desymbolize_keys!(conditions) 57 | conditions.each do |key, value| 58 | add_fact_to_index(key) unless indexed_fact?(key) 59 | unless index[key][value.to_s].nil? 60 | matching_nodes -= index[key][value.to_s] 61 | raise Octofacts::Errors::NoFactsError if matching_nodes.empty? 62 | end 63 | end 64 | 65 | @nodes = matching_nodes 66 | self 67 | end 68 | 69 | def prefer(conditions) 70 | Octofacts::Util::Keys.desymbolize_keys!(conditions) 71 | conditions.each do |key, value| 72 | add_fact_to_index(key) unless indexed_fact?(key) 73 | matching_nodes = index[key][value.to_s] 74 | unless matching_nodes.nil? 75 | @nodes = (matching_nodes.to_set + nodes.to_set).to_a 76 | end 77 | end 78 | 79 | self 80 | end 81 | 82 | private 83 | 84 | # If a select/reject/prefer is called and the fact is not in the index, this will 85 | # load the fact files for all currently eligible nodes and then add the fact to the 86 | # in-memory index. This can be memory-intensive and time-intensive depending on the 87 | # number of fact fixtures, so it is possible to disable this by passing 88 | # `:strict_index => true` to the backend constructor, or by setting 89 | # ENV["OCTOFACTS_STRICT_INDEX"] = "true" in the environment. 90 | def add_fact_to_index(fact) 91 | if @strict_index || ENV["OCTOFACTS_STRICT_INDEX"] == "true" 92 | raise Octofacts::Errors::FactNotIndexed, "Fact #{fact} is not indexed and strict indexing is enabled." 93 | end 94 | 95 | index[fact] ||= {} 96 | nodes.each do |node| 97 | v = node_facts(node)[fact] 98 | if v.nil? 99 | # TODO: Index this somehow 100 | else 101 | index[fact][v.to_s] ||= [] 102 | index[fact][v.to_s] << node 103 | end 104 | end 105 | end 106 | 107 | def nodes 108 | @nodes ||= index["_nodes"] 109 | end 110 | 111 | def index 112 | @index ||= YAML.safe_load(File.read(index_path)) 113 | end 114 | 115 | def indexed_fact?(fact) 116 | index.key?(fact) 117 | end 118 | 119 | def node_facts(node) 120 | @node_facts[node] ||= begin 121 | f = YAML.safe_load(File.read("#{fixture_path}/#{node}.yaml")) 122 | Octofacts::Util::Keys.desymbolize_keys!(f) 123 | f 124 | end 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/octofacts/backends/yaml_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | require "yaml" 4 | 5 | module Octofacts 6 | module Backends 7 | class YamlFile < Base 8 | attr_reader :filename, :options 9 | 10 | def initialize(filename, options = {}) 11 | raise(Errno::ENOENT, "The file #{filename} does not exist") unless File.file?(filename) 12 | 13 | @filename = filename 14 | @options = options 15 | @facts = nil 16 | end 17 | 18 | def facts 19 | @facts ||= begin 20 | f = YAML.safe_load(File.read(filename)) 21 | Octofacts::Util::Keys.symbolize_keys!(f) 22 | f 23 | end 24 | end 25 | 26 | def select(conditions) 27 | Octofacts::Util::Keys.symbolize_keys!(conditions) 28 | raise Octofacts::Errors::NoFactsError unless (conditions.to_a - facts.to_a).empty? 29 | end 30 | 31 | def reject(conditions) 32 | Octofacts::Util::Keys.symbolize_keys!(conditions) 33 | raise Octofacts::Errors::NoFactsError if (conditions.to_a - facts.to_a).empty? 34 | end 35 | 36 | def prefer(conditions) 37 | # noop 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/octofacts/constructors/from_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Octofacts 3 | # Octofacts.from_file(filename, options) - Construct Octofacts::Facts from a filename. 4 | # 5 | # filename - Relative or absolute path to the file containing the facts. 6 | # opts[:octofacts_fixture_path] - Directory where fact fixture files are found (default: ENV["OCTOFACTS_FIXTURE_PATH"]) 7 | # 8 | # Returns an Octofacts::Facts object. 9 | def self.from_file(filename, opts = {}) 10 | unless filename.start_with? "/" 11 | dir = Octofacts::Util::Config.fetch(:octofacts_fixture_path, opts) 12 | raise ArgumentError, ".from_file needs to know :octofacts_fixture_path or environment OCTOFACTS_FIXTURE_PATH" unless dir 13 | raise Errno::ENOENT, "The provided fixture path #{dir} is invalid" unless File.directory?(dir) 14 | filename = File.join(dir, filename) 15 | end 16 | 17 | Octofacts::Facts.new(backend: Octofacts::Backends::YamlFile.new(filename), options: opts) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/octofacts/constructors/from_index.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Octofacts 3 | # Octofacts.from_index(options) - Construct Octofacts::Facts from an index file. 4 | # 5 | # Returns an Octofacts::Facts object. 6 | def self.from_index(opts = {}) 7 | Octofacts::Facts.new(backend: Octofacts::Backends::Index.new(opts), options: opts) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/octofacts/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Octofacts 3 | class Errors 4 | class FactNotIndexed < RuntimeError; end 5 | class OperationNotPermitted < RuntimeError; end 6 | class NoFactsError < RuntimeError; end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/octofacts/facts.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | require "yaml" 4 | 5 | module Octofacts 6 | class Facts 7 | attr_writer :facts 8 | 9 | # Constructor. 10 | # 11 | # backend - An Octofacts::Backends object (preferred) 12 | # options - Additional options (e.g., downcase keys, symbolize keys, etc.) 13 | def initialize(args = {}) 14 | @backend = args.fetch(:backend) 15 | @facts_manipulated = false 16 | 17 | options = args.fetch(:options, {}) 18 | @downcase_keys = args.fetch(:downcase_keys, options.fetch(:downcase_keys, true)) 19 | end 20 | 21 | # To hash. (This method is intended to be called by rspec-puppet.) 22 | # 23 | # This loads the fact file and downcases, desymbolizes, and otherwise manipulates the keys. 24 | # The output is suitable for consumption by rspec-puppet. 25 | def to_hash 26 | f = facts 27 | downcase_keys!(f) if @downcase_keys 28 | desymbolize_keys!(f) 29 | f 30 | end 31 | alias_method :to_h, :to_hash 32 | 33 | # To fact hash. (This method is intended to be called by developers.) 34 | # 35 | # This loads the fact file and downcases, symbolizes, and otherwise manipulates the keys. 36 | # This is very similar to 'to_hash' except that it returns symbolized keys. 37 | # The output is suitable for consumption by rspec-puppet (note that rspec-puppet will 38 | # de-symbolize all the keys in the hash object though). 39 | def facts 40 | @facts ||= begin 41 | f = @backend.facts 42 | downcase_keys!(f) if @downcase_keys 43 | symbolize_keys!(f) 44 | f 45 | end 46 | end 47 | 48 | # Calls to backend methods. 49 | # 50 | # These calls are passed through directly to backend methods. 51 | def select(*args) 52 | if @facts_manipulated 53 | raise Octofacts::Errors::OperationNotPermitted, "Cannot call select() after backend facts have been manipulated" 54 | end 55 | @backend.select(*args) 56 | self 57 | end 58 | 59 | def reject(*args) 60 | if @facts_manipulated 61 | raise Octofacts::Errors::OperationNotPermitted, "Cannot call reject() after backend facts have been manipulated" 62 | end 63 | @backend.reject(*args) 64 | self 65 | end 66 | 67 | def prefer(*args) 68 | if @facts_manipulated 69 | raise Octofacts::Errors::OperationNotPermitted, "Cannot call prefer() after backend facts have been manipulated" 70 | end 71 | @backend.prefer(*args) 72 | self 73 | end 74 | 75 | # Missing method - this is used to dispatch to manipulators or to call a Hash method in the facts. 76 | # 77 | # Try calling a Manipulator method, delegate to the facts hash or else error out. 78 | # 79 | # Returns this object (so that calls to manipulators can be chained). 80 | def method_missing(name, *args, &block) 81 | if Octofacts::Manipulators.run(self, name, *args, &block) 82 | @facts_manipulated = true 83 | return self 84 | end 85 | 86 | if facts.respond_to?(name, false) 87 | if args[0].is_a?(String) || args[0].is_a?(Symbol) 88 | args[0] = string_or_symbolized_key(args[0]) 89 | end 90 | return facts.send(name, *args) 91 | end 92 | 93 | raise NameError, "Unknown method '#{name}' in #{self.class}" 94 | end 95 | 96 | def respond_to?(method, include_all = false) 97 | camelized_name = (method.to_s).split("_").collect(&:capitalize).join 98 | super || Kernel.const_get("Octofacts::Manipulators::#{camelized_name}") 99 | rescue NameError 100 | return facts.respond_to?(method, include_all) 101 | end 102 | 103 | private 104 | 105 | def downcase_keys!(input) 106 | Octofacts::Util::Keys.downcase_keys!(input) 107 | end 108 | 109 | def symbolize_keys!(input) 110 | Octofacts::Util::Keys.symbolize_keys!(input) 111 | end 112 | 113 | def desymbolize_keys!(input) 114 | Octofacts::Util::Keys.desymbolize_keys!(input) 115 | end 116 | 117 | def string_or_symbolized_key(input) 118 | return input.to_s if facts.key?(input.to_s) 119 | return input.to_sym if facts.key?(input.to_sym) 120 | input 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/octofacts/manipulators.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "manipulators/replace" 3 | 4 | # Octofacts::Manipulators - our fact manipulation API. 5 | # Each method in Octofacts::Manipulators will operate on one fact set at a time. These 6 | # methods do not need to be aware of the existence of multiple fact sets. 7 | module Octofacts 8 | class Manipulators 9 | # Locate and run manipulator. 10 | # 11 | # Returns true if the manipulator was located and executed, false otherwise. 12 | def self.run(obj, name, *args, &block) 13 | camelized_name = (name.to_s).split("_").collect(&:capitalize).join 14 | 15 | begin 16 | manipulator = Kernel.const_get("Octofacts::Manipulators::#{camelized_name}") 17 | rescue NameError 18 | return false 19 | end 20 | 21 | raise "Unable to run manipulator method '#{name}' on object type #{obj.class}" unless obj.is_a?(Octofacts::Facts) 22 | facts = obj.facts 23 | manipulator.send(:execute, facts, *args, &block) 24 | obj.facts = facts 25 | true 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/octofacts/manipulators/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Octofacts 3 | class Manipulators 4 | # Delete a fact from a hash. 5 | # 6 | # fact_set - The hash of facts 7 | # fact_name - Fact to delete, either as a string, symbol, or "multi::level::hash::key" 8 | def self.delete(fact_set, fact_name) 9 | if fact_name.to_s !~ /::/ 10 | fact_set.delete(fact_name.to_sym) 11 | return 12 | end 13 | 14 | # Convert level1::level2::level3 into { "level1" => { "level2" => { "level3" => ... } } } 15 | # The delimiter is 2 colons. 16 | levels = fact_name.to_s.split("::") 17 | key_name = levels.pop.to_sym 18 | pointer = fact_set 19 | while levels.any? 20 | next_key = levels.shift.to_sym 21 | return unless pointer.key?(next_key) && pointer[next_key].is_a?(Hash) 22 | pointer = pointer[next_key] 23 | end 24 | 25 | pointer.delete(key_name) 26 | end 27 | 28 | # Determine if a fact exists in a hash. 29 | # 30 | # fact_set - The hash of facts 31 | # fact_name - Fact to check, either as a string, symbol, or "multi::level::hash::key" 32 | # 33 | # Returns true if the fact exists, false otherwise. 34 | def self.exists?(fact_set, fact_name) 35 | !get(fact_set, fact_name).nil? 36 | end 37 | 38 | # Retrieves the value of a fact from a hash. 39 | # 40 | # fact_set - The hash of facts 41 | # fact_name - Fact to retrieve, either as a string, symbol, or "multi::level::hash::key" 42 | # 43 | # Returns the value of the fact. 44 | def self.get(fact_set, fact_name) 45 | return fact_set[fact_name.to_sym] unless fact_name.to_s =~ /::/ 46 | 47 | # Convert level1::level2::level3 into { "level1" => { "level2" => { "level3" => ... } } } 48 | # The delimiter is 2 colons. 49 | levels = fact_name.to_s.split("::") 50 | key_name = levels.pop.to_sym 51 | pointer = fact_set 52 | while levels.any? 53 | next_key = levels.shift.to_sym 54 | return unless pointer.key?(next_key) && pointer[next_key].is_a?(Hash) 55 | pointer = pointer[next_key] 56 | end 57 | pointer[key_name] 58 | end 59 | 60 | # Sets the value of a fact in a hash. 61 | # 62 | # The new value can be a string, integer, etc., which will directly set the value of 63 | # the fact. Instead, you may pass a lambda in place of the value, which will evaluate 64 | # with three parameters: lambda { |fact_set|, |fact_name|, |old_value| ... }, 65 | # or with one parameter: lambda { |old_value| ...}. 66 | # If the value of the fact as evaluated is `nil` then the fact is deleted instead of set. 67 | # 68 | # fact_set - The hash of facts 69 | # fact_name - Fact to set, either as a string, symbol, or "multi::level::hash::key" 70 | # value - A lambda with new code, or a string, integer, etc. 71 | def self.set(fact_set, fact_name, value) 72 | fact = fact_name.to_s 73 | 74 | if fact !~ /::/ 75 | fact_set[fact_name.to_sym] = _set(fact_set, fact_name, fact_set[fact_name.to_sym], value) 76 | fact_set.delete(fact_name.to_sym) if fact_set[fact_name.to_sym].nil? 77 | return 78 | end 79 | 80 | # Convert level1::level2::level3 into { "level1" => { "level2" => { "level3" => ... } } } 81 | # The delimiter is 2 colons. 82 | levels = fact_name.to_s.split("::") 83 | key_name = levels.pop.to_sym 84 | pointer = fact_set 85 | while levels.any? 86 | next_key = levels.shift.to_sym 87 | pointer[next_key] = {} unless pointer[next_key].is_a? Hash 88 | pointer = pointer[next_key] 89 | end 90 | pointer[key_name] = _set(fact_set, fact_name, pointer[key_name], value) 91 | pointer.delete(key_name) if pointer[key_name].nil? 92 | end 93 | 94 | # Internal method: Determine the value you're setting to. 95 | # 96 | # This handles dispatching to the lambda function or putting the new value in place. 97 | def self._set(fact_set, fact_name, old_value, new_value) 98 | if new_value.is_a?(Proc) 99 | if new_value.arity == 1 100 | new_value.call(old_value) 101 | elsif new_value.arity == 3 102 | new_value.call(fact_set, fact_name, old_value) 103 | else 104 | raise ArgumentError, "Lambda method expected 1 or 3 parameters, got #{new_value.arity}" 105 | end 106 | else 107 | new_value 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/octofacts/manipulators/replace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "base" 3 | 4 | module Octofacts 5 | class Manipulators 6 | class Replace < Octofacts::Manipulators 7 | # Public: Executor for the .replace command. 8 | # 9 | # Sets the fact to the specified value. If the fact didn't exist before, it's created. 10 | # 11 | # facts - Hash of current facts 12 | # args - Arguments, here consisting of an array of hashes with replacement parameters 13 | def self.execute(facts, *args, &_block) 14 | args.each do |arg| 15 | raise ArgumentError, "Must pass a hash of target facts to .replace - got #{arg}" unless arg.is_a?(Hash) 16 | arg.each { |key, val| set(facts, key, val) } 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/octofacts/util/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Retrieves configuration parameters from: 3 | # - input hash 4 | # - rspec configuration 5 | # - environment 6 | module Octofacts 7 | module Util 8 | class Config 9 | # Fetch a variable from various sources 10 | def self.fetch(variable_name, hash_in = {}, default = nil) 11 | if hash_in.key?(variable_name) 12 | return hash_in[variable_name] 13 | end 14 | 15 | begin 16 | rspec_value = RSpec.configuration.send(variable_name) 17 | return rspec_value if rspec_value 18 | rescue NoMethodError 19 | # Just skip if undefined 20 | end 21 | 22 | env_key = variable_name.to_s.upcase 23 | return ENV[env_key] if ENV.key?(env_key) 24 | 25 | default 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/octofacts/util/keys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Octofacts 3 | module Util 4 | class Keys 5 | # Downcase all keys. 6 | # 7 | # rspec-puppet does this internally, but depending on how Octofacts is called, this logic may not 8 | # be triggered. Therefore, we downcase all keys ourselves. 9 | def self.downcase_keys!(input) 10 | raise ArgumentError, "downcase_keys! expects Hash, not #{input.class}" unless input.is_a?(Hash) 11 | 12 | input_keys = input.keys.dup 13 | input_keys.each do |k| 14 | downcase_keys!(input[k]) if input[k].is_a?(Hash) 15 | next if k.to_s == k.to_s.downcase 16 | new_key = k.is_a?(Symbol) ? k.to_s.downcase.to_sym : k.downcase 17 | input[new_key] = input.delete(k) 18 | end 19 | input 20 | end 21 | 22 | # Symbolize all keys. 23 | # 24 | # Many people work with symbolized keys rather than string keys when dealing with fact fixtures. 25 | # This method recursively converts all keys to symbols. 26 | def self.symbolize_keys!(input) 27 | raise ArgumentError, "symbolize_keys! expects Hash, not #{input.class}" unless input.is_a?(Hash) 28 | 29 | input_keys = input.keys.dup 30 | input_keys.each do |k| 31 | symbolize_keys!(input[k]) if input[k].is_a?(Hash) 32 | input[k.to_sym] = input.delete(k) unless k.is_a?(Symbol) 33 | end 34 | input 35 | end 36 | 37 | # De-symbolize all keys. 38 | # 39 | # rspec-puppet ultimately wants stringified keys, so this is a method to turn symbols back into strings. 40 | def self.desymbolize_keys!(input) 41 | raise ArgumentError, "desymbolize_keys! expects Hash, not #{input.class}" unless input.is_a?(Hash) 42 | 43 | input_keys = input.keys.dup 44 | input_keys.each do |k| 45 | desymbolize_keys!(input[k]) if input[k].is_a?(Hash) 46 | input[k.to_s] = input.delete(k) unless k.is_a?(String) 47 | end 48 | input 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/octofacts/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Octofacts 3 | VERSION = File.read(File.expand_path("../../.version", File.dirname(__FILE__))).strip 4 | end 5 | -------------------------------------------------------------------------------- /lib/octofacts_updater.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "octofacts_updater/cli" 3 | require "octofacts_updater/fact" 4 | require "octofacts_updater/fact_index" 5 | require "octofacts_updater/fixture" 6 | require "octofacts_updater/plugin" 7 | require "octofacts_updater/plugins/ip" 8 | require "octofacts_updater/plugins/ssh" 9 | require "octofacts_updater/plugins/static" 10 | require "octofacts_updater/service/base" 11 | require "octofacts_updater/service/enc" 12 | require "octofacts_updater/service/github" 13 | require "octofacts_updater/service/local_file" 14 | require "octofacts_updater/service/puppetdb" 15 | require "octofacts_updater/service/ssh" 16 | require "octofacts_updater/version" 17 | 18 | module OctofactsUpdater 19 | # 20 | end 21 | -------------------------------------------------------------------------------- /lib/octofacts_updater/fact.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This class represents a fact, either structured or unstructured. 3 | # The fact has a name and a value. The name is a string, and the value 4 | # can either be a string/integer/boolean (unstructured) or a hash (structured). 5 | # This class also has methods used to deal with structured facts (in particular, allowing 6 | # representation of a structure delimited with ::). 7 | 8 | module OctofactsUpdater 9 | class Fact 10 | attr_reader :name 11 | 12 | # Constructor. 13 | # 14 | # name - The String naming the fact. 15 | # value - The arbitrary object with the value of the fact. 16 | def initialize(name, value) 17 | @name = name 18 | @value = value 19 | end 20 | 21 | # Get the value of the fact. If the name is specified, this will dig into a structured fact to pull 22 | # out the value within the structure. 23 | # 24 | # name_in - An optional String to dig into the structure (formatted with :: indicating hash delimiters) 25 | # 26 | # Returns the value of the fact. 27 | def value(name_in = nil) 28 | # Just a normal lookup -- return the value 29 | return @value if name_in.nil? 30 | 31 | # Structured lookup returns nil unless the fact is actually structured. 32 | return unless @value.is_a?(Hash) 33 | 34 | # Dig into the hash to pull out the desired value. 35 | pointer = @value 36 | parts = name_in.split("::") 37 | last_part = parts.pop 38 | 39 | parts.each do |part| 40 | return unless pointer[part].is_a?(Hash) 41 | pointer = pointer[part] 42 | end 43 | 44 | pointer[last_part] 45 | end 46 | 47 | # Set the value of the fact. 48 | # 49 | # new_value - An object with the new value for the fact 50 | def value=(new_value) 51 | set_value(new_value) 52 | end 53 | 54 | # Set the value of the fact. If the name is specified, this will dig into a structured fact to set 55 | # the value within the structure. 56 | # 57 | # new_value - An object with the new value for the fact 58 | # name_in - An optional String to dig into the structure (formatted with :: indicating hash delimiters) 59 | def set_value(new_value, name_in = nil) 60 | if name_in.nil? 61 | if new_value.is_a?(Proc) 62 | return @value = new_value.call(@value) 63 | end 64 | 65 | return @value = new_value 66 | end 67 | 68 | parts = if name_in.is_a?(String) 69 | name_in.split("::") 70 | elsif name_in.is_a?(Array) 71 | name_in.map do |item| 72 | if item.is_a?(String) 73 | item 74 | elsif item.is_a?(Hash) && item.key?("regexp") 75 | Regexp.new(item["regexp"]) 76 | else 77 | raise ArgumentError, "Unable to interpret structure item: #{item.inspect}" 78 | end 79 | end 80 | else 81 | raise ArgumentError, "Unable to interpret structure: #{name_in.inspect}" 82 | end 83 | 84 | set_structured_value(@value, parts, new_value) 85 | end 86 | 87 | private 88 | 89 | # Set a value in the data structure of a structured fact. This is intended to be 90 | # called recursively. 91 | # 92 | # subhash - The Hash, part of the fact, being operated upon 93 | # parts - The Array to dig in to the hash 94 | # value - The value to set the ultimate last part to 95 | # 96 | # Does not return anything, but modifies 'subhash' 97 | def set_structured_value(subhash, parts, value) 98 | return if subhash.nil? 99 | raise ArgumentError, "Cannot set structured value at #{parts.first.inspect}" unless subhash.is_a?(Hash) 100 | raise ArgumentError, "parts must be an Array, got #{parts.inspect}" unless parts.is_a?(Array) 101 | 102 | # At the top level, find all keys that match the first item in the parts. 103 | matching_keys = subhash.keys.select do |key| 104 | if parts.first.is_a?(String) 105 | key == parts.first 106 | elsif parts.first.is_a?(Regexp) 107 | parts.first.match(key) 108 | else 109 | # :nocov: 110 | # This is a bug - this code should be unreachable because of the checking in `set_value` 111 | raise ArgumentError, "part must be a string or regexp, got #{parts.first.inspect}" 112 | # :nocov: 113 | end 114 | end 115 | 116 | # Auto-create a new hash if there is a value, the part is a string, and the key doesn't exist. 117 | if parts.first.is_a?(String) && !value.nil? && !subhash.key?(parts.first) 118 | subhash[parts.first] = {} 119 | matching_keys << parts.first 120 | end 121 | return unless matching_keys.any? 122 | 123 | # If we are at the end, set the value or delete the key. 124 | if parts.size == 1 125 | if value.nil? 126 | matching_keys.each { |k| subhash.delete(k) } 127 | elsif value.is_a?(Proc) 128 | matching_keys.each do |k| 129 | new_value = value.call(subhash[k]) 130 | if new_value.nil? 131 | subhash.delete(k) 132 | else 133 | subhash[k] = new_value 134 | end 135 | end 136 | else 137 | matching_keys.each { |k| subhash[k] = value } 138 | end 139 | return 140 | end 141 | 142 | # We are not at the end. Recurse down to the next level. 143 | matching_keys.each { |k| set_structured_value(subhash[k], parts[1..-1], value) } 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/octofacts_updater/fact_index.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This class represents a fact index, which is ultimately represented by a YAML file of 3 | # each index fact, the values seen, and the node(s) containing each value. 4 | # 5 | # fact_one: 6 | # value_one: 7 | # - node-1.example.net 8 | # - node-2.example.net 9 | # value_three: 10 | # - node-3.example.net 11 | # fact_two: 12 | # value_abc: 13 | # - node-1.example.net 14 | # value_def: 15 | # - node-2.example.net 16 | # - node-3.example.net 17 | 18 | require "set" 19 | require "yaml" 20 | 21 | module OctofactsUpdater 22 | class FactIndex 23 | # We will create a pseudo-fact that simply lists all of the nodes that were considered 24 | # in the index. Define the name of that pseudo-fact here. 25 | TOP_LEVEL_NODES_KEY = "_nodes".freeze 26 | 27 | attr_reader :index_data 28 | 29 | # Load an index from the YAML file. 30 | # 31 | # filename - A String with the file to be loaded. 32 | # 33 | # Returns a OctofactsUpdater::FactIndex object. 34 | def self.load_file(filename) 35 | unless File.file?(filename) 36 | raise Errno::ENOENT, "load_index cannot load #{filename.inspect}" 37 | end 38 | 39 | data = YAML.safe_load(File.read(filename)) 40 | new(data, filename: filename) 41 | end 42 | 43 | # Constructor. 44 | # 45 | # data - A Hash of existing index data. 46 | # filename - Optionally, a String with a file name to write the index to 47 | def initialize(data = {}, filename: nil) 48 | @index_data = data 49 | @filename = filename 50 | end 51 | 52 | # Add a fact to the index. If the fact already exists in the index, this will overwrite it. 53 | # 54 | # fact_name - A String with the name of the fact 55 | # fixtures - An Array with fact fixtures (must respond to .facts and .hostname) 56 | def add(fact_name, fixtures) 57 | @index_data[fact_name] ||= {} 58 | fixtures.each do |fixture| 59 | fact_value = get_fact(fixture, fact_name) 60 | next if fact_value.nil? 61 | @index_data[fact_name][fact_value] ||= [] 62 | @index_data[fact_name][fact_value] << fixture.hostname 63 | end 64 | end 65 | 66 | # Get a list of all of the nodes in the index. This supports a quick mode (default) where the 67 | # TOP_LEVEL_NODES_KEY key is used, and a more detailed mode where this digs through each indexed 68 | # fact and value to build a list of nodes. 69 | # 70 | # quick_mode - Boolean whether to use quick mode (default=true) 71 | # 72 | # Returns an Array of nodes whose facts are indexed. 73 | def nodes(quick_mode = true) 74 | if quick_mode && @index_data.key?(TOP_LEVEL_NODES_KEY) 75 | return @index_data[TOP_LEVEL_NODES_KEY] 76 | end 77 | 78 | seen_hosts = Set.new 79 | @index_data.each do |fact_name, fact_values| 80 | next if fact_name == TOP_LEVEL_NODES_KEY 81 | fact_values.each do |_fact_value, nodes| 82 | seen_hosts.merge(nodes) 83 | end 84 | end 85 | seen_hosts.to_a.sort 86 | end 87 | 88 | # Rebuild an index with a specified list of facts. This will remove any indexed facts that 89 | # are not on the list of facts to use. 90 | # 91 | # facts_to_index - An Array of Strings with facts to index 92 | # fixtures - An Array with fact fixtures (must respond to .facts and .hostname) 93 | def reindex(facts_to_index, fixtures) 94 | @index_data = {} 95 | facts_to_index.each { |fact| add(fact, fixtures) } 96 | set_top_level_nodes_fact(fixtures) 97 | end 98 | 99 | # Create the top level nodes pseudo-fact. 100 | # 101 | # fixtures - An Array with fact fixtures (must respond to .hostname) 102 | def set_top_level_nodes_fact(fixtures) 103 | @index_data[TOP_LEVEL_NODES_KEY] = fixtures.map { |f| f.hostname }.sort 104 | end 105 | 106 | # Get YAML representation of the index. 107 | # This sorts the hash and any arrays without modifying the object. 108 | def to_yaml 109 | YAML.dump(recursive_sort(index_data)) 110 | end 111 | 112 | def recursive_sort(object_in) 113 | if object_in.is_a?(Hash) 114 | object_out = {} 115 | object_in.keys.sort.each { |k| object_out[k] = recursive_sort(object_in[k]) } 116 | object_out 117 | elsif object_in.is_a?(Array) 118 | object_in.sort.map { |v| recursive_sort(v) } 119 | else 120 | object_in 121 | end 122 | end 123 | 124 | # Write the fact index out to a YAML file. 125 | # 126 | # filename - A String with the file to write (defaults to filename from constructor if available) 127 | def write_file(filename = nil) 128 | filename ||= @filename 129 | unless filename.is_a?(String) 130 | raise ArgumentError, "Called write_file() for fact_index without a filename" 131 | end 132 | File.open(filename, "w") { |f| f.write(to_yaml) } 133 | end 134 | 135 | private 136 | 137 | # Extract a (possibly) structured fact. 138 | # 139 | # fixture - Fact fixture, must respond to .facts 140 | # fact_name - A String with the name of the fact 141 | # 142 | # Returns the value of the fact, or nil if fact or structure does not exist. 143 | def get_fact(fixture, fact_name) 144 | pointer = fixture.facts 145 | 146 | # Get the fact of interest from the fixture, whether structured or not. 147 | components = fact_name.split(".") 148 | first_component = components.shift 149 | return unless pointer.key?(first_component) 150 | 151 | # For simple non-structured facts, just return the value. 152 | return pointer[first_component].value if components.empty? 153 | 154 | # Structured facts: dig into the structure. 155 | pointer = pointer[first_component].value 156 | last_component = components.pop 157 | components.each do |part| 158 | return unless pointer.key?(part) 159 | return unless pointer[part].is_a?(Hash) 160 | pointer = pointer[part] 161 | end 162 | pointer[last_component] 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/octofacts_updater/fixture.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This class represents a fact fixture, which is a set of facts along with a node name. 3 | # Facts are OctofactsUpdater::Fact objects, and internally are stored as a hash table 4 | # with the key being the fact name and the value being the OctofactsUpdater::Fact object. 5 | 6 | require "yaml" 7 | 8 | module OctofactsUpdater 9 | class Fixture 10 | attr_reader :facts, :hostname 11 | 12 | # Make a fact fixture for the specified host name by consulting data sources 13 | # specified in the configuration. 14 | # 15 | # hostname - A String with the FQDN of the host. 16 | # config - A Hash with configuration data. 17 | # 18 | # Returns the OctofactsUpdater::Fixture object. 19 | def self.make(hostname, config) 20 | fact_hash = facts_from_configured_datasource(hostname, config) 21 | 22 | if config.key?("enc") 23 | enc_data = OctofactsUpdater::Service::ENC.run_enc(hostname, config) 24 | if enc_data.key?("parameters") 25 | fact_hash.merge! enc_data["parameters"] 26 | end 27 | end 28 | 29 | obj = new(hostname, config, fact_hash) 30 | obj.execute_plugins! 31 | end 32 | 33 | # Get fact hash from the first configured and working data source. 34 | # 35 | # hostname - A String with the FQDN of the host. 36 | # config - A Hash with configuration data. 37 | # 38 | # Returns a Hash with the facts for the specified node; raises exception if this was not possible. 39 | def self.facts_from_configured_datasource(hostname, config) 40 | last_exception = nil 41 | data_sources = %w(LocalFile PuppetDB SSH) 42 | data_sources.each do |ds| 43 | next if config.fetch(:options, {})[:datasource] && config[:options][:datasource] != ds.downcase.to_sym 44 | next unless config.key?(ds.downcase) 45 | clazz = Kernel.const_get("OctofactsUpdater::Service::#{ds}") 46 | begin 47 | result = clazz.send(:facts, hostname, config) 48 | return result["values"] if result["values"].is_a?(Hash) 49 | return result 50 | rescue => e 51 | last_exception = e 52 | end 53 | end 54 | 55 | raise last_exception if last_exception 56 | raise ArgumentError, "No fact data sources were configured" 57 | end 58 | 59 | # Load a fact fixture from a file. This helps create an index without the more expensive operation 60 | # of actually looking up the facts from the data source. 61 | # 62 | # hostname - A String with the FQDN of the host. 63 | # filename - A String with the filename of the existing host. 64 | # 65 | # Returns the OctofactsUpdater::Fixture object. 66 | def self.load_file(hostname, filename) 67 | unless File.file?(filename) 68 | raise Errno::ENOENT, "Could not load facts from #{filename} because it does not exist" 69 | end 70 | 71 | data = YAML.safe_load(File.read(filename)) 72 | new(hostname, {}, data) 73 | end 74 | 75 | # Constructor. 76 | # 77 | # hostname - A String with the FQDN of the host. 78 | # config - A Hash with configuration data. 79 | # fact_hash - A Hash with the facts (key = fact name, value = fact value). 80 | def initialize(hostname, config, fact_hash = {}) 81 | @hostname = hostname 82 | @config = config 83 | @facts = Hash[fact_hash.collect { |k, v| [k, OctofactsUpdater::Fact.new(k, v)] }] 84 | end 85 | 86 | # Execute plugins to clean up facts as per configuration. This modifies the value of the facts 87 | # stored in this object. Any facts with a value of nil are removed. 88 | # 89 | # Returns a copy of this object. 90 | def execute_plugins! 91 | return self unless @config["facts"].is_a?(Hash) 92 | 93 | @config["facts"].each do |fact_tag, args| 94 | fact_names(fact_tag, args).each do |fact_name| 95 | @facts[fact_name] ||= OctofactsUpdater::Fact.new(fact_name, nil) 96 | plugin_name = args.fetch("plugin", "noop") 97 | OctofactsUpdater::Plugin.execute(plugin_name, @facts[fact_name], args, @facts) 98 | @facts.delete(fact_name) if @facts[fact_name].value.nil? 99 | end 100 | end 101 | 102 | self 103 | end 104 | 105 | # Get fact names associated with a particular data structure. Implements: 106 | # - Default behavior, where YAML key = fact name 107 | # - Regexp behavior, where YAML "regexp" key is used to match against all facts 108 | # - Override behavior, where YAML "fact" key overrides whatever is in the tag 109 | # 110 | # fact_tag - A String with the YAML key 111 | # args - A Hash with the arguments 112 | # 113 | # Returns an Array of Strings with all fact names matched. 114 | def fact_names(fact_tag, args = {}) 115 | return [args["fact"]] if args.key?("fact") 116 | return [fact_tag] unless args.key?("regexp") 117 | rexp = Regexp.new(args["regexp"]) 118 | @facts.keys.select { |k| rexp.match(k) } 119 | end 120 | 121 | # Write this fixture to a file. 122 | # 123 | # filename - A String with the filename to write. 124 | def write_file(filename) 125 | File.open(filename, "w") { |f| f.write(to_yaml) } 126 | end 127 | 128 | # YAML representation of the fact fixture. 129 | # 130 | # Returns a String containing the YAML representation of the fact fixture. 131 | def to_yaml 132 | sorted_facts = @facts.sort.to_h 133 | facts_hash_with_expanded_values = Hash[sorted_facts.collect { |k, v| [k, v.value] }] 134 | YAML.dump(facts_hash_with_expanded_values) 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/octofacts_updater/plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This class provides the base methods for fact manipulation plugins. 3 | 4 | require "digest" 5 | 6 | module OctofactsUpdater 7 | class Plugin 8 | # Register a plugin. 9 | # 10 | # plugin_name - A Symbol which is the name of the plugin. 11 | # block - A block of code that constitutes the plugin. See sample plugins for expected format. 12 | def self.register(plugin_name, &block) 13 | @plugins ||= {} 14 | if @plugins.key?(plugin_name.to_sym) 15 | raise ArgumentError, "A plugin named #{plugin_name} is already registered." 16 | end 17 | @plugins[plugin_name.to_sym] = block 18 | end 19 | 20 | # Execute a plugin 21 | # 22 | # plugin_name - A Symbol which is the name of the plugin. 23 | # fact - An OctofactsUpdater::Fact object 24 | # args - An optional Hash of additional configuration arguments 25 | # all_facts - A Hash of all of the facts 26 | # 27 | # Returns nothing, but may adjust the "fact" 28 | def self.execute(plugin_name, fact, args = {}, all_facts = {}) 29 | unless @plugins.key?(plugin_name.to_sym) 30 | raise NoMethodError, "A plugin named #{plugin_name} could not be found." 31 | end 32 | 33 | begin 34 | @plugins[plugin_name.to_sym].call(fact, args, all_facts) 35 | rescue => e 36 | warn "#{e.class} occurred executing #{plugin_name} on #{fact.name} with value #{fact.value.inspect}" 37 | raise e 38 | end 39 | end 40 | 41 | # Clear out a plugin definition. (Useful for testing.) 42 | # 43 | # plugin_name - The name of the plugin to clear. 44 | def self.clear!(plugin_name) 45 | @plugins ||= {} 46 | @plugins.delete(plugin_name.to_sym) 47 | end 48 | 49 | # Get the plugins hash. 50 | def self.plugins 51 | @plugins 52 | end 53 | 54 | # --------------------------- 55 | # Below this point are shared methods intended to be called by plugins. 56 | # --------------------------- 57 | 58 | # Randomize a long string. This method accepts a string (consisting of, for example, a SSH key) 59 | # and returns a string of the same length, but with randomized characters. 60 | # 61 | # string_in - A String with the original fact value. 62 | # 63 | # Returns a String with the same length as string_in. 64 | def self.randomize_long_string(string_in) 65 | seed = Digest::SHA512.hexdigest(string_in).to_i(36) 66 | 67 | prng = Random.new(seed) 68 | chars = [("a".."z"), ("A".."Z"), ("0".."9")].flat_map(&:to_a) 69 | (1..(string_in.length)).map { chars[prng.rand(chars.length)] }.join 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/octofacts_updater/plugins/ip.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This file is part of the octofacts updater fact manipulation plugins. This plugin provides 3 | # methods to update facts that are IP addresses in order to anonymize or randomize them. 4 | 5 | require "ipaddr" 6 | 7 | # ipv4_anonymize. This method modifies an IP (version 4) address and 8 | # sets it to a randomized (yet consistent) address in the given 9 | # network. 10 | # 11 | # Supported parameters in args: 12 | # - subnet: (Required) The network prefix in CIDR notation 13 | OctofactsUpdater::Plugin.register(:ipv4_anonymize) do |fact, args = {}, facts| 14 | raise ArgumentError, "ipv4_anonymize requires a subnet" if args["subnet"].nil? 15 | 16 | subnet_range = IPAddr.new(args["subnet"], Socket::AF_INET).to_range 17 | # Convert the original IP to an integer representation that we can use as seed 18 | seed = IPAddr.new(fact.value(args["structure"]), Socket::AF_INET).to_i 19 | srand seed 20 | random_ip = IPAddr.new(rand(subnet_range.first.to_i..subnet_range.last.to_i), Socket::AF_INET) 21 | fact.set_value(random_ip.to_s, args["structure"]) 22 | end 23 | 24 | # ipv6_anonymize. This method modifies an IP (version 6) address and 25 | # sets it to a randomized (yet consistent) address in the given 26 | # network. 27 | # 28 | # Supported parameters in args: 29 | # - subnet: (Required) The network prefix in CIDR notation 30 | OctofactsUpdater::Plugin.register(:ipv6_anonymize) do |fact, args = {}, facts| 31 | raise ArgumentError, "ipv6_anonymize requires a subnet" if args["subnet"].nil? 32 | 33 | subnet_range = IPAddr.new(args["subnet"], Socket::AF_INET6).to_range 34 | # Convert the hostname to an integer representation that we can use as seed 35 | seed = IPAddr.new(fact.value(args["structure"]), Socket::AF_INET6).to_i 36 | srand seed 37 | random_ip = IPAddr.new(rand(subnet_range.first.to_i..subnet_range.last.to_i), Socket::AF_INET6) 38 | fact.set_value(random_ip.to_s, args["structure"]) 39 | end 40 | -------------------------------------------------------------------------------- /lib/octofacts_updater/plugins/ssh.rb: -------------------------------------------------------------------------------- 1 | # This file is part of the octofacts updater fact manipulation plugins. This plugin provides 2 | # frozen_string_literal: true 3 | # methods to update facts that are SSH keys, since we do not desire to commit SSH keys from 4 | # actual hosts into the source code repository. 5 | 6 | # sshfp. This method randomizes the secret key for sshfp formatted keys. Each key is replaced 7 | # by a randomized (yet consistent) string the same length as the input key. 8 | # The input looks like this: 9 | # sshfp_ecdsa: |- 10 | # SSHFP 3 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 11 | # SSHFP 3 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 12 | OctofactsUpdater::Plugin.register(:sshfp_randomize) do |fact, args = {}| 13 | blk = Proc.new do |val| 14 | lines = val.split("\n").map(&:strip) 15 | result = lines.map do |line| 16 | unless line =~ /\ASSHFP (\d+) (\d+) (\w+)/ 17 | raise "Unparseable pattern: #{line}" 18 | end 19 | "SSHFP #{Regexp.last_match(1)} #{Regexp.last_match(2)} #{OctofactsUpdater::Plugin.randomize_long_string(Regexp.last_match(3))}" 20 | end 21 | result.join("\n") 22 | end 23 | fact.set_value(blk, args["structure"]) 24 | end 25 | -------------------------------------------------------------------------------- /lib/octofacts_updater/plugins/static.rb: -------------------------------------------------------------------------------- 1 | # This file is part of the octofacts updater fact manipulation plugins. This plugin provides 2 | # frozen_string_literal: true 3 | # methods to do static operations on facts -- delete, add, or set to a known value. 4 | 5 | # Delete. This method deletes the fact or the identified portion. Setting the value to nil 6 | # causes the tooling to remove any such portions of the value. 7 | # 8 | # Supported parameters in args: 9 | # - structure: A String or Array of a structure within a structured fact 10 | OctofactsUpdater::Plugin.register(:delete) do |fact, args = {}, _all_facts = {}| 11 | fact.set_value(nil, args["structure"]) 12 | end 13 | 14 | # Set. This method sets the fact or the identified portion to a static value. 15 | # 16 | # Supported parameters in args: 17 | # - structure: A String or Array of a structure within a structured fact 18 | # - value: The new value to set the fact to 19 | OctofactsUpdater::Plugin.register(:set) do |fact, args = {}, _all_facts = {}| 20 | fact.set_value(args["value"], args["structure"]) 21 | end 22 | 23 | # Remove matching objects from a delimited string. Requires that the delimiter 24 | # and regular expression be set. This is useful, for example, to transform a 25 | # string like `foo,bar,baz,fizz` into `foo,fizz` (by removing /^ba/). 26 | # 27 | # Supported parameters in args: 28 | # - delimiter: (Required) Character that is the delimiter. 29 | # - regexp: (Required) String used to construct a regular expression of items to remove 30 | OctofactsUpdater::Plugin.register(:remove_from_delimited_string) do |fact, args = {}, _all_facts = {}| 31 | unless fact.value.nil? 32 | unless args["delimiter"] 33 | raise ArgumentError, "remove_from_delimited_string requires a delimiter, got #{args.inspect}" 34 | end 35 | unless args["regexp"] 36 | raise ArgumentError, "remove_from_delimited_string requires a regexp, got #{args.inspect}" 37 | end 38 | parts = fact.value.split(args["delimiter"]) 39 | regexp = Regexp.new(args["regexp"]) 40 | parts.delete_if { |part| regexp.match(part) } 41 | fact.set_value(parts.join(args["delimiter"])) 42 | end 43 | end 44 | 45 | # No-op. Do nothing at all. 46 | OctofactsUpdater::Plugin.register(:noop) do |_fact, _args = {}, _all_facts = {}| 47 | # 48 | end 49 | 50 | # Randomize long string. This is just a wrapper around OctofactsUpdater::Plugin.randomize_long_string 51 | OctofactsUpdater::Plugin.register(:randomize_long_string) do |fact, args = {}, _all_facts = {}| 52 | blk = Proc.new { |val| OctofactsUpdater::Plugin.randomize_long_string(val) } 53 | fact.set_value(blk, args["structure"]) 54 | end 55 | -------------------------------------------------------------------------------- /lib/octofacts_updater/service/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This contains handy utility methods that might be used in any of the other classes. 3 | 4 | require "yaml" 5 | 6 | module OctofactsUpdater 7 | module Service 8 | class Base 9 | # Parse a YAML fact file from PuppetServer. This removes the header (e.g. "--- !ruby/object:Puppet::Node::Facts") 10 | # so that it's not necessary to bring in all of Puppet. 11 | # 12 | # yaml_string - A String with YAML to parse. 13 | # 14 | # Returns a Hash with the facts. 15 | def self.parse_yaml(yaml_string) 16 | # Convert first "---" after any comments and blank lines. 17 | yaml_array = yaml_string.to_s.split("\n") 18 | yaml_array.each_with_index do |line, index| 19 | next if line =~ /\A\s*#/ 20 | next if line.strip == "" 21 | if line.start_with?("---") 22 | yaml_array[index] = "---" 23 | end 24 | break 25 | end 26 | 27 | # Parse the YAML file 28 | result = YAML.safe_load(yaml_array.join("\n")) 29 | 30 | # Pull out "values" if this is in a name-values format. Otherwise just return the hash. 31 | return result["values"] if result["values"].is_a?(Hash) 32 | result 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/octofacts_updater/service/enc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This class contains methods to interact with an external node classifier. 3 | 4 | require "open3" 5 | require "shellwords" 6 | require "yaml" 7 | 8 | module OctofactsUpdater 9 | module Service 10 | class ENC 11 | # Execute the external node classifier script. This expects the value of "path" to be 12 | # set in the configuration. 13 | # 14 | # hostname - A String with the FQDN of the host. 15 | # config - A Hash with configuration data. 16 | # 17 | # Returns a Hash consisting of the parsed output of the ENC. 18 | def self.run_enc(hostname, config) 19 | unless config["enc"].is_a?(Hash) 20 | raise ArgumentError, "The ENC configuration must be defined" 21 | end 22 | 23 | unless config["enc"]["path"].is_a?(String) 24 | raise ArgumentError, "The ENC path must be defined" 25 | end 26 | 27 | unless File.file?(config["enc"]["path"]) 28 | raise Errno::ENOENT, "The ENC script could not be found at #{config['enc']['path'].inspect}" 29 | end 30 | 31 | command = [config["enc"]["path"], hostname].map { |x| Shellwords.escape(x) }.join(" ") 32 | stdout, stderr, exitstatus = Open3.capture3(command) 33 | unless exitstatus.exitstatus == 0 34 | output = { "stdout" => stdout, "stderr" => stderr, "exitstatus" => exitstatus.exitstatus } 35 | raise "Error executing #{command.inspect}: #{output.to_yaml}" 36 | end 37 | 38 | YAML.load(stdout) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/octofacts_updater/service/local_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This class reads a YAML file from the local file system so that it can be used as a source 3 | # in octofacts-updater. This was originally intended for a quickstart tutorial, since it requires 4 | # no real configuration. However it could also be used in production, if the user wants to create 5 | # their own fact obtaining logic outside of octofacts-updater and simply feed in the results. 6 | 7 | require_relative "base" 8 | 9 | module OctofactsUpdater 10 | module Service 11 | class LocalFile < OctofactsUpdater::Service::Base 12 | # Get the facts from a local file, without using PuppetDB, SSH, or any of the other automated methods. 13 | # 14 | # node - A String with the FQDN for which to retrieve facts 15 | # config - A Hash with configuration settings 16 | # 17 | # Returns a Hash with the facts. 18 | def self.facts(node, config = {}) 19 | unless config["localfile"].is_a?(Hash) 20 | raise ArgumentError, "OctofactsUpdater::Service::LocalFile requires localfile section" 21 | end 22 | config_localfile = config["localfile"].dup 23 | 24 | path_raw = config_localfile.delete("path") 25 | unless path_raw 26 | raise ArgumentError, "OctofactsUpdater::Service::LocalFile requires 'path' in the localfile section" 27 | end 28 | path = path_raw.gsub("%%NODE%%", node) 29 | unless File.file?(path) 30 | raise Errno::ENOENT, "OctofactsUpdater::Service::LocalFile cannot find a file at #{path.inspect}" 31 | end 32 | 33 | parse_yaml(File.read(path)) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/octofacts_updater/service/puppetdb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This class interacts with puppetdb to pull the facts from the recent 3 | # run of Puppet on a given node. This uses octocatalog-diff on the back end to 4 | # pull the facts from puppetdb. 5 | 6 | require "octocatalog-diff" 7 | 8 | module OctofactsUpdater 9 | module Service 10 | class PuppetDB 11 | # Get the facts for a specific node. 12 | # 13 | # node - A String with the FQDN for which to retrieve facts 14 | # config - An optional Hash with configuration settings 15 | # 16 | # Returns a Hash with the facts (via octocatalog-diff) 17 | def self.facts(node, config = {}) 18 | fact_obj = OctocatalogDiff::Facts.new( 19 | node: node.strip, 20 | backend: :puppetdb, 21 | puppetdb_url: puppetdb_url(config) 22 | ) 23 | facts = fact_obj.facts(node) 24 | return facts unless facts.nil? 25 | raise OctocatalogDiff::Errors::FactSourceError, "Fact retrieval failed for #{node}" 26 | end 27 | 28 | # Get the puppetdb URL from the configuration or environment. 29 | # 30 | # config - An optional Hash with configuration settings 31 | # 32 | # Returns a String with the PuppetDB URL 33 | def self.puppetdb_url(config = {}) 34 | answer = [ 35 | config.fetch("puppetdb", {}).fetch("url", nil), 36 | ENV["PUPPETDB_URL"] 37 | ].compact 38 | raise "PuppetDB URL not configured or set in environment" unless answer.any? 39 | answer.first 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/octofacts_updater/service/ssh.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This class interacts with a puppetserver to obtain facts from that server's YAML cache. 3 | # This is achieved by SSH-ing to the server and obtaining the fact file directly from the 4 | # puppetserver's cache. This can also be used to SSH to an actual node and run a command, 5 | # e.g. `facter -p --yaml` to grab actual facts from a running production node. 6 | 7 | require "net/ssh" 8 | require "shellwords" 9 | 10 | require_relative "base" 11 | 12 | module OctofactsUpdater 13 | module Service 14 | class SSH < OctofactsUpdater::Service::Base 15 | CACHE_DIR = "/opt/puppetlabs/server/data/puppetserver/yaml/facts" 16 | COMMAND = "cat %%NODE%%.yaml" 17 | 18 | # Get the facts for a specific node. 19 | # 20 | # node - A String with the FQDN for which to retrieve facts 21 | # config - A Hash with configuration settings 22 | # 23 | # Returns a Hash with the facts. 24 | def self.facts(node, config = {}) 25 | unless config["ssh"].is_a?(Hash) 26 | raise ArgumentError, "OctofactsUpdater::Service::SSH requires ssh section" 27 | end 28 | config_ssh = config["ssh"].dup 29 | 30 | server_raw = config_ssh.delete("server") 31 | unless server_raw 32 | raise ArgumentError, "OctofactsUpdater::Service::SSH requires 'server' in the ssh section" 33 | end 34 | server = server_raw.gsub("%%NODE%%", node) 35 | 36 | user = config_ssh.delete("user") || ENV["USER"] 37 | unless user 38 | raise ArgumentError, "OctofactsUpdater::Service::SSH requires 'user' in the ssh section" 39 | end 40 | 41 | # Default is to 'cd (puppetserver cache dir) && cat (node).yaml' but this can 42 | # be overridden by specifying a command in the SSH options. "%%NODE%%" will always 43 | # be replaced by the FQDN of the node in the overall result. 44 | cache_dir = config_ssh.delete("cache_dir") || CACHE_DIR 45 | command_raw = config_ssh.delete("command") || "cd #{Shellwords.escape(cache_dir)} && #{COMMAND}" 46 | command = command_raw.gsub("%%NODE%%", node) 47 | 48 | # Everything left over in config["ssh"] (once server, user, command, and cache_dir are removed) is 49 | # symbolized and passed directory to Net::SSH. 50 | net_ssh_opts = config_ssh.map { |k, v| [k.to_sym, v] }.to_h || {} 51 | ret = Net::SSH.start(server, user, net_ssh_opts) do |ssh| 52 | ssh.exec! command 53 | end 54 | return { "name" => node, "values" => parse_yaml(ret.to_s.strip) } if ret.exitstatus == 0 55 | raise "ssh failed with exitcode=#{ret.exitstatus}: #{ret.to_s.strip}" 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/octofacts_updater/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module OctofactsUpdater 3 | VERSION = File.read(File.expand_path("../../.version", File.dirname(__FILE__))).strip 4 | end 5 | -------------------------------------------------------------------------------- /octofacts-updater.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # coding: utf-8 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "octofacts-updater" 6 | spec.version = File.read(File.expand_path("./.version", File.dirname(__FILE__))).strip 7 | spec.authors = ["GitHub, Inc.", "Kevin Paulisse", "Antonio Santos"] 8 | spec.email = "opensource+octofacts@github.com" 9 | 10 | spec.summary = "Scripts to update octofacts fixtures from recent Puppet runs" 11 | spec.description = <<-EOS 12 | Octofacts-updater is a series of scripts to construct the fact fixture files and index files consumed by octofacts. 13 | EOS 14 | spec.homepage = "https://github.com/github/octofacts" 15 | spec.license = "MIT" 16 | spec.executables = "octofacts-updater" 17 | spec.files = [ 18 | "bin/octofacts-updater", 19 | Dir.glob("lib/octofacts_updater/**/*.rb"), 20 | "lib/octofacts_updater.rb", 21 | ".version" 22 | ].flatten 23 | spec.require_paths = ["lib"] 24 | 25 | spec.required_ruby_version = ">= 2.7.0" 26 | spec.add_dependency "diffy", ">= 3.1.0" 27 | spec.add_dependency "octocatalog-diff", ">= 2.1.0" 28 | spec.add_dependency "octokit", ">= 4.2.0" 29 | spec.add_dependency "net-ssh", ">= 2.9" 30 | end 31 | -------------------------------------------------------------------------------- /octofacts.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # coding: utf-8 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "octofacts" 6 | spec.version = File.read(File.expand_path("./.version", File.dirname(__FILE__))).strip 7 | spec.authors = ["GitHub, Inc.", "Kevin Paulisse", "Antonio Santos"] 8 | spec.email = "opensource+octofacts@github.com" 9 | 10 | spec.summary = "Run your rspec-puppet tests against fake hosts that present almost real facts" 11 | spec.description = <<-EOS 12 | Octofacts provides fact fixtures built from recently-updated Puppet facts to rspec-puppet tests. 13 | EOS 14 | spec.homepage = "https://github.com/github/octofacts" 15 | spec.license = "MIT" 16 | 17 | spec.files = [Dir.glob("lib/octofacts/**/*.rb"), "lib/octofacts.rb", ".version"].flatten 18 | spec.require_paths = ["lib"] 19 | 20 | spec.required_ruby_version = ">= 2.7.0" 21 | end 22 | -------------------------------------------------------------------------------- /rake/gem.rb: -------------------------------------------------------------------------------- 1 | ## Releasing a new version of octofacts 2 | ## 3 | ## 1. Update `.version` with new version number 4 | ## 2. Run `script/bootstrap` to update Gemfile.lock 5 | ## 3. Commit changes, PR, and merge to main 6 | ## 4. Check out main branch locally 7 | ## 5. Run `bundle exec rake gem:release` 8 | # frozen_string_literal: true 9 | require "fileutils" 10 | require "open3" 11 | require "shellwords" 12 | 13 | module Octofacts 14 | # A class to contain methods and constants for cleaner code 15 | class Gem 16 | BASEDIR = File.expand_path("..", File.dirname(__FILE__)).freeze 17 | GEMS = ["octofacts", "octofacts-updater"].freeze 18 | PKGDIR = File.join(BASEDIR, "pkg").freeze 19 | 20 | # Verify that Gemfile.lock matches .version and that it's committed, since `bundle exec ...` will 21 | # update the file for us. 22 | def self.verify_gemfile_version! 23 | bundler = Bundler::LockfileParser.new(Bundler.read_file(File.expand_path("../Gemfile.lock", File.dirname(__FILE__)))) 24 | gems = bundler.specs.select { |specs| GEMS.include?(specs.name) } 25 | GEMS.each do |gem| 26 | this_gem = gems.detect { |g| g.name == gem } 27 | unless this_gem 28 | raise "Did not find #{gem} in Gemfile.lock" 29 | end 30 | unless this_gem.version.to_s == version 31 | raise "Gem #{gem} is version #{this_gem.version}, not #{version}" 32 | end 33 | end 34 | 35 | puts "Ensuring that all changes are committed." 36 | exec_command("git diff-index --quiet HEAD --") 37 | puts "OK: All gems on #{version} and no uncommitted changes here." 38 | end 39 | 40 | # Read the version number from the .version file in the root of the project. 41 | def self.version 42 | @version ||= File.read(File.expand_path("../.version", File.dirname(__FILE__))).strip 43 | end 44 | 45 | # Determine what branch we are on 46 | def self.branch 47 | exec_command("git rev-parse --abbrev-ref HEAD").strip 48 | end 49 | 50 | # Build the gem and put it into the 'pkg' directory 51 | def self.build 52 | Dir.mkdir PKGDIR unless File.directory?(PKGDIR) 53 | GEMS.each do |gem| 54 | begin 55 | output_file = File.join(BASEDIR, "#{gem}-#{version}.gem") 56 | target_file = File.join(PKGDIR, "#{gem}-#{version}.gem") 57 | exec_command("gem build #{gem}.gemspec") 58 | unless File.file?(output_file) 59 | raise "gem #{gem} failed to create expected output file" 60 | end 61 | FileUtils.mv output_file, target_file 62 | puts "Generated #{target_file}" 63 | ensure 64 | # Clean up the *.gem generated in the main directory if it's still there 65 | FileUtils.rm(output_file) if File.file?(output_file) 66 | end 67 | end 68 | end 69 | 70 | # Push the gem to rubygems 71 | def self.push 72 | GEMS.each do |gem| 73 | target_file = File.join(PKGDIR, "#{gem}-#{version}.gem") 74 | unless File.file?(target_file) 75 | raise "Cannot push: #{target_file} does not exist" 76 | end 77 | end 78 | GEMS.each do |gem| 79 | target_file = File.join(PKGDIR, "#{gem}-#{version}.gem") 80 | exec_command("gem push #{Shellwords.escape(target_file)}") 81 | end 82 | end 83 | 84 | # Tag the release on GitHub 85 | def self.tag 86 | # Make sure we have not released this version before 87 | exec_command("git fetch -t origin") 88 | tags = exec_command("git tag -l").split("\n") 89 | raise "There is already a #{version} tag" if tags.include?(version) 90 | 91 | # Tag it 92 | exec_command("git tag #{Shellwords.escape(version)}") 93 | exec_command("git push origin main") 94 | exec_command("git push origin #{Shellwords.escape(version)}") 95 | end 96 | 97 | # Yank gem from rubygems 98 | def self.yank 99 | GEMS.each do |gem| 100 | exec_command("gem yank #{gem} -v #{Shellwords.escape(version)}") 101 | end 102 | end 103 | 104 | # Utility method: Execute command 105 | def self.exec_command(command) 106 | STDERR.puts "Command: #{command}" 107 | output, code = Open3.capture2e(command, chdir: BASEDIR) 108 | return output if code.exitstatus.zero? 109 | STDERR.puts "Output:\n#{output}" 110 | STDERR.puts "Exit code: #{code.exitstatus}" 111 | exit code.exitstatus 112 | end 113 | end 114 | end 115 | 116 | namespace :gem do 117 | task "build" do 118 | branch = Octofacts::Gem.branch 119 | unless branch == "main" 120 | raise "On a non-main branch #{branch}; use gem:force-build if you really want to do this" 121 | end 122 | Octofacts::Gem.build 123 | end 124 | 125 | task "check" do 126 | Octofacts::Gem.verify_gemfile_version! 127 | end 128 | 129 | task "force-build" do 130 | branch = Octofacts::Gem.branch 131 | unless branch == "main" 132 | warn "WARNING: Force-building from non-main branch #{branch}" 133 | end 134 | Octofacts::Gem.build 135 | end 136 | 137 | task "push" do 138 | Octofacts::Gem.push 139 | end 140 | 141 | task "release" do 142 | branch = Octofacts::Gem.branch 143 | unless branch == "main" 144 | raise "On a non-main branch #{branch}; refusing to release" 145 | end 146 | [:check, :build, :tag, :push].each { |t| Rake::Task["gem:#{t}"].invoke } 147 | end 148 | 149 | task "tag" do 150 | branch = Octofacts::Gem.branch 151 | raise "On a non-main branch #{branch}; refusing to tag" unless branch == "main" 152 | Octofacts::Gem.tag 153 | end 154 | 155 | task "yank" do 156 | Octofacts::Gem.yank 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | [ -z "$DEBUG" ] || set -x 5 | 6 | echo 'Starting script/bootstrap' 7 | 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" 9 | 10 | rm -rf "${DIR}/.bundle" 11 | 12 | echo 'Running bundler' 13 | bundle config set --local no_prune 'true' 14 | bundle config set --local path 'vendor/bundle' 15 | bundle install --local 16 | bundle clean 17 | bundle binstubs --force puppet pry rake rspec-core rubocop 18 | chmod 0755 bin/octofacts-updater 19 | 20 | echo 'Completed script/bootstrap successfully' 21 | exit 0 22 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rspec_puppet_versions="3.0.0" 4 | puppet_versions="7.30.0" 5 | 6 | set -e 7 | 8 | [ -z "$DEBUG" ] || set -x 9 | 10 | DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd)" 11 | cd "$DIR" 12 | export RBENV_VERSION="$(cat ${DIR}/.ruby-version)" 13 | 14 | TEMPDIR=$(mktemp -d -t cibuild-XXXXXX) 15 | function cleanup() { 16 | rm -rf "${TEMPDIR}" 17 | } 18 | trap cleanup EXIT 19 | 20 | test -d "/usr/share/rbenv/shims" && { 21 | export PATH="/usr/share/rbenv/shims:$PATH" 22 | } 23 | 24 | echo "==> Bootstrapping..." 25 | "${DIR}/script/bootstrap" 26 | 27 | PATH="${DIR}/bin:$PATH" 28 | 29 | echo "==> Running rubocop..." 30 | RUBOCOP_YML="${DIR}/.rubocop.yml" 31 | bundle exec rubocop --config "$RUBOCOP_YML" --no-color -D "lib" "spec/octofacts" "spec/octofacts_updater" "spec/*.rb" \ 32 | && EXIT_RUBOCOP=$? || EXIT_RUBOCOP=$? 33 | 34 | echo "==> Running spec tests for octofacts..." 35 | bundle exec rake octofacts:spec:octofacts && EXIT_OCTOFACTS_RSPEC=$? || EXIT_OCTOFACTS_RSPEC=$? 36 | COVERAGE_OCTOFACTS=$(grep "covered_percent" "$DIR/lib/octofacts/coverage/.last_run.json" | awk '{ print $2 }') 37 | 38 | echo "==> Running spec tests for octofacts_updater..." 39 | bundle exec rake octofacts:spec:octofacts_updater && EXIT_UPDATER_RSPEC=$? || EXIT_UPDATER_RSPEC=$? 40 | COVERAGE_UPDATER=$(grep "covered_percent" "$DIR/lib/octofacts_updater/coverage/.last_run.json" | awk '{ print $2 }') 41 | 42 | # Integration tests 43 | EXIT_INTEGRATION=0 44 | for puppet_version in $puppet_versions; do 45 | for rspec_puppet_version in $rspec_puppet_versions; do 46 | export RSPEC_PUPPET_VERSION=$rspec_puppet_version 47 | export PUPPET_VERSION=$puppet_version 48 | echo "==> Running integration tests (puppet ${PUPPET_VERSION}, rspec-puppet ${RSPEC_PUPPET_VERSION})" 49 | if "${DIR}/script/bootstrap" > "$TEMPDIR/bootstrap.log" 2>&1; then 50 | rm -f "$TEMPDIR/bootstrap.log" 51 | else 52 | cat "$TEMPDIR/bootstrap.log" 53 | exit 1 54 | fi 55 | bundle exec rake octofacts:spec:octofacts_integration && local_integration_rspec=$? || local_integration_rspec=$? 56 | if [ "$local_integration_rspec" -ne 0 ]; then EXIT_INTEGRATION=$local_integration_rspec; fi 57 | done 58 | done 59 | 60 | echo "" 61 | echo "==> Summary Results" 62 | echo "Rubocop: Exit ${EXIT_RUBOCOP}" 63 | echo "octofacts rspec: Exit ${EXIT_OCTOFACTS_RSPEC}, Coverage ${COVERAGE_OCTOFACTS}" 64 | echo "octofacts-updater rspec: Exit ${EXIT_UPDATER_RSPEC}, Coverage ${COVERAGE_UPDATER}" 65 | echo "Integration: Exit ${EXIT_INTEGRATION}" 66 | echo "" 67 | 68 | if [ "$EXIT_RUBOCOP" == 0 ] && [ "$EXIT_OCTOFACTS_RSPEC" == 0 ] && [ "$EXIT_UPDATER_RSPEC" == 0 ] && [ "$EXIT_INTEGRATION" == 0 ]; then 69 | if [ "$COVERAGE_OCTOFACTS" == "100.0" ] && [ "$COVERAGE_UPDATER" == "100.0" ]; then 70 | exit 0 71 | else 72 | echo "All tests passed, but test coverage is not 100%" 73 | exit 1 74 | fi 75 | fi 76 | 77 | exit 1 78 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "octofacts" 6 | 7 | require "pry" 8 | Pry.start 9 | -------------------------------------------------------------------------------- /script/git-pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is being invoked via /.git/hooks, hence the 4 | # base directory is up two levels, not just one. 5 | 6 | set -e 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd ../.. && pwd )" 8 | cd "$DIR" 9 | 10 | echo "==> Analyzing code with rubocop" 11 | 12 | # Make sure we can use git correctly 13 | if git rev-parse --verify HEAD >/dev/null 2>&1; then 14 | : 15 | else 16 | echo "Unable to determine revision of this git repo" 17 | exit 1 18 | fi 19 | 20 | # Check whitespace problems 21 | if git diff-index --check --cached HEAD --; then 22 | : 23 | else 24 | echo "Please address these whitespace issues and then try committing again" 25 | exit 1 26 | fi 27 | 28 | # Run rubocop on any ruby files that have been changed or added 29 | 30 | files=$(git diff-index --diff-filter=AM --name-only --cached HEAD | tr "\n" " ") 31 | if [ -n "$files" ]; then 32 | cmd="bundle exec rubocop --config '${DIR}/.rubocop.yml' -D $files" 33 | bundle exec rubocop --config "${DIR}/.rubocop.yml" -D $files 34 | if [ $? -ne 0 ]; then 35 | cat >&2 < 80 | iam: {} 81 | local-ipv4: 172.16.1.2 82 | local-hostname: ip-172-16-1-2.example.net 83 | ec2_metrics_vhostmd: 84 | ec2_network_interfaces_macs_0a:11:22:33:44:55_device_number: '0' 85 | ec2_network_interfaces_macs_0a:11:22:33:44:55_interface_id: eni-000decaf 86 | ec2_network_interfaces_macs_0a:11:22:33:44:55_local_hostname: ip-172-16-1-2.example.net 87 | ec2_network_interfaces_macs_0a:11:22:33:44:55_local_ipv4s: 172.16.1.2 88 | ec2_network_interfaces_macs_0a:11:22:33:44:55_mac: 0a:11:22:33:44:55 89 | ec2_network_interfaces_macs_0a:11:22:33:44:55_owner_id: '987654321012' 90 | ec2_network_interfaces_macs_0a:11:22:33:44:55_security_group_ids: sg-000decaf 91 | ec2_network_interfaces_macs_0a:11:22:33:44:55_security_groups: default 92 | ec2_network_interfaces_macs_0a:11:22:33:44:55_subnet_id: subnet-000decaf 93 | ec2_network_interfaces_macs_0a:11:22:33:44:55_subnet_ipv4_cidr_block: 172.16.0.0/20 94 | ec2_network_interfaces_macs_0a:11:22:33:44:55_vpc_id: vpc-000decaf 95 | ec2_network_interfaces_macs_0a:11:22:33:44:55_vpc_ipv4_cidr_block: 172.16.0.0/16 96 | ec2_network_interfaces_macs_0a:11:22:33:44:55_vpc_ipv4_cidr_blocks: 172.16.0.0/16 97 | ec2_placement_availability_zone: us-east-1a 98 | ec2_profile: default-hvm 99 | ec2_region: us-east-1 100 | ec2_reservation_id: r-0123456789abcdef0 101 | ec2_security_groups: default 102 | ec2_services_domain: amazonaws.com 103 | ec2_services_partition: aws 104 | enable_pager: 'false' 105 | eth0-txrx-0_irq: '86' 106 | eth0-txrx-1_irq: '87' 107 | eth0_irq: '88' 108 | facterversion: 2.4.6 109 | filesystems: btrfs,ext2,ext3,ext4,hfs,hfsplus,jfs,minix,msdos,ntfs,qnx4,ufs,vfat,xfs 110 | fqdn: somenode.example.net 111 | gateway: 172.16.0.1 112 | gid: root 113 | hardwareisa: unknown 114 | hardwaremodel: x86_64 115 | hostname: somenode 116 | id: root 117 | interfaces: bond0,eth0,lo 118 | ip6tables_version: 1.4.21 119 | ipaddress: 172.16.1.2 120 | ipaddress_eth0: 172.16.1.2 121 | ipaddress_lo: 127.0.0.1 122 | is_virtual: true 123 | kernel: Linux 124 | kernelmajversion: '3.16' 125 | kernelrelease: 3.16.0-4-amd64 126 | kernelversion: 3.16.0 127 | lsbdistcodename: jessie 128 | lsbdistdescription: Debian GNU/Linux 8.8 (jessie) 129 | lsbdistid: Debian 130 | lsbdistrelease: '8.8' 131 | lsbmajdistrelease: '8' 132 | lsbminordistrelease: '8' 133 | lsbrelease: core-2.0-amd64:core-2.0-noarch:core-3.0-amd64:core-3.0-noarch:core-3.1-amd64:core-3.1-noarch:core-3.2-amd64:core-3.2-noarch:core-4.0-amd64:core-4.0-noarch:core-4.1-amd64:core-4.1-noarch:security-4.0-amd64:security-4.0-noarch:security-4.1-amd64:security-4.1-noarch 134 | macaddress: 0a:11:22:33:44:55 135 | macaddress_bond0: 0a:11:22:33:44:55 136 | macaddress_eth0: 0a:11:22:33:44:55 137 | manufacturer: Xen 138 | memorysize: 15.71 GB 139 | memorysize_gb: 15.71 140 | memorysize_mb: '16082.46' 141 | mounts: "/,/run,/sys/kernel/security,/run/lock,/sys/fs/pstore,/dev/mqueue,/sys/kernel/debug,/dev/hugepages,/boot" 142 | mtu_bond0: 1500 143 | mtu_eth0: 9001 144 | mtu_lo: 65536 145 | netmask: 255.255.240.0 146 | netmask_eth0: 255.255.240.0 147 | netmask_lo: 255.0.0.0 148 | network_eth0: 172.16.0.0 149 | network_lo: 127.0.0.0 150 | operatingsystem: Debian 151 | operatingsystemmajrelease: '8' 152 | operatingsystemrelease: '8.8' 153 | os: 154 | family: Debian 155 | lsb: 156 | distcodename: jessie 157 | distdescription: Debian GNU/Linux 8.8 (jessie) 158 | distid: Debian 159 | distrelease: '8.8' 160 | majdistrelease: '8' 161 | minordistrelease: '8' 162 | release: core-2.0-amd64:core-2.0-noarch:core-3.0-amd64:core-3.0-noarch:core-3.1-amd64:core-3.1-noarch:core-3.2-amd64:core-3.2-noarch:core-4.0-amd64:core-4.0-noarch:core-4.1-amd64:core-4.1-noarch:security-4.0-amd64:security-4.0-noarch:security-4.1-amd64:security-4.1-noarch 163 | name: Debian 164 | release: 165 | full: '8.8' 166 | major: '8' 167 | minor: '8' 168 | osfamily: Debian 169 | partitions: 170 | xvda1: 171 | label: biosboot 172 | size: '14336' 173 | xvda2: 174 | filesystem: ext2 175 | label: primary 176 | mount: "/boot" 177 | size: '360448' 178 | uuid: 09809809-0980-0980-0980-098098098098 179 | xvda3: 180 | filesystem: LVM2_member 181 | label: Linux LVM 182 | size: '41551839' 183 | physicalprocessorcount: 1 184 | processor0: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz 185 | processor1: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz 186 | processor2: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz 187 | processor3: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz 188 | processorcount: 4 189 | processors: 190 | count: 4 191 | models: 192 | - Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz 193 | - Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz 194 | - Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz 195 | - Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz 196 | physicalcount: 1 197 | productname: HVM domU 198 | ps: ps -ef 199 | puppet_environmentpath: '' 200 | region: us-east-1 201 | root_home: "/root" 202 | shorthost: somenode 203 | swapsize: 0.00 MB 204 | swapsize_mb: '0.00' 205 | timezone: UTC 206 | type: Other 207 | virtual: xenhvm 208 | -------------------------------------------------------------------------------- /spec/fixtures/facts/ops-consul-12345.dc2.example.com.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fqdn: ops-consul-12345.dc2.example.com 3 | datacenter: dc2 4 | app: ops 5 | env: production 6 | role: consul 7 | lsbdistcodename: precise 8 | shorthost: ops-consul-12345 9 | -------------------------------------------------------------------------------- /spec/fixtures/facts/ops-consul-67890.dc1.example.com.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fqdn: ops-consul-67890.dc1.example.com 3 | datacenter: dc1 4 | app: ops 5 | env: production 6 | role: consul 7 | lsbdistcodename: jessie 8 | shorthost: ops-consul-67890 9 | -------------------------------------------------------------------------------- /spec/fixtures/facts/puppet-puppetserver-00decaf.dc1.example.com.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fqdn: puppet-puppetserver-00decaf.dc1.example.com 3 | datacenter: dc1 4 | app: puppet 5 | env: staging 6 | role: puppetserver 7 | lsbdistcodename: jessie 8 | shorthost: puppet-puppetserver-00decaf 9 | -------------------------------------------------------------------------------- /spec/fixtures/facts/puppet-puppetserver-12345.dc1.example.com.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fqdn: puppet-puppetserver-12345.dc1.example.com 3 | datacenter: dc1 4 | app: puppet 5 | env: production 6 | role: puppetserver 7 | lsbdistcodename: precise 8 | shorthost: puppet-puppetserver-12345 9 | -------------------------------------------------------------------------------- /spec/fixtures/index-no-nodes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | _nodes: 3 | - broken.example.com 4 | app: 5 | ops: 6 | - ops-consul-67890.dc1.example.com 7 | - ops-consul-12345.dc2.example.com 8 | puppet: 9 | - puppet-puppetserver-12345.dc1.example.com 10 | - puppet-puppetserver-00decaf.dc1.example.com 11 | datacenter: 12 | dc1: 13 | - ops-consul-67890.dc1.example.com 14 | - puppet-puppetserver-12345.dc1.example.com 15 | - puppet-puppetserver-00decaf.dc1.example.com 16 | dc2: 17 | - ops-consul-12345.dc2.example.com 18 | env: 19 | production: 20 | - ops-consul-67890.dc1.example.com 21 | - ops-consul-12345.dc2.example.com 22 | - puppet-puppetserver-12345.dc1.example.com 23 | staging: 24 | - puppet-puppetserver-00decaf.dc1.example.com 25 | lsbdistcodename: 26 | jessie: 27 | - ops-consul-67890.dc1.example.com 28 | - puppet-puppetserver-00decaf.dc1.example.com 29 | precise: 30 | - ops-consul-12345.dc2.example.com 31 | - puppet-puppetserver-12345.dc1.example.com 32 | role: 33 | consul: 34 | - ops-consul-67890.dc1.example.com 35 | - ops-consul-12345.dc2.example.com 36 | puppetserver: 37 | - puppet-puppetserver-12345.dc1.example.com 38 | - puppet-puppetserver-00decaf.dc1.example.com 39 | -------------------------------------------------------------------------------- /spec/fixtures/index.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | _nodes: 3 | - ops-consul-67890.dc1.example.com 4 | - puppet-puppetserver-12345.dc1.example.com 5 | - puppet-puppetserver-00decaf.dc1.example.com 6 | - ops-consul-12345.dc2.example.com 7 | app: 8 | ops: 9 | - ops-consul-67890.dc1.example.com 10 | - ops-consul-12345.dc2.example.com 11 | puppet: 12 | - puppet-puppetserver-12345.dc1.example.com 13 | - puppet-puppetserver-00decaf.dc1.example.com 14 | datacenter: 15 | dc1: 16 | - ops-consul-67890.dc1.example.com 17 | - puppet-puppetserver-12345.dc1.example.com 18 | - puppet-puppetserver-00decaf.dc1.example.com 19 | dc2: 20 | - ops-consul-12345.dc2.example.com 21 | env: 22 | staging: 23 | - puppet-puppetserver-00decaf.dc1.example.com 24 | production: 25 | - ops-consul-67890.dc1.example.com 26 | - ops-consul-12345.dc2.example.com 27 | - puppet-puppetserver-12345.dc1.example.com 28 | lsbdistcodename: 29 | jessie: 30 | - ops-consul-67890.dc1.example.com 31 | - puppet-puppetserver-00decaf.dc1.example.com 32 | precise: 33 | - ops-consul-12345.dc2.example.com 34 | - puppet-puppetserver-12345.dc1.example.com 35 | role: 36 | consul: 37 | - ops-consul-67890.dc1.example.com 38 | - ops-consul-12345.dc2.example.com 39 | puppetserver: 40 | - puppet-puppetserver-12345.dc1.example.com 41 | - puppet-puppetserver-00decaf.dc1.example.com 42 | -------------------------------------------------------------------------------- /spec/fixtures/sorted-index.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | _nodes: 3 | - ops-consul-12345.dc2.example.com 4 | - ops-consul-67890.dc1.example.com 5 | - puppet-puppetserver-00decaf.dc1.example.com 6 | - puppet-puppetserver-12345.dc1.example.com 7 | app: 8 | ops: 9 | - ops-consul-12345.dc2.example.com 10 | - ops-consul-67890.dc1.example.com 11 | puppet: 12 | - puppet-puppetserver-00decaf.dc1.example.com 13 | - puppet-puppetserver-12345.dc1.example.com 14 | datacenter: 15 | dc1: 16 | - ops-consul-67890.dc1.example.com 17 | - puppet-puppetserver-00decaf.dc1.example.com 18 | - puppet-puppetserver-12345.dc1.example.com 19 | dc2: 20 | - ops-consul-12345.dc2.example.com 21 | env: 22 | production: 23 | - ops-consul-12345.dc2.example.com 24 | - ops-consul-67890.dc1.example.com 25 | - puppet-puppetserver-12345.dc1.example.com 26 | staging: 27 | - puppet-puppetserver-00decaf.dc1.example.com 28 | lsbdistcodename: 29 | jessie: 30 | - ops-consul-67890.dc1.example.com 31 | - puppet-puppetserver-00decaf.dc1.example.com 32 | precise: 33 | - ops-consul-12345.dc2.example.com 34 | - puppet-puppetserver-12345.dc1.example.com 35 | role: 36 | consul: 37 | - ops-consul-12345.dc2.example.com 38 | - ops-consul-67890.dc1.example.com 39 | puppetserver: 40 | - puppet-puppetserver-00decaf.dc1.example.com 41 | - puppet-puppetserver-12345.dc1.example.com 42 | -------------------------------------------------------------------------------- /spec/integration/hiera.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | -------------------------------------------------------------------------------- /spec/integration/manifests/defaults.pp: -------------------------------------------------------------------------------- 1 | # Nothing here. 2 | -------------------------------------------------------------------------------- /spec/integration/modules/test/manifests/init.pp: -------------------------------------------------------------------------------- 1 | class test {} 2 | -------------------------------------------------------------------------------- /spec/integration/modules/test/manifests/one.pp: -------------------------------------------------------------------------------- 1 | class test::one { 2 | file { "/tmp/system-info.txt": 3 | ensure => file, 4 | owner => $::id, 5 | group => $::gid, 6 | content => template("test/one/system-info.txt"), 7 | } 8 | 9 | file { "/etc/hosts": 10 | content => "127.0.0.1 localhost ${::shorthost}", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spec/integration/modules/test/spec/classes/test_one_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "../../../../spec/spec_helper" 3 | 4 | describe "test::one" do 5 | context "with facts hard-coded" do 6 | # This does not exercise octofacts. However, it helps to confirm that rspec-puppet is set 7 | # up correctly before we get to the tests below which do use octofacts. 8 | let(:facts) do 9 | { 10 | ec2: true, 11 | ec2_metadata: { placement: { "availability-zone": "us-foo-1a" } }, 12 | gid: "root", 13 | id: "root" 14 | } 15 | end 16 | 17 | it "should contain the file resource" do 18 | is_expected.to contain_file("/tmp/system-info.txt").with( 19 | owner: "root", 20 | group: "root", 21 | content: /availability-zone: us-foo-1a/ 22 | ) 23 | end 24 | end 25 | 26 | context "using straight octofacts from file explicitly converted to hash" do 27 | let(:facts) { Octofacts.from_file("basic.yaml").facts } 28 | 29 | it "should contain the file resource" do 30 | is_expected.to contain_file("/tmp/system-info.txt").with( 31 | owner: "root", 32 | group: "root", 33 | content: /availability-zone: us-east-1a/ 34 | ) 35 | end 36 | end 37 | 38 | context "using straight octofacts from file but not converted to hash" do 39 | let(:facts) { Octofacts.from_file("basic.yaml") } 40 | 41 | it "should contain the file resource" do 42 | is_expected.to contain_file("/tmp/system-info.txt").with( 43 | owner: "root", 44 | group: "root", 45 | content: /availability-zone: us-east-1a/ 46 | ) 47 | end 48 | end 49 | 50 | context "using straight octofacts from file with manipulation converted to hash" do 51 | let(:facts) { Octofacts.from_file("basic.yaml").replace("ec2_metadata::placement::availability-zone" => "us-hats-1a").facts } 52 | 53 | it "should contain the file resource" do 54 | is_expected.to contain_file("/tmp/system-info.txt").with( 55 | owner: "root", 56 | group: "root", 57 | content: /availability-zone: us-hats-1a/ 58 | ) 59 | end 60 | end 61 | 62 | context "using straight octofacts from file with manipulation but not converted to hash" do 63 | let(:facts) { Octofacts.from_file("basic.yaml").replace("ec2_metadata::placement::availability-zone" => "us-hats-1a") } 64 | 65 | it "should contain the file resource" do 66 | is_expected.to contain_file("/tmp/system-info.txt").with( 67 | owner: "root", 68 | group: "root", 69 | content: /availability-zone: us-hats-1a/ 70 | ) 71 | end 72 | end 73 | 74 | context "using straight octofacts from file with manipulation of symbol converted to hash" do 75 | let(:facts) { Octofacts.from_file("basic.yaml").replace("ec2_metadata::placement::availability-zone": "us-hats-1a").facts } 76 | 77 | it "should contain the file resource" do 78 | is_expected.to contain_file("/tmp/system-info.txt").with( 79 | owner: "root", 80 | group: "root", 81 | content: /availability-zone: us-hats-1a/ 82 | ) 83 | end 84 | end 85 | 86 | context "using straight octofacts from file with manipulation of symbol not converted to hash" do 87 | let(:facts) { Octofacts.from_file("basic.yaml").replace("ec2_metadata::placement::availability-zone" => "us-hats-1a") } 88 | 89 | it "should contain the file resource" do 90 | is_expected.to contain_file("/tmp/system-info.txt").with( 91 | owner: "root", 92 | group: "root", 93 | content: /availability-zone: us-hats-1a/ 94 | ) 95 | end 96 | end 97 | 98 | context "using pure ruby interface for manipulation" do 99 | context "without converting to hash" do 100 | let(:facts) { Octofacts.from_file("basic.yaml").merge("ec2" => false) } 101 | 102 | it "should contain the file resource" do 103 | is_expected.to contain_file("/tmp/system-info.txt").with( 104 | owner: "root", 105 | group: "root", 106 | content: /Not an EC2 instance/ 107 | ) 108 | end 109 | end 110 | 111 | context "with converting to hash" do 112 | let(:facts) { Octofacts.from_file("basic.yaml").facts.merge(ec2: false) } 113 | 114 | it "should contain the file resource" do 115 | is_expected.to contain_file("/tmp/system-info.txt").with( 116 | owner: "root", 117 | group: "root", 118 | content: /Not an EC2 instance/ 119 | ) 120 | end 121 | end 122 | end 123 | 124 | context "using chained manipulators" do 125 | let(:facts) { Octofacts.from_file("basic.yaml").replace(id: "hats", gid: "caps").replace(ec2: false) } 126 | 127 | it "should contain the file resource" do 128 | is_expected.to contain_file("/tmp/system-info.txt").with( 129 | owner: "hats", 130 | group: "caps", 131 | content: /Not an EC2 instance/ 132 | ) 133 | end 134 | end 135 | 136 | context "passing parameters to the index constructor" do 137 | let(:facts) { Octofacts.from_index(app: "puppet", env: "production") } 138 | 139 | it "should contain the file resource" do 140 | is_expected.to contain_file("/etc/hosts").with( 141 | content: /127.0.0.1 localhost puppet-puppetserver-12345/ 142 | ) 143 | end 144 | end 145 | 146 | context "using index + select" do 147 | let(:facts) { Octofacts.from_index.select(app: "puppet", env: "production") } 148 | 149 | it "should contain the file resource" do 150 | is_expected.to contain_file("/etc/hosts").with( 151 | content: /127.0.0.1 localhost puppet-puppetserver-12345/ 152 | ) 153 | end 154 | end 155 | 156 | context "using chained selectors" do 157 | let(:facts) { Octofacts.from_index.select(app: "puppet").reject(env: "production") } 158 | 159 | it "should contain the file resource" do 160 | is_expected.to contain_file("/etc/hosts").with( 161 | content: /127.0.0.1 localhost puppet-puppetserver-00decaf/ 162 | ) 163 | end 164 | end 165 | 166 | context "tests accessing facts as if it was a hash" do 167 | context "with []" do 168 | let(:facts) { Octofacts.from_file("basic.yaml") } 169 | 170 | it "should contain /etc/hosts with a symbol key" do 171 | is_expected.to contain_file("/etc/hosts").with( 172 | content: "127.0.0.1 localhost #{facts[:shorthost]}" 173 | ) 174 | end 175 | end 176 | 177 | context "with fetch" do 178 | let(:facts) { Octofacts.from_file("basic.yaml") } 179 | 180 | it "should contain /etc/hosts with a symbol key" do 181 | is_expected.to contain_file("/etc/hosts").with( 182 | content: "127.0.0.1 localhost #{facts.fetch(:shorthost)}" 183 | ) 184 | end 185 | end 186 | end 187 | 188 | context "tests accessing facts as if it was a string" do 189 | context "with []" do 190 | let(:facts) { Octofacts.from_file("basic.yaml") } 191 | 192 | it "should contain /etc/hosts with a symbol key" do 193 | is_expected.to contain_file("/etc/hosts").with( 194 | content: "127.0.0.1 localhost #{facts['shorthost']}" 195 | ) 196 | end 197 | end 198 | 199 | context "with fetch" do 200 | let(:facts) { Octofacts.from_file("basic.yaml") } 201 | 202 | it "should contain /etc/hosts with a symbol key" do 203 | is_expected.to contain_file("/etc/hosts").with( 204 | content: "127.0.0.1 localhost #{facts.fetch('shorthost')}" 205 | ) 206 | end 207 | end 208 | end 209 | end 210 | -------------------------------------------------------------------------------- /spec/integration/modules/test/templates/one/system-info.txt: -------------------------------------------------------------------------------- 1 | <%- if @ec2 -%> 2 | availability-zone: <%= @ec2_metadata["placement"]["availability-zone"] %> 3 | <%- else -%> 4 | Not an EC2 instance 5 | <%- end -%> 6 | -------------------------------------------------------------------------------- /spec/integration/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # spec_helper for rspec-puppet fixture 3 | 4 | require_relative "../../../lib/octofacts" 5 | require "rspec-puppet" 6 | 7 | def puppet_root 8 | File.expand_path("..", File.dirname(__FILE__)) 9 | end 10 | 11 | def repo_root 12 | File.expand_path("../../..", File.dirname(__FILE__)) 13 | end 14 | 15 | RSpec.configure do |c| 16 | c.module_path = File.join(puppet_root, "modules") 17 | c.hiera_config = File.join(puppet_root, "hiera.yaml") 18 | c.manifest_dir = File.join(puppet_root, "manifests") 19 | c.manifest = File.join(puppet_root, "manifests", "defaults.pp") 20 | c.add_setting :octofacts_fixture_path 21 | c.octofacts_fixture_path = File.join(repo_root, "spec", "fixtures", "facts") 22 | c.add_setting :octofacts_index_path 23 | c.octofacts_index_path = File.join(repo_root, "spec", "fixtures", "index.yaml") 24 | end 25 | -------------------------------------------------------------------------------- /spec/octofacts/backends/yaml_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe Octofacts::Backends::YamlFile do 5 | let(:fixture_file) { File.join(Octofacts::Spec.fixture_root, "facts", "basic.yaml") } 6 | let(:subject) { described_class.new(fixture_file) } 7 | 8 | describe "initialization" do 9 | it "can't be called with no arguments" do 10 | expect { described_class.new }.to raise_error(ArgumentError) 11 | end 12 | 13 | it "needs to be passed a file" do 14 | expect { described_class.new("/tmp/thisfiledoesnotexist.yml") }.to raise_error(Errno::ENOENT) 15 | end 16 | end 17 | 18 | describe "#facts" do 19 | it "will return a hash" do 20 | expect(subject.facts).to be_a(Hash) 21 | end 22 | 23 | it "will return the proper hash" do 24 | expect(subject.facts[:"ec2_network_interfaces_macs_0a:11:22:33:44:55_owner_id"]).to eq("987654321012") 25 | end 26 | end 27 | 28 | describe "#select" do 29 | it "will do nothing if the file matches the conditions with symbolized keys" do 30 | expect { subject.select(domain: "example.net") }.not_to raise_error 31 | end 32 | 33 | it "will do nothing if the file matches the conditions with string keys" do 34 | expect { subject.select("domain" => "example.net") }.not_to raise_error 35 | end 36 | 37 | it "will raise an error if the file can't match the conditions" do 38 | expect { subject.select("domain" => "wrongdomain.net") }.to raise_error(Octofacts::Errors::NoFactsError) 39 | end 40 | end 41 | 42 | describe "#reject" do 43 | it "will do nothing if the file can't match the conditions with symbolized keys" do 44 | expect { subject.reject(domain: "wrongdomain.net") }.not_to raise_error 45 | end 46 | 47 | it "will do nothing if the file can't match the conditions with string keys" do 48 | expect { subject.reject("domain" => "wrongdomain.net") }.not_to raise_error 49 | end 50 | 51 | it "will raise an error if the file matches the conditions" do 52 | expect { subject.reject("domain" => "example.net") }.to raise_error(Octofacts::Errors::NoFactsError) 53 | end 54 | end 55 | 56 | describe "#prefer" do 57 | it "is a noop in this backend" do 58 | expect { subject.prefer("domain" => "example.net") }.not_to raise_error 59 | expect { subject.prefer(domain: "example.net") }.not_to raise_error 60 | expect { subject.prefer("domain" => "wrongdomain.net") }.not_to raise_error 61 | expect { subject.prefer(domain: "wrongdomain.net") }.not_to raise_error 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/octofacts/constructors/from_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe Octofacts do 5 | describe "#from_file" do 6 | before(:each) { ENV.delete("OCTOFACTS_FIXTURE_PATH") } 7 | after(:all) { ENV.delete("OCTOFACTS_FIXTURE_PATH") } 8 | 9 | it "should load from a full filename" do 10 | ENV["OCTOFACTS_FIXTURE_PATH"] = File.join(Octofacts::Spec.fixture_root, "does", "not", "exist") 11 | filename = File.join(Octofacts::Spec.fixture_root, "facts", "basic.yaml") 12 | test_obj = Octofacts.from_file(filename) 13 | expect(test_obj.facts[:ec2_ami_id]).to eq("ami-000decaf") 14 | end 15 | 16 | it "should load from a relative filename plus environment variable path" do 17 | ENV["OCTOFACTS_FIXTURE_PATH"] = File.join(Octofacts::Spec.fixture_root, "facts") 18 | filename = "basic.yaml" 19 | test_obj = Octofacts.from_file(filename) 20 | expect(test_obj.facts[:ec2_ami_id]).to eq("ami-000decaf") 21 | end 22 | 23 | it "should load from a relative filename plus provided path" do 24 | ENV["OCTOFACTS_FIXTURE_PATH"] = File.join(Octofacts::Spec.fixture_root, "does", "not", "exist") 25 | path = File.join(Octofacts::Spec.fixture_root, "facts") 26 | filename = "basic.yaml" 27 | test_obj = Octofacts.from_file(filename, octofacts_fixture_path: path) 28 | expect(test_obj.facts[:ec2_ami_id]).to eq("ami-000decaf") 29 | end 30 | 31 | it "should fail with no provided or environment path" do 32 | filename = "basic.yaml" 33 | expect { Octofacts.from_file(filename) }.to raise_error(ArgumentError, /.from_file needs to know :octofacts_fixture_path/) 34 | end 35 | 36 | it "should fail if the fixture path does not exist" do 37 | ENV["OCTOFACTS_FIXTURE_PATH"] = File.join(Octofacts::Spec.fixture_root, "does", "not", "exist") 38 | filename = "basic.yaml" 39 | expect { Octofacts.from_file(filename) }.to raise_error(Errno::ENOENT, /The provided fixture path/) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/octofacts/examples_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Make sure the examples in the README document actually work. :-) 3 | 4 | require "spec_helper" 5 | 6 | describe "Examples from README.md" do 7 | let(:index_path) { File.join(Octofacts::Spec.fixture_root, "index.yaml") } 8 | let(:fixture_path) { File.join(Octofacts::Spec.fixture_root, "facts") } 9 | let(:subject) { described_class.new(index_path: index_path, fixture_path: fixture_path) } 10 | 11 | before(:each) do 12 | ENV["OCTOFACTS_INDEX_PATH"] = index_path 13 | ENV["OCTOFACTS_FIXTURE_PATH"] = fixture_path 14 | end 15 | 16 | after(:each) do 17 | ENV["OCTOFACTS_INDEX_PATH"] = nil 18 | ENV["OCTOFACTS_FIXTURE_PATH"] = fixture_path 19 | end 20 | 21 | it "should grab a match from the index" do 22 | result = Octofacts.from_index(app: "ops", role: "consul", datacenter: "dc1") 23 | expect(result).to be_a_kind_of(Octofacts::Facts) 24 | expect(result.facts).to eq( 25 | { 26 | fqdn: "ops-consul-67890.dc1.example.com", 27 | datacenter: "dc1", 28 | app: "ops", 29 | env: "production", 30 | role: "consul", 31 | lsbdistcodename: "jessie", 32 | shorthost: "ops-consul-67890" 33 | } 34 | ) 35 | end 36 | 37 | it "should grab a match from the index and replace" do 38 | result = Octofacts.from_index(app: "ops", role: "consul", datacenter: "dc1").replace(lsbdistcodename: "hats") 39 | expect(result).to be_a_kind_of(Octofacts::Facts) 40 | expect(result.facts).to eq( 41 | { 42 | fqdn: "ops-consul-67890.dc1.example.com", 43 | datacenter: "dc1", 44 | app: "ops", 45 | env: "production", 46 | role: "consul", 47 | lsbdistcodename: "hats", 48 | shorthost: "ops-consul-67890" 49 | } 50 | ) 51 | end 52 | 53 | it "should work with plain old ruby calling `facts`" do 54 | f = Octofacts.from_index(app: "ops", role: "consul", datacenter: "dc1").facts 55 | f[:lsbdistcodename] = "hats" 56 | f.delete(:env) 57 | expect(f).to eq( 58 | { 59 | fqdn: "ops-consul-67890.dc1.example.com", 60 | datacenter: "dc1", 61 | app: "ops", 62 | role: "consul", 63 | lsbdistcodename: "hats", 64 | shorthost: "ops-consul-67890" 65 | } 66 | ) 67 | end 68 | 69 | it "should work with plain old ruby without calling `facts`" do 70 | f = Octofacts.from_index(app: "ops", role: "consul", datacenter: "dc1") 71 | f[:lsbdistcodename] = "hats" 72 | f.delete(:env) 73 | expect(f).to be_a_kind_of(Octofacts::Facts) 74 | expect(f[:fqdn]).to eq("ops-consul-67890.dc1.example.com") 75 | expect(f[:datacenter]).to eq("dc1") 76 | expect(f[:app]).to eq("ops") 77 | expect(f[:role]).to eq("consul") 78 | expect(f[:lsbdistcodename]).to eq("hats") 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/octofacts/manipulators/replace_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | require "yaml" 4 | 5 | describe Octofacts::Manipulators do 6 | before(:each) do 7 | fixture = File.join(Octofacts::Spec.fixture_root, "facts", "basic.yaml") 8 | @obj = Octofacts.from_file(fixture) 9 | end 10 | 11 | it "should contain the expected facts" do 12 | expect(@obj.facts[:fqdn]).to eq("somenode.example.net") 13 | expect(@obj.facts[:hostname]).to eq("somenode") 14 | expect(@obj.facts[:processor0]).to eq("Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz") 15 | expect(@obj.facts[:os][:family]).to eq("Debian") 16 | end 17 | 18 | it "should do nothing when called with no arguments" do 19 | @obj.replace 20 | expect(@obj.facts[:fqdn]).to eq("somenode.example.net") 21 | expect(@obj.facts[:hostname]).to eq("somenode") 22 | expect(@obj.facts[:processor0]).to eq("Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz") 23 | expect(@obj.facts[:os][:family]).to eq("Debian") 24 | end 25 | 26 | it "should set a simple stringified fact at the top level, addressed by symbol" do 27 | @obj.replace(operatingsystem: "OctoAwesome OS") 28 | expect(@obj.facts[:operatingsystem]).to eq("OctoAwesome OS") 29 | end 30 | 31 | it "should set a simple stringified fact at the top level, addressed by string" do 32 | @obj.replace(operatingsystem: "OctoAwesome OS") 33 | expect(@obj.facts[:operatingsystem]).to eq("OctoAwesome OS") 34 | end 35 | 36 | it "should set two facts at the same time" do 37 | @obj.replace(operatingsystem: "OctoAwesome OS", hostname: "octoawesome") 38 | expect(@obj.facts[:hostname]).to eq("octoawesome") 39 | expect(@obj.facts[:operatingsystem]).to eq("OctoAwesome OS") 40 | end 41 | 42 | it "should instantiate a fact that did not exist before" do 43 | @obj.replace(hats: "OctoAwesome OS", hostname: "octoawesome") 44 | expect(@obj.facts[:hostname]).to eq("octoawesome") 45 | expect(@obj.facts[:hats]).to eq("OctoAwesome OS") 46 | end 47 | 48 | it "should set a nested fact" do 49 | @obj.replace("ec2_metadata::placement::availability-zone" => "the-moon-1a") 50 | expect(@obj.facts[:ec2_metadata][:placement][:"availability-zone"]).to eq("the-moon-1a") 51 | end 52 | 53 | it "should be possible to chain replace operators" do 54 | @obj.replace(operatingsystem: "OctoAwesome OS").replace(hostname: "octoawesome") 55 | expect(@obj.facts[:hostname]).to eq("octoawesome") 56 | expect(@obj.facts[:operatingsystem]).to eq("OctoAwesome OS") 57 | end 58 | 59 | it "should accept a single argument lambda" do 60 | @obj.replace(operatingsystem: lambda { |value| value.upcase }) 61 | expect(@obj.facts[:hostname]).to eq("somenode") 62 | expect(@obj.facts[:operatingsystem]).to eq("DEBIAN") 63 | end 64 | 65 | it "should accept a 3 argument lambda" do 66 | @obj.replace(operatingsystem: lambda { |fact_set, key, value| fact_set[:hostname] + value }) 67 | expect(@obj.facts[:operatingsystem]).to eq("somenodeDebian") 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/octofacts/manipulators_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe Octofacts::Manipulators do 5 | describe "#self.run" do 6 | let(:backend) { Octofacts::Backends::Hash.new(foo: "bar") } 7 | 8 | let(:facts_object) { Octofacts::Facts.new(backend: backend) } 9 | 10 | it "should return false if no manipulator exists" do 11 | result = described_class.run(facts_object, "no_such_manipulator") 12 | expect(result).to eq(false) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/octofacts/octofacts_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | require "spec_helper" 4 | 5 | # FIXME: Remove when real specs are added 6 | # This has only been checked in to test our CI job 7 | 8 | describe Octofacts::VERSION do 9 | it "is set" do 10 | expect(Octofacts::VERSION).to_not be_nil 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/octofacts/octofacts_spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Octofacts 3 | class Spec 4 | def self.fixture_root 5 | File.expand_path("../fixtures", File.dirname(__FILE__)) 6 | end 7 | end 8 | 9 | module Backends 10 | class Hash < Base 11 | attr_reader :facts, :select_called, :reject_called, :prefer_called 12 | 13 | def initialize(hash_in) 14 | @facts = hash_in 15 | end 16 | 17 | def select(_) 18 | @select_called = true 19 | self 20 | end 21 | 22 | def reject(_) 23 | @reject_called = true 24 | self 25 | end 26 | 27 | def prefer(_) 28 | @prefer_called = true 29 | self 30 | end 31 | end 32 | end 33 | 34 | class Manipulators 35 | class Fake < Octofacts::Manipulators 36 | def self.execute(facts, *args) 37 | # noop 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/octofacts/util/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe Octofacts::Util::Config do 5 | before(:all) do 6 | RSpec.configure do |c| 7 | c.add_setting :fake_setting 8 | c.fake_setting = "kittens" 9 | end 10 | ENV["FAKE_SETTING"] = "hats" 11 | ENV["FAKE_SETTING_2"] = "cats" 12 | end 13 | 14 | after(:all) do 15 | RSpec.configure do |c| 16 | c.fake_setting = nil 17 | end 18 | ENV.delete("FAKE_SETTING") 19 | ENV.delete("FAKE_SETTING_2") 20 | end 21 | 22 | describe "#fetch" do 23 | it "should return a value from the hash" do 24 | h = { fake_setting: "chickens" } 25 | expect(described_class.fetch(:fake_setting, h, "dogs")).to eq("chickens") 26 | end 27 | 28 | it "should return a value from the rspec configuration" do 29 | expect(described_class.fetch(:fake_setting, {}, "dogs")).to eq("kittens") 30 | end 31 | 32 | it "should return a value from the environment" do 33 | expect(described_class.fetch(:fake_setting_2, {}, "dogs")).to eq("cats") 34 | end 35 | 36 | it "should return the default value" do 37 | expect(described_class.fetch(:fake_setting_3, {}, "dogs")).to eq("dogs") 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/octofacts/util/keys_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | describe Octofacts::Util::Keys do 3 | describe "#downcase_keys!" do 4 | let(:facts) do 5 | { "foo" => "foo-value", "Bar" => "bar-value", :baz => "baz-value", :buZZ => "buzz-value" } 6 | end 7 | 8 | it "should work" do 9 | f = facts 10 | result = described_class.downcase_keys!(f) 11 | expect(f).to eq({"foo" => "foo-value", baz: "baz-value", "bar" => "bar-value", buzz: "buzz-value"}) 12 | expect(result).to eq({"foo" => "foo-value", baz: "baz-value", "bar" => "bar-value", buzz: "buzz-value"}) 13 | end 14 | end 15 | 16 | describe "#symbolize_keys!" do 17 | let(:facts) do 18 | { "foo" => "foo-value", "Bar" => "bar-value", :baz => "baz-value", :buZZ => "buzz-value" } 19 | end 20 | 21 | it "should work" do 22 | f = facts 23 | result = described_class.symbolize_keys!(f) 24 | expect(f).to eq({foo: "foo-value", baz: "baz-value", Bar: "bar-value", buZZ: "buzz-value"}) 25 | expect(result).to eq({foo: "foo-value", baz: "baz-value", Bar: "bar-value", buZZ: "buzz-value"}) 26 | end 27 | end 28 | 29 | describe "#desymbolize_keys!" do 30 | let(:facts) do 31 | { "foo" => "foo-value", "Bar" => "bar-value", :baz => "baz-value", :buZZ => "buzz-value" } 32 | end 33 | 34 | it "should work" do 35 | f = facts 36 | result = described_class.desymbolize_keys!(f) 37 | expect(f).to eq({"foo"=>"foo-value", "Bar"=>"bar-value", "baz"=>"baz-value", "buZZ"=>"buzz-value"}) 38 | expect(result).to eq({"foo"=>"foo-value", "Bar"=>"bar-value", "baz"=>"baz-value", "buZZ"=>"buzz-value"}) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/octofacts_updater/fact_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe OctofactsUpdater::Fact do 5 | describe "#value" do 6 | it "should return value if structured value is not requested" do 7 | subject = described_class.new("foo", "bar") 8 | expect(subject.value).to eq("bar") 9 | end 10 | 11 | it "should return nil if structured value is requested for non-structured fact" do 12 | subject = described_class.new("foo", "bar") 13 | expect(subject.value("foo")).to be_nil 14 | expect(subject.value("baz")).to be_nil 15 | end 16 | 17 | it "should return nil if not all keys exist in the path to a value" do 18 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 19 | expect(subject.value("hats::baz::fizz")).to be_nil 20 | expect(subject.value("bar::hats::fizz")).to be_nil 21 | end 22 | 23 | it "should return nil if the last key does not exist" do 24 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 25 | expect(subject.value("bar::baz::hats")).to be_nil 26 | expect(subject.value("bar::baz::fizz::buzz")).to be_nil 27 | end 28 | 29 | it "should return the value from the structured fact" do 30 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 31 | expect(subject.value("bar::baz::fizz")).to eq("buzz") 32 | expect(subject.value("bar::baz")).to eq("fizz" => "buzz") 33 | end 34 | end 35 | 36 | describe "#value=" do 37 | it "should set the overall fact value if structured value is not requested" do 38 | subject = described_class.new("foo", "bar") 39 | subject.value = "baz" 40 | expect(subject.value).to eq("baz") 41 | end 42 | end 43 | 44 | describe "#set_value" do 45 | it "should set the overall fact value if structured value is not requested" do 46 | subject = described_class.new("foo", "bar") 47 | subject.set_value("baz") 48 | expect(subject.value).to eq("baz") 49 | end 50 | 51 | it "should call a block if provided instead of a static value" do 52 | subject = described_class.new("foo", "bar") 53 | blk = Proc.new { |val| val.upcase } 54 | subject.set_value(blk) 55 | expect(subject.value).to eq("BAR") 56 | end 57 | 58 | it "should raise an error if structured value is specified for non-structured fact" do 59 | subject = described_class.new("foo", "bar") 60 | expect { subject.set_value("baz", "foo") }.to raise_error(ArgumentError, /Cannot set structured value at "foo"/) 61 | expect { subject.set_value("baz", "key") }.to raise_error(ArgumentError, /Cannot set structured value at "key"/) 62 | expect { subject.set_value("baz", "bar::baz") }.to raise_error(ArgumentError, /Cannot set structured value at "bar"/) 63 | end 64 | 65 | it "should raise an error if it encounters a non-structured value in the path" do 66 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 67 | expect { subject.set_value("baz", "bar::baz::fizz::buzz") }.to raise_error(ArgumentError, /Cannot set structured value at "buzz"/) 68 | end 69 | 70 | it "should create all missing hashes in structure to set value" do 71 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 72 | subject.set_value("baz", "bar::baz::hats::caps") 73 | expect(subject.value("bar::baz::hats::caps")).to eq("baz") 74 | expect(subject.value("bar::baz")).to eq({"fizz"=>"buzz", "hats"=>{"caps"=>"baz"}}) 75 | end 76 | 77 | it "should not create missing hashes if new value is nil" do 78 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 79 | subject.set_value(nil, "bar::baz::hats::caps") 80 | expect(subject.value("bar::baz::hats::caps")).to be_nil 81 | expect(subject.value("bar::baz")).to eq("fizz" => "buzz") 82 | end 83 | 84 | it "should delete the structured value if new value is nil (at end)" do 85 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 86 | subject.set_value(nil, "bar::baz::fizz") 87 | expect(subject.value("bar::baz::fizz")).to be_nil 88 | expect(subject.value("bar::baz")).to eq({}) 89 | end 90 | 91 | it "should delete the structured value if new value is nil (in middle)" do 92 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 93 | subject.set_value(nil, "bar::baz") 94 | expect(subject.value("bar::baz::fizz")).to be_nil 95 | expect(subject.value("bar")).to eq({}) 96 | end 97 | 98 | it "should set value to new value within the structure" do 99 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 100 | subject.set_value("kittens", "bar::baz::fizz") 101 | expect(subject.value("bar::baz::fizz")).to eq("kittens") 102 | end 103 | 104 | it "should accept an array of strings when describing the structure" do 105 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 106 | subject.set_value("kittens", %w(bar baz fizz)) 107 | expect(subject.value("bar::baz::fizz")).to eq("kittens") 108 | end 109 | 110 | it "should handle a structure at the top level of a structured fact" do 111 | subject = described_class.new("foo", "bar" => "baz") 112 | subject.set_value("kittens", "bar") 113 | expect(subject.value).to eq({ "bar" => "kittens" }) 114 | expect(subject.value("bar")).to eq("kittens") 115 | end 116 | 117 | it "should handle regular expressions" do 118 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 119 | subject.set_value("kittens", ["bar", { "regexp" => "^ba" }, { "regexp" => "zz" }]) 120 | expect(subject.value("bar::baz::fizz")).to eq("kittens") 121 | end 122 | 123 | it "should not auto-create keys based on regular expressions" do 124 | subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } }) 125 | subject.set_value("kittens", ["bar", { "regexp" => "^boo" }, { "regexp" => "zz" }]) 126 | expect(subject.value).to eq("bar" => { "baz" => { "fizz" => "buzz" } }) 127 | end 128 | 129 | it "should match multiple keys when using regular expressions" do 130 | subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" }) 131 | subject.set_value("kittens", [{ "regexp" => "^ba" }]) 132 | expect(subject.value).to eq({"bar"=>"kittens", "baz"=>"kittens", "fizz"=>"!fizz"}) 133 | end 134 | 135 | it "should call a Proc when matching multiple keys" do 136 | blk = Proc.new { |val| val.upcase } 137 | subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" }) 138 | subject.set_value(blk, [{ "regexp" => "^ba" }]) 139 | expect(subject.value).to eq({"bar"=>"!BAR", "baz"=>"!BAZ", "fizz"=>"!fizz"}) 140 | end 141 | 142 | it "should delete values from a Proc when matching multiple keys" do 143 | blk = Proc.new { |val| val == "!bar" ? val.upcase : nil } 144 | subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" }) 145 | subject.set_value(blk, [{ "regexp" => "^ba" }]) 146 | expect(subject.value).to eq({"bar"=>"!BAR", "fizz"=>"!fizz"}) 147 | end 148 | 149 | it "should raise an error if a part is not a string or regexp" do 150 | subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" }) 151 | expect { subject.set_value("kittens", [:foo]) }.to raise_error(ArgumentError, /Unable to interpret structure item: :foo/) 152 | end 153 | 154 | it "should raise an error if the structure cannot be interpreted" do 155 | subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" }) 156 | expect { subject.set_value("kittens", :foo) }.to raise_error(ArgumentError, /Unable to interpret structure: :foo/) 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /spec/octofacts_updater/octofacts_updater_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe OctofactsUpdater::VERSION do 5 | it "is set" do 6 | expect(OctofactsUpdater::VERSION).to_not be_nil 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/octofacts_updater/plugin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe OctofactsUpdater::Plugin do 5 | before(:each) do 6 | described_class.clear!(:test1) 7 | end 8 | 9 | after(:all) do 10 | described_class.clear!(:test1) 11 | end 12 | 13 | describe "#register" do 14 | let(:blk) { Proc.new { |_fact, _args| true } } 15 | 16 | it "should raise error upon attempting to register a plugin 2x" do 17 | expect do 18 | described_class.register(:test1, &blk) 19 | end.not_to raise_error 20 | 21 | expect do 22 | described_class.register(:test1, &blk) 23 | end.to raise_error(ArgumentError, /A plugin named test1 is already registered/) 24 | end 25 | 26 | it "should register a plugin such that it can be executed later" do 27 | described_class.register(:test1, &blk) 28 | expect(described_class.plugins.key?(:test1)).to eq(true) 29 | expect(described_class.plugins[:test1]).to eq(blk) 30 | end 31 | end 32 | 33 | describe "#execute" do 34 | it "should raise an error if the plugin method is not found" do 35 | dummy_fact = instance_double("OctofactsUpdater::Fact") 36 | expect { described_class.execute(:test1, dummy_fact, {}) }.to raise_error(NoMethodError, /A plugin named test1/) 37 | end 38 | 39 | it "should execute the plugin code if the plugin method is found" do 40 | fact = OctofactsUpdater::Fact.new("foo", "bar") 41 | blk = Proc.new { |fact, args| fact.value = args["value"] } 42 | described_class.register(:test1, &blk) 43 | described_class.execute(:test1, fact, { "plugin" => "test1", "value" => "value1" }) 44 | expect(fact.value).to eq("value1") 45 | described_class.execute(:test1, fact, { "plugin" => "test1", "value" => "value2" }) 46 | expect(fact.value).to eq("value2") 47 | end 48 | end 49 | 50 | describe "#randomize_long_string" do 51 | it "should return the expected result" do 52 | string_in = "abcdefghijklmnop" 53 | result = described_class.randomize_long_string(string_in) 54 | expect(result.length).to eq(string_in.length) 55 | expect(result).not_to eq(string_in) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/octofacts_updater/plugins/ip_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | require "ipaddr" 4 | 5 | describe "ipv4_anonymize plugin" do 6 | let(:plugin) { OctofactsUpdater::Plugin.plugins[:ipv4_anonymize] } 7 | let(:fact) { OctofactsUpdater::Fact.new("ipv4", "192.168.42.42") } 8 | let(:structured_fact) do 9 | OctofactsUpdater::Fact.new("networking", 10 | { 11 | "ip" => "192.168.42.42", 12 | "interfaces" => { 13 | "eth0" => { 14 | "ip" => "192.168.42.42" 15 | } 16 | } 17 | } 18 | ) 19 | end 20 | 21 | it "should be defined" do 22 | expect(plugin).to be_a_kind_of(Proc) 23 | end 24 | 25 | it "should raise an error if the subnet is not passed" do 26 | args = { "plugin" => "ipv4_anonymize" } 27 | expect(OctofactsUpdater::Plugin).to receive(:warn) 28 | .with("ArgumentError occurred executing ipv4_anonymize on ipv4 with value \"192.168.42.42\"") 29 | expect do 30 | OctofactsUpdater::Plugin.execute(:ipv4_anonymize, fact, args) 31 | end.to raise_error(ArgumentError, /ipv4_anonymize requires a subnet/) 32 | end 33 | 34 | it "should change the IP to a given subnet" do 35 | args = { "plugin" => "ipv4_anonymize", "subnet" => "192.168.1.0/24" } 36 | OctofactsUpdater::Plugin.execute(:ipv4_anonymize, fact, args, { "hostname" => "myhostname" }) 37 | expect(fact.value).to eq("192.168.1.60") 38 | end 39 | 40 | it "should properly update a structured fact at the top level" do 41 | args = { "plugin" => "ipv4_anonymize", "subnet" => "192.168.1.0/24", "structure" => "ip" } 42 | OctofactsUpdater::Plugin.execute(:ipv4_anonymize, structured_fact, args, { "hostname" => "myhostname" }) 43 | expect(structured_fact.value["ip"]).to eq("192.168.1.60") 44 | end 45 | 46 | it "should properly update a structured fact nested within" do 47 | args = { "plugin" => "ipv4_anonymize", "subnet" => "192.168.1.0/24", "structure" => "interfaces::eth0::ip" } 48 | OctofactsUpdater::Plugin.execute(:ipv4_anonymize, structured_fact, args, { "hostname" => "myhostname" }) 49 | expect(structured_fact.value["interfaces"]["eth0"]["ip"]).to eq("192.168.1.60") 50 | end 51 | 52 | it "should be consistent" do 53 | args = { "plugin" => "ipv4_anonymize", "subnet" => "10.0.0.0/8" } 54 | original_fact = fact.dup 55 | OctofactsUpdater::Plugin.execute(:ipv4_anonymize, fact, args, { "hostname" => "myhostname" }) 56 | expect(fact.value).to eq("10.67.98.60") 57 | fact = original_fact 58 | OctofactsUpdater::Plugin.execute(:ipv4_anonymize, fact, args, { "hostname" => "myhostname" }) 59 | expect(fact.value).to eq("10.67.98.60") 60 | end 61 | end 62 | 63 | describe "ipv6_anonymize plugin" do 64 | let(:plugin) { OctofactsUpdater::Plugin.plugins[:ipv6_anonymize] } 65 | let(:fact) { OctofactsUpdater::Fact.new("ipv6", "fd00::/8") } 66 | let(:structured_fact) do 67 | OctofactsUpdater::Fact.new("networking", 68 | { 69 | "ip6" => "fd00::/8", 70 | "interfaces" => { 71 | "eth0" => { 72 | "ip6" => "fd00::/8" 73 | } 74 | } 75 | } 76 | ) 77 | end 78 | 79 | it "should be defined" do 80 | expect(plugin).to be_a_kind_of(Proc) 81 | end 82 | 83 | it "should raise an error if the subnet is not passed" do 84 | args = { "plugin" => "ipv6_anonymize" } 85 | expect(OctofactsUpdater::Plugin).to receive(:warn) 86 | .with("ArgumentError occurred executing ipv6_anonymize on ipv6 with value \"fd00::/8\"") 87 | expect do 88 | OctofactsUpdater::Plugin.execute(:ipv6_anonymize, fact, args) 89 | end.to raise_error(ArgumentError, /ipv6_anonymize requires a subnet/) 90 | end 91 | 92 | it "should change the IP to a given subnet" do 93 | args = { "plugin" => "ipv6_anonymize", "subnet" => "fd00::/8" } 94 | OctofactsUpdater::Plugin.execute(:ipv6_anonymize, fact, args, { "hostname" => "myhostname" }) 95 | expect(fact.value).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364") 96 | end 97 | 98 | it "should properly update a structured fact at the top level" do 99 | args = { "plugin" => "ipv4_anonymize", "subnet" => "fd00::/8", "structure" => "ip6" } 100 | OctofactsUpdater::Plugin.execute(:ipv6_anonymize, structured_fact, args, { "hostname" => "myhostname" }) 101 | expect(structured_fact.value(args["structure"])).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364") 102 | end 103 | 104 | it "should properly update a structured fact nested within" do 105 | args = { "plugin" => "ipv4_anonymize", "subnet" => "fd00::/8", "structure" => "interfaces::eth0::ip6" } 106 | OctofactsUpdater::Plugin.execute(:ipv6_anonymize, structured_fact, args, { "hostname" => "myhostname" }) 107 | expect(structured_fact.value(args["structure"])).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364") 108 | end 109 | 110 | it "should be consistent" do 111 | args = { "plugin" => "ipv6_anonymize", "subnet" => "fd00::/8" } 112 | original_fact = fact.dup 113 | OctofactsUpdater::Plugin.execute(:ipv6_anonymize, fact, args, { "hostname" => "myhostname" }) 114 | expect(fact.value).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364") 115 | fact = original_fact 116 | OctofactsUpdater::Plugin.execute(:ipv6_anonymize, fact, args, { "hostname" => "myhostname" }) 117 | expect(fact.value).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364") 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/octofacts_updater/plugins/ssh_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe "sshfp_randomize plugin" do 5 | let(:plugin) { OctofactsUpdater::Plugin.plugins[:sshfp_randomize] } 6 | let(:value) { "SSHFP 1 1 0123456789abcdef0123456789abcdef01234567\nSSHFP 1 2 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } 7 | let(:args) { { "plugin" => "sshfp_randomize" } } 8 | 9 | it "should be defined" do 10 | expect(plugin).to be_a_kind_of(Proc) 11 | end 12 | 13 | it "should raise an error if the input is not a sshfp key" do 14 | fact = OctofactsUpdater::Fact.new("foo", "kittens123") 15 | expect(OctofactsUpdater::Plugin).to receive(:warn) 16 | .with("RuntimeError occurred executing sshfp_randomize on foo with value \"kittens123\"") 17 | expect do 18 | OctofactsUpdater::Plugin.execute(:sshfp_randomize, fact, args) 19 | end.to raise_error(/Unparseable pattern: kittens123/) 20 | end 21 | 22 | it "should randomize a sshfp key" do 23 | allow(OctofactsUpdater::Plugin).to receive(:randomize_long_string) { |arg| "random:#{arg}" } 24 | fact = OctofactsUpdater::Fact.new("foo", value) 25 | OctofactsUpdater::Plugin.execute(:sshfp_randomize, fact, args) 26 | expect(fact.value).to eq("SSHFP 1 1 random:0123456789abcdef0123456789abcdef01234567\nSSHFP 1 2 random:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/octofacts_updater/plugins/static_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe "delete plugin" do 5 | let(:plugin) { OctofactsUpdater::Plugin.plugins[:delete] } 6 | 7 | it "should be defined" do 8 | expect(plugin).to be_a_kind_of(Proc) 9 | end 10 | 11 | it "should set the value of a fact to nil" do 12 | fact = OctofactsUpdater::Fact.new("foo", "bar") 13 | args = { "plugin" => "delete" } 14 | OctofactsUpdater::Plugin.execute(:delete, fact, args) 15 | expect(fact.value).to be_nil 16 | end 17 | 18 | it "should remove a value within a structured fact" do 19 | value = { "one" => 1, "two" => 2 } 20 | fact = OctofactsUpdater::Fact.new("foo", value) 21 | args = { "plugin" => "delete", "structure" => "one" } 22 | OctofactsUpdater::Plugin.execute(:delete, fact, args) 23 | expect(fact.value).to eq({"two"=>2}) 24 | end 25 | end 26 | 27 | describe "set plugin" do 28 | let(:plugin) { OctofactsUpdater::Plugin.plugins[:set] } 29 | 30 | it "should be defined" do 31 | expect(plugin).to be_a_kind_of(Proc) 32 | end 33 | 34 | it "should set the value of a fact" do 35 | value = { "one" => 1, "two" => 2 } 36 | fact = OctofactsUpdater::Fact.new("foo", value) 37 | args = { "plugin" => "set", "value" => "kittens" } 38 | OctofactsUpdater::Plugin.execute(:set, fact, args) 39 | expect(fact.value).to eq("kittens") 40 | end 41 | 42 | it "should set the value of a structured fact" do 43 | value = { "one" => 1, "two" => 2 } 44 | fact = OctofactsUpdater::Fact.new("foo", value) 45 | args = { "plugin" => "set", "value" => "kittens", "structure" => "one" } 46 | OctofactsUpdater::Plugin.execute(:set, fact, args) 47 | expect(fact.value).to eq({"one"=>"kittens", "two"=>2}) 48 | end 49 | 50 | it "should add the value to a structured fact" do 51 | value = { "one" => 1, "two" => 2 } 52 | fact = OctofactsUpdater::Fact.new("foo", value) 53 | args = { "plugin" => "set", "value" => "kittens", "structure" => "three" } 54 | OctofactsUpdater::Plugin.execute(:set, fact, args) 55 | expect(fact.value).to eq({"one"=>1, "two"=>2, "three"=>"kittens"}) 56 | end 57 | end 58 | 59 | describe "remove_from_delimited_string plugin" do 60 | let(:plugin) { OctofactsUpdater::Plugin.plugins[:remove_from_delimited_string] } 61 | let(:fact) { OctofactsUpdater::Fact.new("foo", "foo,bar,baz,fizz") } 62 | 63 | it "should be defined" do 64 | expect(plugin).to be_a_kind_of(Proc) 65 | end 66 | 67 | it "should raise ArgumentError if delimiter is not provided" do 68 | args = { "plugin" => "remove_from_delimited_string", "regexp" => ".*" } 69 | expect(OctofactsUpdater::Plugin).to receive(:warn) 70 | .with("ArgumentError occurred executing remove_from_delimited_string on foo with value \"foo,bar,baz,fizz\"") 71 | expect do 72 | OctofactsUpdater::Plugin.execute(:remove_from_delimited_string, fact, args) 73 | end.to raise_error(ArgumentError, /remove_from_delimited_string requires a delimiter/) 74 | end 75 | 76 | it "should raise ArgumentError if regexp is not provided" do 77 | args = { "plugin" => "remove_from_delimited_string", "delimiter" => "," } 78 | expect(OctofactsUpdater::Plugin).to receive(:warn) 79 | .with("ArgumentError occurred executing remove_from_delimited_string on foo with value \"foo,bar,baz,fizz\"") 80 | expect do 81 | OctofactsUpdater::Plugin.execute(:remove_from_delimited_string, fact, args) 82 | end.to raise_error(ArgumentError, /remove_from_delimited_string requires a regexp/) 83 | end 84 | 85 | it "should return joined string with elements matching regexp removed" do 86 | args = { "plugin" => "remove_from_delimited_string", "delimiter" => ",", "regexp" => "^b" } 87 | OctofactsUpdater::Plugin.execute(:remove_from_delimited_string, fact, args) 88 | expect(fact.value).to eq("foo,fizz") 89 | end 90 | 91 | it "should be a no-op if no elements match the regexp" do 92 | args = { "plugin" => "remove_from_delimited_string", "delimiter" => ",", "regexp" => "does-not-match" } 93 | OctofactsUpdater::Plugin.execute(:remove_from_delimited_string, fact, args) 94 | expect(fact.value).to eq("foo,bar,baz,fizz") 95 | end 96 | end 97 | 98 | describe "noop plugin" do 99 | let(:plugin) { OctofactsUpdater::Plugin.plugins[:noop] } 100 | 101 | it "should be defined" do 102 | expect(plugin).to be_a_kind_of(Proc) 103 | end 104 | 105 | it "should do nothing at all" do 106 | fact = OctofactsUpdater::Fact.new("foo", "kittens") 107 | args = { "plugin" => "noop" } 108 | OctofactsUpdater::Plugin.execute(:noop, fact, args) 109 | expect(fact.value).to eq("kittens") 110 | end 111 | end 112 | 113 | describe "randomize_long_string plugin" do 114 | let(:plugin) { OctofactsUpdater::Plugin.plugins[:randomize_long_string] } 115 | let(:value) { "1234567890abcdef" } 116 | let(:args) { { "plugin" => "randomize_long_string" } } 117 | 118 | it "should be defined" do 119 | expect(plugin).to be_a_kind_of(Proc) 120 | end 121 | 122 | it "should randomize a string" do 123 | allow(OctofactsUpdater::Plugin).to receive(:randomize_long_string) { |arg| "random:#{arg}" } 124 | fact = OctofactsUpdater::Fact.new("foo", value) 125 | OctofactsUpdater::Plugin.execute(:randomize_long_string, fact, args) 126 | expect(fact.value).to eq("random:#{value}") 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /spec/octofacts_updater/service/base_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe OctofactsUpdater::Service::Base do 6 | describe "#parse_yaml" do 7 | it "should convert first '--- (whatever)' to just '---'" do 8 | text = <<-EOF 9 | --- !ruby/object:Puppet::Node::Facts 10 | name: foo-bar.example.net 11 | values: 12 | agent_specified_environment: production 13 | EOF 14 | result = described_class.parse_yaml(text) 15 | expect(result).to eq({"agent_specified_environment"=>"production"}) 16 | end 17 | 18 | it "should convert first '--- (whatever)' to just '---' after comments" do 19 | text = <<-EOF 20 | # Facts for foo-bar.example.net 21 | --- !ruby/object:Puppet::Node::Facts 22 | name: foo-bar.example.net 23 | values: 24 | agent_specified_environment: production 25 | EOF 26 | result = described_class.parse_yaml(text) 27 | expect(result).to eq({"agent_specified_environment"=>"production"}) 28 | end 29 | 30 | it "should convert first '--- (whatever)' to just '---' after blank lines" do 31 | text = <<-EOF 32 | 33 | 34 | --- !ruby/object:Puppet::Node::Facts 35 | name: foo-bar.example.net 36 | values: 37 | agent_specified_environment: production 38 | EOF 39 | result = described_class.parse_yaml(text) 40 | expect(result).to eq({"agent_specified_environment"=>"production"}) 41 | end 42 | 43 | it "should work correctly when first non-comment line is not '---'" do 44 | text = <<-EOF 45 | # Test 123 46 | 47 | # Test 456 48 | name: foo-bar.example.net 49 | values: 50 | agent_specified_environment: production 51 | EOF 52 | result = described_class.parse_yaml(text) 53 | expect(result).to eq({"agent_specified_environment"=>"production"}) 54 | end 55 | 56 | it "should convert a plain formatted fact file" do 57 | text = <<-EOF 58 | --- 59 | agent_specified_environment: production 60 | EOF 61 | result = described_class.parse_yaml(text) 62 | expect(result).to eq({"agent_specified_environment"=>"production"}) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/octofacts_updater/service/enc_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | require "ostruct" 6 | 7 | describe OctofactsUpdater::Service::ENC do 8 | let(:config) { { "enc" => { "path" => "/tmp/foo.enc" }} } 9 | 10 | describe "#run_enc" do 11 | it "should raise ArgumentError if no configuration for the ENC is defined" do 12 | expect { described_class.run_enc("HostName", {}) }.to raise_error(ArgumentError, /The ENC configuration must be defined/) 13 | end 14 | 15 | it "should raise ArgumentError if configuration does not have a path" do 16 | expect { described_class.run_enc("HostName", { "enc" => {} }) }.to raise_error(ArgumentError, /The ENC path must be defined/) 17 | end 18 | 19 | it "should raise Errno::ENOENT if the script doesn't exist at the path" do 20 | allow(File).to receive(:"file?").and_call_original 21 | allow(File).to receive(:"file?").with("/tmp/foo.enc").and_return(false) 22 | expect { described_class.run_enc("HostName", config) }.to raise_error(Errno::ENOENT, /The ENC script could not be found/) 23 | end 24 | 25 | it "should raise RuntimeError if the exit status from the ENC is nonzero" do 26 | allow(File).to receive(:"file?").and_call_original 27 | allow(File).to receive(:"file?").with("/tmp/foo.enc").and_return(true) 28 | open3_response = ["", "Whoopsie", OpenStruct.new(exitstatus: 1)] 29 | allow(Open3).to receive(:capture3).with("/tmp/foo.enc HostName").and_return(open3_response) 30 | expect { described_class.run_enc("HostName", config) }.to raise_error(%r{Error executing "/tmp/foo.enc HostName"}) 31 | end 32 | 33 | it "should return the parsed YAML output from the ENC" do 34 | allow(File).to receive(:"file?").and_call_original 35 | allow(File).to receive(:"file?").with("/tmp/foo.enc").and_return(true) 36 | yaml_out = { "parameters" => { "foo" => "bar" } }.to_yaml 37 | open3_response = [yaml_out, "", OpenStruct.new(exitstatus: 0)] 38 | allow(Open3).to receive(:capture3).with("/tmp/foo.enc HostName").and_return(open3_response) 39 | expect(described_class.run_enc("HostName", config)).to eq("parameters" => { "foo" => "bar" }) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/octofacts_updater/service/local_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe OctofactsUpdater::Service::LocalFile do 5 | describe "#facts" do 6 | let(:node) { "foo.example.net" } 7 | let(:custom_exception) { RuntimeError.new("custom exception for testing") } 8 | 9 | context "when localfile is not configured" do 10 | it "should raise ArgumentError when localfile is undefined" do 11 | config = {} 12 | expect { described_class.facts(node, config) }.to raise_error(ArgumentError, /requires localfile section/) 13 | end 14 | 15 | it "should raise ArgumentError when localfile is not a hash" do 16 | config = {"localfile" => :do_it} 17 | expect { described_class.facts(node, config) }.to raise_error(ArgumentError, /requires localfile section/) 18 | end 19 | end 20 | 21 | context "when localfile is configured" do 22 | let(:file_path) { File.expand_path("../../fixtures/facts", File.dirname(__FILE__)) } 23 | 24 | it "should raise error if the path is undefined" do 25 | config = { "localfile" => {} } 26 | expect { described_class.facts(node, config) }.to raise_error(ArgumentError, /requires 'path' in the localfile section/) 27 | end 28 | 29 | it "should raise error if the path does not exist" do 30 | config = { "localfile" => { "path" => File.join(file_path, "missing.yaml") } } 31 | expect { described_class.facts(node, config) }.to raise_error(Errno::ENOENT, /LocalFile cannot find a file at/) 32 | end 33 | 34 | it "should return the proper object from the parsed file" do 35 | config = { "localfile" => { "path" => File.join(file_path, "basic.yaml") } } 36 | result = described_class.facts(node, config) 37 | desired_result = YAML.safe_load(File.read(File.join(file_path, "basic.yaml"))) 38 | expect(result).to eq(desired_result) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/octofacts_updater/service/puppetdb_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | require "octocatalog-diff" 6 | 7 | describe OctofactsUpdater::Service::PuppetDB do 8 | before(:each) do 9 | ENV.delete("PUPPETDB_URL") 10 | end 11 | 12 | after(:each) do 13 | ENV.delete("PUPPETDB_URL") 14 | end 15 | 16 | describe "#facts" do 17 | it "should return facts from octocatalog-diff" do 18 | facts_double = instance_double(OctocatalogDiff::Facts) 19 | facts_answer = { "foo" => "bar" } 20 | expected_args = {node: "foo.bar.node", backend: :puppetdb, puppetdb_url: "https://puppetdb.fake:8443"} 21 | expect(described_class).to receive(:puppetdb_url).and_return("https://puppetdb.fake:8443") 22 | expect(OctocatalogDiff::Facts).to receive(:new).with(expected_args).and_return(facts_double) 23 | expect(facts_double).to receive(:facts).and_return(facts_answer) 24 | expect(described_class.facts("foo.bar.node", {})).to eq(facts_answer) 25 | end 26 | 27 | it "should raise an error if facts cannot be determined" do 28 | facts_double = instance_double(OctocatalogDiff::Facts) 29 | expected_args = {node: "foo.bar.node", backend: :puppetdb, puppetdb_url: "https://puppetdb.fake:8443"} 30 | expect(described_class).to receive(:puppetdb_url).and_return("https://puppetdb.fake:8443") 31 | expect(OctocatalogDiff::Facts).to receive(:new).with(expected_args).and_return(facts_double) 32 | expect(facts_double).to receive(:facts).and_return(nil) 33 | expect { described_class.facts("foo.bar.node", {}) }.to raise_error(OctocatalogDiff::Errors::FactSourceError) 34 | end 35 | end 36 | 37 | describe "#puppetdb_url" do 38 | let(:fake_url) { "https://puppetdb.fake:8443" } 39 | it "should return puppetdb_url from configuration" do 40 | expect(described_class.puppetdb_url("puppetdb" => { "url" => fake_url })).to eq(fake_url) 41 | end 42 | 43 | it "should return PUPPETDB_URL from environment" do 44 | ENV["PUPPETDB_URL"] = fake_url 45 | expect(described_class.puppetdb_url).to eq(fake_url) 46 | end 47 | 48 | it "should raise an error if puppetdb URL cannot be determined" do 49 | expect { described_class.puppetdb_url }.to raise_error(/PuppetDB URL not configured or set in environment/) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/octofacts_updater/service/ssh_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "spec_helper" 3 | 4 | describe OctofactsUpdater::Service::SSH do 5 | describe "#facts" do 6 | let(:node) { "foo.example.net" } 7 | let(:custom_exception) { RuntimeError.new("custom exception for testing") } 8 | 9 | context "when ssh is not configured" do 10 | it "should raise ArgumentError when ssh is undefined" do 11 | config = {} 12 | expect { described_class.facts(node, config) }.to raise_error(ArgumentError, /requires ssh section/) 13 | end 14 | 15 | it "should raise ArgumentError when ssh is not a hash" do 16 | config = {"ssh" => :do_it} 17 | expect { described_class.facts(node, config) }.to raise_error(ArgumentError, /requires ssh section/) 18 | end 19 | end 20 | 21 | context "when ssh is configured" do 22 | it "should raise error if no server is configured" do 23 | config = { "ssh" => {} } 24 | expect { described_class.facts(node, config) }.to raise_error(ArgumentError, /requires 'server' in the ssh section/) 25 | end 26 | 27 | context "when user is unspecified" do 28 | before(:each) do 29 | @user_save = ENV.delete("USER") 30 | end 31 | 32 | after(:each) do 33 | if @user_save 34 | ENV["USER"] = @user_save 35 | else 36 | ENV.delete("USER") 37 | end 38 | end 39 | 40 | it "should raise error if no user is configured" do 41 | config = { "ssh" => { "server" => "puppetserver.example.net" } } 42 | expect { described_class.facts(node, config) }.to raise_error(ArgumentError, /requires 'user' in the ssh section/) 43 | end 44 | 45 | it "should use USER from environment if no user is configured" do 46 | ENV["USER"] = "ssh-user-from-env" 47 | config = { "ssh" => { "server" => "puppetserver.example.net" } } 48 | expect(Net::SSH).to receive(:start).with("puppetserver.example.net", "ssh-user-from-env", {}).and_raise(custom_exception) 49 | expect { described_class.facts(node, config) }.to raise_error(custom_exception) 50 | end 51 | end 52 | 53 | it "should raise error if SSH call fails" do 54 | config = { "ssh" => { "server" => "puppetserver.example.net", "user" => "foo", "extra" => "bar" } } 55 | ssh = double 56 | ssh_result = double 57 | allow(ssh_result).to receive(:exitstatus).and_return(1) 58 | allow(ssh_result).to receive(:to_s).and_return("Failed to cat foo: no such file or directory") 59 | expect(ssh).to receive(:"exec!").and_return(ssh_result) 60 | allow(Net::SSH).to receive(:start).with("puppetserver.example.net", "foo", hash_including(extra: "bar")).and_yield(ssh) 61 | expect { described_class.facts(node, config) }.to raise_error(/ssh failed with exitcode=1: Failed to cat foo/) 62 | end 63 | 64 | it "should return data if SSH call succeeds" do 65 | config = { "ssh" => { "server" => "puppetserver.example.net", "user" => "foo", "extra" => "bar" } } 66 | ssh = double 67 | ssh_result = double 68 | allow(ssh_result).to receive(:exitstatus).and_return(0) 69 | allow(ssh_result).to receive(:to_s).and_return("---\nname: #{node}\nvalues:\n foo: bar\n") 70 | expect(ssh).to receive(:"exec!").and_return(ssh_result) 71 | expect(Net::SSH).to receive(:start).with("puppetserver.example.net", "foo", hash_including(extra: "bar")).and_yield(ssh) 72 | expect(described_class.facts(node, config)).to eq("name" => node, "values" => { "foo" => "bar" }) 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | if ENV["SPEC_NAME"] 3 | require "simplecov" 4 | require "simplecov-json" 5 | 6 | SimpleCov.root File.expand_path("..", File.dirname(__FILE__)) 7 | SimpleCov.coverage_dir File.expand_path("../lib/#{ENV['SPEC_NAME']}/coverage", File.dirname(__FILE__)) 8 | 9 | if ENV["JOB_NAME"] 10 | SimpleCov.formatters = [SimpleCov::Formatter::JSONFormatter] 11 | else 12 | SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::JSONFormatter] 13 | end 14 | 15 | SimpleCov.start do 16 | add_filter "spec/" 17 | if ENV["SPEC_NAME"] == "octofacts" 18 | add_filter "lib/octofacts_updater.rb" 19 | add_filter "lib/octofacts_updater/" 20 | elsif ENV["SPEC_NAME"] == "octofacts_updater" 21 | add_filter "lib/octofacts.rb" 22 | add_filter "lib/octofacts/" 23 | end 24 | end 25 | 26 | require ENV["SPEC_NAME"] 27 | require_relative "octofacts/octofacts_spec_helper" if ENV["SPEC_NAME"] == "octofacts" 28 | else 29 | require "octofacts" 30 | require_relative "octofacts/octofacts_spec_helper" 31 | require "octofacts_updater" 32 | end 33 | 34 | RSpec.configure do |config| 35 | # Prohibit using the should syntax 36 | config.expect_with :rspec do |spec| 37 | spec.syntax = :expect 38 | end 39 | 40 | config.before(:each) do 41 | ENV.delete("OCTOFACTS_INDEX_PATH") 42 | ENV.delete("OCTOFACTS_FIXTURE_PATH") 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /vendor/cache/activesupport-7.1.3.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/activesupport-7.1.3.4.gem -------------------------------------------------------------------------------- /vendor/cache/addressable-2.8.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/addressable-2.8.7.gem -------------------------------------------------------------------------------- /vendor/cache/ast-2.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/ast-2.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/base64-0.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/base64-0.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/bigdecimal-3.1.8.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/bigdecimal-3.1.8.gem -------------------------------------------------------------------------------- /vendor/cache/coderay-1.1.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/coderay-1.1.3.gem -------------------------------------------------------------------------------- /vendor/cache/concurrent-ruby-1.3.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/concurrent-ruby-1.3.4.gem -------------------------------------------------------------------------------- /vendor/cache/connection_pool-2.4.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/connection_pool-2.4.1.gem -------------------------------------------------------------------------------- /vendor/cache/csv-3.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/csv-3.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/deep_merge-1.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/deep_merge-1.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/diff-lcs-1.5.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/diff-lcs-1.5.1.gem -------------------------------------------------------------------------------- /vendor/cache/diffy-3.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/diffy-3.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/docile-1.4.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/docile-1.4.1.gem -------------------------------------------------------------------------------- /vendor/cache/drb-2.2.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/drb-2.2.1.gem -------------------------------------------------------------------------------- /vendor/cache/facter-4.6.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/facter-4.6.1.gem -------------------------------------------------------------------------------- /vendor/cache/faraday-2.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/faraday-2.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/fast_gettext-2.4.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/fast_gettext-2.4.0.gem -------------------------------------------------------------------------------- /vendor/cache/forwardable-1.3.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/forwardable-1.3.3.gem -------------------------------------------------------------------------------- /vendor/cache/hashdiff-1.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/hashdiff-1.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/hiera-3.12.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/hiera-3.12.0.gem -------------------------------------------------------------------------------- /vendor/cache/hocon-1.4.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/hocon-1.4.0.gem -------------------------------------------------------------------------------- /vendor/cache/httparty-0.22.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/httparty-0.22.0.gem -------------------------------------------------------------------------------- /vendor/cache/i18n-1.14.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/i18n-1.14.5.gem -------------------------------------------------------------------------------- /vendor/cache/json-2.7.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/json-2.7.2.gem -------------------------------------------------------------------------------- /vendor/cache/language_server-protocol-3.17.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/language_server-protocol-3.17.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/locale-2.1.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/locale-2.1.4.gem -------------------------------------------------------------------------------- /vendor/cache/method_source-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/method_source-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/mini_mime-1.1.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/mini_mime-1.1.5.gem -------------------------------------------------------------------------------- /vendor/cache/minitest-5.24.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/minitest-5.24.1.gem -------------------------------------------------------------------------------- /vendor/cache/multi_json-1.15.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/multi_json-1.15.0.gem -------------------------------------------------------------------------------- /vendor/cache/multi_xml-0.6.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/multi_xml-0.6.0.gem -------------------------------------------------------------------------------- /vendor/cache/mutex_m-0.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/mutex_m-0.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/net-ssh-7.2.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/net-ssh-7.2.3.gem -------------------------------------------------------------------------------- /vendor/cache/octocatalog-diff-2.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/octocatalog-diff-2.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/octokit-9.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/octokit-9.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/parallel-1.26.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/parallel-1.26.3.gem -------------------------------------------------------------------------------- /vendor/cache/parser-3.3.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/parser-3.3.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/prime-0.1.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/prime-0.1.2.gem -------------------------------------------------------------------------------- /vendor/cache/pry-0.14.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/pry-0.14.2.gem -------------------------------------------------------------------------------- /vendor/cache/public_suffix-5.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/public_suffix-5.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/puppet-7.30.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/puppet-7.30.0.gem -------------------------------------------------------------------------------- /vendor/cache/puppet-resource_api-1.9.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/puppet-resource_api-1.9.0.gem -------------------------------------------------------------------------------- /vendor/cache/racc-1.8.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/racc-1.8.1.gem -------------------------------------------------------------------------------- /vendor/cache/rack-3.1.12.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rack-3.1.12.gem -------------------------------------------------------------------------------- /vendor/cache/rainbow-3.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rainbow-3.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/rake-13.2.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rake-13.2.1.gem -------------------------------------------------------------------------------- /vendor/cache/regexp_parser-2.9.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/regexp_parser-2.9.2.gem -------------------------------------------------------------------------------- /vendor/cache/rexml-3.3.9.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rexml-3.3.9.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-3.13.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rspec-3.13.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-core-3.13.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rspec-core-3.13.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-expectations-3.13.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rspec-expectations-3.13.1.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-mocks-3.13.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rspec-mocks-3.13.1.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-puppet-3.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rspec-puppet-3.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-support-3.13.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rspec-support-3.13.1.gem -------------------------------------------------------------------------------- /vendor/cache/rubocop-1.65.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rubocop-1.65.1.gem -------------------------------------------------------------------------------- /vendor/cache/rubocop-ast-1.32.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rubocop-ast-1.32.0.gem -------------------------------------------------------------------------------- /vendor/cache/rubocop-github-0.20.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rubocop-github-0.20.0.gem -------------------------------------------------------------------------------- /vendor/cache/rubocop-performance-1.21.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rubocop-performance-1.21.1.gem -------------------------------------------------------------------------------- /vendor/cache/rubocop-rails-2.25.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rubocop-rails-2.25.1.gem -------------------------------------------------------------------------------- /vendor/cache/ruby-progressbar-1.13.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/ruby-progressbar-1.13.0.gem -------------------------------------------------------------------------------- /vendor/cache/ruby2_keywords-0.0.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/ruby2_keywords-0.0.5.gem -------------------------------------------------------------------------------- /vendor/cache/rugged-1.7.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/rugged-1.7.2.gem -------------------------------------------------------------------------------- /vendor/cache/sawyer-0.9.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/sawyer-0.9.2.gem -------------------------------------------------------------------------------- /vendor/cache/scanf-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/scanf-1.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/semantic_puppet-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/semantic_puppet-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/simplecov-0.22.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/simplecov-0.22.0.gem -------------------------------------------------------------------------------- /vendor/cache/simplecov-html-0.12.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/simplecov-html-0.12.3.gem -------------------------------------------------------------------------------- /vendor/cache/simplecov-json-0.2.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/simplecov-json-0.2.3.gem -------------------------------------------------------------------------------- /vendor/cache/simplecov_json_formatter-0.1.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/simplecov_json_formatter-0.1.4.gem -------------------------------------------------------------------------------- /vendor/cache/singleton-0.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/singleton-0.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/thor-1.3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/thor-1.3.1.gem -------------------------------------------------------------------------------- /vendor/cache/tzinfo-2.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/tzinfo-2.0.6.gem -------------------------------------------------------------------------------- /vendor/cache/unicode-display_width-2.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/octofacts/3ca1e44938b69480144324f2f17f63f0821690bb/vendor/cache/unicode-display_width-2.5.0.gem --------------------------------------------------------------------------------