├── .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
--------------------------------------------------------------------------------