зеркало из https://github.com/github/octofacts.git
Initial public release
This commit is contained in:
Коммит
88e4a98fb7
|
@ -0,0 +1,31 @@
|
|||
<!--
|
||||
Hi there! If you are reporting a bug or a problem, please use this template so that we can collect the information that we need in order to help. If you are opening an issue for a reason other than reporting a problem, e.g. to make a feature request or start a discussion of new functionality, then you do not need to follow the template.
|
||||
|
||||
Please remember that all activity in this project, including issues, needs to comply with the Code of Conduct, found in the CODE_OF_CONDUCT.md document in the root of this repository.
|
||||
-->
|
||||
|
||||
> Description of problem
|
||||
|
||||
- What did you do?
|
||||
- What happened?
|
||||
- What did you expect to happen?
|
||||
- How can someone reproduce the problem?
|
||||
|
||||
> Command/code used
|
||||
|
||||
```
|
||||
Paste the exact command or code here.
|
||||
```
|
||||
|
||||
> Platform and version information
|
||||
|
||||
- Your OS: <!-- Mac OS? Linux? Which version and distribution? -->
|
||||
- Your Ruby version: <!-- type `ruby --version` at the command prompt -->
|
||||
- Your version of Puppet: <!-- version number, and whether it's open source or Puppet Enterprise -->
|
||||
- Your version of octofacts: <!-- Look at the .version file in the root of this repository. -->
|
||||
|
||||
> Do the tests pass from a clean checkout?
|
||||
|
||||
<!-- Please refer to the README file to check out the code, bootstrap the repository, and run the tests. If you encounter any errors from the tests, please post them here. -->
|
||||
|
||||
> Anything else to add that you think will be helpful?
|
|
@ -0,0 +1,26 @@
|
|||
<!--
|
||||
Hi there! We are delighted that you have chosen to contribute to octofacts.
|
||||
|
||||
If you have not already done so, please read CONTRIBUTING.md document in the root of this repository.
|
||||
|
||||
Please remember that all activity in this project, including pull requests, needs to comply with the Code of Conduct, found in the CODE_OF_CONDUCT.md document in the root of this repository.
|
||||
|
||||
Any contributions to this project must be made under the MIT license.
|
||||
|
||||
You do NOT need to bump the version number as part of your submission. We will do this for you at or after the time we merge your contribution.
|
||||
-->
|
||||
|
||||
## Overview
|
||||
|
||||
This pull request [introduces/changes/removes] [functionality/feature].
|
||||
|
||||
(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.)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
|
||||
/cc [related issues] [teams and individuals, making sure to mention why you're CC-ing them]
|
|
@ -0,0 +1,54 @@
|
|||
*.rbc
|
||||
/.config
|
||||
/coverage/
|
||||
/InstalledFiles
|
||||
/pkg/
|
||||
/lib/octofacts/coverage
|
||||
/lib/octofacts_updater/coverage
|
||||
/spec/reports/
|
||||
/spec/examples.txt
|
||||
/test/tmp/
|
||||
/test/version_tmp/
|
||||
/tmp/
|
||||
|
||||
# Used by dotenv library to load environment variables.
|
||||
# .env
|
||||
|
||||
## Specific to RubyMotion:
|
||||
.dat*
|
||||
.repl_history
|
||||
build/
|
||||
*.bridgesupport
|
||||
build-iPhoneOS/
|
||||
build-iPhoneSimulator/
|
||||
|
||||
## Specific to RubyMotion (use of CocoaPods):
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# vendor/Pods/
|
||||
|
||||
# Binstubs - if this gem ships any binstubs we'll need to whitelist them.
|
||||
/bin/*
|
||||
!/bin/octofacts-updater
|
||||
|
||||
## Documentation cache and generated files:
|
||||
/.yardoc/
|
||||
/_yardoc/
|
||||
/rdoc/
|
||||
|
||||
## Environment normalization:
|
||||
/.bundle/
|
||||
/vendor/bundle
|
||||
/lib/bundler/man/
|
||||
|
||||
# for a library or gem, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# Gemfile.lock
|
||||
# .ruby-version
|
||||
# .ruby-gemset
|
||||
|
||||
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
||||
.rvmrc
|
|
@ -0,0 +1,9 @@
|
|||
inherit_gem:
|
||||
rubocop-github:
|
||||
- config/default.yml
|
||||
|
||||
AllCops:
|
||||
DisplayCopNames: true
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.1
|
|
@ -0,0 +1 @@
|
|||
2.1.9
|
|
@ -0,0 +1 @@
|
|||
0.2.0
|
|
@ -0,0 +1,46 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
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.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Scope
|
||||
|
||||
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.
|
||||
|
||||
## Enforcement
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
@ -0,0 +1,33 @@
|
|||
## Contributing
|
||||
|
||||
[fork]: https://github.com/github/octofacts/fork
|
||||
[pr]: https://github.com/github/octofacts/compare
|
||||
[style]: https://github.com/styleguide/ruby
|
||||
[code-of-conduct]: CODE_OF_CONDUCT.md
|
||||
|
||||
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
|
||||
|
||||
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.
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
0. [Fork][fork] and clone the repository
|
||||
0. Configure and install the dependencies: `script/bootstrap`
|
||||
0. Make sure the tests pass on your machine: `bundle exec rake`
|
||||
0. Create a new branch: `git checkout -b my-branch-name`
|
||||
0. Make your change, add tests, and make sure the tests still pass
|
||||
0. Push to your fork and [submit a pull request][pr]
|
||||
0. Pat your self on the back and wait for your pull request to be reviewed and merged.
|
||||
|
||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||
|
||||
- Follow the [style guide][style].
|
||||
- Write tests. We require 100% rspec test coverage in this project.
|
||||
- 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.
|
||||
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
|
||||
## Resources
|
||||
|
||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||
- [GitHub Help](https://help.github.com)
|
|
@ -0,0 +1,17 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
gemspec name: "octofacts"
|
||||
gemspec name: "octofacts-updater"
|
||||
|
||||
group :development do
|
||||
gem "parallel", "= 1.12.0"
|
||||
gem "pry", "~> 0.10"
|
||||
gem "rake", "~> 10.0"
|
||||
gem "rubocop-github", "~> 0.5.0"
|
||||
gem "simplecov", ">= 0.14.1"
|
||||
gem "simplecov-json", "~> 0.2"
|
||||
|
||||
# Integration test
|
||||
gem "rspec-puppet", "~> #{ENV['RSPEC_PUPPET_VERSION'] || '2.6.2'}"
|
||||
gem "puppet", "~> #{ENV['PUPPET_VERSION'] || '4.10.4'}"
|
||||
end
|
|
@ -0,0 +1,125 @@
|
|||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
octofacts (0.2.0)
|
||||
octofacts-updater (0.2.0)
|
||||
diffy (>= 3.1.0)
|
||||
net-ssh (>= 2.9)
|
||||
octocatalog-diff (>= 1.4.1)
|
||||
octokit (>= 4.2.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
ast (2.3.0)
|
||||
coderay (1.1.1)
|
||||
diff-lcs (1.3)
|
||||
diffy (3.2.0)
|
||||
docile (1.1.5)
|
||||
facter (2.4.6)
|
||||
faraday (0.13.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_gettext (1.1.0)
|
||||
gettext (3.2.3)
|
||||
locale (>= 2.0.5)
|
||||
text (>= 1.3.0)
|
||||
gettext-setup (0.25)
|
||||
fast_gettext (~> 1.1.0)
|
||||
gettext (>= 3.0.2)
|
||||
locale
|
||||
hashdiff (0.3.6)
|
||||
hiera (3.3.1)
|
||||
httparty (0.15.6)
|
||||
multi_xml (>= 0.5.2)
|
||||
json (2.1.0)
|
||||
json_pure (1.8.6)
|
||||
locale (2.1.2)
|
||||
method_source (0.8.2)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
net-ssh (4.2.0)
|
||||
octocatalog-diff (1.4.1)
|
||||
diffy (>= 3.1.0)
|
||||
hashdiff (>= 0.3.0)
|
||||
httparty (>= 0.11.0)
|
||||
rugged (>= 0.25.0b2)
|
||||
octokit (4.7.0)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
parallel (1.12.0)
|
||||
parser (2.4.0.0)
|
||||
ast (~> 2.2)
|
||||
powerpack (0.1.1)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
slop (~> 3.4)
|
||||
public_suffix (3.0.0)
|
||||
puppet (4.10.4)
|
||||
facter (> 2.0, < 4)
|
||||
gettext-setup (>= 0.10, < 1)
|
||||
hiera (>= 2.0, < 4)
|
||||
json_pure (~> 1.8)
|
||||
locale (~> 2.1)
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
rake (10.5.0)
|
||||
rspec (3.6.0)
|
||||
rspec-core (~> 3.6.0)
|
||||
rspec-expectations (~> 3.6.0)
|
||||
rspec-mocks (~> 3.6.0)
|
||||
rspec-core (3.6.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-expectations (3.6.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-mocks (3.6.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-puppet (2.6.2)
|
||||
rspec
|
||||
rspec-support (3.6.0)
|
||||
rubocop (0.49.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.3.3.1, < 3.0)
|
||||
powerpack (~> 0.1)
|
||||
rainbow (>= 1.99.1, < 3.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
rubocop-github (0.5.0)
|
||||
rubocop (~> 0.49)
|
||||
ruby-progressbar (1.8.1)
|
||||
rugged (0.26.0)
|
||||
sawyer (0.8.1)
|
||||
addressable (>= 2.3.5, < 2.6)
|
||||
faraday (~> 0.8, < 1.0)
|
||||
simplecov (0.14.1)
|
||||
docile (~> 1.1.0)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.1)
|
||||
simplecov-json (0.2)
|
||||
json
|
||||
simplecov
|
||||
slop (3.6.0)
|
||||
text (1.3.1)
|
||||
unicode-display_width (1.3.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
octofacts!
|
||||
octofacts-updater!
|
||||
parallel (= 1.12.0)
|
||||
pry (~> 0.10)
|
||||
puppet (~> 4.10.4)
|
||||
rake (~> 10.0)
|
||||
rspec-puppet (~> 2.6.2)
|
||||
rubocop-github (~> 0.5.0)
|
||||
simplecov (>= 0.14.1)
|
||||
simplecov-json (~> 0.2)
|
||||
|
||||
BUNDLED WITH
|
||||
1.15.1
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,69 @@
|
|||
# octofacts
|
||||
|
||||
`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.
|
||||
|
||||
`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.
|
||||
|
||||
## Overview
|
||||
|
||||
The `octofacts` project is distributed with two components:
|
||||
|
||||
- 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.
|
||||
|
||||
- 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.
|
||||
|
||||
## Requirements
|
||||
|
||||
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.
|
||||
|
||||
To use `octofacts-updater`, we recommend using PuppetDB, and if you do you'll need version 3.0 or higher.
|
||||
|
||||
## Example
|
||||
|
||||
Once you complete the initial setup and generate fact fixtures, you'll be able to use code like this in your rspec-puppet tests:
|
||||
|
||||
```
|
||||
describe "modulename::classname" do
|
||||
let(:node) { "fake-node.example.net" }
|
||||
let(:facts) { Octofacts.from_index(app: "my_app_name", role: "my_role_name") }
|
||||
|
||||
it "should do whatever..."
|
||||
...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Installation and use
|
||||
|
||||
The basics:
|
||||
|
||||
- [Quick start tutorial - covers installation and basic configuration](/doc/tutorial.md) <-- **New users start here**
|
||||
- [Automating fixture generation with octofacts-updater](/doc/octofacts-updater.md)
|
||||
|
||||
More advanced usage:
|
||||
|
||||
- [Plugin reference for octofacts-updater](/doc/plugin-reference.md)
|
||||
- [Using manipulators to adjust fact values](/doc/manipulators.md)
|
||||
- [Additional examples of octofacts capabilities](/doc/more-examples.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see our [contributing document](CONTRIBUTING.md) if you would like to participate!
|
||||
|
||||
We would specifically appreciate contributions in these areas:
|
||||
|
||||
- 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.
|
||||
- Any interesting anonymization plugins you write for octofacts-updater -- you may place these in the [contrib/plugins](/contrib/plugins) directory.
|
||||
|
||||
## Getting help
|
||||
|
||||
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).
|
||||
|
||||
## License
|
||||
|
||||
`octofacts` is licensed under the [MIT license](/LICENSE).
|
||||
|
||||
## Authors
|
||||
|
||||
- [@antonio - Antonio Santos](https://github.com/antonio)
|
||||
- [@kpaulisse - Kevin Paulisse](https://github.com/kpaulisse)
|
|
@ -0,0 +1,26 @@
|
|||
require "rspec/core/rake_task"
|
||||
|
||||
namespace :octofacts do
|
||||
task :default => [:"octofacts:spec:octofacts", :"octofacts:spec:octofacts_updater", :"octofacts:spec:octofacts_integration"] do
|
||||
end
|
||||
end
|
||||
|
||||
RSpec::Core::RakeTask.new(:"octofacts:spec:octofacts") do |t|
|
||||
t.pattern = File.join(File.dirname(__FILE__), "spec/octofacts/**/*_spec.rb")
|
||||
t.name = "octofacts"
|
||||
ENV["SPEC_NAME"] = "octofacts"
|
||||
end
|
||||
|
||||
RSpec::Core::RakeTask.new(:"octofacts:spec:octofacts_updater") do |t|
|
||||
t.pattern = File.join(File.dirname(__FILE__), "spec/octofacts_updater/**/*_spec.rb")
|
||||
t.name = "octofacts-updater"
|
||||
ENV["SPEC_NAME"] = "octofacts_updater"
|
||||
end
|
||||
|
||||
RSpec::Core::RakeTask.new(:"octofacts:spec:octofacts_integration") do |t|
|
||||
t.pattern = File.join(File.dirname(__FILE__), "spec/integration/**/*_spec.rb")
|
||||
t.name = "octofacts-integration"
|
||||
ENV.delete("SPEC_NAME")
|
||||
end
|
||||
|
||||
task default: :"octofacts:default"
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require "bundler/setup"
|
||||
require "octofacts_updater"
|
||||
cli = OctofactsUpdater::CLI.new(ARGV)
|
||||
cli.run
|
|
@ -0,0 +1,99 @@
|
|||
# Manipulators - Modifying facts before use
|
||||
|
||||
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.
|
||||
|
||||
## Available manipulators
|
||||
|
||||
### `.replace` - Replace or set facts
|
||||
|
||||
For example, this replaces two facts with string values:
|
||||
|
||||
```
|
||||
Octofacts.from_index(environment: "test").replace(operatingsystem: "Debian", lsbdistcodename: "jessie")
|
||||
```
|
||||
|
||||
It is also possible to perform replacements in structured facts, using `::` as the delimiter.
|
||||
|
||||
```
|
||||
Octofacts.from_index(environment: "test").replace("os::name" => "Debian", "os::lsb::distcodename" => "jessie")
|
||||
```
|
||||
|
||||
*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.
|
||||
|
||||
*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:
|
||||
|
||||
```
|
||||
# Current fact value: foo = { "existing_level" => { "foo" => "bar" } }
|
||||
Octofacts.from_index(...).replace("foo::new_level::test" => "value")
|
||||
#=> foo = { "existing_level" => { "foo" => "bar" }, "new_level" => { "test" => "value" } }
|
||||
```
|
||||
|
||||
*Note*: The "replace" method accepts keys (fact names) both as strings and as symbols. `.replace(foo: "bar")` and `.replace("foo" => "bar")` are equivalent.
|
||||
|
||||
## Advanced
|
||||
|
||||
### Using regular ruby
|
||||
|
||||
If you prefer to use regular ruby without using (or after using) our manipulators, you are free to do so. For example:
|
||||
|
||||
```
|
||||
let(:facts) do
|
||||
f = Octofacts.from_index(environment: "test")
|
||||
f.merge!(foo: "FOO", bar: "BAR")
|
||||
f.delete(:baz)
|
||||
f
|
||||
end
|
||||
```
|
||||
|
||||
### Using lambdas as new values
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
Octofacts.from_index(environment: "test").replace(operatingsystem: lambda { |old_value| old_value.upcase })
|
||||
#=> operatingsystem = "DEBIAN"
|
||||
```
|
||||
|
||||
The lambda method can be defined with one parameter or three parameters as follows.
|
||||
|
||||
```
|
||||
# One parameter - operates on the old value of the fact
|
||||
lambda { |old_value| ... }
|
||||
|
||||
# Three parameters - takes into account the entire fact set
|
||||
# 1. fact_set - The Hash of all of the current facts
|
||||
# 2. fact_name - The name of the fact being operated upon
|
||||
# 3. old_value - The current (old) value of the fact
|
||||
lambda { |fact_set, fact_name, old_value| ... }
|
||||
```
|
||||
|
||||
*Note*: If a lambda function returns `nil`, the key is deleted.
|
||||
|
||||
## Limitations
|
||||
|
||||
### Order is important
|
||||
|
||||
#### Left to right evaluation
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
Octofacts.from_index(environment: "test").replace(foo: "bar").replace(foo: "baz")
|
||||
#=> foo = "baz"
|
||||
```
|
||||
|
||||
#### Select before manipulating
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
Octofacts.from_index(environment: "test").replace(foo: "bar").select(operatingsystem: "Debian")
|
||||
#=> Error!
|
||||
```
|
||||
|
||||
You can instead do this, which works fine:
|
||||
|
||||
```
|
||||
Octofacts.from_index(environment: "production").select(operatingsystem: "Debian").replace(foo: "bar")
|
||||
#=> Works
|
||||
```
|
|
@ -0,0 +1,67 @@
|
|||
# Octofacts examples
|
||||
|
||||
## Manipulating facts with built-in functions
|
||||
|
||||
We provide some helper functions to manipulate facts easily, since we take care of symbolizing and lower-casing keys for you:
|
||||
|
||||
```
|
||||
describe modulename::classname do
|
||||
let(:node) { "fake-node.example.net" }
|
||||
let(:facts) { Octofacts.from_index(app: "my_app_name", role: "my_role_name").replace("fact-name", "new-value") }
|
||||
|
||||
it "should do whatever..."
|
||||
...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Manipulating facts with pure ruby
|
||||
|
||||
If you don't want to use our helper functions, you can use the object as a normal ruby hash:
|
||||
|
||||
```
|
||||
describe modulename::classname do
|
||||
let(:node) { "fake-node.example.net" }
|
||||
let(:facts) do
|
||||
f = Octofacts.from_index(app: "my_app_name", role: "my_role_name")
|
||||
f.merge!(:some_fact, "new-value")
|
||||
f.delete(:some_other_fact)
|
||||
f
|
||||
end
|
||||
|
||||
it "should do whatever..."
|
||||
...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Defining your own helper functions
|
||||
|
||||
You can also define your own helper functions by adding them to your `spec_helper` with no need to modify our code:
|
||||
|
||||
```
|
||||
# spec/spec_helper.rb
|
||||
# --
|
||||
|
||||
module Octofacts
|
||||
class Manipulators
|
||||
class AddFakeDrive
|
||||
def self.execute(fact_set, args, _)
|
||||
fact_set[:blockdevices] = (fact_set[:blockdevices] || "").split(",").concat(args[0]).join(",")
|
||||
fact_set[:"blockdevice_#{args[0]}_size"] = args[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# modules/modulename/spec/classes/classname_spec.rb
|
||||
# --
|
||||
describe modulename::classname do
|
||||
let(:node) { "fake-node.example.net" }
|
||||
let(:facts) { Octofacts.from_index(app: "my_app_name", role: "my_role_name").add_fake_drive("sdz", 21474836480) }
|
||||
|
||||
it "should do whatever..."
|
||||
...
|
||||
end
|
||||
end
|
||||
```
|
|
@ -0,0 +1,296 @@
|
|||
# Configuring octofacts-updater
|
||||
|
||||
`octofacts-updater` is a command line utility that anonymizes and sanitizes facts, builds and maintains index files, and (optionally) interacts directly with the GitHub API to create pull requests if there are any changes.
|
||||
|
||||
If you followed the [quick start tutorial](/doc/tutorial.md), you manually obtained a fact fixture by running `facter` on a host. However, since you did so without setting up a configuration file, your fact fixture may have contained sensitive information (e.g. the private SSH keys for the host). You also had to SSH into a host manually to run `facter` which is non-ideal for an automated setup.
|
||||
|
||||
This document will help you address all of these security and automation non-optimalities.
|
||||
|
||||
## Configuration file quick start
|
||||
|
||||
The easiest way to get started with `octofacts-updater` is to download and install our "quickstart" configuration file. In the git repo, this is found at [/examples/config/quickstart.yaml](/examples/config/quickstart.yaml). If you'd like to download the latest version directly from GitHub, you can use wget, like this:
|
||||
|
||||
```
|
||||
wget -O octofacts-updater.yaml https://raw.githubusercontent.com/github/octofacts/master/examples/config/quickstart.yaml
|
||||
```
|
||||
|
||||
## Data sources
|
||||
|
||||
`octofacts-updater` can obtain facts from the following data sources:
|
||||
|
||||
- Local files
|
||||
- PuppetDB
|
||||
- SSH
|
||||
|
||||
`octofacts-updater` will attempt to obtain facts from each of those data sources, in the order listed above. If retrieving facts from one data source succeeds, the subsequent data sources will not be contacted. If all of the data sources are unconfigured or fail, then an error is raised.
|
||||
|
||||
If you are running `octofacts-updater` from the command line, you can force a specific data source to be used with the `--datasource` option. For example:
|
||||
|
||||
```
|
||||
octofacts-updater --datasource localfiles ...
|
||||
octofacts-updater --datasource puppetdb ...
|
||||
octofacts-updater --datasource ssh ...
|
||||
```
|
||||
|
||||
### Local files
|
||||
|
||||
As seen in the [quick start tutorial](/doc/tutorial.md), if you make the facts for a node available in a YAML file, `octofacts-updater` can import it. This is great for testing, but it can also be used in complex environments where you cannot easily use the other built-in capabilities. In such a case, you can generate the YAML file with facts via some other method, and then import the result into octofacts.
|
||||
|
||||
There is no configuration needed in the `octofacts-updater` configuration file for the local file data source. Simply provide the full path to the file containing the facts on the command line as follows:
|
||||
|
||||
```
|
||||
octofacts-updater --datasource localfiles --config-override localfile:path=/tmp/facts.yaml --hostname <hostname> ...
|
||||
```
|
||||
|
||||
### PuppetDB
|
||||
|
||||
`octofacts-updater` can connect to PuppetDB (version 3.0 or higher) and retrieve the facts from the most recently reported run of Puppet on the node.
|
||||
|
||||
You can configure the PuppetDB connection by supplying the URL in the `octofacts-updater` configuration file.
|
||||
|
||||
```title=octofacts-updater.yaml
|
||||
puppetdb:
|
||||
url: https://puppetdb.example.net:8081
|
||||
```
|
||||
|
||||
### SSH
|
||||
|
||||
`octofacts-updater` can SSH to a node and run the command of your choice. There are two common strategies for this option: obtaining the facts from the cache of a puppetserver, or contacting an actual node to ask for its facts.
|
||||
|
||||
When configuring SSH connectivity, you must supply the following parameters:
|
||||
|
||||
- `server`: The system to SSH to.
|
||||
- `user`: The user to log in as.
|
||||
- `command`: The command to run on the target system.
|
||||
|
||||
You may use `%%NODE%%` in either the `server` or `command` parameter. This will be replaced with the hostname you have requested via the `--hostname` parameter.
|
||||
|
||||
Under the hood, `octofacts-updater` uses the [Ruby net-ssh gem](https://github.com/net-ssh/net-ssh). Any other options you supply to the `ssh` section will be symbolized and passed to the module. For example, you may use:
|
||||
|
||||
- `password`: Hard-code a password, instead of using public key authentication.
|
||||
- `port`: Choose a port other than the default port, 22.
|
||||
- `passphrase`: Hard-code the passphrase for a key.
|
||||
|
||||
#### Obtaining the facts from the cache of a puppetserver
|
||||
|
||||
In its default installation, the puppetserver will save a copy of the most recent facts for a node in the `/opt/puppetlabs/server/data/puppetserver/yaml/facts` directory. You can configure `octofacts-updater` to SSH to the puppetserver and `cat` the node's file.
|
||||
|
||||
```title=octofacts-updater.yaml
|
||||
ssh:
|
||||
server: puppetserver.example.net
|
||||
user: puppet
|
||||
command: cat /opt/puppetlabs/server/data/puppetserver/yaml/facts/%%NODE%%.yaml
|
||||
```
|
||||
|
||||
Note that the `/opt/puppetlabs/server/data/puppetserver/yaml/facts` may be limited, via Unix file permissions, to be accessible only to a `puppet` user. You may need to work around this by adding the `user` to an appropriate group, or by enabling the command to run under `sudo` without a password.
|
||||
|
||||
#### Contacting an actual node to ask for its facts
|
||||
|
||||
The SSH data source can be leveraged to contact the node whose facts are being determined, by using `%%NODE%%` as the server name. The following command will contact the node in question and run `facter` to get the facts in YAML format.
|
||||
|
||||
```title=octofacts-updater.yaml
|
||||
ssh:
|
||||
server: %%NODE%%
|
||||
user: puppet
|
||||
command: /opt/puppetlabs/puppet/bin/facter -p --yaml
|
||||
```
|
||||
|
||||
Note that `facter` may need to run as root to gather all of the facts for the system. You may need to work around this by enabling the command to run under `sudo` without a password.
|
||||
|
||||
Also, if you are using Puppet 4 or later, but are relying on "legacy" facts that were used in Puppet 3, you may need to add `--show-legacy` to the `facter` command line.
|
||||
|
||||
## Anonymizing and rewriting facts
|
||||
|
||||
To avoid committing sensitive information into source control, and to prevent rspec-puppet tests from inadvertently contacting actual systems, `octofacts-updater` supports anonymizing and rewriting facts. For example, you might remove or scramble SSH keys, delete or hard-code facts like system uptime that change upon each run, or change IP addresses.
|
||||
|
||||
You can configure this in the `octofacts-updater` configuration file. The [quickstart example configuration](/examples/config/quickstart.yaml) contains several examples.
|
||||
|
||||
`octofacts-updater` comes with several pre-built methods to adjust facts, and supports a plugin system to allow users to extend the functionality in their own environment. (If you write a plugin that you believe may be of general use, please check our [Contributing Document](/CONTRIBUTING.md) and look in the [plugins directory](/lib/octofacts_updater/plugins).)
|
||||
|
||||
To configure fact adjusting, define `facts` as a hash in the configuration file, with one entry per adjustment.
|
||||
|
||||
### Common actions
|
||||
|
||||
#### Deleting facts
|
||||
|
||||
If a fact does not contain useful information to your Puppet code, you may choose to remove it from the fact fixtures.
|
||||
|
||||
The following example will delete the `ec2_userdata` fact:
|
||||
|
||||
```
|
||||
facts:
|
||||
ec2_userdata:
|
||||
plugin: delete
|
||||
```
|
||||
|
||||
#### Setting fact to a static value
|
||||
|
||||
If a fact changes frequently, you may choose to set it to a static value. This will avoid making changes to the fact fixtures each time the updater runs.
|
||||
|
||||
The following example will set the `uptime_seconds` fact to `12345`, which will avoid rewriting the value of this fact each time the updater runs:
|
||||
|
||||
```
|
||||
facts:
|
||||
uptime_seconds:
|
||||
plugin: set
|
||||
value: 12345
|
||||
```
|
||||
|
||||
#### Obscuring SSH private keys
|
||||
|
||||
To avoid committing the SSH private key of a node (or any similar credential) into source control, you can use the `randomize_long_string` plugin. This will generate a string of random characters that is the same length as the original key.
|
||||
|
||||
The [quickstart example configuration](/examples/config/quickstart.yaml) invokes this for the standard SSH facts. In the following example, all facts that match the regular expression will have their values randomized.
|
||||
|
||||
```
|
||||
facts:
|
||||
ssh_keys:
|
||||
regexp: ^ssh\w+key$
|
||||
plugin: randomize_long_string
|
||||
```
|
||||
|
||||
Note that it is not possible to reconstruct the original key from the random key; however, the original key is used to seed the random number generator so the random key will be consistent so long as the original key does not change.
|
||||
|
||||
#### Randomizing IP addresses
|
||||
|
||||
To prevent rspec-puppet tests from contacting or accessing actual hosts, you may choose to use random IP addresses to replace actual IP addresses. (Best practice would dictate that you isolate your continuous integration environment from your production environment, but this doesn't always happen...)
|
||||
|
||||
You can use the IP anonymization plugin to adjust `ipaddress` fact as follows:
|
||||
|
||||
```
|
||||
facts:
|
||||
ipaddress:
|
||||
plugin: ipv4_anonymize
|
||||
subnet: "10.0.1.0/24"
|
||||
```
|
||||
|
||||
The original IP address is used to seed the random number generator, so the random IP address chosen will be consistent so long as the original IP address does not change. There is a corresponding `ipv6_anonymize` plugin to generate random IPv6 addresses on a specified IPv6 subnet.
|
||||
|
||||
### Handling structured or multiple facts
|
||||
|
||||
Structured facts are organized in a hash structure, like in this example:
|
||||
|
||||
```
|
||||
ssh:
|
||||
dsa:
|
||||
fingerprints:
|
||||
sha1: SSHFP 2 1 abcdefabcdefabcdefabcdefabcdefabcdefabcd
|
||||
sha256: SSHFP 2 2 abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd
|
||||
key: AAAAxxxxxxxxxxxx...
|
||||
ecdsa:
|
||||
fingerprints:
|
||||
sha1: SSHFP 3 1 abcdefabcdefabcdefabcdefabcdefabcdefabcd
|
||||
sha256: SSHFP 3 2 abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd
|
||||
key: AAAAxxxxxxxxxxxx...
|
||||
...
|
||||
```
|
||||
|
||||
It is possible to use regular expressions to "dig" into the structure with the following format:
|
||||
|
||||
```
|
||||
facts:
|
||||
ssh:
|
||||
structure:
|
||||
- regexp: .+
|
||||
- regexp: ^key$
|
||||
plugin: randomize_long_string
|
||||
```
|
||||
|
||||
In the example above, the code will explore the structured fact named `ssh`. At the first level, it will match the regular expression `.+` (which is any number of any character, i.e., every element). Then in each matching level, it will match the regular expression `^key$` (which is an exact match of the string "key"). For each match, the plugin will be executed. In the example above:
|
||||
|
||||
```
|
||||
ssh:
|
||||
dsa:
|
||||
fingerprints:
|
||||
sha1: SSHFP 2 1 abcdefabcdefabcdefabcdefabcdefabcdefabcd
|
||||
sha256: SSHFP 2 2 abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd
|
||||
key: THIS_IS_RANDOMIZED...
|
||||
ecdsa:
|
||||
fingerprints:
|
||||
sha1: SSHFP 3 1 abcdefabcdefabcdefabcdefabcdefabcdefabcd
|
||||
sha256: SSHFP 3 2 abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd
|
||||
key: THIS_IS_RANDOMIZED...
|
||||
...
|
||||
```
|
||||
|
||||
### Extending `octofacts-updater` with plugins
|
||||
|
||||
`octofacts-updater` supports a plugin architecture to add additional fact anonymizers. The methods included with the gem are also written in this plugin architecture. You can read the [plugin reference documentation](/doc/plugin-reference.md) or browse the [plugins directory](/lib/octofacts_updater/plugins).
|
||||
|
||||
If you find yourself needing to create additional plugins for your site-specific needs, you can include those plugins by placing entries in the octofacts-updater configuration file that reference the files where those plugins exist:
|
||||
|
||||
```title=octofacts-updater.yaml
|
||||
plugins:
|
||||
- /usr/local/lib/octofacts-updater/custom-plugins.rb
|
||||
- /usr/local/lib/octofacts-updater/more-plugins.rb
|
||||
```
|
||||
|
||||
Each plugin is created with code in the following general structure:
|
||||
|
||||
```title=plugin.rb
|
||||
# Plugin name: name_of_plugin
|
||||
#
|
||||
# fact - OctofactsUpdater::Fact object of the fact being manipulated
|
||||
# args - Hash with arguments specified in the configuration file (e.g. "structure" plus any other parameters)
|
||||
# facts - Hash of { fact_name, OctofactsUpdater::Fact } for every fact defined (in case one fact needs to reference another)
|
||||
#
|
||||
# The method should adjust `fact` by calling methods such as `.set_value`. It should NOT modify `args` or `facts`.
|
||||
#
|
||||
OctofactsUpdater::Plugin.register(:name_of_plugin) do |fact, args = {}, facts|
|
||||
value = fact.value
|
||||
new_value = # Your code here
|
||||
fact.set_value(new_value)
|
||||
end
|
||||
```
|
||||
|
||||
## Automating Pull Request creation with GitHub
|
||||
|
||||
To configure `octofacts-updater` to push changes to a branch on GitHub and open pull requests, use the following configuration:
|
||||
|
||||
```title=octofacts-updater.yaml
|
||||
github:
|
||||
branch: octofacts-updater
|
||||
pr_subject: Automated fact fixture update for octofacts
|
||||
pr_body: A nice inspiring and helpful message to yourself
|
||||
repository: example/puppet
|
||||
commit_message: Automated fact fixture update for octofacts
|
||||
base_directory: /Users/hubot/projects/puppet
|
||||
# token: We recommend that you set ENV["OCTOKIT_TOKEN"] instead of hard-coding a token here.
|
||||
```
|
||||
|
||||
#### Your personal access token
|
||||
|
||||
Head to [https://github.com/settings/tokens](https://github.com/settings/tokens) to generate an access token.
|
||||
|
||||
It is possible to place the token into the configuration file like:
|
||||
|
||||
```
|
||||
github:
|
||||
token: abcdefg1234567
|
||||
```
|
||||
|
||||
However, we recommend that you do not do this, especially if the configuration file is going to be checked in to a source code repository. Instead, you may set the environment variable `OCTOKIT_TOKEN` with the contents of your token. ("octokit" is a reference to the [octokit gem](https://github.com/octokit/octokit.rb), which underlies the GitHub integration of `octofacts-updater`.)
|
||||
|
||||
#### Using the GitHub integration
|
||||
|
||||
1. The GitHub integration is only available when running with `--action bulk`, as it is designed to push a comprehensive change set.
|
||||
|
||||
2. To trigger the GitHub integration, supply `--github` as a command line argument when running `octofacts-updater`. (This will raise an error if there is no `github` section in the configuration, or if required parameters are missing.)
|
||||
|
||||
3. The `base_directory` setting distinguishes between the directory paths on the system you're running on, and the repository you're committing to. As an example, consider that a user has checked out their Puppet code to `/Users/hubot/projects/puppet` and their octofacts-managed fact fixtures are in `/Users/hubot/projects/puppet/spec/fixtures/facts/octofacts`. They want to create and manage files in `spec/fixtures/facts/octofacts` within the repo, but don't want to create a `/Users/hubot/projects/puppet` directory there. As a reminder, `--config-override github:base_directory=/Users/hubot/projects/puppet` is available to override parameters in the configuration file.
|
||||
|
||||
#### Tips
|
||||
|
||||
1. Be sure that you delete the branch on GitHub once you've merged it, so that it can be recreated from the most recent default branch the next time `octofacts-updater` is executed.
|
||||
|
||||
2. The GitHub integration does not merge the default branch into your branch automatically. You can do this on GitHub with the "Update Branch" button.
|
||||
|
||||
## Putting it all together
|
||||
|
||||
Once you've configured your data source and paths, and optionally the integration to GitHub, it's time to run `octofacts-updater`. Assuming you've followed our examples, your command would look like this to build the initial list of fixtures:
|
||||
|
||||
```
|
||||
bin/octofacts-updater --config octofacts-updater.yaml -a bulk -l <fqdn1>,<fqdn2>,<fqdn3>,...
|
||||
```
|
||||
|
||||
Thereafter, the list of nodes to index will be pulled from the index file, so you won't need to list them out each time.
|
|
@ -0,0 +1,245 @@
|
|||
# Plugin refeerence for octofacts-updater
|
||||
|
||||
Please refer to the [octofacts-updater documentation](/doc/octofacts-updater.md) for general instructions to configure the system.
|
||||
|
||||
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.
|
||||
|
||||
## delete
|
||||
|
||||
Source: [static.rb](/lib/octofacts_updater/plugins/static.rb)
|
||||
|
||||
Description: Deletes a fact or component of a structured fact.
|
||||
|
||||
Parameters: (None)
|
||||
|
||||
Supports structured facts: Yes
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
facts:
|
||||
some_fact_to_delete:
|
||||
plugin: delete
|
||||
some_structured_fact:
|
||||
structure:
|
||||
- regexp: .+
|
||||
- regexp: _key$
|
||||
plugin: delete
|
||||
```
|
||||
|
||||
## ipv4_anonymize
|
||||
|
||||
Source: [ip.rb](/lib/octofacts_updater/plugins/ip.rb)
|
||||
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
|
||||
| Parameter | Required? | Description |
|
||||
| --------- | --------- | ----------- |
|
||||
| `subnet` | Yes | CIDR notation of subnet from which random IP is to be chosen |
|
||||
|
||||
Supports structured facts: Yes
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
ipaddress:
|
||||
plugin: ipv4_randomize
|
||||
subnet: 10.1.0.0/24
|
||||
```
|
||||
|
||||
## ipv6_anonymize
|
||||
|
||||
Source: [ip.rb](/lib/octofacts_updater/plugins/ip.rb)
|
||||
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
|
||||
| Parameter | Required? | Description |
|
||||
| --------- | --------- | ----------- |
|
||||
| `subnet` | Yes | CIDR notation of subnet from which random IP is to be chosen |
|
||||
|
||||
Supports structured facts: Yes
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
ipaddress:
|
||||
plugin: ipv6_randomize
|
||||
subnet: "fd00::/8"
|
||||
```
|
||||
|
||||
## noop
|
||||
|
||||
Source: [static.rb](/lib/octofacts_updater/plugins/static.rb)
|
||||
|
||||
Description: Does nothing at all.
|
||||
|
||||
Parameters: (None)
|
||||
|
||||
Supports structured facts: Yes
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
facts:
|
||||
ec2_userdata:
|
||||
plugin: noop
|
||||
```
|
||||
|
||||
## randomize_long_string
|
||||
|
||||
Source: [static.rb](/lib/octofacts_updater/plugins/static.rb)
|
||||
|
||||
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).
|
||||
|
||||
Parameters: (None)
|
||||
|
||||
Supports structured facts: Yes
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
facts:
|
||||
some_fact_to_modify:
|
||||
plugin: randomize_long_string
|
||||
some_structured_fact:
|
||||
structure:
|
||||
- regexp: .+
|
||||
- regexp: _key$
|
||||
plugin: randomize_long_string
|
||||
```
|
||||
|
||||
Example result:
|
||||
|
||||
```
|
||||
some_fact_to_modify: randomrandomrandom
|
||||
some_structured_fact:
|
||||
foo:
|
||||
ssl_cert: ABCDEF...
|
||||
ssl_key: randomrandomrandom
|
||||
bar:
|
||||
ssl_cert: 012345...
|
||||
ssl_key: randomrandomrandom
|
||||
```
|
||||
|
||||
## remove_from_delimited_string
|
||||
|
||||
Source: [static.rb](/lib/octofacts_updater/plugins/static.rb)
|
||||
|
||||
Description: Given a string that is delimited, remove all elements from that string that match the provided regular expression.
|
||||
|
||||
Parameters:
|
||||
|
||||
| Parameter | Required? | Description |
|
||||
| --------- | --------- | ----------- |
|
||||
| `delimiter` | Yes | Character that is the delimiter |
|
||||
| `regexp` | Yes | Remove all items from the string matching this regexp |
|
||||
|
||||
Supports structured facts: Yes
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
facts:
|
||||
interfaces:
|
||||
plugin: remove_from_delimited_string
|
||||
delimiter: ,
|
||||
regexp: ^tun\d+
|
||||
```
|
||||
|
||||
Example result:
|
||||
|
||||
```
|
||||
# Before
|
||||
interfaces: eth0,eth1,bond0,tun0,tun1,tun2,lo
|
||||
|
||||
# After
|
||||
interfaces: eth0,eth1,bond0,lo
|
||||
```
|
||||
|
||||
## set
|
||||
|
||||
Source: [static.rb](/lib/octofacts_updater/plugins/static.rb)
|
||||
|
||||
Description: Sets the value of a fact or component of a structured fact to a pre-determined value.
|
||||
|
||||
Parameters:
|
||||
|
||||
| Parameter | Required? | Description |
|
||||
| --------- | --------- | ----------- |
|
||||
| `value` | Yes | Static value to set |
|
||||
|
||||
Supports structured facts: Yes
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
facts:
|
||||
some_fact_to_modify:
|
||||
plugin: set
|
||||
value: new_value_of_fact
|
||||
some_structured_fact:
|
||||
structure:
|
||||
- regexp: .+
|
||||
- regexp: _key$
|
||||
plugin: set
|
||||
value: we_dont_include_keys
|
||||
```
|
||||
|
||||
Example result:
|
||||
|
||||
```
|
||||
some_fact_to_modify: new_value_of_fact
|
||||
some_structured_fact:
|
||||
foo:
|
||||
ssl_cert: ABCDEF...
|
||||
ssl_key: we_dont_include_keys
|
||||
bar:
|
||||
ssl_cert: 012345...
|
||||
ssl_key: we_dont_include_keys
|
||||
```
|
||||
|
||||
## sshfp_randomize
|
||||
|
||||
Source: [ssh.rb](/lib/octofacts_updater/plugins/ssh.rb)
|
||||
|
||||
Description: Sets the SSH fingerprint portion of a fact to a random string, while preserving the numeric portion and other structure.
|
||||
|
||||
Parameters: (None)
|
||||
|
||||
Supports structured facts: Yes
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
facts:
|
||||
ssh:
|
||||
plugin: sshfp_randomize
|
||||
structure:
|
||||
- regexp: .*
|
||||
- regexp: ^fingerprints$
|
||||
- regexp: ^sha\d+
|
||||
```
|
||||
|
||||
Example result:
|
||||
|
||||
```
|
||||
# Before
|
||||
ssh:
|
||||
rsa:
|
||||
fingerprints:
|
||||
sha1: SSHFP 1 1 abcdefabcdefabcdefabcdefabcdefabcdefabcd
|
||||
sha256: SSHFP 1 2 abcdefabcdefabcdefabcdefabcdefabcdefabcdabcdefabcdefabcdefabcdef
|
||||
key: AAAA0123456012345601234560123456
|
||||
|
||||
# After
|
||||
ssh:
|
||||
rsa:
|
||||
fingerprints:
|
||||
sha1: SSHFP 1 1 randomrandomrandomrandomrandomrandomrand
|
||||
sha256: SSHFP 1 2 randomrandomrandomrandomrandomrandomrandomrandomrandomrandomrand
|
||||
key: AAAA0123456012345601234560123456
|
||||
```
|
|
@ -0,0 +1,141 @@
|
|||
# Octofacts quick-start tutorial
|
||||
|
||||
Hello there! This tutorial is intended to get you up and running quickly with octofacts, so that you can see its capabilities.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you get started with this tutorial, please make sure that the following prerequisites are in place.
|
||||
|
||||
- You should already have [rspec-puppet](http://rspec-puppet.com/) up and running for your Puppet repository.
|
||||
- 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/).
|
||||
- You should have at least one rspec-puppet test that is passing.
|
||||
|
||||
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.
|
||||
|
||||
## Installing octofacts and octofacts-updater
|
||||
|
||||
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:
|
||||
|
||||
- https://rubygems.org/gems/octofacts
|
||||
- https://rubygems.org/gems/octofacts-updater
|
||||
|
||||
Alternatively, you can directly install octofacts and octofacts-updater into your current ruby environment using:
|
||||
|
||||
```
|
||||
gem install octofacts octofacts-updater
|
||||
```
|
||||
|
||||
## Creating the directory structure
|
||||
|
||||
The remainder of this tutorial assumes you will be using the following layout for octofacts fixture files:
|
||||
|
||||
```
|
||||
- <base directory>/
|
||||
- spec/
|
||||
- spec_helper.rb
|
||||
- fixtures/
|
||||
- facts/
|
||||
- octofacts/
|
||||
- node-1.example.net.yaml
|
||||
- node-2.example.net.yaml
|
||||
- node-3.example.net.yaml
|
||||
- octofacts-index.yaml
|
||||
```
|
||||
|
||||
To create the necessary directory structure, `cd` to the "spec" directory of your checkout, and then make the directories.
|
||||
|
||||
```
|
||||
cd <base_directory>
|
||||
cd spec
|
||||
mkdir -p fixtures/facts/octofacts
|
||||
```
|
||||
|
||||
## Get your first set of facts
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
your-workstation$ export TARGET_HOSTNAME="some-host.yourdomain.com" #<-- change as needed for your situation
|
||||
|
||||
your-workstation$ ssh "$TARGET_HOSTNAME"
|
||||
|
||||
some-host$ sudo facter -p --yaml > facts.yaml
|
||||
some-host$ exit
|
||||
|
||||
your-workstation$ scp "$TARGET_HOSTNAME":~/facts.yaml /tmp/facts.yaml
|
||||
```
|
||||
|
||||
Now you can run `octofacts-updater` to import this set of facts into your code repository.
|
||||
|
||||
```
|
||||
cd <base_directory>
|
||||
|
||||
# If you installed with `gem install`
|
||||
octofacts-updater --action facts --hostname "$TARGET_HOSTNAME" \
|
||||
--datasource localfile --config-override localfile:path=/tmp/facts.yaml \
|
||||
--output-file "spec/fixtures/facts/octofacts/${TARGET_HOSTNAME}.yaml"
|
||||
|
||||
# If you installed with `bundler`
|
||||
bundle exec bin/octofacts-updater --action facts --hostname "$TARGET_HOSTNAME" \
|
||||
--datasource localfile --config-override localfile:path=/tmp/facts.yaml \
|
||||
--output-file "spec/fixtures/facts/octofacts/${TARGET_HOSTNAME}.yaml"
|
||||
|
||||
# Once you've done either of those commands, you should be able to see your file
|
||||
cat "spec/fixtures/facts/octofacts/${TARGET_HOSTNAME}.yaml"
|
||||
```
|
||||
|
||||
: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.
|
||||
|
||||
## Update your rspec-puppet spec helper to use octofacts
|
||||
|
||||
Add the following lines to your `spec/spec_helper.rb` file:
|
||||
|
||||
```title=spec_helper.rb
|
||||
require "octofacts"
|
||||
ENV["OCTOFACTS_FIXTURE_PATH"] ||= File.expand_path("fixtures/facts/octofacts", File.dirname(__FILE__))
|
||||
ENV["OCTOFACTS_INDEX_PATH"] ||= File.expand_path("fixtures/facts/octofacts-index.yaml", File.dirname(__FILE__))
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Update your rspec-puppet test to use the facts you just installed
|
||||
|
||||
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.
|
||||
|
||||
Your existing test might look something like this:
|
||||
|
||||
```title=example_spec.rb
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'module::class' do
|
||||
let(:node) { 'some-host.yourdomain.com' }
|
||||
|
||||
let(:facts) do
|
||||
{
|
||||
...
|
||||
}
|
||||
end
|
||||
|
||||
it 'should do something' do
|
||||
is_expected.to ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Change *only* the facts section to:
|
||||
|
||||
```
|
||||
let(:facts) { Octofacts.from_file('some-host.yourdomain.com.yaml') }
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Now, run your test. If it passes, then congratulations -- you have successfully set up octofacts!
|
||||
|
||||
## Next steps
|
||||
|
||||
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.
|
||||
|
||||
- [Configuring octofacts-updater](/doc/octofacts-updater.md)
|
|
@ -0,0 +1,94 @@
|
|||
# Configuration file for the octofacts updater.
|
||||
---
|
||||
# This section configures a connection to PuppetDB, so you can retrieve facts from there.
|
||||
# Your PuppetDB must support query API v4 (which is supported in PuppetDB 3.0 and higher.)
|
||||
# puppetdb:
|
||||
# url: https://puppetdb.example.net:8081
|
||||
|
||||
# This section configures an SSH connection to a Puppet Server, so you can retrieve facts
|
||||
# from its cache. The 'server' and 'user' parameters are required.
|
||||
#
|
||||
# 'command' defaults to running "cat /opt/puppetlabs/server/data/puppetserver/yaml/facts/%%NODE%%.yaml"
|
||||
# on the target system. Please note that the user you log in as must be sufficiently privileged, as normally
|
||||
# this directory is locked down only to the puppet user.
|
||||
#
|
||||
# Any other parameters you provide will be symbolized and passed to net-ssh (https://github.com/net-ssh/net-ssh).
|
||||
# ssh:
|
||||
# server: puppetserver.example.net
|
||||
# user: puppet
|
||||
# command: cat /opt/puppetlabs/server/data/puppetserver/yaml/facts/%%NODE%%.yaml
|
||||
# password: secret001
|
||||
|
||||
# You can also configure "ssh" to log in to a specific server. This is useful if you want to run "facter" on
|
||||
# that system and capture the results. Similar to the previous section, "%%NODE%%" is replaced with the FQDN
|
||||
# of the node whose facts you are gathering.
|
||||
# ssh:
|
||||
# server: %%NODE%%
|
||||
# user: puppet
|
||||
# command: /opt/puppetlabs/puppet/bin/facter -p --yaml
|
||||
|
||||
# This section controls your index file. If you'd like to select appropriate fact fixtures
|
||||
# without explicitly naming a node, list the facts here that you would like to have indexed.
|
||||
# You can also specify the path to the index file. Be sure to adjust it for your site.
|
||||
index:
|
||||
file: /var/lib/jenkins/workspace/puppet/spec/fixtures/facts/octofacts-index.yaml
|
||||
indexed_facts:
|
||||
- virtual
|
||||
|
||||
# This section cleans up and anonymizes facts. We have added a number of facts to this list
|
||||
# that contain sensitive information or change frequently. You should add any facts from your
|
||||
# site as needed.
|
||||
facts:
|
||||
ec2_userdata:
|
||||
plugin: delete
|
||||
trusted:
|
||||
plugin: delete
|
||||
ec2_metadata:
|
||||
structure: iam::info
|
||||
plugin: delete
|
||||
ec2_iam_info_NNN:
|
||||
regexp: ^ec2_iam_info_\d+
|
||||
plugin: delete
|
||||
uptime:
|
||||
regexp: ^uptime
|
||||
plugin: delete
|
||||
memoryfree:
|
||||
regexp: ^memoryfree
|
||||
plugin: delete
|
||||
swapfree:
|
||||
regexp: ^swapfree
|
||||
plugin: delete
|
||||
load_averages:
|
||||
plugin: delete
|
||||
memory_stats:
|
||||
fact: memory
|
||||
structure:
|
||||
- regexp: .+
|
||||
- regexp: ^(available|available_bytes|capacity|used|used_bytes)$
|
||||
plugin: delete
|
||||
system_uptime:
|
||||
value:
|
||||
days: 17
|
||||
hours: 415
|
||||
seconds: 1495197
|
||||
uptime: 17 days
|
||||
plugin: set
|
||||
ssh_keys:
|
||||
regexp: ^ssh\w+key$
|
||||
plugin: randomize_long_string
|
||||
sshfp_keys:
|
||||
regexp: ^sshfp_\w+$
|
||||
plugin: sshfp_randomize
|
||||
ssh_structured_keys:
|
||||
fact: ssh
|
||||
structure:
|
||||
- regexp: .+
|
||||
- regexp: ^key$
|
||||
plugin: randomize_long_string
|
||||
ssh_structured_fingerprints:
|
||||
fact: ssh
|
||||
structure:
|
||||
- regexp: .+
|
||||
- regexp: ^fingerprints$
|
||||
- regexp: ^sha\d+$
|
||||
plugin: sshfp_randomize
|
|
@ -0,0 +1,15 @@
|
|||
require "octofacts/constructors/from_file"
|
||||
require "octofacts/constructors/from_index"
|
||||
require "octofacts/manipulators"
|
||||
require "octofacts/errors"
|
||||
require "octofacts/facts"
|
||||
require "octofacts/backends/base"
|
||||
require "octofacts/backends/index"
|
||||
require "octofacts/backends/yaml_file"
|
||||
require "octofacts/util/config"
|
||||
require "octofacts/util/keys"
|
||||
require "octofacts/version"
|
||||
|
||||
module Octofacts
|
||||
#
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
module Octofacts
|
||||
module Backends
|
||||
# This is a template class to define the minimum API to be implemented
|
||||
class Base
|
||||
# Returns a hash of the facts selected based on current criteria. Once this is done,
|
||||
# it is no longer possible to select, reject, or prefer.
|
||||
def facts
|
||||
# :nocov:
|
||||
raise NotImplementedError, "This method needs to be implemented in the subclass"
|
||||
# :nocov:
|
||||
end
|
||||
|
||||
# Filters the possible fact sets based on the criteria.
|
||||
def select(*)
|
||||
# :nocov:
|
||||
raise NotImplementedError, "This method needs to be implemented in the subclass"
|
||||
# :nocov:
|
||||
end
|
||||
|
||||
# Removes possible fact sets based on the criteria.
|
||||
def reject(*)
|
||||
# :nocov:
|
||||
raise NotImplementedError, "This method needs to be implemented in the subclass"
|
||||
# :nocov:
|
||||
end
|
||||
|
||||
# Reorders possible fact sets based on the criteria.
|
||||
def prefer(*)
|
||||
# :nocov:
|
||||
raise NotImplementedError, "This method needs to be implemented in the subclass"
|
||||
# :nocov:
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,127 @@
|
|||
require "yaml"
|
||||
require "set"
|
||||
|
||||
module Octofacts
|
||||
module Backends
|
||||
class Index < Base
|
||||
attr_reader :index_path, :fixture_path, :options
|
||||
attr_writer :facts
|
||||
attr_accessor :nodes
|
||||
|
||||
def initialize(args = {})
|
||||
index_path = Octofacts::Util::Config.fetch(:octofacts_index_path, args)
|
||||
fixture_path = Octofacts::Util::Config.fetch(:octofacts_fixture_path, args)
|
||||
strict_index = Octofacts::Util::Config.fetch(:octofacts_strict_index, args, false)
|
||||
|
||||
raise(ArgumentError, "No index passed and ENV['OCTOFACTS_INDEX_PATH'] is not defined") if index_path.nil?
|
||||
raise(ArgumentError, "No fixture path passed and ENV['OCTOFACTS_FIXTURE_PATH'] is not defined") if fixture_path.nil?
|
||||
raise(Errno::ENOENT, "The index file #{index_path} does not exist") unless File.file?(index_path)
|
||||
raise(Errno::ENOENT, "The fixture path #{fixture_path} does not exist") unless File.directory?(fixture_path)
|
||||
|
||||
@index_path = index_path
|
||||
@fixture_path = fixture_path
|
||||
@strict_index = strict_index == true || strict_index == "true"
|
||||
@facts = nil
|
||||
@options = args
|
||||
|
||||
@node_facts = {}
|
||||
|
||||
# If there are any other arguments treat them as `select` conditions.
|
||||
remaining_args = args.dup
|
||||
remaining_args.delete(:octofacts_index_path)
|
||||
remaining_args.delete(:octofacts_fixture_path)
|
||||
remaining_args.delete(:octofacts_strict_index)
|
||||
select(remaining_args) if remaining_args
|
||||
end
|
||||
|
||||
def facts
|
||||
@facts ||= node_facts(nodes.first)
|
||||
end
|
||||
|
||||
def select(conditions)
|
||||
Octofacts::Util::Keys.desymbolize_keys!(conditions)
|
||||
conditions.each do |key, value|
|
||||
add_fact_to_index(key) unless indexed_fact?(key)
|
||||
matching_nodes = index[key][value.to_s]
|
||||
raise Octofacts::Errors::NoFactsError if matching_nodes.nil?
|
||||
self.nodes = nodes & matching_nodes
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def reject(conditions)
|
||||
matching_nodes = nodes
|
||||
Octofacts::Util::Keys.desymbolize_keys!(conditions)
|
||||
conditions.each do |key, value|
|
||||
add_fact_to_index(key) unless indexed_fact?(key)
|
||||
unless index[key][value.to_s].nil?
|
||||
matching_nodes -= index[key][value.to_s]
|
||||
raise Octofacts::Errors::NoFactsError if matching_nodes.empty?
|
||||
end
|
||||
end
|
||||
|
||||
self.nodes = matching_nodes
|
||||
self
|
||||
end
|
||||
|
||||
def prefer(conditions)
|
||||
Octofacts::Util::Keys.desymbolize_keys!(conditions)
|
||||
conditions.each do |key, value|
|
||||
add_fact_to_index(key) unless indexed_fact?(key)
|
||||
matching_nodes = index[key][value.to_s]
|
||||
unless matching_nodes.nil?
|
||||
self.nodes = (matching_nodes.to_set + nodes.to_set).to_a
|
||||
end
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# If a select/reject/prefer is called and the fact is not in the index, this will
|
||||
# load the fact files for all currently eligible nodes and then add the fact to the
|
||||
# in-memory index. This can be memory-intensive and time-intensive depending on the
|
||||
# number of fact fixtures, so it is possible to disable this by passing
|
||||
# `:strict_index => true` to the backend constructor, or by setting
|
||||
# ENV["OCTOFACTS_STRICT_INDEX"] = "true" in the environment.
|
||||
def add_fact_to_index(fact)
|
||||
if @strict_index || ENV["OCTOFACTS_STRICT_INDEX"] == "true"
|
||||
raise Octofacts::Errors::FactNotIndexed, "Fact #{fact} is not indexed and strict indexing is enabled."
|
||||
end
|
||||
|
||||
index[fact] ||= {}
|
||||
nodes.each do |node|
|
||||
v = node_facts(node)[fact]
|
||||
if v.nil?
|
||||
# TODO: Index this somehow
|
||||
else
|
||||
index[fact][v.to_s] ||= []
|
||||
index[fact][v.to_s] << node
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def nodes
|
||||
@nodes ||= index["_nodes"]
|
||||
end
|
||||
|
||||
def index
|
||||
@index ||= YAML.safe_load(File.read(index_path))
|
||||
end
|
||||
|
||||
def indexed_fact?(fact)
|
||||
index.key?(fact)
|
||||
end
|
||||
|
||||
def node_facts(node)
|
||||
@node_facts[node] ||= begin
|
||||
f = YAML.safe_load(File.read("#{fixture_path}/#{node}.yaml"))
|
||||
Octofacts::Util::Keys.desymbolize_keys!(f)
|
||||
f
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
require "yaml"
|
||||
|
||||
module Octofacts
|
||||
module Backends
|
||||
class YamlFile < Base
|
||||
attr_reader :filename, :options
|
||||
|
||||
def initialize(filename, options = {})
|
||||
raise(Errno::ENOENT, "The file #{filename} does not exist") unless File.file?(filename)
|
||||
|
||||
@filename = filename
|
||||
@options = options
|
||||
@facts = nil
|
||||
end
|
||||
|
||||
def facts
|
||||
@facts ||= begin
|
||||
f = YAML.safe_load(File.read(filename))
|
||||
Octofacts::Util::Keys.symbolize_keys!(f)
|
||||
f
|
||||
end
|
||||
end
|
||||
|
||||
def select(conditions)
|
||||
Octofacts::Util::Keys.symbolize_keys!(conditions)
|
||||
raise Octofacts::Errors::NoFactsError unless (conditions.to_a - facts.to_a).empty?
|
||||
end
|
||||
|
||||
def reject(conditions)
|
||||
Octofacts::Util::Keys.symbolize_keys!(conditions)
|
||||
raise Octofacts::Errors::NoFactsError if (conditions.to_a - facts.to_a).empty?
|
||||
end
|
||||
|
||||
def prefer(conditions)
|
||||
# noop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
module Octofacts
|
||||
# Octofacts.from_file(filename, options) - Construct Octofacts::Facts from a filename.
|
||||
#
|
||||
# filename - Relative or absolute path to the file containing the facts.
|
||||
# opts[:octofacts_fixture_path] - Directory where fact fixture files are found (default: ENV["OCTOFACTS_FIXTURE_PATH"])
|
||||
#
|
||||
# Returns an Octofacts::Facts object.
|
||||
def self.from_file(filename, opts = {})
|
||||
unless filename.start_with? "/"
|
||||
dir = Octofacts::Util::Config.fetch(:octofacts_fixture_path, opts)
|
||||
raise ArgumentError, ".from_file needs to know :octofacts_fixture_path or environment OCTOFACTS_FIXTURE_PATH" unless dir
|
||||
raise Errno::ENOENT, "The provided fixture path #{dir} is invalid" unless File.directory?(dir)
|
||||
filename = File.join(dir, filename)
|
||||
end
|
||||
|
||||
Octofacts::Facts.new(backend: Octofacts::Backends::YamlFile.new(filename), options: opts)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
module Octofacts
|
||||
# Octofacts.from_index(options) - Construct Octofacts::Facts from an index file.
|
||||
#
|
||||
# Returns an Octofacts::Facts object.
|
||||
def self.from_index(opts = {})
|
||||
Octofacts::Facts.new(backend: Octofacts::Backends::Index.new(opts), options: opts)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
module Octofacts
|
||||
class Errors
|
||||
class FactNotIndexed < RuntimeError; end
|
||||
class OperationNotPermitted < RuntimeError; end
|
||||
class NoFactsError < RuntimeError; end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,121 @@
|
|||
require "yaml"
|
||||
|
||||
module Octofacts
|
||||
class Facts
|
||||
attr_writer :facts
|
||||
|
||||
# Constructor.
|
||||
#
|
||||
# backend - An Octofacts::Backends object (preferred)
|
||||
# options - Additional options (e.g., downcase keys, symbolize keys, etc.)
|
||||
def initialize(args = {})
|
||||
@backend = args.fetch(:backend)
|
||||
@facts_manipulated = false
|
||||
|
||||
options = args.fetch(:options, {})
|
||||
@downcase_keys = args.fetch(:downcase_keys, options.fetch(:downcase_keys, true))
|
||||
end
|
||||
|
||||
# To hash. (This method is intended to be called by rspec-puppet.)
|
||||
#
|
||||
# This loads the fact file and downcases, desymbolizes, and otherwise manipulates the keys.
|
||||
# The output is suitable for consumption by rspec-puppet.
|
||||
def to_hash
|
||||
f = facts
|
||||
downcase_keys!(f) if @downcase_keys
|
||||
desymbolize_keys!(f)
|
||||
f
|
||||
end
|
||||
alias_method :to_h, :to_hash
|
||||
|
||||
# To fact hash. (This method is intended to be called by developers.)
|
||||
#
|
||||
# This loads the fact file and downcases, symbolizes, and otherwise manipulates the keys.
|
||||
# This is very similar to 'to_hash' except that it returns symbolized keys.
|
||||
# The output is suitable for consumption by rspec-puppet (note that rspec-puppet will
|
||||
# de-symbolize all the keys in the hash object though).
|
||||
def facts
|
||||
@facts ||= begin
|
||||
f = @backend.facts
|
||||
downcase_keys!(f) if @downcase_keys
|
||||
symbolize_keys!(f)
|
||||
f
|
||||
end
|
||||
end
|
||||
|
||||
# Calls to backend methods.
|
||||
#
|
||||
# These calls are passed through directly to backend methods.
|
||||
def select(*args)
|
||||
if @facts_manipulated
|
||||
raise Octofacts::Errors::OperationNotPermitted, "Cannot call select() after backend facts have been manipulated"
|
||||
end
|
||||
@backend.select(*args)
|
||||
self
|
||||
end
|
||||
|
||||
def reject(*args)
|
||||
if @facts_manipulated
|
||||
raise Octofacts::Errors::OperationNotPermitted, "Cannot call reject() after backend facts have been manipulated"
|
||||
end
|
||||
@backend.reject(*args)
|
||||
self
|
||||
end
|
||||
|
||||
def prefer(*args)
|
||||
if @facts_manipulated
|
||||
raise Octofacts::Errors::OperationNotPermitted, "Cannot call prefer() after backend facts have been manipulated"
|
||||
end
|
||||
@backend.prefer(*args)
|
||||
self
|
||||
end
|
||||
|
||||
# Missing method - this is used to dispatch to manipulators or to call a Hash method in the facts.
|
||||
#
|
||||
# Try calling a Manipulator method, delegate to the facts hash or else error out.
|
||||
#
|
||||
# Returns this object (so that calls to manipulators can be chained).
|
||||
def method_missing(name, *args, &block)
|
||||
if Octofacts::Manipulators.run(self, name, *args, &block)
|
||||
@facts_manipulated = true
|
||||
return self
|
||||
end
|
||||
|
||||
if facts.respond_to?(name)
|
||||
if args[0].is_a?(String) || args[0].is_a?(Symbol)
|
||||
args[0] = string_or_symbolized_key(args[0])
|
||||
end
|
||||
return facts.send(name, *args)
|
||||
end
|
||||
|
||||
raise NameError, "Unknown method '#{name}' in #{self.class}"
|
||||
end
|
||||
|
||||
def respond_to?(method)
|
||||
camelized_name = (method.to_s).split("_").collect(&:capitalize).join
|
||||
super || Kernel.const_get("Octofacts::Manipulators::#{camelized_name}")
|
||||
rescue NameError
|
||||
return facts.respond_to?(method)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def downcase_keys!(input)
|
||||
Octofacts::Util::Keys.downcase_keys!(input)
|
||||
end
|
||||
|
||||
def symbolize_keys!(input)
|
||||
Octofacts::Util::Keys.symbolize_keys!(input)
|
||||
end
|
||||
|
||||
def desymbolize_keys!(input)
|
||||
Octofacts::Util::Keys.desymbolize_keys!(input)
|
||||
end
|
||||
|
||||
def string_or_symbolized_key(input)
|
||||
return input.to_s if facts.key?(input.to_s)
|
||||
return input.to_sym if facts.key?(input.to_sym)
|
||||
input
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
require_relative "manipulators/replace"
|
||||
|
||||
# Octofacts::Manipulators - our fact manipulation API.
|
||||
# Each method in Octofacts::Manipulators will operate on one fact set at a time. These
|
||||
# methods do not need to be aware of the existence of multiple fact sets.
|
||||
module Octofacts
|
||||
class Manipulators
|
||||
# Locate and run manipulator.
|
||||
#
|
||||
# Returns true if the manipulator was located and executed, false otherwise.
|
||||
def self.run(obj, name, *args, &block)
|
||||
camelized_name = (name.to_s).split("_").collect(&:capitalize).join
|
||||
|
||||
begin
|
||||
manipulator = Kernel.const_get("Octofacts::Manipulators::#{camelized_name}")
|
||||
rescue NameError
|
||||
return false
|
||||
end
|
||||
|
||||
raise "Unable to run manipulator method '#{name}' on object type #{obj.class}" unless obj.is_a?(Octofacts::Facts)
|
||||
facts = obj.facts
|
||||
manipulator.send(:execute, facts, *args, &block)
|
||||
obj.facts = facts
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,110 @@
|
|||
module Octofacts
|
||||
class Manipulators
|
||||
# Delete a fact from a hash.
|
||||
#
|
||||
# fact_set - The hash of facts
|
||||
# fact_name - Fact to delete, either as a string, symbol, or "multi::level::hash::key"
|
||||
def self.delete(fact_set, fact_name)
|
||||
if fact_name.to_s !~ /::/
|
||||
fact_set.delete(fact_name.to_sym)
|
||||
return
|
||||
end
|
||||
|
||||
# Convert level1::level2::level3 into { "level1" => { "level2" => { "level3" => ... } } }
|
||||
# The delimiter is 2 colons.
|
||||
levels = fact_name.to_s.split("::")
|
||||
key_name = levels.pop.to_sym
|
||||
pointer = fact_set
|
||||
while levels.any?
|
||||
next_key = levels.shift.to_sym
|
||||
return unless pointer.key?(next_key) && pointer[next_key].is_a?(Hash)
|
||||
pointer = pointer[next_key]
|
||||
end
|
||||
|
||||
pointer.delete(key_name)
|
||||
end
|
||||
|
||||
# Determine if a fact exists in a hash.
|
||||
#
|
||||
# fact_set - The hash of facts
|
||||
# fact_name - Fact to check, either as a string, symbol, or "multi::level::hash::key"
|
||||
#
|
||||
# Returns true if the fact exists, false otherwise.
|
||||
def self.exists?(fact_set, fact_name)
|
||||
!get(fact_set, fact_name).nil?
|
||||
end
|
||||
|
||||
# Retrieves the value of a fact from a hash.
|
||||
#
|
||||
# fact_set - The hash of facts
|
||||
# fact_name - Fact to retrieve, either as a string, symbol, or "multi::level::hash::key"
|
||||
#
|
||||
# Returns the value of the fact.
|
||||
def self.get(fact_set, fact_name)
|
||||
return fact_set[fact_name.to_sym] unless fact_name.to_s =~ /::/
|
||||
|
||||
# Convert level1::level2::level3 into { "level1" => { "level2" => { "level3" => ... } } }
|
||||
# The delimiter is 2 colons.
|
||||
levels = fact_name.to_s.split("::")
|
||||
key_name = levels.pop.to_sym
|
||||
pointer = fact_set
|
||||
while levels.any?
|
||||
next_key = levels.shift.to_sym
|
||||
return unless pointer.key?(next_key) && pointer[next_key].is_a?(Hash)
|
||||
pointer = pointer[next_key]
|
||||
end
|
||||
pointer[key_name]
|
||||
end
|
||||
|
||||
# Sets the value of a fact in a hash.
|
||||
#
|
||||
# The new value can be a string, integer, etc., which will directly set the value of
|
||||
# the fact. Instead, you may pass a lambda in place of the value, which will evaluate
|
||||
# with three parameters: lambda { |fact_set|, |fact_name|, |old_value| ... },
|
||||
# or with one parameter: lambda { |old_value| ...}.
|
||||
# If the value of the fact as evaluated is `nil` then the fact is deleted instead of set.
|
||||
#
|
||||
# fact_set - The hash of facts
|
||||
# fact_name - Fact to set, either as a string, symbol, or "multi::level::hash::key"
|
||||
# value - A lambda with new code, or a string, integer, etc.
|
||||
def self.set(fact_set, fact_name, value)
|
||||
fact = fact_name.to_s
|
||||
|
||||
if fact !~ /::/
|
||||
fact_set[fact_name.to_sym] = _set(fact_set, fact_name, fact_set[fact_name.to_sym], value)
|
||||
fact_set.delete(fact_name.to_sym) if fact_set[fact_name.to_sym].nil?
|
||||
return
|
||||
end
|
||||
|
||||
# Convert level1::level2::level3 into { "level1" => { "level2" => { "level3" => ... } } }
|
||||
# The delimiter is 2 colons.
|
||||
levels = fact_name.to_s.split("::")
|
||||
key_name = levels.pop.to_sym
|
||||
pointer = fact_set
|
||||
while levels.any?
|
||||
next_key = levels.shift.to_sym
|
||||
pointer[next_key] = {} unless pointer[next_key].is_a? Hash
|
||||
pointer = pointer[next_key]
|
||||
end
|
||||
pointer[key_name] = _set(fact_set, fact_name, pointer[key_name], value)
|
||||
pointer.delete(key_name) if pointer[key_name].nil?
|
||||
end
|
||||
|
||||
# Internal method: Determine the value you're setting to.
|
||||
#
|
||||
# This handles dispatching to the lambda function or putting the new value in place.
|
||||
def self._set(fact_set, fact_name, old_value, new_value)
|
||||
if new_value.is_a?(Proc)
|
||||
if new_value.arity == 1
|
||||
new_value.call(old_value)
|
||||
elsif new_value.arity == 3
|
||||
new_value.call(fact_set, fact_name, old_value)
|
||||
else
|
||||
raise ArgumentError, "Lambda method expected 1 or 3 parameters, got #{new_value.arity}"
|
||||
end
|
||||
else
|
||||
new_value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
require_relative "base"
|
||||
|
||||
module Octofacts
|
||||
class Manipulators
|
||||
class Replace < Octofacts::Manipulators
|
||||
# Public: Executor for the .replace command.
|
||||
#
|
||||
# Sets the fact to the specified value. If the fact didn't exist before, it's created.
|
||||
#
|
||||
# facts - Hash of current facts
|
||||
# args - Arguments, here consisting of an array of hashes with replacement parameters
|
||||
def self.execute(facts, *args, &_block)
|
||||
args.each do |arg|
|
||||
raise ArgumentError, "Must pass a hash of target facts to .replace - got #{arg}" unless arg.is_a?(Hash)
|
||||
arg.each { |key, val| set(facts, key, val) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# Retrieves configuration parameters from:
|
||||
# - input hash
|
||||
# - rspec configuration
|
||||
# - environment
|
||||
module Octofacts
|
||||
module Util
|
||||
class Config
|
||||
# Fetch a variable from various sources
|
||||
def self.fetch(variable_name, hash_in = {}, default = nil)
|
||||
if hash_in.key?(variable_name)
|
||||
return hash_in[variable_name]
|
||||
end
|
||||
|
||||
begin
|
||||
rspec_value = RSpec.configuration.send(variable_name)
|
||||
return rspec_value if rspec_value
|
||||
rescue NoMethodError
|
||||
# Just skip if undefined
|
||||
end
|
||||
|
||||
env_key = variable_name.to_s.upcase
|
||||
return ENV[env_key] if ENV.key?(env_key)
|
||||
|
||||
default
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,51 @@
|
|||
module Octofacts
|
||||
module Util
|
||||
class Keys
|
||||
# Downcase all keys.
|
||||
#
|
||||
# rspec-puppet does this internally, but depending on how Octofacts is called, this logic may not
|
||||
# be triggered. Therefore, we downcase all keys ourselves.
|
||||
def self.downcase_keys!(input)
|
||||
raise ArgumentError, "downcase_keys! expects Hash, not #{input.class}" unless input.is_a?(Hash)
|
||||
|
||||
input_keys = input.keys.dup
|
||||
input_keys.each do |k|
|
||||
downcase_keys!(input[k]) if input[k].is_a?(Hash)
|
||||
next if k.to_s == k.to_s.downcase
|
||||
new_key = k.is_a?(Symbol) ? k.to_s.downcase.to_sym : k.downcase
|
||||
input[new_key] = input.delete(k)
|
||||
end
|
||||
input
|
||||
end
|
||||
|
||||
# Symbolize all keys.
|
||||
#
|
||||
# Many people work with symbolized keys rather than string keys when dealing with fact fixtures.
|
||||
# This method recursively converts all keys to symbols.
|
||||
def self.symbolize_keys!(input)
|
||||
raise ArgumentError, "symbolize_keys! expects Hash, not #{input.class}" unless input.is_a?(Hash)
|
||||
|
||||
input_keys = input.keys.dup
|
||||
input_keys.each do |k|
|
||||
symbolize_keys!(input[k]) if input[k].is_a?(Hash)
|
||||
input[k.to_sym] = input.delete(k) unless k.is_a?(Symbol)
|
||||
end
|
||||
input
|
||||
end
|
||||
|
||||
# De-symbolize all keys.
|
||||
#
|
||||
# rspec-puppet ultimately wants stringified keys, so this is a method to turn symbols back into strings.
|
||||
def self.desymbolize_keys!(input)
|
||||
raise ArgumentError, "desymbolize_keys! expects Hash, not #{input.class}" unless input.is_a?(Hash)
|
||||
|
||||
input_keys = input.keys.dup
|
||||
input_keys.each do |k|
|
||||
desymbolize_keys!(input[k]) if input[k].is_a?(Hash)
|
||||
input[k.to_s] = input.delete(k) unless k.is_a?(String)
|
||||
end
|
||||
input
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
module Octofacts
|
||||
VERSION = File.read(File.expand_path("../../.version", File.dirname(__FILE__))).strip
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
require "octofacts_updater/cli"
|
||||
require "octofacts_updater/fact"
|
||||
require "octofacts_updater/fact_index"
|
||||
require "octofacts_updater/fixture"
|
||||
require "octofacts_updater/plugin"
|
||||
require "octofacts_updater/plugins/ip"
|
||||
require "octofacts_updater/plugins/ssh"
|
||||
require "octofacts_updater/plugins/static"
|
||||
require "octofacts_updater/service/base"
|
||||
require "octofacts_updater/service/enc"
|
||||
require "octofacts_updater/service/github"
|
||||
require "octofacts_updater/service/local_file"
|
||||
require "octofacts_updater/service/puppetdb"
|
||||
require "octofacts_updater/service/ssh"
|
||||
require "octofacts_updater/version"
|
||||
|
||||
module OctofactsUpdater
|
||||
#
|
||||
end
|
|
@ -0,0 +1,239 @@
|
|||
# :nocov:
|
||||
require "optparse"
|
||||
|
||||
module OctofactsUpdater
|
||||
class CLI
|
||||
# Constructor.
|
||||
#
|
||||
# argv - The Array with command line arguments.
|
||||
def initialize(argv)
|
||||
@opts = {}
|
||||
OptionParser.new(argv) do |opts|
|
||||
opts.banner = "Usage: octofacts-updater [options]"
|
||||
|
||||
opts.on("-a", "--action <action>", String, "Action to take") do |a|
|
||||
@opts[:action] = a
|
||||
end
|
||||
|
||||
opts.on("-c", "--config <config_file>", String, "Path to configuration file") do |f|
|
||||
raise "Invalid configuration file" unless File.file?(f)
|
||||
@opts[:config] = f
|
||||
end
|
||||
|
||||
opts.on("-H", "--hostname <hostname>", String, "FQDN of the host whose facts are to be gathered") do |h|
|
||||
@opts[:hostname] = h
|
||||
end
|
||||
|
||||
opts.on("-o", "--output-file <filename>", String, "Path to output file to write") do |i|
|
||||
@opts[:output_file] = i
|
||||
end
|
||||
|
||||
opts.on("-l", "--list <host1,host2,...>", Array, "List of hosts to update or index") do |l|
|
||||
@opts[:host_list] = l
|
||||
end
|
||||
|
||||
opts.on("--[no-]quick", "Quick indexing: Use existing YAML fact fixtures when available") do |q|
|
||||
@opts[:quick] = q
|
||||
end
|
||||
|
||||
opts.on("-p", "--path <directory>", "Path where to read/write host fixtures when working in bulk") do |path|
|
||||
@opts[:path] = path
|
||||
end
|
||||
|
||||
opts.on("--github", "Push any changes to a branch on GitHub (requires --action=bulk)") do
|
||||
@opts[:github] ||= {}
|
||||
@opts[:github][:enabled] = true
|
||||
end
|
||||
|
||||
opts.on("--datasource <datasource>", "Specify the data source to use when retrieving facts (localfile, puppetdb, ssh)") do |ds|
|
||||
unless %w{localfile puppetdb ssh}.include?(ds)
|
||||
raise ArgumentError, "Invalid datasource #{ds.inspect}. Acceptable values: localfile, puppetdb, ssh."
|
||||
end
|
||||
@opts[:datasource] = ds.to_sym
|
||||
end
|
||||
|
||||
opts.on("--config-override <section:key=value>", Array, "Override a portion of the configuration") do |co_array|
|
||||
co_array.each do |co|
|
||||
if co =~ /\A(\w+):(\S+?)=(.+?)\z/
|
||||
@opts[Regexp.last_match(1).to_sym] ||= {}
|
||||
@opts[Regexp.last_match(1).to_sym][Regexp.last_match(2).to_sym] = Regexp.last_match(3)
|
||||
else
|
||||
raise ArgumentError, "Malformed argument: --config-override must be in the format section:key=value"
|
||||
end
|
||||
end
|
||||
end
|
||||
end.parse!
|
||||
validate_cli
|
||||
end
|
||||
|
||||
def usage
|
||||
puts "Usage: octofacts-updater --action <action> [--config-file /path/to/config.yaml] [other options]"
|
||||
puts ""
|
||||
puts "Available actions:"
|
||||
puts " bulk: Update fixtures and index in bulk"
|
||||
puts " facts: Obtain facts for one node (requires --hostname <hostname>)"
|
||||
puts ""
|
||||
end
|
||||
|
||||
# Run method. Call this to run the octofacts updater with the object that was
|
||||
# previously construcuted.
|
||||
def run
|
||||
unless opts[:action]
|
||||
usage
|
||||
exit 255
|
||||
end
|
||||
|
||||
@config = {}
|
||||
|
||||
if opts[:config]
|
||||
@config = YAML.load_file(opts[:config])
|
||||
substitute_relative_paths!(@config, File.dirname(opts[:config]))
|
||||
load_plugins(@config["plugins"]) if @config.key?("plugins")
|
||||
end
|
||||
|
||||
@config[:options] = {}
|
||||
opts.each do |k, v|
|
||||
if v.is_a?(Hash)
|
||||
@config[k.to_s] ||= {}
|
||||
v.each do |v_key, v_val|
|
||||
@config[k.to_s][v_key.to_s] = v_val
|
||||
@config[k.to_s].delete(v_key.to_s) if v_val.nil?
|
||||
end
|
||||
else
|
||||
@config[:options][k] = v
|
||||
end
|
||||
end
|
||||
|
||||
return handle_action_bulk if opts[:action] == "bulk"
|
||||
return handle_action_facts if opts[:action] == "facts"
|
||||
|
||||
usage
|
||||
exit 255
|
||||
end
|
||||
|
||||
def substitute_relative_paths!(object_in, basedir)
|
||||
if object_in.is_a?(Hash)
|
||||
object_in.each { |k, v| object_in[k] = substitute_relative_paths!(v, basedir) }
|
||||
elsif object_in.is_a?(Array)
|
||||
object_in.map! { |v| substitute_relative_paths!(v, basedir) }
|
||||
elsif object_in.is_a?(String)
|
||||
if object_in =~ %r{^\.\.?(/|\z)}
|
||||
object_in = File.expand_path(object_in, basedir)
|
||||
end
|
||||
object_in
|
||||
else
|
||||
object_in
|
||||
end
|
||||
end
|
||||
|
||||
def handle_action_bulk
|
||||
facts_to_index = @config.fetch("index", {})["indexed_facts"]
|
||||
unless facts_to_index.is_a?(Array)
|
||||
raise ArgumentError, "Must declare index:indexed_facts in configuration to use bulk update"
|
||||
end
|
||||
|
||||
nodes = if opts[:host_list]
|
||||
opts[:host_list]
|
||||
elsif opts[:hostname]
|
||||
[opts[:hostname]]
|
||||
else
|
||||
OctofactsUpdater::FactIndex.load_file(index_file).nodes(true)
|
||||
end
|
||||
if nodes.empty?
|
||||
raise ArgumentError, "Cannot run bulk update with no nodes to check"
|
||||
end
|
||||
|
||||
path = opts[:path] || @config.fetch("index", {})["node_path"]
|
||||
paths = []
|
||||
|
||||
fixtures = nodes.map do |hostname|
|
||||
if opts[:quick] && path && File.file?(File.join(path, "#{hostname}.yaml"))
|
||||
OctofactsUpdater::Fixture.load_file(hostname, File.join(path, "#{hostname}.yaml"))
|
||||
else
|
||||
fixture = OctofactsUpdater::Fixture.make(hostname, @config)
|
||||
if path && File.directory?(path)
|
||||
fixture.write_file(File.join(path, "#{hostname}.yaml"))
|
||||
paths << File.join(path, "#{hostname}.yaml")
|
||||
end
|
||||
fixture
|
||||
end
|
||||
end
|
||||
|
||||
index = OctofactsUpdater::FactIndex.load_file(index_file)
|
||||
index.reindex(facts_to_index, fixtures)
|
||||
index.write_file
|
||||
paths << index_file
|
||||
|
||||
if opts[:github] && opts[:github][:enabled]
|
||||
OctofactsUpdater::Service::GitHub.run(config["github"]["base_directory"], paths, @config)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_action_facts
|
||||
unless opts[:hostname]
|
||||
raise ArgumentError, "--hostname <hostname> must be specified to use --action facts"
|
||||
end
|
||||
|
||||
facts_for_one_node
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :config, :opts
|
||||
|
||||
# Determine the facts for one node and print to the console or write to the specified file.
|
||||
def facts_for_one_node
|
||||
fixture = OctofactsUpdater::Fixture.make(opts[:hostname], @config)
|
||||
print_or_write(fixture.to_yaml)
|
||||
end
|
||||
|
||||
# Get the index file from the options or configuration file. Raise error if it does not exist or
|
||||
# was not specified.
|
||||
def index_file
|
||||
@index_file ||= begin
|
||||
if config.fetch("index", {})["file"]
|
||||
return config["index"]["file"] if File.file?(config["index"]["file"])
|
||||
raise Errno::ENOENT, "Index file (#{config['index']['file'].inspect}) does not exist"
|
||||
end
|
||||
raise ArgumentError, "No index file specified on command line (--index-file) or in configuration file"
|
||||
end
|
||||
end
|
||||
|
||||
# Load plugins as per configuration file. Note: all plugins embedded in this gem are automatically
|
||||
# loaded. This is just for user-specified plugins.
|
||||
#
|
||||
# plugins - An Array of file names to load
|
||||
def load_plugins(plugins)
|
||||
unless plugins.is_a?(Array)
|
||||
raise ArgumentError, "load_plugins expects an array, got #{plugins.inspect}"
|
||||
end
|
||||
|
||||
plugins.each do |plugin|
|
||||
plugin_file = plugin.start_with?("/") ? plugin : File.expand_path("../../#{plugin}", File.dirname(__FILE__))
|
||||
unless File.file?(plugin_file)
|
||||
raise Errno::ENOENT, "Failed to find plugin #{plugin.inspect} at #{plugin_file}"
|
||||
end
|
||||
require plugin_file
|
||||
end
|
||||
end
|
||||
|
||||
# Print or write to file depending on whether or not the output file was set.
|
||||
#
|
||||
# data - Data to print or write.
|
||||
def print_or_write(data)
|
||||
if opts[:output_file]
|
||||
File.open(opts[:output_file], "w") { |f| f.write(data) }
|
||||
else
|
||||
puts data
|
||||
end
|
||||
end
|
||||
|
||||
# Validate command line options. Kick out invalid combinations of options immediately.
|
||||
def validate_cli
|
||||
if opts[:path] && !File.directory?(opts[:path])
|
||||
raise Errno::ENOENT, "An existing directory must be specified with -p/--path"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# :nocov:
|
|
@ -0,0 +1,145 @@
|
|||
# This class represents a fact, either structured or unstructured.
|
||||
# The fact has a name and a value. The name is a string, and the value
|
||||
# can either be a string/integer/boolean (unstructured) or a hash (structured).
|
||||
# This class also has methods used to deal with structured facts (in particular, allowing
|
||||
# representation of a structure delimited with ::).
|
||||
|
||||
module OctofactsUpdater
|
||||
class Fact
|
||||
attr_reader :name
|
||||
|
||||
# Constructor.
|
||||
#
|
||||
# name - The String naming the fact.
|
||||
# value - The arbitrary object with the value of the fact.
|
||||
def initialize(name, value)
|
||||
@name = name
|
||||
@value = value
|
||||
end
|
||||
|
||||
# Get the value of the fact. If the name is specified, this will dig into a structured fact to pull
|
||||
# out the value within the structure.
|
||||
#
|
||||
# name_in - An optional String to dig into the structure (formatted with :: indicating hash delimiters)
|
||||
#
|
||||
# Returns the value of the fact.
|
||||
def value(name_in = nil)
|
||||
# Just a normal lookup -- return the value
|
||||
return @value if name_in.nil?
|
||||
|
||||
# Structured lookup returns nil unless the fact is actually structured.
|
||||
return unless @value.is_a?(Hash)
|
||||
|
||||
# Dig into the hash to pull out the desired value.
|
||||
pointer = @value
|
||||
parts = name_in.split("::")
|
||||
last_part = parts.pop
|
||||
|
||||
parts.each do |part|
|
||||
return unless pointer[part].is_a?(Hash)
|
||||
pointer = pointer[part]
|
||||
end
|
||||
|
||||
pointer[last_part]
|
||||
end
|
||||
|
||||
# Set the value of the fact.
|
||||
#
|
||||
# new_value - An object with the new value for the fact
|
||||
def value=(new_value)
|
||||
set_value(new_value)
|
||||
end
|
||||
|
||||
# Set the value of the fact. If the name is specified, this will dig into a structured fact to set
|
||||
# the value within the structure.
|
||||
#
|
||||
# new_value - An object with the new value for the fact
|
||||
# name_in - An optional String to dig into the structure (formatted with :: indicating hash delimiters)
|
||||
def set_value(new_value, name_in = nil)
|
||||
if name_in.nil?
|
||||
if new_value.is_a?(Proc)
|
||||
return @value = new_value.call(@value)
|
||||
end
|
||||
|
||||
return @value = new_value
|
||||
end
|
||||
|
||||
parts = if name_in.is_a?(String)
|
||||
name_in.split("::")
|
||||
elsif name_in.is_a?(Array)
|
||||
name_in.map do |item|
|
||||
if item.is_a?(String)
|
||||
item
|
||||
elsif item.is_a?(Hash) && item.key?("regexp")
|
||||
Regexp.new(item["regexp"])
|
||||
else
|
||||
raise ArgumentError, "Unable to interpret structure item: #{item.inspect}"
|
||||
end
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "Unable to interpret structure: #{name_in.inspect}"
|
||||
end
|
||||
|
||||
set_structured_value(@value, parts, new_value)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Set a value in the data structure of a structured fact. This is intended to be
|
||||
# called recursively.
|
||||
#
|
||||
# subhash - The Hash, part of the fact, being operated upon
|
||||
# parts - The Array to dig in to the hash
|
||||
# value - The value to set the ultimate last part to
|
||||
#
|
||||
# Does not return anything, but modifies 'subhash'
|
||||
def set_structured_value(subhash, parts, value)
|
||||
return if subhash.nil?
|
||||
raise ArgumentError, "Cannot set structured value at #{parts.first.inspect}" unless subhash.is_a?(Hash)
|
||||
raise ArgumentError, "parts must be an Array, got #{parts.inspect}" unless parts.is_a?(Array)
|
||||
|
||||
# At the top level, find all keys that match the first item in the parts.
|
||||
matching_keys = subhash.keys.select do |key|
|
||||
if parts.first.is_a?(String)
|
||||
key == parts.first
|
||||
elsif parts.first.is_a?(Regexp)
|
||||
parts.first.match(key)
|
||||
else
|
||||
# :nocov:
|
||||
# This is a bug - this code should be unreachable because of the checking in `set_value`
|
||||
raise ArgumentError, "part must be a string or regexp, got #{parts.first.inspect}"
|
||||
# :nocov:
|
||||
end
|
||||
end
|
||||
|
||||
# Auto-create a new hash if there is a value, the part is a string, and the key doesn't exist.
|
||||
if parts.first.is_a?(String) && !value.nil? && !subhash.key?(parts.first)
|
||||
subhash[parts.first] = {}
|
||||
matching_keys << parts.first
|
||||
end
|
||||
return unless matching_keys.any?
|
||||
|
||||
# If we are at the end, set the value or delete the key.
|
||||
if parts.size == 1
|
||||
if value.nil?
|
||||
matching_keys.each { |k| subhash.delete(k) }
|
||||
elsif value.is_a?(Proc)
|
||||
matching_keys.each do |k|
|
||||
new_value = value.call(subhash[k])
|
||||
if new_value.nil?
|
||||
subhash.delete(k)
|
||||
else
|
||||
subhash[k] = new_value
|
||||
end
|
||||
end
|
||||
else
|
||||
matching_keys.each { |k| subhash[k] = value }
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
# We are not at the end. Recurse down to the next level.
|
||||
matching_keys.each { |k| set_structured_value(subhash[k], parts[1..-1], value) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,164 @@
|
|||
# This class represents a fact index, which is ultimately represented by a YAML file of
|
||||
# each index fact, the values seen, and the node(s) containing each value.
|
||||
#
|
||||
# fact_one:
|
||||
# value_one:
|
||||
# - node-1.example.net
|
||||
# - node-2.example.net
|
||||
# value_three:
|
||||
# - node-3.example.net
|
||||
# fact_two:
|
||||
# value_abc:
|
||||
# - node-1.example.net
|
||||
# value_def:
|
||||
# - node-2.example.net
|
||||
# - node-3.example.net
|
||||
|
||||
require "set"
|
||||
require "yaml"
|
||||
|
||||
module OctofactsUpdater
|
||||
class FactIndex
|
||||
# We will create a pseudo-fact that simply lists all of the nodes that were considered
|
||||
# in the index. Define the name of that pseudo-fact here.
|
||||
TOP_LEVEL_NODES_KEY = "_nodes".freeze
|
||||
|
||||
attr_reader :index_data
|
||||
|
||||
# Load an index from the YAML file.
|
||||
#
|
||||
# filename - A String with the file to be loaded.
|
||||
#
|
||||
# Returns a OctofactsUpdater::FactIndex object.
|
||||
def self.load_file(filename)
|
||||
unless File.file?(filename)
|
||||
raise Errno::ENOENT, "load_index cannot load #{filename.inspect}"
|
||||
end
|
||||
|
||||
data = YAML.safe_load(File.read(filename))
|
||||
new(data, filename: filename)
|
||||
end
|
||||
|
||||
# Constructor.
|
||||
#
|
||||
# data - A Hash of existing index data.
|
||||
# filename - Optionally, a String with a file name to write the index to
|
||||
def initialize(data = {}, filename: nil)
|
||||
@index_data = data
|
||||
@filename = filename
|
||||
end
|
||||
|
||||
# Add a fact to the index. If the fact already exists in the index, this will overwrite it.
|
||||
#
|
||||
# fact_name - A String with the name of the fact
|
||||
# fixtures - An Array with fact fixtures (must respond to .facts and .hostname)
|
||||
def add(fact_name, fixtures)
|
||||
@index_data[fact_name] ||= {}
|
||||
fixtures.each do |fixture|
|
||||
fact_value = get_fact(fixture, fact_name)
|
||||
next if fact_value.nil?
|
||||
@index_data[fact_name][fact_value] ||= []
|
||||
@index_data[fact_name][fact_value] << fixture.hostname
|
||||
end
|
||||
end
|
||||
|
||||
# Get a list of all of the nodes in the index. This supports a quick mode (default) where the
|
||||
# TOP_LEVEL_NODES_KEY key is used, and a more detailed mode where this digs through each indexed
|
||||
# fact and value to build a list of nodes.
|
||||
#
|
||||
# quick_mode - Boolean whether to use quick mode (default=true)
|
||||
#
|
||||
# Returns an Array of nodes whose facts are indexed.
|
||||
def nodes(quick_mode = true)
|
||||
if quick_mode && @index_data.key?(TOP_LEVEL_NODES_KEY)
|
||||
return @index_data[TOP_LEVEL_NODES_KEY]
|
||||
end
|
||||
|
||||
seen_hosts = Set.new
|
||||
@index_data.each do |fact_name, fact_values|
|
||||
next if fact_name == TOP_LEVEL_NODES_KEY
|
||||
fact_values.each do |_fact_value, nodes|
|
||||
seen_hosts.merge(nodes)
|
||||
end
|
||||
end
|
||||
seen_hosts.to_a.sort
|
||||
end
|
||||
|
||||
# Rebuild an index with a specified list of facts. This will remove any indexed facts that
|
||||
# are not on the list of facts to use.
|
||||
#
|
||||
# facts_to_index - An Array of Strings with facts to index
|
||||
# fixtures - An Array with fact fixtures (must respond to .facts and .hostname)
|
||||
def reindex(facts_to_index, fixtures)
|
||||
@index_data = {}
|
||||
facts_to_index.each { |fact| add(fact, fixtures) }
|
||||
set_top_level_nodes_fact(fixtures)
|
||||
end
|
||||
|
||||
# Create the top level nodes pseudo-fact.
|
||||
#
|
||||
# fixtures - An Array with fact fixtures (must respond to .hostname)
|
||||
def set_top_level_nodes_fact(fixtures)
|
||||
@index_data[TOP_LEVEL_NODES_KEY] = fixtures.map { |f| f.hostname }.sort
|
||||
end
|
||||
|
||||
# Get YAML representation of the index.
|
||||
# This sorts the hash and any arrays without modifying the object.
|
||||
def to_yaml
|
||||
YAML.dump(recursive_sort(index_data))
|
||||
end
|
||||
|
||||
def recursive_sort(object_in)
|
||||
if object_in.is_a?(Hash)
|
||||
object_out = {}
|
||||
object_in.keys.sort.each { |k| object_out[k] = recursive_sort(object_in[k]) }
|
||||
object_out
|
||||
elsif object_in.is_a?(Array)
|
||||
object_in.sort.map { |v| recursive_sort(v) }
|
||||
else
|
||||
object_in
|
||||
end
|
||||
end
|
||||
|
||||
# Write the fact index out to a YAML file.
|
||||
#
|
||||
# filename - A String with the file to write (defaults to filename from constructor if available)
|
||||
def write_file(filename = nil)
|
||||
filename ||= @filename
|
||||
unless filename.is_a?(String)
|
||||
raise ArgumentError, "Called write_file() for fact_index without a filename"
|
||||
end
|
||||
File.open(filename, "w") { |f| f.write(to_yaml) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Extract a (possibly) structured fact.
|
||||
#
|
||||
# fixture - Fact fixture, must respond to .facts
|
||||
# fact_name - A String with the name of the fact
|
||||
#
|
||||
# Returns the value of the fact, or nil if fact or structure does not exist.
|
||||
def get_fact(fixture, fact_name)
|
||||
pointer = fixture.facts
|
||||
|
||||
# Get the fact of interest from the fixture, whether structured or not.
|
||||
components = fact_name.split(".")
|
||||
first_component = components.shift
|
||||
return unless pointer.key?(first_component)
|
||||
|
||||
# For simple non-structured facts, just return the value.
|
||||
return pointer[first_component].value if components.empty?
|
||||
|
||||
# Structured facts: dig into the structure.
|
||||
pointer = pointer[first_component].value
|
||||
last_component = components.pop
|
||||
components.each do |part|
|
||||
return unless pointer.key?(part)
|
||||
return unless pointer[part].is_a?(Hash)
|
||||
pointer = pointer[part]
|
||||
end
|
||||
pointer[last_component]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,136 @@
|
|||
# This class represents a fact fixture, which is a set of facts along with a node name.
|
||||
# Facts are OctofactsUpdater::Fact objects, and internally are stored as a hash table
|
||||
# with the key being the fact name and the value being the OctofactsUpdater::Fact object.
|
||||
|
||||
require "yaml"
|
||||
|
||||
module OctofactsUpdater
|
||||
class Fixture
|
||||
attr_reader :facts, :hostname
|
||||
|
||||
# Make a fact fixture for the specified host name by consulting data sources
|
||||
# specified in the configuration.
|
||||
#
|
||||
# hostname - A String with the FQDN of the host.
|
||||
# config - A Hash with configuration data.
|
||||
#
|
||||
# Returns the OctofactsUpdater::Fixture object.
|
||||
def self.make(hostname, config)
|
||||
fact_hash = facts_from_configured_datasource(hostname, config)
|
||||
|
||||
if config.key?("enc")
|
||||
enc_data = OctofactsUpdater::Service::ENC.run_enc(hostname, config)
|
||||
if enc_data.key?("parameters")
|
||||
fact_hash.merge! enc_data["parameters"]
|
||||
end
|
||||
end
|
||||
|
||||
obj = new(hostname, config, fact_hash)
|
||||
obj.execute_plugins!
|
||||
end
|
||||
|
||||
# Get fact hash from the first configured and working data source.
|
||||
#
|
||||
# hostname - A String with the FQDN of the host.
|
||||
# config - A Hash with configuration data.
|
||||
#
|
||||
# Returns a Hash with the facts for the specified node; raises exception if this was not possible.
|
||||
def self.facts_from_configured_datasource(hostname, config)
|
||||
last_exception = nil
|
||||
data_sources = %w(LocalFile PuppetDB SSH)
|
||||
data_sources.each do |ds|
|
||||
next if config.fetch(:options, {})[:datasource] && config[:options][:datasource] != ds.downcase.to_sym
|
||||
next unless config.key?(ds.downcase)
|
||||
clazz = Kernel.const_get("OctofactsUpdater::Service::#{ds}")
|
||||
begin
|
||||
result = clazz.send(:facts, hostname, config)
|
||||
return result["values"] if result["values"].is_a?(Hash)
|
||||
return result
|
||||
rescue => e
|
||||
last_exception = e
|
||||
end
|
||||
end
|
||||
|
||||
raise last_exception if last_exception
|
||||
raise ArgumentError, "No fact data sources were configured"
|
||||
end
|
||||
|
||||
# Load a fact fixture from a file. This helps create an index without the more expensive operation
|
||||
# of actually looking up the facts from the data source.
|
||||
#
|
||||
# hostname - A String with the FQDN of the host.
|
||||
# filename - A String with the filename of the existing host.
|
||||
#
|
||||
# Returns the OctofactsUpdater::Fixture object.
|
||||
def self.load_file(hostname, filename)
|
||||
unless File.file?(filename)
|
||||
raise Errno::ENOENT, "Could not load facts from #{filename} because it does not exist"
|
||||
end
|
||||
|
||||
data = YAML.safe_load(File.read(filename))
|
||||
new(hostname, {}, data)
|
||||
end
|
||||
|
||||
# Constructor.
|
||||
#
|
||||
# hostname - A String with the FQDN of the host.
|
||||
# config - A Hash with configuration data.
|
||||
# fact_hash - A Hash with the facts (key = fact name, value = fact value).
|
||||
def initialize(hostname, config, fact_hash = {})
|
||||
@hostname = hostname
|
||||
@config = config
|
||||
@facts = Hash[fact_hash.collect { |k, v| [k, OctofactsUpdater::Fact.new(k, v)] }]
|
||||
end
|
||||
|
||||
# Execute plugins to clean up facts as per configuration. This modifies the value of the facts
|
||||
# stored in this object. Any facts with a value of nil are removed.
|
||||
#
|
||||
# Returns a copy of this object.
|
||||
def execute_plugins!
|
||||
return self unless @config["facts"].is_a?(Hash)
|
||||
|
||||
@config["facts"].each do |fact_tag, args|
|
||||
fact_names(fact_tag, args).each do |fact_name|
|
||||
@facts[fact_name] ||= OctofactsUpdater::Fact.new(fact_name, nil)
|
||||
plugin_name = args.fetch("plugin", "noop")
|
||||
OctofactsUpdater::Plugin.execute(plugin_name, @facts[fact_name], args, @facts)
|
||||
@facts.delete(fact_name) if @facts[fact_name].value.nil?
|
||||
end
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Get fact names associated with a particular data structure. Implements:
|
||||
# - Default behavior, where YAML key = fact name
|
||||
# - Regexp behavior, where YAML "regexp" key is used to match against all facts
|
||||
# - Override behavior, where YAML "fact" key overrides whatever is in the tag
|
||||
#
|
||||
# fact_tag - A String with the YAML key
|
||||
# args - A Hash with the arguments
|
||||
#
|
||||
# Returns an Array of Strings with all fact names matched.
|
||||
def fact_names(fact_tag, args = {})
|
||||
return [args["fact"]] if args.key?("fact")
|
||||
return [fact_tag] unless args.key?("regexp")
|
||||
rexp = Regexp.new(args["regexp"])
|
||||
@facts.keys.select { |k| rexp.match(k) }
|
||||
end
|
||||
|
||||
# Write this fixture to a file.
|
||||
#
|
||||
# filename - A String with the filename to write.
|
||||
def write_file(filename)
|
||||
File.open(filename, "w") { |f| f.write(to_yaml) }
|
||||
end
|
||||
|
||||
# YAML representation of the fact fixture.
|
||||
#
|
||||
# Returns a String containing the YAML representation of the fact fixture.
|
||||
def to_yaml
|
||||
sorted_facts = @facts.sort.to_h
|
||||
facts_hash_with_expanded_values = Hash[sorted_facts.collect { |k, v| [k, v.value] }]
|
||||
YAML.dump(facts_hash_with_expanded_values)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,70 @@
|
|||
# This class provides the base methods for fact manipulation plugins.
|
||||
|
||||
require "digest"
|
||||
|
||||
module OctofactsUpdater
|
||||
class Plugin
|
||||
# Register a plugin.
|
||||
#
|
||||
# plugin_name - A Symbol which is the name of the plugin.
|
||||
# block - A block of code that constitutes the plugin. See sample plugins for expected format.
|
||||
def self.register(plugin_name, &block)
|
||||
@plugins ||= {}
|
||||
if @plugins.key?(plugin_name.to_sym)
|
||||
raise ArgumentError, "A plugin named #{plugin_name} is already registered."
|
||||
end
|
||||
@plugins[plugin_name.to_sym] = block
|
||||
end
|
||||
|
||||
# Execute a plugin
|
||||
#
|
||||
# plugin_name - A Symbol which is the name of the plugin.
|
||||
# fact - An OctofactsUpdater::Fact object
|
||||
# args - An optional Hash of additional configuration arguments
|
||||
# all_facts - A Hash of all of the facts
|
||||
#
|
||||
# Returns nothing, but may adjust the "fact"
|
||||
def self.execute(plugin_name, fact, args = {}, all_facts = {})
|
||||
unless @plugins.key?(plugin_name.to_sym)
|
||||
raise NoMethodError, "A plugin named #{plugin_name} could not be found."
|
||||
end
|
||||
|
||||
begin
|
||||
@plugins[plugin_name.to_sym].call(fact, args, all_facts)
|
||||
rescue => e
|
||||
warn "#{e.class} occurred executing #{plugin_name} on #{fact.name} with value #{fact.value.inspect}"
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
# Clear out a plugin definition. (Useful for testing.)
|
||||
#
|
||||
# plugin_name - The name of the plugin to clear.
|
||||
def self.clear!(plugin_name)
|
||||
@plugins ||= {}
|
||||
@plugins.delete(plugin_name.to_sym)
|
||||
end
|
||||
|
||||
# Get the plugins hash.
|
||||
def self.plugins
|
||||
@plugins
|
||||
end
|
||||
|
||||
# ---------------------------
|
||||
# Below this point are shared methods intended to be called by plugins.
|
||||
# ---------------------------
|
||||
|
||||
# Randomize a long string. This method accepts a string (consisting of, for example, a SSH key)
|
||||
# and returns a string of the same length, but with randomized characters.
|
||||
#
|
||||
# string_in - A String with the original fact value.
|
||||
#
|
||||
# Returns a String with the same length as string_in.
|
||||
def self.randomize_long_string(string_in)
|
||||
seed = Digest::MD5.hexdigest(string_in).to_i(36)
|
||||
prng = Random.new(seed)
|
||||
chars = [("a".."z"), ("A".."Z"), ("0".."9")].flat_map(&:to_a)
|
||||
(1..(string_in.length)).map { chars[prng.rand(chars.length)] }.join
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# This file is part of the octofacts updater fact manipulation plugins. This plugin provides
|
||||
# methods to update facts that are IP addresses in order to anonymize or randomize them.
|
||||
|
||||
require "ipaddr"
|
||||
|
||||
# ipv4_anonymize. This method modifies an IP (version 4) address and
|
||||
# sets it to a randomized (yet consistent) address in the given
|
||||
# network.
|
||||
#
|
||||
# Supported parameters in args:
|
||||
# - subnet: (Required) The network prefix in CIDR notation
|
||||
OctofactsUpdater::Plugin.register(:ipv4_anonymize) do |fact, args = {}, facts|
|
||||
raise ArgumentError, "ipv4_anonymize requires a subnet" if args["subnet"].nil?
|
||||
|
||||
subnet_range = IPAddr.new(args["subnet"], Socket::AF_INET).to_range
|
||||
# Convert the original IP to an integer representation that we can use as seed
|
||||
seed = IPAddr.new(fact.value(args["structure"]), Socket::AF_INET).to_i
|
||||
srand seed
|
||||
random_ip = IPAddr.new(rand(subnet_range.first.to_i..subnet_range.last.to_i), Socket::AF_INET)
|
||||
fact.set_value(random_ip.to_s, args["structure"])
|
||||
end
|
||||
|
||||
# ipv6_anonymize. This method modifies an IP (version 6) address and
|
||||
# sets it to a randomized (yet consistent) address in the given
|
||||
# network.
|
||||
#
|
||||
# Supported parameters in args:
|
||||
# - subnet: (Required) The network prefix in CIDR notation
|
||||
OctofactsUpdater::Plugin.register(:ipv6_anonymize) do |fact, args = {}, facts|
|
||||
raise ArgumentError, "ipv6_anonymize requires a subnet" if args["subnet"].nil?
|
||||
|
||||
subnet_range = IPAddr.new(args["subnet"], Socket::AF_INET6).to_range
|
||||
# Convert the hostname to an integer representation that we can use as seed
|
||||
seed = IPAddr.new(fact.value(args["structure"]), Socket::AF_INET6).to_i
|
||||
srand seed
|
||||
random_ip = IPAddr.new(rand(subnet_range.first.to_i..subnet_range.last.to_i), Socket::AF_INET6)
|
||||
fact.set_value(random_ip.to_s, args["structure"])
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# This file is part of the octofacts updater fact manipulation plugins. This plugin provides
|
||||
# methods to update facts that are SSH keys, since we do not desire to commit SSH keys from
|
||||
# actual hosts into the source code repository.
|
||||
|
||||
# sshfp. This method randomizes the secret key for sshfp formatted keys. Each key is replaced
|
||||
# by a randomized (yet consistent) string the same length as the input key.
|
||||
# The input looks like this:
|
||||
# sshfp_ecdsa: |-
|
||||
# SSHFP 3 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# SSHFP 3 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
OctofactsUpdater::Plugin.register(:sshfp_randomize) do |fact, args = {}|
|
||||
blk = Proc.new do |val|
|
||||
lines = val.split("\n").map(&:strip)
|
||||
result = lines.map do |line|
|
||||
unless line =~ /\ASSHFP (\d+) (\d+) (\w+)/
|
||||
raise "Unparseable pattern: #{line}"
|
||||
end
|
||||
"SSHFP #{Regexp.last_match(1)} #{Regexp.last_match(2)} #{OctofactsUpdater::Plugin.randomize_long_string(Regexp.last_match(3))}"
|
||||
end
|
||||
result.join("\n")
|
||||
end
|
||||
fact.set_value(blk, args["structure"])
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
# This file is part of the octofacts updater fact manipulation plugins. This plugin provides
|
||||
# methods to do static operations on facts -- delete, add, or set to a known value.
|
||||
|
||||
# Delete. This method deletes the fact or the identified portion. Setting the value to nil
|
||||
# causes the tooling to remove any such portions of the value.
|
||||
#
|
||||
# Supported parameters in args:
|
||||
# - structure: A String or Array of a structure within a structured fact
|
||||
OctofactsUpdater::Plugin.register(:delete) do |fact, args = {}, _all_facts = {}|
|
||||
fact.set_value(nil, args["structure"])
|
||||
end
|
||||
|
||||
# Set. This method sets the fact or the identified portion to a static value.
|
||||
#
|
||||
# Supported parameters in args:
|
||||
# - structure: A String or Array of a structure within a structured fact
|
||||
# - value: The new value to set the fact to
|
||||
OctofactsUpdater::Plugin.register(:set) do |fact, args = {}, _all_facts = {}|
|
||||
fact.set_value(args["value"], args["structure"])
|
||||
end
|
||||
|
||||
# Remove matching objects from a delimited string. Requires that the delimiter
|
||||
# and regular expression be set. This is useful, for example, to transform a
|
||||
# string like `foo,bar,baz,fizz` into `foo,fizz` (by removing /^ba/).
|
||||
#
|
||||
# Supported parameters in args:
|
||||
# - delimiter: (Required) Character that is the delimiter.
|
||||
# - regexp: (Required) String used to construct a regular expression of items to remove
|
||||
OctofactsUpdater::Plugin.register(:remove_from_delimited_string) do |fact, args = {}, _all_facts = {}|
|
||||
unless fact.value.nil?
|
||||
unless args["delimiter"]
|
||||
raise ArgumentError, "remove_from_delimited_string requires a delimiter, got #{args.inspect}"
|
||||
end
|
||||
unless args["regexp"]
|
||||
raise ArgumentError, "remove_from_delimited_string requires a regexp, got #{args.inspect}"
|
||||
end
|
||||
parts = fact.value.split(args["delimiter"])
|
||||
regexp = Regexp.new(args["regexp"])
|
||||
parts.delete_if { |part| regexp.match(part) }
|
||||
fact.set_value(parts.join(args["delimiter"]))
|
||||
end
|
||||
end
|
||||
|
||||
# No-op. Do nothing at all.
|
||||
OctofactsUpdater::Plugin.register(:noop) do |_fact, _args = {}, _all_facts = {}|
|
||||
#
|
||||
end
|
||||
|
||||
# Randomize long string. This is just a wrapper around OctofactsUpdater::Plugin.randomize_long_string
|
||||
OctofactsUpdater::Plugin.register(:randomize_long_string) do |fact, args = {}, _all_facts = {}|
|
||||
blk = Proc.new { |val| OctofactsUpdater::Plugin.randomize_long_string(val) }
|
||||
fact.set_value(blk, args["structure"])
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
# This contains handy utility methods that might be used in any of the other classes.
|
||||
|
||||
require "yaml"
|
||||
|
||||
module OctofactsUpdater
|
||||
module Service
|
||||
class Base
|
||||
# Parse a YAML fact file from PuppetServer. This removes the header (e.g. "--- !ruby/object:Puppet::Node::Facts")
|
||||
# so that it's not necessary to bring in all of Puppet.
|
||||
#
|
||||
# yaml_string - A String with YAML to parse.
|
||||
#
|
||||
# Returns a Hash with the facts.
|
||||
def self.parse_yaml(yaml_string)
|
||||
# Convert first "---" after any comments and blank lines.
|
||||
yaml_array = yaml_string.to_s.split("\n")
|
||||
yaml_array.each_with_index do |line, index|
|
||||
next if line =~ /\A\s*#/
|
||||
next if line.strip == ""
|
||||
if line.start_with?("---")
|
||||
yaml_array[index] = "---"
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
# Parse the YAML file
|
||||
result = YAML.safe_load(yaml_array.join("\n"))
|
||||
|
||||
# Pull out "values" if this is in a name-values format. Otherwise just return the hash.
|
||||
return result["values"] if result["values"].is_a?(Hash)
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# This class contains methods to interact with an external node classifier.
|
||||
|
||||
require "open3"
|
||||
require "shellwords"
|
||||
require "yaml"
|
||||
|
||||
module OctofactsUpdater
|
||||
module Service
|
||||
class ENC
|
||||
# Execute the external node classifier script. This expects the value of "path" to be
|
||||
# set in the configuration.
|
||||
#
|
||||
# hostname - A String with the FQDN of the host.
|
||||
# config - A Hash with configuration data.
|
||||
#
|
||||
# Returns a Hash consisting of the parsed output of the ENC.
|
||||
def self.run_enc(hostname, config)
|
||||
unless config["enc"].is_a?(Hash)
|
||||
raise ArgumentError, "The ENC configuration must be defined"
|
||||
end
|
||||
|
||||
unless config["enc"]["path"].is_a?(String)
|
||||
raise ArgumentError, "The ENC path must be defined"
|
||||
end
|
||||
|
||||
unless File.file?(config["enc"]["path"])
|
||||
raise Errno::ENOENT, "The ENC script could not be found at #{config['enc']['path'].inspect}"
|
||||
end
|
||||
|
||||
command = [config["enc"]["path"], hostname].map { |x| Shellwords.escape(x) }.join(" ")
|
||||
stdout, stderr, exitstatus = Open3.capture3(command)
|
||||
unless exitstatus.exitstatus == 0
|
||||
output = { "stdout" => stdout, "stderr" => stderr, "exitstatus" => exitstatus.exitstatus }
|
||||
raise "Error executing #{command.inspect}: #{output.to_yaml}"
|
||||
end
|
||||
|
||||
YAML.load(stdout)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,230 @@
|
|||
# This class contains methods to interact with the GitHub API.
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "octokit"
|
||||
require "pathname"
|
||||
|
||||
module OctofactsUpdater
|
||||
module Service
|
||||
class GitHub
|
||||
attr_reader :options
|
||||
|
||||
# Callable external method: Push all changes to the indicated paths to GitHub.
|
||||
#
|
||||
# root - A String with the root directory, to which paths are relative.
|
||||
# paths - An Array of Strings, which are relative to the repository root.
|
||||
# options - A Hash with configuration options.
|
||||
#
|
||||
# Returns true if there were any changes made, false otherwise.
|
||||
def self.run(root, paths, options = {})
|
||||
root ||= options.fetch("github", {})["base_directory"]
|
||||
unless root && File.directory?(root)
|
||||
raise ArgumentError, "Base directory must be specified"
|
||||
end
|
||||
github = new(options)
|
||||
project_root = Pathname.new(root)
|
||||
paths.each do |path|
|
||||
absolute_path = Pathname.new(path)
|
||||
stripped_path = absolute_path.relative_path_from(project_root)
|
||||
github.commit_data(stripped_path.to_s, File.read(path))
|
||||
end
|
||||
github.finalize_commit
|
||||
end
|
||||
|
||||
# Constructor.
|
||||
#
|
||||
# options - Hash with options
|
||||
def initialize(options = {})
|
||||
@options = options
|
||||
@verbose = github_options.fetch("verbose", false)
|
||||
@changes = []
|
||||
end
|
||||
|
||||
# Commit a file to a location in the repository with the provided message. This will return true if there was
|
||||
# an actual change, and false otherwise. This method does not actually do the commit, but rather it batches up
|
||||
# all of the changes which must be realized later.
|
||||
#
|
||||
# path - A String with the path at which to commit the file
|
||||
# new_content - A String with the new contents
|
||||
#
|
||||
# Returns true (and updates @changes) if there was actually a change, false otherwise.
|
||||
def commit_data(path, new_content)
|
||||
ensure_branch_exists
|
||||
|
||||
old_content = nil
|
||||
begin
|
||||
contents = octokit.contents(repository, path: path, ref: branch)
|
||||
old_content = Base64.decode64(contents.content)
|
||||
rescue Octokit::NotFound
|
||||
verbose("No old content found in #{repository.inspect} at #{path.inspect} in #{branch.inspect}")
|
||||
# Fine, we will add below.
|
||||
end
|
||||
|
||||
if new_content == old_content
|
||||
verbose("Content of #{path} matches, no commit needed")
|
||||
return false
|
||||
else
|
||||
verbose("Content of #{path} does not match. A commit is needed.")
|
||||
verbose(Diffy::Diff.new(old_content, new_content))
|
||||
end
|
||||
|
||||
@changes << Hash(
|
||||
path: path,
|
||||
mode: "100644",
|
||||
type: "blob",
|
||||
sha: octokit.create_blob(repository, new_content)
|
||||
)
|
||||
|
||||
verbose("Batched update of #{path}")
|
||||
true
|
||||
end
|
||||
|
||||
# Finalize the GitHub commit by actually pushing any of the changes. This will not do anything if there
|
||||
# are not any changes batched via the `commit_data` method.
|
||||
#
|
||||
# message - A String with a commit message, defaults to the overall configured commit message.
|
||||
def finalize_commit(message = commit_message)
|
||||
return unless @changes.any?
|
||||
|
||||
ensure_branch_exists
|
||||
branch_ref = octokit.branch(repository, branch)
|
||||
commit = octokit.git_commit(repository, branch_ref[:commit][:sha])
|
||||
tree = commit["tree"]
|
||||
new_tree = octokit.create_tree(repository, @changes, base_tree: tree["sha"])
|
||||
new_commit = octokit.create_commit(repository, message, new_tree["sha"], commit["sha"])
|
||||
octokit.update_ref(repository, "heads/#{branch}", new_commit["sha"])
|
||||
verbose("Committed #{@changes.size} change(s) to GitHub")
|
||||
find_or_create_pull_request
|
||||
end
|
||||
|
||||
# Delete a file from the repository. Because of the way the GitHub API works, this will generate an
|
||||
# immediate commit and push. It will NOT be batched for later application.
|
||||
#
|
||||
# path - A String with the path at which to commit the file
|
||||
# message - A String with a commit message, defaults to the overall configured commit message.
|
||||
#
|
||||
# Returns true if the file existed before and was deleted. Returns false if the file didn't exist anyway.
|
||||
def delete_file(path, message = commit_message)
|
||||
ensure_branch_exists
|
||||
contents = octokit.contents(repository, path: path, ref: branch)
|
||||
blob_sha = contents.sha
|
||||
octokit.delete_contents(repository, path, message, blob_sha, branch: branch)
|
||||
verbose("Deleted #{path}")
|
||||
find_or_create_pull_request
|
||||
true
|
||||
rescue Octokit::NotFound
|
||||
verbose("Deleted #{path} (already gone)")
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Private: Build an octokit object from the provided options.
|
||||
#
|
||||
# Returns an octokit object.
|
||||
def octokit
|
||||
@octokit ||= begin
|
||||
token = options.fetch("github", {})["token"] || ENV["OCTOKIT_TOKEN"]
|
||||
if token
|
||||
Octokit::Client.new(access_token: token)
|
||||
else
|
||||
raise ArgumentError, "Access token must be provided in config file or OCTOKIT_TOKEN environment variable."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Private: Get the default branch from the repository. Unless default_branch is specified in the options, then use
|
||||
# that instead.
|
||||
#
|
||||
# Returns a String with the name of the default branch.
|
||||
def default_branch
|
||||
github_options["default_branch"] || octokit.repo(repository)[:default_branch]
|
||||
end
|
||||
|
||||
# Private: Ensure branch exists. This will use octokit to create the branch on GitHub if the branch
|
||||
# does not already exist.
|
||||
def ensure_branch_exists
|
||||
@ensure_branch_exists ||= begin
|
||||
created = false
|
||||
begin
|
||||
if octokit.branch(repository, branch)
|
||||
verbose("Branch #{branch} already exists in #{repository}.")
|
||||
created = true
|
||||
end
|
||||
rescue Octokit::NotFound
|
||||
# Fine, we'll create it
|
||||
end
|
||||
|
||||
unless created
|
||||
base_sha = octokit.branch(repository, default_branch)[:commit][:sha]
|
||||
octokit.create_ref(repository, "heads/#{branch}", base_sha)
|
||||
verbose("Created branch #{branch} based on #{default_branch} #{base_sha}.")
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# Private: Find an existing pull request for the branch, and commit a new pull request if
|
||||
# there was not an existing one open.
|
||||
#
|
||||
# Returns the pull request object that was created.
|
||||
def find_or_create_pull_request
|
||||
@find_or_create_pull_request ||= begin
|
||||
prs = octokit.pull_requests(repository, head: "github:#{branch}", state: "open")
|
||||
if prs && !prs.empty?
|
||||
verbose("Found existing PR #{prs.first.html_url}")
|
||||
prs.first
|
||||
else
|
||||
new_pr = octokit.create_pull_request(
|
||||
repository,
|
||||
default_branch,
|
||||
branch,
|
||||
pr_subject,
|
||||
pr_body
|
||||
)
|
||||
verbose("Created a new PR #{new_pr.html_url}")
|
||||
new_pr
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Simple methods not covered by unit tests explicitly.
|
||||
# :nocov:
|
||||
|
||||
# Log a verbose message.
|
||||
#
|
||||
# message - A String with the message to print.
|
||||
def verbose(message)
|
||||
return unless @verbose
|
||||
puts "*** #{Time.now}: #{message}"
|
||||
end
|
||||
|
||||
def github_options
|
||||
return {} unless options.is_a?(Hash)
|
||||
options.fetch("github", {})
|
||||
end
|
||||
|
||||
def repository
|
||||
github_options.fetch("repository")
|
||||
end
|
||||
|
||||
def branch
|
||||
github_options.fetch("branch")
|
||||
end
|
||||
|
||||
def commit_message
|
||||
github_options.fetch("commit_message")
|
||||
end
|
||||
|
||||
def pr_subject
|
||||
github_options.fetch("pr_subject")
|
||||
end
|
||||
|
||||
def pr_body
|
||||
github_options.fetch("pr_body")
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
# This class reads a YAML file from the local file system so that it can be used as a source
|
||||
# in octofacts-updater. This was originally intended for a quickstart tutorial, since it requires
|
||||
# no real configuration. However it could also be used in production, if the user wants to create
|
||||
# their own fact obtaining logic outside of octofacts-updater and simply feed in the results.
|
||||
|
||||
require_relative "base"
|
||||
|
||||
module OctofactsUpdater
|
||||
module Service
|
||||
class LocalFile < OctofactsUpdater::Service::Base
|
||||
# Get the facts from a local file, without using PuppetDB, SSH, or any of the other automated methods.
|
||||
#
|
||||
# node - A String with the FQDN for which to retrieve facts
|
||||
# config - A Hash with configuration settings
|
||||
#
|
||||
# Returns a Hash with the facts.
|
||||
def self.facts(node, config = {})
|
||||
unless config["localfile"].is_a?(Hash)
|
||||
raise ArgumentError, "OctofactsUpdater::Service::LocalFile requires localfile section"
|
||||
end
|
||||
config_localfile = config["localfile"].dup
|
||||
|
||||
path_raw = config_localfile.delete("path")
|
||||
unless path_raw
|
||||
raise ArgumentError, "OctofactsUpdater::Service::LocalFile requires 'path' in the localfile section"
|
||||
end
|
||||
path = path_raw.gsub("%%NODE%%", node)
|
||||
unless File.file?(path)
|
||||
raise Errno::ENOENT, "OctofactsUpdater::Service::LocalFile cannot find a file at #{path.inspect}"
|
||||
end
|
||||
|
||||
parse_yaml(File.read(path))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
# This class interacts with puppetdb to pull the facts from the recent
|
||||
# run of Puppet on a given node. This uses octocatalog-diff on the back end to
|
||||
# pull the facts from puppetdb.
|
||||
|
||||
require "octocatalog-diff"
|
||||
|
||||
module OctofactsUpdater
|
||||
module Service
|
||||
class PuppetDB
|
||||
# Get the facts for a specific node.
|
||||
#
|
||||
# node - A String with the FQDN for which to retrieve facts
|
||||
# config - An optional Hash with configuration settings
|
||||
#
|
||||
# Returns a Hash with the facts (via octocatalog-diff)
|
||||
def self.facts(node, config = {})
|
||||
fact_obj = OctocatalogDiff::Facts.new(
|
||||
node: node.strip,
|
||||
backend: :puppetdb,
|
||||
puppetdb_url: puppetdb_url(config)
|
||||
)
|
||||
facts = fact_obj.facts(node)
|
||||
return facts unless facts.nil?
|
||||
raise OctocatalogDiff::Errors::FactSourceError, "Fact retrieval failed for #{node}"
|
||||
end
|
||||
|
||||
# Get the puppetdb URL from the configuration or environment.
|
||||
#
|
||||
# config - An optional Hash with configuration settings
|
||||
#
|
||||
# Returns a String with the PuppetDB URL
|
||||
def self.puppetdb_url(config = {})
|
||||
answer = [
|
||||
config.fetch("puppetdb", {}).fetch("url", nil),
|
||||
ENV["PUPPETDB_URL"]
|
||||
].compact
|
||||
raise "PuppetDB URL not configured or set in environment" unless answer.any?
|
||||
answer.first
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,58 @@
|
|||
# This class interacts with a puppetserver to obtain facts from that server's YAML cache.
|
||||
# This is achieved by SSH-ing to the server and obtaining the fact file directly from the
|
||||
# puppetserver's cache. This can also be used to SSH to an actual node and run a command,
|
||||
# e.g. `facter -p --yaml` to grab actual facts from a running production node.
|
||||
|
||||
require "net/ssh"
|
||||
require "shellwords"
|
||||
|
||||
require_relative "base"
|
||||
|
||||
module OctofactsUpdater
|
||||
module Service
|
||||
class SSH < OctofactsUpdater::Service::Base
|
||||
CACHE_DIR = "/opt/puppetlabs/server/data/puppetserver/yaml/facts"
|
||||
COMMAND = "cat %%NODE%%.yaml"
|
||||
|
||||
# Get the facts for a specific node.
|
||||
#
|
||||
# node - A String with the FQDN for which to retrieve facts
|
||||
# config - A Hash with configuration settings
|
||||
#
|
||||
# Returns a Hash with the facts.
|
||||
def self.facts(node, config = {})
|
||||
unless config["ssh"].is_a?(Hash)
|
||||
raise ArgumentError, "OctofactsUpdater::Service::SSH requires ssh section"
|
||||
end
|
||||
config_ssh = config["ssh"].dup
|
||||
|
||||
server_raw = config_ssh.delete("server")
|
||||
unless server_raw
|
||||
raise ArgumentError, "OctofactsUpdater::Service::SSH requires 'server' in the ssh section"
|
||||
end
|
||||
server = server_raw.gsub("%%NODE%%", node)
|
||||
|
||||
user = config_ssh.delete("user") || ENV["USER"]
|
||||
unless user
|
||||
raise ArgumentError, "OctofactsUpdater::Service::SSH requires 'user' in the ssh section"
|
||||
end
|
||||
|
||||
# Default is to 'cd (puppetserver cache dir) && cat (node).yaml' but this can
|
||||
# be overridden by specifying a command in the SSH options. "%%NODE%%" will always
|
||||
# be replaced by the FQDN of the node in the overall result.
|
||||
cache_dir = config_ssh.delete("cache_dir") || CACHE_DIR
|
||||
command_raw = config_ssh.delete("command") || "cd #{Shellwords.escape(cache_dir)} && #{COMMAND}"
|
||||
command = command_raw.gsub("%%NODE%%", node)
|
||||
|
||||
# Everything left over in config["ssh"] (once server, user, command, and cache_dir are removed) is
|
||||
# symbolized and passed directory to Net::SSH.
|
||||
net_ssh_opts = config_ssh.map { |k, v| [k.to_sym, v] }.to_h || {}
|
||||
ret = Net::SSH.start(server, user, net_ssh_opts) do |ssh|
|
||||
ssh.exec! command
|
||||
end
|
||||
return { "name" => node, "values" => parse_yaml(ret.to_s.strip) } if ret.exitstatus == 0
|
||||
raise "ssh failed with exitcode=#{ret.exitstatus}: #{ret.to_s.strip}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
module OctofactsUpdater
|
||||
VERSION = File.read(File.expand_path("../../.version", File.dirname(__FILE__))).strip
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# coding: utf-8
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "octofacts-updater"
|
||||
spec.version = File.read(File.expand_path("./.version", File.dirname(__FILE__))).strip
|
||||
spec.authors = ["GitHub, Inc.", "Kevin Paulisse", "Antonio Santos"]
|
||||
spec.email = "opensource+octofacts@github.com"
|
||||
|
||||
spec.summary = "Scripts to update octofacts fixtures from recent Puppet runs"
|
||||
spec.description = <<-EOS
|
||||
Octofacts-updater is a series of scripts to construct the fact fixture files and index files consumed by octofacts.
|
||||
EOS
|
||||
spec.homepage = "https://github.com/github/octofacts"
|
||||
spec.license = "MIT"
|
||||
spec.executables = "octofacts-updater"
|
||||
spec.files = [
|
||||
"bin/octofacts-updater",
|
||||
Dir.glob("lib/octofacts_updater/**/*.rb"),
|
||||
"lib/octofacts_updater.rb",
|
||||
".version"
|
||||
].flatten
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.required_ruby_version = ">= 2.1.0"
|
||||
spec.add_dependency "diffy", ">= 3.1.0"
|
||||
spec.add_dependency "octocatalog-diff", ">= 1.4.1"
|
||||
spec.add_dependency "octokit", ">= 4.2.0"
|
||||
spec.add_dependency "net-ssh", ">= 2.9"
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# coding: utf-8
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "octofacts"
|
||||
spec.version = File.read(File.expand_path("./.version", File.dirname(__FILE__))).strip
|
||||
spec.authors = ["GitHub, Inc.", "Kevin Paulisse", "Antonio Santos"]
|
||||
spec.email = "opensource+octofacts@github.com"
|
||||
|
||||
spec.summary = "Run your rspec-puppet tests against fake hosts that present almost real facts"
|
||||
spec.description = <<-EOS
|
||||
Octofacts provides fact fixtures built from recently-updated Puppet facts to rspec-puppet tests.
|
||||
EOS
|
||||
spec.homepage = "https://github.com/github/octofacts"
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.files = [Dir.glob("lib/octofacts/**/*.rb"), "lib/octofacts.rb", ".version"].flatten
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.required_ruby_version = ">= 2.1.0"
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
[ -z "$DEBUG" ] || set -x
|
||||
|
||||
echo 'Starting script/bootstrap'
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
|
||||
|
||||
rm -rf "${DIR}/.bundle"
|
||||
|
||||
echo 'Running bundler'
|
||||
bundle install --no-prune --path vendor/bundle --local
|
||||
bundle clean
|
||||
bundle binstubs --force puppet pry rake rspec-core rubocop
|
||||
chmod 0755 bin/octofacts-updater
|
||||
|
||||
echo 'Completed script/bootstrap successfully'
|
||||
exit 0
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/bash
|
||||
|
||||
rspec_puppet_versions="2.3.2 2.4.0 2.5.0 2.6.2"
|
||||
puppet_versions="4.10.4"
|
||||
|
||||
set -e
|
||||
|
||||
[ -z "$DEBUG" ] || set -x
|
||||
|
||||
DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd)"
|
||||
cd "$DIR"
|
||||
export RBENV_VERSION="$(cat ${DIR}/.ruby-version)"
|
||||
|
||||
TEMPDIR=$(mktemp -d -t cibuild-XXXXXX)
|
||||
function cleanup() {
|
||||
rm -rf "${TEMPDIR}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
test -d "/usr/share/rbenv/shims" && {
|
||||
export PATH="/usr/share/rbenv/shims:$PATH"
|
||||
}
|
||||
|
||||
echo "==> Bootstrapping..."
|
||||
"${DIR}/script/bootstrap"
|
||||
|
||||
PATH="${DIR}/bin:$PATH"
|
||||
|
||||
echo "==> Running rubocop..."
|
||||
RUBOCOP_YML="${DIR}/.rubocop.yml"
|
||||
bundle exec rubocop --config "$RUBOCOP_YML" --no-color -D "lib" "spec/octofacts" "spec/octofacts_updater" "spec/*.rb" \
|
||||
&& EXIT_RUBOCOP=$? || EXIT_RUBOCOP=$?
|
||||
|
||||
echo "==> Running spec tests for octofacts..."
|
||||
bundle exec rake octofacts:spec:octofacts && EXIT_OCTOFACTS_RSPEC=$? || EXIT_OCTOFACTS_RSPEC=$?
|
||||
COVERAGE_OCTOFACTS=$(grep "covered_percent" "$DIR/lib/octofacts/coverage/.last_run.json" | awk '{ print $2 }')
|
||||
|
||||
echo "==> Running spec tests for octofacts_updater..."
|
||||
bundle exec rake octofacts:spec:octofacts_updater && EXIT_UPDATER_RSPEC=$? || EXIT_UPDATER_RSPEC=$?
|
||||
COVERAGE_UPDATER=$(grep "covered_percent" "$DIR/lib/octofacts_updater/coverage/.last_run.json" | awk '{ print $2 }')
|
||||
|
||||
# Integration tests
|
||||
EXIT_INTEGRATION=0
|
||||
for puppet_version in $puppet_versions; do
|
||||
for rspec_puppet_version in $rspec_puppet_versions; do
|
||||
export RSPEC_PUPPET_VERSION=$rspec_puppet_version
|
||||
export PUPPET_VERSION=$puppet_version
|
||||
echo "==> Running integration tests (puppet ${PUPPET_VERSION}, rspec-puppet ${RSPEC_PUPPET_VERSION})"
|
||||
if "${DIR}/script/bootstrap" > "$TEMPDIR/bootstrap.log" 2>&1; then
|
||||
rm -f "$TEMPDIR/bootstrap.log"
|
||||
else
|
||||
cat "$TEMPDIR/bootstrap.log"
|
||||
exit 1
|
||||
fi
|
||||
bundle exec rake octofacts:spec:octofacts_integration && local_integration_rspec=$? || local_integration_rspec=$?
|
||||
if [ "$local_integration_rspec" -ne 0 ]; then EXIT_INTEGRATION=$local_integration_rspec; fi
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "==> Summary Results"
|
||||
echo "Rubocop: Exit ${EXIT_RUBOCOP}"
|
||||
echo "octofacts rspec: Exit ${EXIT_OCTOFACTS_RSPEC}, Coverage ${COVERAGE_OCTOFACTS}"
|
||||
echo "octofacts-updater rspec: Exit ${EXIT_UPDATER_RSPEC}, Coverage ${COVERAGE_UPDATER}"
|
||||
echo "Integration: Exit ${EXIT_INTEGRATION}"
|
||||
echo ""
|
||||
|
||||
if [ "$EXIT_RUBOCOP" == 0 ] && [ "$EXIT_OCTOFACTS_RSPEC" == 0 ] && [ "$EXIT_UPDATER_RSPEC" == 0 ] && [ "$EXIT_INTEGRATION" == 0 ]; then
|
||||
if [ "$COVERAGE_OCTOFACTS" == "100.0" ] && [ "$COVERAGE_UPDATER" == "100.0" ]; then
|
||||
exit 0
|
||||
else
|
||||
echo "All tests passed, but test coverage is not 100%"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 1
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require "bundler/setup"
|
||||
require "octofacts"
|
||||
|
||||
require "pry"
|
||||
Pry.start
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script is being invoked via <root dir>/.git/hooks, hence the
|
||||
# base directory is up two levels, not just one.
|
||||
|
||||
set -e
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd ../.. && pwd )"
|
||||
cd "$DIR"
|
||||
|
||||
echo "==> Analyzing code with rubocop"
|
||||
|
||||
# Make sure we can use git correctly
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
||||
:
|
||||
else
|
||||
echo "Unable to determine revision of this git repo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check whitespace problems
|
||||
if git diff-index --check --cached HEAD --; then
|
||||
:
|
||||
else
|
||||
echo "Please address these whitespace issues and then try committing again"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run rubocop on any ruby files that have been changed or added
|
||||
|
||||
files=$(git diff-index --diff-filter=AM --name-only --cached HEAD | tr "\n" " ")
|
||||
if [ -n "$files" ]; then
|
||||
cmd="bundle exec rubocop --config '${DIR}/.rubocop.yml' -D $files"
|
||||
bundle exec rubocop --config "${DIR}/.rubocop.yml" -D $files
|
||||
if [ $? -ne 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
|
||||
Rubocop returned an error for the following command:
|
||||
|
||||
$cmd
|
||||
|
||||
Please address these style or syntax issues and then try committing again
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
exit 0
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
architecture: amd64
|
||||
attributes:
|
||||
ec2:
|
||||
availability_zone: us-east-1a
|
||||
image_id: ami-000decaf
|
||||
instance_id: i-12345678901234567
|
||||
instance_type: m4.xlarge
|
||||
region: us-east-1
|
||||
state: running
|
||||
vpc_id: vpc-000decaf
|
||||
augeasversion: 1.2.0
|
||||
bios_release_date: 02/16/2017
|
||||
bios_vendor: Xen
|
||||
bios_version: 4.2.amazon
|
||||
blockdevice_xvda_size: 21474836480
|
||||
blockdevices: xvda
|
||||
bonding_bond0_mode: balance-rr
|
||||
bonding_bond0_slaves: none
|
||||
bonding_bond0_status: down
|
||||
bonding_interfaces: bond0
|
||||
clientcert: somenode.example.net
|
||||
clientnoop: false
|
||||
clientversion: 3.8.7
|
||||
cores_mounted: false
|
||||
datacenter: ec2
|
||||
domain: example.net
|
||||
ec2: true
|
||||
ec2_ami_id: ami-000decaf
|
||||
ec2_ami_launch_index: '0'
|
||||
ec2_ami_manifest_path: "(unknown)"
|
||||
ec2_block_device_mapping_ami: "/dev/sda1"
|
||||
ec2_block_device_mapping_root: "/dev/sda1"
|
||||
ec2_hostname: ip-172-16-1-2.example.net
|
||||
ec2_instance_action: none
|
||||
ec2_instance_id: i-12345678901234567
|
||||
ec2_instance_type: m4.xlarge
|
||||
ec2_local_hostname: ip-172-16-1-2.example.net
|
||||
ec2_local_ipv4: 172.16.1.2
|
||||
ec2_mac: 0a:11:22:33:44:55
|
||||
ec2_metadata:
|
||||
mac: 0a:11:22:33:44:55
|
||||
hostname: ip-172-16-1-2.example.net
|
||||
ami-id: ami-000decaf
|
||||
services:
|
||||
domain: amazonaws.com
|
||||
partition: aws
|
||||
instance-type: m4.xlarge
|
||||
placement:
|
||||
availability-zone: us-east-1a
|
||||
instance-action: none
|
||||
profile: default-hvm
|
||||
ami-manifest-path: "(unknown)"
|
||||
reservation-id: r-0123456789abcdef0
|
||||
network:
|
||||
interfaces:
|
||||
macs:
|
||||
0a:11:22:33:44:55:
|
||||
mac: 0a:11:22:33:44:55
|
||||
security-group-ids: sg-000decaf
|
||||
vpc-id: vpc-000decaf
|
||||
vpc-ipv4-cidr-blocks: 172.16.0.0/16
|
||||
local-ipv4s: 172.16.1.2
|
||||
subnet-ipv4-cidr-block: 172.16.0.0/20
|
||||
subnet-id: subnet-000decaf
|
||||
vpc-ipv4-cidr-block: 172.16.0.0/16
|
||||
security-groups: default
|
||||
owner-id: '987654321012'
|
||||
device-number: '0'
|
||||
interface-id: eni-000decaf
|
||||
local-hostname: ip-172-16-1-2.example.net
|
||||
instance-id: i-12345678901234567
|
||||
security-groups: default
|
||||
ami-launch-index: '0'
|
||||
block-device-mapping:
|
||||
ami: "/dev/sda1"
|
||||
root: "/dev/sda1"
|
||||
metrics:
|
||||
vhostmd: <?xml version="1.0" encoding="UTF-8"?>
|
||||
iam: {}
|
||||
local-ipv4: 172.16.1.2
|
||||
local-hostname: ip-172-16-1-2.example.net
|
||||
ec2_metrics_vhostmd: <?xml version="1.0" encoding="UTF-8"?>
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_device_number: '0'
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_interface_id: eni-000decaf
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_local_hostname: ip-172-16-1-2.example.net
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_local_ipv4s: 172.16.1.2
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_mac: 0a:11:22:33:44:55
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_owner_id: '987654321012'
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_security_group_ids: sg-000decaf
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_security_groups: default
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_subnet_id: subnet-000decaf
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_subnet_ipv4_cidr_block: 172.16.0.0/20
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_vpc_id: vpc-000decaf
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_vpc_ipv4_cidr_block: 172.16.0.0/16
|
||||
ec2_network_interfaces_macs_0a:11:22:33:44:55_vpc_ipv4_cidr_blocks: 172.16.0.0/16
|
||||
ec2_placement_availability_zone: us-east-1a
|
||||
ec2_profile: default-hvm
|
||||
ec2_region: us-east-1
|
||||
ec2_reservation_id: r-0123456789abcdef0
|
||||
ec2_security_groups: default
|
||||
ec2_services_domain: amazonaws.com
|
||||
ec2_services_partition: aws
|
||||
enable_pager: 'false'
|
||||
eth0-txrx-0_irq: '86'
|
||||
eth0-txrx-1_irq: '87'
|
||||
eth0_irq: '88'
|
||||
facterversion: 2.4.6
|
||||
filesystems: btrfs,ext2,ext3,ext4,hfs,hfsplus,jfs,minix,msdos,ntfs,qnx4,ufs,vfat,xfs
|
||||
fqdn: somenode.example.net
|
||||
gateway: 172.16.0.1
|
||||
gid: root
|
||||
hardwareisa: unknown
|
||||
hardwaremodel: x86_64
|
||||
hostname: somenode
|
||||
id: root
|
||||
interfaces: bond0,eth0,lo
|
||||
ip6tables_version: 1.4.21
|
||||
ipaddress: 172.16.1.2
|
||||
ipaddress_eth0: 172.16.1.2
|
||||
ipaddress_lo: 127.0.0.1
|
||||
is_virtual: true
|
||||
kernel: Linux
|
||||
kernelmajversion: '3.16'
|
||||
kernelrelease: 3.16.0-4-amd64
|
||||
kernelversion: 3.16.0
|
||||
lsbdistcodename: jessie
|
||||
lsbdistdescription: Debian GNU/Linux 8.8 (jessie)
|
||||
lsbdistid: Debian
|
||||
lsbdistrelease: '8.8'
|
||||
lsbmajdistrelease: '8'
|
||||
lsbminordistrelease: '8'
|
||||
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
|
||||
macaddress: 0a:11:22:33:44:55
|
||||
macaddress_bond0: 0a:11:22:33:44:55
|
||||
macaddress_eth0: 0a:11:22:33:44:55
|
||||
manufacturer: Xen
|
||||
memorysize: 15.71 GB
|
||||
memorysize_gb: 15.71
|
||||
memorysize_mb: '16082.46'
|
||||
mounts: "/,/run,/sys/kernel/security,/run/lock,/sys/fs/pstore,/dev/mqueue,/sys/kernel/debug,/dev/hugepages,/boot"
|
||||
mtu_bond0: 1500
|
||||
mtu_eth0: 9001
|
||||
mtu_lo: 65536
|
||||
netmask: 255.255.240.0
|
||||
netmask_eth0: 255.255.240.0
|
||||
netmask_lo: 255.0.0.0
|
||||
network_eth0: 172.16.0.0
|
||||
network_lo: 127.0.0.0
|
||||
operatingsystem: Debian
|
||||
operatingsystemmajrelease: '8'
|
||||
operatingsystemrelease: '8.8'
|
||||
os:
|
||||
family: Debian
|
||||
lsb:
|
||||
distcodename: jessie
|
||||
distdescription: Debian GNU/Linux 8.8 (jessie)
|
||||
distid: Debian
|
||||
distrelease: '8.8'
|
||||
majdistrelease: '8'
|
||||
minordistrelease: '8'
|
||||
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
|
||||
name: Debian
|
||||
release:
|
||||
full: '8.8'
|
||||
major: '8'
|
||||
minor: '8'
|
||||
osfamily: Debian
|
||||
partitions:
|
||||
xvda1:
|
||||
label: biosboot
|
||||
size: '14336'
|
||||
xvda2:
|
||||
filesystem: ext2
|
||||
label: primary
|
||||
mount: "/boot"
|
||||
size: '360448'
|
||||
uuid: 09809809-0980-0980-0980-098098098098
|
||||
xvda3:
|
||||
filesystem: LVM2_member
|
||||
label: Linux LVM
|
||||
size: '41551839'
|
||||
physicalprocessorcount: 1
|
||||
processor0: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
|
||||
processor1: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
|
||||
processor2: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
|
||||
processor3: Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
|
||||
processorcount: 4
|
||||
processors:
|
||||
count: 4
|
||||
models:
|
||||
- Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
|
||||
- Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
|
||||
- Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
|
||||
- Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
|
||||
physicalcount: 1
|
||||
productname: HVM domU
|
||||
ps: ps -ef
|
||||
puppet_environmentpath: ''
|
||||
region: us-east-1
|
||||
root_home: "/root"
|
||||
shorthost: somenode
|
||||
swapsize: 0.00 MB
|
||||
swapsize_mb: '0.00'
|
||||
timezone: UTC
|
||||
type: Other
|
||||
virtual: xenhvm
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
fqdn: ops-consul-12345.dc2.example.com
|
||||
datacenter: dc2
|
||||
app: ops
|
||||
env: production
|
||||
role: consul
|
||||
lsbdistcodename: precise
|
||||
shorthost: ops-consul-12345
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
fqdn: ops-consul-67890.dc1.example.com
|
||||
datacenter: dc1
|
||||
app: ops
|
||||
env: production
|
||||
role: consul
|
||||
lsbdistcodename: jessie
|
||||
shorthost: ops-consul-67890
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
fqdn: puppet-puppetserver-00decaf.dc1.example.com
|
||||
datacenter: dc1
|
||||
app: puppet
|
||||
env: staging
|
||||
role: puppetserver
|
||||
lsbdistcodename: jessie
|
||||
shorthost: puppet-puppetserver-00decaf
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
fqdn: puppet-puppetserver-12345.dc1.example.com
|
||||
datacenter: dc1
|
||||
app: puppet
|
||||
env: production
|
||||
role: puppetserver
|
||||
lsbdistcodename: precise
|
||||
shorthost: puppet-puppetserver-12345
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
_nodes:
|
||||
- broken.example.com
|
||||
app:
|
||||
ops:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- ops-consul-12345.dc2.example.com
|
||||
puppet:
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
datacenter:
|
||||
dc1:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
dc2:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
env:
|
||||
production:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- ops-consul-12345.dc2.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
staging:
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
lsbdistcodename:
|
||||
jessie:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
precise:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
role:
|
||||
consul:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- ops-consul-12345.dc2.example.com
|
||||
puppetserver:
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
_nodes:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
- ops-consul-12345.dc2.example.com
|
||||
app:
|
||||
ops:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- ops-consul-12345.dc2.example.com
|
||||
puppet:
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
datacenter:
|
||||
dc1:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
dc2:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
env:
|
||||
staging:
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
production:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- ops-consul-12345.dc2.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
lsbdistcodename:
|
||||
jessie:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
precise:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
role:
|
||||
consul:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- ops-consul-12345.dc2.example.com
|
||||
puppetserver:
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
_nodes:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
app:
|
||||
ops:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
- ops-consul-67890.dc1.example.com
|
||||
puppet:
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
datacenter:
|
||||
dc1:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
dc2:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
env:
|
||||
production:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
staging:
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
lsbdistcodename:
|
||||
jessie:
|
||||
- ops-consul-67890.dc1.example.com
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
precise:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
||||
role:
|
||||
consul:
|
||||
- ops-consul-12345.dc2.example.com
|
||||
- ops-consul-67890.dc1.example.com
|
||||
puppetserver:
|
||||
- puppet-puppetserver-00decaf.dc1.example.com
|
||||
- puppet-puppetserver-12345.dc1.example.com
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Nothing here.
|
|
@ -0,0 +1 @@
|
|||
class test {}
|
|
@ -0,0 +1,12 @@
|
|||
class test::one {
|
||||
file { "/tmp/system-info.txt":
|
||||
ensure => file,
|
||||
owner => $::id,
|
||||
group => $::gid,
|
||||
content => template("test/one/system-info.txt"),
|
||||
}
|
||||
|
||||
file { "/etc/hosts":
|
||||
content => "127.0.0.1 localhost ${::shorthost}",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
require_relative "../../../../spec/spec_helper"
|
||||
|
||||
describe "test::one" do
|
||||
context "with facts hard-coded" do
|
||||
# This does not exercise octofacts. However, it helps to confirm that rspec-puppet is set
|
||||
# up correctly before we get to the tests below which do use octofacts.
|
||||
let(:facts) do
|
||||
{
|
||||
ec2: true,
|
||||
ec2_metadata: { placement: { :"availability-zone" => "us-foo-1a" } },
|
||||
gid: "root",
|
||||
id: "root"
|
||||
}
|
||||
end
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "root",
|
||||
group: "root",
|
||||
content: /availability-zone: us-foo-1a/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "using straight octofacts from file explicitly converted to hash" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml").facts }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "root",
|
||||
group: "root",
|
||||
content: /availability-zone: us-east-1a/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "using straight octofacts from file but not converted to hash" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml") }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "root",
|
||||
group: "root",
|
||||
content: /availability-zone: us-east-1a/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "using straight octofacts from file with manipulation converted to hash" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml").replace("ec2_metadata::placement::availability-zone" => "us-hats-1a").facts }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "root",
|
||||
group: "root",
|
||||
content: /availability-zone: us-hats-1a/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "using straight octofacts from file with manipulation but not converted to hash" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml").replace("ec2_metadata::placement::availability-zone" => "us-hats-1a") }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "root",
|
||||
group: "root",
|
||||
content: /availability-zone: us-hats-1a/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "using straight octofacts from file with manipulation of symbol converted to hash" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml").replace(:"ec2_metadata::placement::availability-zone" => "us-hats-1a").facts }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "root",
|
||||
group: "root",
|
||||
content: /availability-zone: us-hats-1a/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "using straight octofacts from file with manipulation of symbol not converted to hash" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml").replace("ec2_metadata::placement::availability-zone" => "us-hats-1a") }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "root",
|
||||
group: "root",
|
||||
content: /availability-zone: us-hats-1a/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "using pure ruby interface for manipulation" do
|
||||
context "without converting to hash" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml").merge("ec2" => false) }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "root",
|
||||
group: "root",
|
||||
content: /Not an EC2 instance/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with converting to hash" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml").facts.merge(ec2: false) }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "root",
|
||||
group: "root",
|
||||
content: /Not an EC2 instance/
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "using chained manipulators" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml").replace(id: "hats", gid: "caps").replace(ec2: false) }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/tmp/system-info.txt").with(
|
||||
owner: "hats",
|
||||
group: "caps",
|
||||
content: /Not an EC2 instance/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "passing parameters to the index constructor" do
|
||||
let(:facts) { Octofacts.from_index(app: "puppet", env: "production") }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/etc/hosts").with(
|
||||
content: /127.0.0.1 localhost puppet-puppetserver-12345/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "using index + select" do
|
||||
let(:facts) { Octofacts.from_index.select(app: "puppet", env: "production") }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/etc/hosts").with(
|
||||
content: /127.0.0.1 localhost puppet-puppetserver-12345/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "using chained selectors" do
|
||||
let(:facts) { Octofacts.from_index.select(app: "puppet").reject(env: "production") }
|
||||
|
||||
it "should contain the file resource" do
|
||||
is_expected.to contain_file("/etc/hosts").with(
|
||||
content: /127.0.0.1 localhost puppet-puppetserver-00decaf/
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "tests accessing facts as if it was a hash" do
|
||||
context "with []" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml") }
|
||||
|
||||
it "should contain /etc/hosts with a symbol key" do
|
||||
is_expected.to contain_file("/etc/hosts").with(
|
||||
content: "127.0.0.1 localhost #{facts[:shorthost]}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with fetch" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml") }
|
||||
|
||||
it "should contain /etc/hosts with a symbol key" do
|
||||
is_expected.to contain_file("/etc/hosts").with(
|
||||
content: "127.0.0.1 localhost #{facts.fetch(:shorthost)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "tests accessing facts as if it was a string" do
|
||||
context "with []" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml") }
|
||||
|
||||
it "should contain /etc/hosts with a symbol key" do
|
||||
is_expected.to contain_file("/etc/hosts").with(
|
||||
content: "127.0.0.1 localhost #{facts['shorthost']}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with fetch" do
|
||||
let(:facts) { Octofacts.from_file("basic.yaml") }
|
||||
|
||||
it "should contain /etc/hosts with a symbol key" do
|
||||
is_expected.to contain_file("/etc/hosts").with(
|
||||
content: "127.0.0.1 localhost #{facts.fetch('shorthost')}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
<%- if @ec2 -%>
|
||||
availability-zone: <%= @ec2_metadata["placement"]["availability-zone"] %>
|
||||
<%- else -%>
|
||||
Not an EC2 instance
|
||||
<%- end -%>
|
|
@ -0,0 +1,23 @@
|
|||
# spec_helper for rspec-puppet fixture
|
||||
|
||||
require_relative "../../../lib/octofacts"
|
||||
require "rspec-puppet"
|
||||
|
||||
def puppet_root
|
||||
File.expand_path("..", File.dirname(__FILE__))
|
||||
end
|
||||
|
||||
def repo_root
|
||||
File.expand_path("../../..", File.dirname(__FILE__))
|
||||
end
|
||||
|
||||
RSpec.configure do |c|
|
||||
c.module_path = File.join(puppet_root, "modules")
|
||||
c.hiera_config = File.join(puppet_root, "hiera.yaml")
|
||||
c.manifest_dir = File.join(puppet_root, "manifests")
|
||||
c.manifest = File.join(puppet_root, "manifests", "defaults.pp")
|
||||
c.add_setting :octofacts_fixture_path
|
||||
c.octofacts_fixture_path = File.join(repo_root, "spec", "fixtures", "facts")
|
||||
c.add_setting :octofacts_index_path
|
||||
c.octofacts_index_path = File.join(repo_root, "spec", "fixtures", "index.yaml")
|
||||
end
|
|
@ -0,0 +1,188 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Octofacts::Backends::Index do
|
||||
let(:index_path) { File.join(Octofacts::Spec.fixture_root, "index.yaml") }
|
||||
let(:fixture_path) { File.join(Octofacts::Spec.fixture_root, "facts") }
|
||||
let(:backend) { described_class.new(octofacts_index_path: index_path, octofacts_fixture_path: fixture_path) }
|
||||
let(:subject) { Octofacts::Facts.new(backend: backend) }
|
||||
|
||||
before(:each) do
|
||||
ENV["OCTOFACTS_INDEX_PATH"] = index_path
|
||||
ENV["OCTOFACTS_FIXTURE_PATH"] = fixture_path
|
||||
end
|
||||
|
||||
describe "initialization" do
|
||||
it "can be called with no arguments" do
|
||||
expect { described_class.new }.not_to raise_error
|
||||
end
|
||||
|
||||
it "raises an error if the env variable is not defined and not index path passed" do
|
||||
ENV.delete("OCTOFACTS_INDEX_PATH")
|
||||
expect { described_class.new }.to raise_error(ArgumentError, /not defined/)
|
||||
end
|
||||
|
||||
it "raises an error if the env variable is not defined and not fixture path passed" do
|
||||
ENV.delete("OCTOFACTS_FIXTURE_PATH")
|
||||
expect { described_class.new }.to raise_error(ArgumentError, /not defined/)
|
||||
end
|
||||
|
||||
it "raises an error if the index path does not exist" do
|
||||
expect { described_class.new(octofacts_index_path: "notafile") }.to raise_error(Errno::ENOENT, /not exist/)
|
||||
end
|
||||
|
||||
it "raises an error if the fixture path does not exist" do
|
||||
expect { described_class.new(octofacts_fixture_path: "notadirectory") }.to raise_error(Errno::ENOENT, /not exist/)
|
||||
end
|
||||
|
||||
it "can be passed an index file" do
|
||||
ENV["OCTOFACTS_INDEX_PATH"] = nil
|
||||
expect { described_class.new(octofacts_index_path: index_path) }.not_to raise_error
|
||||
end
|
||||
|
||||
it "can be passed a fixture path" do
|
||||
ENV["OCTOFACTS_FIXTURE_PATH"] = nil
|
||||
expect { described_class.new(octofacts_index_path: index_path, octofacts_fixture_path: fixture_path) }.not_to raise_error
|
||||
end
|
||||
|
||||
it "can handle select conditions in addition to the built-in options" do
|
||||
args = {
|
||||
octofacts_index_path: index_path,
|
||||
octofacts_fixture_path: fixture_path,
|
||||
octofacts_strict_index: true,
|
||||
datacenter: "dc2"
|
||||
}
|
||||
answer = {
|
||||
"fqdn" => "ops-consul-12345.dc2.example.com",
|
||||
"datacenter" => "dc2",
|
||||
"app" => "ops",
|
||||
"env" => "production",
|
||||
"role" => "consul",
|
||||
"lsbdistcodename" => "precise",
|
||||
"shorthost" => "ops-consul-12345"
|
||||
}
|
||||
obj = described_class.new(datacenter: "dc2")
|
||||
expect(obj.send(:nodes)).to eq(["ops-consul-12345.dc2.example.com"])
|
||||
expect(obj.facts).to eq(answer)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#facts" do
|
||||
it "returns a hash" do
|
||||
expect(subject.facts).to be_a(Hash)
|
||||
end
|
||||
|
||||
it "returns the facts for the first node in the collection" do
|
||||
expect(subject.facts[:fqdn]).to eq("ops-consul-67890.dc1.example.com")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#select" do
|
||||
it "selects the nodes that meet the conditions" do
|
||||
expect(subject.select(datacenter: "dc2").facts[:fqdn]).to eq("ops-consul-12345.dc2.example.com")
|
||||
end
|
||||
|
||||
it "selects the nodes that meet the conditions with string keys" do
|
||||
expect(subject.select("datacenter" => "dc2").facts[:fqdn]).to eq("ops-consul-12345.dc2.example.com")
|
||||
end
|
||||
|
||||
it "raises an error if no node can't meet the conditions" do
|
||||
expect { subject.select(datacenter: "dc3") }.to raise_error(Octofacts::Errors::NoFactsError)
|
||||
end
|
||||
|
||||
it "can be passed more than one condition" do
|
||||
expect(subject.select(env: "staging", role: "puppetserver").facts[:fqdn]).to eq("puppet-puppetserver-00decaf.dc1.example.com")
|
||||
end
|
||||
|
||||
it "returns itself so that it can be chained" do
|
||||
expect(subject.select(datacenter: "dc2")).to be(subject)
|
||||
end
|
||||
|
||||
it "indexes and then selects based on an unindexed stringified fact" do
|
||||
obj = subject.select(fqdn: "ops-consul-12345.dc2.example.com").select(app: "ops")
|
||||
expect(backend.send("nodes")).to eq(["ops-consul-12345.dc2.example.com"])
|
||||
end
|
||||
|
||||
it "indexes and then selects based on an unindexed symbolized fact" do
|
||||
obj = subject.select(fqdn: "ops-consul-12345.dc2.example.com").select(app: "ops")
|
||||
expect(backend.send(:nodes)).to eq(["ops-consul-12345.dc2.example.com"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#reject" do
|
||||
it "removes nodes that don't meet the conditions" do
|
||||
expect(subject.reject(datacenter: "dc1").facts[:fqdn]).to eq("ops-consul-12345.dc2.example.com")
|
||||
end
|
||||
|
||||
it "removes nodes that don't meet the conditions with string keys" do
|
||||
expect(subject.reject("datacenter" => "dc1").facts[:fqdn]).to eq("ops-consul-12345.dc2.example.com")
|
||||
end
|
||||
|
||||
it "does nothing if no node meets the conditions" do
|
||||
expect(subject.reject(datacenter: "dc3").facts[:fqdn]).to eq("ops-consul-67890.dc1.example.com")
|
||||
end
|
||||
|
||||
it "can be passed more than one condition" do
|
||||
expect(subject.reject(app: "puppet", datacenter: "dc1").facts[:fqdn]).to eq("ops-consul-12345.dc2.example.com")
|
||||
end
|
||||
|
||||
it "returns itself so that it can be chained" do
|
||||
expect(subject.reject(datacenter: "dc2")).to be(subject)
|
||||
end
|
||||
|
||||
it "returns an error if we reject everything" do
|
||||
expect { subject.reject(datacenter: "dc1").reject(datacenter: "dc2") }.to raise_error(Octofacts::Errors::NoFactsError)
|
||||
end
|
||||
|
||||
it "indexes and then rejects based on an unindexed stringified fact" do
|
||||
obj = subject.reject(fqdn: "ops-consul-12345.dc2.example.com").select(app: "ops")
|
||||
expect(backend.send("nodes")).to eq(["ops-consul-67890.dc1.example.com"])
|
||||
end
|
||||
|
||||
it "indexes and then rejects based on an unindexed symbolized fact" do
|
||||
obj = subject.reject(fqdn: "ops-consul-12345.dc2.example.com").select(app: "ops")
|
||||
expect(backend.send(:nodes)).to eq(["ops-consul-67890.dc1.example.com"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#prefer" do
|
||||
it "sorts the nodes correctly if the conditions are met" do
|
||||
expect(subject.prefer(datacenter: "dc2").facts[:fqdn]).to eq("ops-consul-12345.dc2.example.com")
|
||||
end
|
||||
|
||||
it "sorts the nodes correctly if the conditions are met with string keys" do
|
||||
expect(subject.prefer("datacenter" => "dc2").facts[:fqdn]).to eq("ops-consul-12345.dc2.example.com")
|
||||
end
|
||||
|
||||
it "does not do anything if the conditions are not met" do
|
||||
expect(subject.prefer(datacenter: "dc3").facts[:fqdn]).to eq("ops-consul-67890.dc1.example.com")
|
||||
end
|
||||
|
||||
it "returns itself so that it can be chained" do
|
||||
expect(subject.prefer(datacenter: "dc2")).to be(subject)
|
||||
end
|
||||
|
||||
it "indexes and then prefers based on an unindexed fact" do
|
||||
obj = subject.prefer(fqdn: "ops-consul-67890.dc1.example.com").select(app: "ops")
|
||||
expect(backend.send(:nodes)).to eq(["ops-consul-67890.dc1.example.com", "ops-consul-12345.dc2.example.com"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#add_fact_to_index" do
|
||||
it "raises an error when an unindexed fact is used and strict_index is true" do
|
||||
backend2 = described_class.new(octofacts_index_path: index_path, octofacts_fixture_path: fixture_path, octofacts_strict_index: true)
|
||||
subject2 = Octofacts::Facts.new(backend: backend2)
|
||||
expect { subject2.prefer(fqdn: "ops-consul-67890.dc1.example.com") }.to raise_error(Octofacts::Errors::FactNotIndexed)
|
||||
end
|
||||
|
||||
it "raises an error when an unindexed fact is used and OCTOFACTS_STRICT_INDEX is true" do
|
||||
begin
|
||||
ENV["OCTOFACTS_STRICT_INDEX"] = "true"
|
||||
backend2 = described_class.new(octofacts_index_path: index_path, octofacts_fixture_path: fixture_path)
|
||||
subject2 = Octofacts::Facts.new(backend: backend2)
|
||||
expect { subject2.prefer(fqdn: "ops-consul-67890.dc1.example.com") }.to raise_error(Octofacts::Errors::FactNotIndexed)
|
||||
ensure
|
||||
ENV.delete("OCTOFACTS_STRICT_INDEX")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,63 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Octofacts::Backends::YamlFile do
|
||||
let(:fixture_file) { File.join(Octofacts::Spec.fixture_root, "facts", "basic.yaml") }
|
||||
let(:subject) { described_class.new(fixture_file) }
|
||||
|
||||
describe "initialization" do
|
||||
it "can't be called with no arguments" do
|
||||
expect { described_class.new }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "needs to be passed a file" do
|
||||
expect { described_class.new("/tmp/thisfiledoesnotexist.yml") }.to raise_error(Errno::ENOENT)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#facts" do
|
||||
it "will return a hash" do
|
||||
expect(subject.facts).to be_a(Hash)
|
||||
end
|
||||
|
||||
it "will return the proper hash" do
|
||||
expect(subject.facts[:"ec2_network_interfaces_macs_0a:11:22:33:44:55_owner_id"]).to eq("987654321012")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#select" do
|
||||
it "will do nothing if the file matches the conditions with symbolized keys" do
|
||||
expect { subject.select(domain: "example.net") }.not_to raise_error
|
||||
end
|
||||
|
||||
it "will do nothing if the file matches the conditions with string keys" do
|
||||
expect { subject.select("domain" => "example.net") }.not_to raise_error
|
||||
end
|
||||
|
||||
it "will raise an error if the file can't match the conditions" do
|
||||
expect { subject.select("domain" => "wrongdomain.net") }.to raise_error(Octofacts::Errors::NoFactsError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#reject" do
|
||||
it "will do nothing if the file can't match the conditions with symbolized keys" do
|
||||
expect { subject.reject(domain: "wrongdomain.net") }.not_to raise_error
|
||||
end
|
||||
|
||||
it "will do nothing if the file can't match the conditions with string keys" do
|
||||
expect { subject.reject("domain" => "wrongdomain.net") }.not_to raise_error
|
||||
end
|
||||
|
||||
it "will raise an error if the file matches the conditions" do
|
||||
expect { subject.reject("domain" => "example.net") }.to raise_error(Octofacts::Errors::NoFactsError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#prefer" do
|
||||
it "is a noop in this backend" do
|
||||
expect { subject.prefer("domain" => "example.net") }.not_to raise_error
|
||||
expect { subject.prefer(domain: "example.net") }.not_to raise_error
|
||||
expect { subject.prefer("domain" => "wrongdomain.net") }.not_to raise_error
|
||||
expect { subject.prefer(domain: "wrongdomain.net") }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Octofacts do
|
||||
describe "#from_file" do
|
||||
before(:each) { ENV.delete("OCTOFACTS_FIXTURE_PATH") }
|
||||
after(:all) { ENV.delete("OCTOFACTS_FIXTURE_PATH") }
|
||||
|
||||
it "should load from a full filename" do
|
||||
ENV["OCTOFACTS_FIXTURE_PATH"] = File.join(Octofacts::Spec.fixture_root, "does", "not", "exist")
|
||||
filename = File.join(Octofacts::Spec.fixture_root, "facts", "basic.yaml")
|
||||
test_obj = Octofacts.from_file(filename)
|
||||
expect(test_obj.facts[:ec2_ami_id]).to eq("ami-000decaf")
|
||||
end
|
||||
|
||||
it "should load from a relative filename plus environment variable path" do
|
||||
ENV["OCTOFACTS_FIXTURE_PATH"] = File.join(Octofacts::Spec.fixture_root, "facts")
|
||||
filename = "basic.yaml"
|
||||
test_obj = Octofacts.from_file(filename)
|
||||
expect(test_obj.facts[:ec2_ami_id]).to eq("ami-000decaf")
|
||||
end
|
||||
|
||||
it "should load from a relative filename plus provided path" do
|
||||
ENV["OCTOFACTS_FIXTURE_PATH"] = File.join(Octofacts::Spec.fixture_root, "does", "not", "exist")
|
||||
path = File.join(Octofacts::Spec.fixture_root, "facts")
|
||||
filename = "basic.yaml"
|
||||
test_obj = Octofacts.from_file(filename, octofacts_fixture_path: path)
|
||||
expect(test_obj.facts[:ec2_ami_id]).to eq("ami-000decaf")
|
||||
end
|
||||
|
||||
it "should fail with no provided or environment path" do
|
||||
filename = "basic.yaml"
|
||||
expect { Octofacts.from_file(filename) }.to raise_error(ArgumentError, /.from_file needs to know :octofacts_fixture_path/)
|
||||
end
|
||||
|
||||
it "should fail if the fixture path does not exist" do
|
||||
ENV["OCTOFACTS_FIXTURE_PATH"] = File.join(Octofacts::Spec.fixture_root, "does", "not", "exist")
|
||||
filename = "basic.yaml"
|
||||
expect { Octofacts.from_file(filename) }.to raise_error(Errno::ENOENT, /The provided fixture path/)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,79 @@
|
|||
# Make sure the examples in the README document actually work. :-)
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
describe "Examples from README.md" do
|
||||
let(:index_path) { File.join(Octofacts::Spec.fixture_root, "index.yaml") }
|
||||
let(:fixture_path) { File.join(Octofacts::Spec.fixture_root, "facts") }
|
||||
let(:subject) { described_class.new(index_path: index_path, fixture_path: fixture_path) }
|
||||
|
||||
before(:each) do
|
||||
ENV["OCTOFACTS_INDEX_PATH"] = index_path
|
||||
ENV["OCTOFACTS_FIXTURE_PATH"] = fixture_path
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
ENV["OCTOFACTS_INDEX_PATH"] = nil
|
||||
ENV["OCTOFACTS_FIXTURE_PATH"] = fixture_path
|
||||
end
|
||||
|
||||
it "should grab a match from the index" do
|
||||
result = Octofacts.from_index(app: "ops", role: "consul", datacenter: "dc1")
|
||||
expect(result).to be_a_kind_of(Octofacts::Facts)
|
||||
expect(result.facts).to eq(
|
||||
{
|
||||
fqdn: "ops-consul-67890.dc1.example.com",
|
||||
datacenter: "dc1",
|
||||
app: "ops",
|
||||
env: "production",
|
||||
role: "consul",
|
||||
lsbdistcodename: "jessie",
|
||||
shorthost: "ops-consul-67890"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it "should grab a match from the index and replace" do
|
||||
result = Octofacts.from_index(app: "ops", role: "consul", datacenter: "dc1").replace(lsbdistcodename: "hats")
|
||||
expect(result).to be_a_kind_of(Octofacts::Facts)
|
||||
expect(result.facts).to eq(
|
||||
{
|
||||
fqdn: "ops-consul-67890.dc1.example.com",
|
||||
datacenter: "dc1",
|
||||
app: "ops",
|
||||
env: "production",
|
||||
role: "consul",
|
||||
lsbdistcodename: "hats",
|
||||
shorthost: "ops-consul-67890"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it "should work with plain old ruby calling `facts`" do
|
||||
f = Octofacts.from_index(app: "ops", role: "consul", datacenter: "dc1").facts
|
||||
f[:lsbdistcodename] = "hats"
|
||||
f.delete(:env)
|
||||
expect(f).to eq(
|
||||
{
|
||||
fqdn: "ops-consul-67890.dc1.example.com",
|
||||
datacenter: "dc1",
|
||||
app: "ops",
|
||||
role: "consul",
|
||||
lsbdistcodename: "hats",
|
||||
shorthost: "ops-consul-67890"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it "should work with plain old ruby without calling `facts`" do
|
||||
f = Octofacts.from_index(app: "ops", role: "consul", datacenter: "dc1")
|
||||
f[:lsbdistcodename] = "hats"
|
||||
f.delete(:env)
|
||||
expect(f).to be_a_kind_of(Octofacts::Facts)
|
||||
expect(f[:fqdn]).to eq("ops-consul-67890.dc1.example.com")
|
||||
expect(f[:datacenter]).to eq("dc1")
|
||||
expect(f[:app]).to eq("ops")
|
||||
expect(f[:role]).to eq("consul")
|
||||
expect(f[:lsbdistcodename]).to eq("hats")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,201 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Octofacts::Facts do
|
||||
describe "#select" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar") }
|
||||
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should pass through select method to backend" do
|
||||
result = subject.select(foo: "bar")
|
||||
expect(result).to be_a_kind_of(Octofacts::Facts)
|
||||
expect(result.facts).to eq({ foo: "bar" })
|
||||
expect(backend.select_called).to eq(true)
|
||||
end
|
||||
|
||||
it "should raise OperationNotPermitted if called after facts were manipulated" do
|
||||
result = subject.replace(foo: "baz")
|
||||
expect(result.facts).to eq({ foo: "baz" })
|
||||
expect { result.select(foo: "bar") }.to raise_error(Octofacts::Errors::OperationNotPermitted)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#reject" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar") }
|
||||
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should pass through reject method to backend" do
|
||||
result = subject.reject(foo: "bar")
|
||||
expect(result).to be_a_kind_of(Octofacts::Facts)
|
||||
expect(result.facts).to eq({foo: "bar"})
|
||||
expect(backend.reject_called).to eq(true)
|
||||
end
|
||||
|
||||
it "should raise OperationNotPermitted if called after facts were manipulated" do
|
||||
result = subject.replace(foo: "baz")
|
||||
expect(result.facts).to eq({ foo: "baz" })
|
||||
expect { result.reject(foo: "bar") }.to raise_error(Octofacts::Errors::OperationNotPermitted)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#prefer" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar") }
|
||||
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should pass through prefer method to backend" do
|
||||
result = subject.prefer(foo: "bar")
|
||||
expect(result).to be_a_kind_of(Octofacts::Facts)
|
||||
expect(result.facts).to eq({foo: "bar"})
|
||||
expect(backend.prefer_called).to eq(true)
|
||||
end
|
||||
|
||||
it "should raise OperationNotPermitted if called after facts were manipulated" do
|
||||
result = subject.replace(foo: "baz")
|
||||
expect(result.facts).to eq({ foo: "baz" })
|
||||
expect { result.prefer(foo: "bar") }.to raise_error(Octofacts::Errors::OperationNotPermitted)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_hash" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar", baz: { buzz: "fizz" }) }
|
||||
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should return a de-symbolized (string values) hash" do
|
||||
expect(subject.to_hash).to eq({"foo"=>"bar", "baz"=>{"buzz"=>"fizz"}})
|
||||
end
|
||||
end
|
||||
|
||||
describe "#method_missing" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar", baz: { buzz: "fizz" }) }
|
||||
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should raise NameError when the method does not exist" do
|
||||
expect { subject.call(:thismethoddoesnotexistanywhere) }.to raise_error(NameError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#[]" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar", baz: { buzz: "fizz" }) }
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should transparently get data from the backend with a symbol key" do
|
||||
expect(subject[:foo]).to eq("bar")
|
||||
end
|
||||
|
||||
it "should transparently get data from the backend with a string key" do
|
||||
expect(subject["foo"]).to eq("bar")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#fetch" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar", baz: { buzz: "fizz" }) }
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should transparently get data from the backend with a symbol key" do
|
||||
expect(subject.fetch(:foo, "hats")).to eq("bar")
|
||||
expect(subject.fetch(:foo)).to eq("bar")
|
||||
end
|
||||
|
||||
it "should transparently get data from the backend with a string key" do
|
||||
expect(subject.fetch("foo", "hats")).to eq("bar")
|
||||
expect(subject.fetch("foo")).to eq("bar")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#[]=" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar", baz: { buzz: "fizz" }) }
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should set new facts with a symbol key" do
|
||||
subject[:xyz] = "zyx"
|
||||
expect(subject).to be_a_kind_of(Octofacts::Facts)
|
||||
expect(subject[:xyz]).to eq("zyx")
|
||||
expect(subject["xyz"]).to eq("zyx")
|
||||
end
|
||||
|
||||
it "should set new facts with a symbol key" do
|
||||
subject["xyz"] = "zyx"
|
||||
expect(subject).to be_a_kind_of(Octofacts::Facts)
|
||||
expect(subject["xyz"]).to eq("zyx")
|
||||
expect(subject[:xyz]).to eq("zyx")
|
||||
end
|
||||
|
||||
it "does not interfere with manipulators" do
|
||||
subject[:xyz] = "zyx"
|
||||
expect(subject).to be_a_kind_of(Octofacts::Facts)
|
||||
subject.replace(xyz: "xyz")
|
||||
expect(subject).to be_a_kind_of(Octofacts::Facts)
|
||||
expect(subject[:xyz]).to eq("xyz")
|
||||
expect(backend.facts[:xyz]).to eq("xyz")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#respond_to?" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar", baz: { buzz: "fizz" }) }
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should return true when the method is available in the class" do
|
||||
expect(subject.respond_to?(:select)).to be_truthy
|
||||
end
|
||||
|
||||
it "should return true when the method matches a manipulator" do
|
||||
expect(subject.respond_to?(:fake)).to be_truthy
|
||||
end
|
||||
|
||||
it "should return true when the method is available in Hash" do
|
||||
expect(subject.respond_to?(:merge)).to be_truthy
|
||||
end
|
||||
|
||||
it "should return false otherwise" do
|
||||
expect(subject.respond_to?(:foobar)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "#replace" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar", baz: { buzz: "fizz" }) }
|
||||
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should return a Octofacts::Facts object when .replace is called" do
|
||||
result = subject.replace(foo: "bar")
|
||||
expect(result).to be_a_kind_of(Octofacts::Facts)
|
||||
end
|
||||
|
||||
it "should return a Octofacts::Facts object when .replace is called multiple times" do
|
||||
result = subject.replace(foo: "bar").replace(baz: "baz").replace(buzz: "buzz")
|
||||
expect(result).to be_a_kind_of(Octofacts::Facts)
|
||||
end
|
||||
|
||||
it "can still be accessed as a hash" do
|
||||
subject.replace(foo: "bar")
|
||||
expect(subject[:foo]).to eq("bar")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#string_or_symbolized_key" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new({}) }
|
||||
let(:subject) { described_class.new(backend: backend) }
|
||||
|
||||
it "should return string key if string key exists" do
|
||||
allow(subject).to receive(:facts).and_return(:foo => "bar", "fizz" => "buzz")
|
||||
expect(subject.send(:string_or_symbolized_key, "fizz")).to eq("fizz")
|
||||
expect(subject.send(:string_or_symbolized_key, :fizz)).to eq("fizz")
|
||||
end
|
||||
|
||||
it "should return symbol key if symbol key exists" do
|
||||
allow(subject).to receive(:facts).and_return(:foo => "bar", "fizz" => "buzz")
|
||||
expect(subject.send(:string_or_symbolized_key, "foo")).to eq(:foo)
|
||||
expect(subject.send(:string_or_symbolized_key, :foo)).to eq(:foo)
|
||||
end
|
||||
|
||||
it "should return input key if neither exist" do
|
||||
allow(subject).to receive(:facts).and_return(:foo => "bar", "fizz" => "buzz")
|
||||
expect(subject.send(:string_or_symbolized_key, "baz")).to eq("baz")
|
||||
expect(subject.send(:string_or_symbolized_key, :baz)).to eq(:baz)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,210 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Octofacts::Manipulators do
|
||||
describe "#delete" do
|
||||
it "Should delete a string key at the top level" do
|
||||
fact_set = { foo: "bar", fizz: "buzz" }
|
||||
described_class.delete(fact_set, "foo")
|
||||
expect(fact_set).to eq(fizz: "buzz")
|
||||
end
|
||||
|
||||
it "Should set a symbolized key at the top level" do
|
||||
fact_set = { foo: "bar", fizz: "buzz" }
|
||||
described_class.delete(fact_set, :foo)
|
||||
expect(fact_set).to eq(fizz: "buzz")
|
||||
end
|
||||
|
||||
it "Should delete an intermediate nested key" do
|
||||
fact_set = { foo: "bar", level1: { string2: "foo", level2: { level3: "level4" } } }
|
||||
described_class.delete(fact_set, "level1::level2")
|
||||
expect(fact_set).to eq({foo: "bar", level1: {string2: "foo"}})
|
||||
end
|
||||
|
||||
it "Should delete a final nested key" do
|
||||
fact_set = { foo: "bar", level1: { string2: "foo", level2: { level3: "level4" } } }
|
||||
described_class.delete(fact_set, "level1::level2::level3")
|
||||
expect(fact_set).to eq({foo: "bar", level1: {string2: "foo", level2: {}}})
|
||||
end
|
||||
|
||||
it "Should not delete anything if the key was not a match" do
|
||||
fact_set = { foo: "bar", level1: { string2: "foo", level2: { level3: "level4" } } }
|
||||
described_class.delete(fact_set, "hats")
|
||||
described_class.delete(fact_set, :hats)
|
||||
described_class.delete(fact_set, "level1::hats::level3")
|
||||
described_class.delete(fact_set, "level1::level2::level3::level4")
|
||||
expect(fact_set).to eq({foo: "bar", level1: {string2: "foo", level2: {level3: "level4"}}})
|
||||
end
|
||||
end
|
||||
|
||||
describe "#exists?" do
|
||||
it "Should operate at the top level with a string key" do
|
||||
fact_set = { foo: "bar" }
|
||||
expect(described_class.exists?(fact_set, "foo")).to eq(true)
|
||||
expect(described_class.exists?(fact_set, "baz")).to eq(false)
|
||||
end
|
||||
|
||||
it "Should operate at the top level with a symbolized key" do
|
||||
fact_set = { foo: "bar" }
|
||||
expect(described_class.exists?(fact_set, :foo)).to eq(true)
|
||||
expect(described_class.exists?(fact_set, :baz)).to eq(false)
|
||||
end
|
||||
|
||||
it "Should dig into a hash and find a nested key" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
expect(described_class.exists?(fact_set, "level1::level2::level3")).to eq(true)
|
||||
end
|
||||
|
||||
it "Should return false if the entire structure does not exist" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
expect(described_class.exists?(fact_set, "hats::hats::hats")).to eq(false)
|
||||
end
|
||||
|
||||
it "Should return false if the partial structure does not exist" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
expect(described_class.exists?(fact_set, "level1::hats::hats")).to eq(false)
|
||||
end
|
||||
|
||||
it "Should return false if the final fact does not exist" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
expect(described_class.exists?(fact_set, "level1::level2::hats")).to eq(false)
|
||||
end
|
||||
|
||||
it "Should return false if it encounters a non-hash intermediate key" do
|
||||
fact_set = { foo: "bar", level1: { string2: "foo", level2: { level3: "level4" } } }
|
||||
expect(described_class.exists?(fact_set, "level1::string2::baz")).to eq(false)
|
||||
expect(described_class.exists?(fact_set, "level1::level2::level3::level4")).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get" do
|
||||
it "Should operate at the top level with a string key" do
|
||||
fact_set = { foo: "bar" }
|
||||
expect(described_class.get(fact_set, "foo")).to eq("bar")
|
||||
expect(described_class.get(fact_set, "baz")).to eq(nil)
|
||||
end
|
||||
|
||||
it "Should operate at the top level with a symbolized key" do
|
||||
fact_set = { foo: "bar" }
|
||||
expect(described_class.get(fact_set, :foo)).to eq("bar")
|
||||
expect(described_class.get(fact_set, :baz)).to eq(nil)
|
||||
end
|
||||
|
||||
it "Should dig into a hash and find a nested key" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
expect(described_class.get(fact_set, "level1::level2::level3")).to eq("level4")
|
||||
end
|
||||
|
||||
it "Should return false if the entire structure does not exist" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
expect(described_class.get(fact_set, "hats::hats::hats")).to eq(nil)
|
||||
end
|
||||
|
||||
it "Should return false if the partial structure does not exist" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
expect(described_class.get(fact_set, "level1::hats::hats")).to eq(nil)
|
||||
end
|
||||
|
||||
it "Should return false if the final fact does not exist" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
expect(described_class.get(fact_set, "level1::level2::hats")).to eq(nil)
|
||||
end
|
||||
|
||||
it "Should return false if it encounters a non-hash intermediate key" do
|
||||
fact_set = { foo: "bar", level1: { string2: "foo", level2: { level3: "level4" } } }
|
||||
expect(described_class.get(fact_set, "level1::string2::baz")).to eq(nil)
|
||||
expect(described_class.get(fact_set, "level1::level2::level3::level4")).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#set" do
|
||||
it "Should set a string key at the top level" do
|
||||
fact_set = { foo: "bar" }
|
||||
described_class.set(fact_set, "fizz", "buzz")
|
||||
expect(fact_set[:foo]).to eq("bar")
|
||||
expect(fact_set[:fizz]).to eq("buzz")
|
||||
end
|
||||
|
||||
it "Should set a symbolized key at the top level" do
|
||||
fact_set = { foo: "bar" }
|
||||
described_class.set(fact_set, :fizz, "buzz")
|
||||
expect(fact_set[:foo]).to eq("bar")
|
||||
expect(fact_set[:fizz]).to eq("buzz")
|
||||
end
|
||||
|
||||
it "Should replace a nested key" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
described_class.set(fact_set, "level1::level2::level3", "new_value")
|
||||
expect(fact_set[:foo]).to eq("bar")
|
||||
expect(fact_set[:level1]).to eq({ level2: { level3: "new_value" }})
|
||||
end
|
||||
|
||||
it "Should auto-create a complete hash structure" do
|
||||
fact_set = { foo: "bar" }
|
||||
described_class.set(fact_set, "level1::level2::level3", "new_value")
|
||||
expect(fact_set[:foo]).to eq("bar")
|
||||
expect(fact_set[:level1]).to eq({ level2: { level3: "new_value" }})
|
||||
end
|
||||
|
||||
it "Should auto-create a partial hash structure" do
|
||||
fact_set = { foo: "bar", level1: {} }
|
||||
described_class.set(fact_set, "level1::level2::level3", "new_value")
|
||||
expect(fact_set[:foo]).to eq("bar")
|
||||
expect(fact_set[:level1]).to eq({ level2: { level3: "new_value" }})
|
||||
end
|
||||
|
||||
it "Should auto-replace parts of a hash structure" do
|
||||
fact_set = { foo: "bar", level1: "foo" }
|
||||
described_class.set(fact_set, "level1::level2::level3", "new_value")
|
||||
expect(fact_set[:foo]).to eq("bar")
|
||||
expect(fact_set[:level1]).to eq({ level2: { level3: "new_value" }})
|
||||
end
|
||||
|
||||
it "Should accept a lambda as the value and apply it at the top level" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
my_lambda = lambda { |fact_set, fact_name, value| value.upcase }
|
||||
described_class.set(fact_set, "foo", my_lambda)
|
||||
expect(fact_set[:foo]).to eq("BAR")
|
||||
end
|
||||
|
||||
it "Should accept a lambda as the value and (attempt to) apply it an intermediate nested level" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
my_lambda = lambda { |fact_set, fact_name, value| value.merge(my_lambda_ran: true) }
|
||||
described_class.set(fact_set, "level1::level2", my_lambda)
|
||||
expect(fact_set[:level1][:level2]).to eq({ level3: "level4", my_lambda_ran: true })
|
||||
end
|
||||
|
||||
it "Should accept a lambda as the value and apply it a final nested level" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
my_lambda = lambda { |fact_set, fact_name, value| value.upcase }
|
||||
described_class.set(fact_set, "level1::level2::level3", my_lambda)
|
||||
expect(fact_set[:level1][:level2][:level3]).to eq("LEVEL4")
|
||||
end
|
||||
|
||||
it "Should accept a lambda for a brand new key" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
my_lambda = lambda { |fact_set, fact_name, value| "Make me new" }
|
||||
described_class.set(fact_set, "baz", my_lambda)
|
||||
expect(fact_set[:baz]).to eq("Make me new")
|
||||
end
|
||||
|
||||
it "Should delete a key if the result of the lambda is nil" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
my_lambda = lambda { |fact_set, fact_name, value| nil }
|
||||
described_class.set(fact_set, "foo", my_lambda)
|
||||
expect(fact_set.key?(:foo)).to eq(false)
|
||||
end
|
||||
|
||||
it "Should accept a single argument lambda" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
my_lambda = lambda { |value| "Make me new" }
|
||||
described_class.set(fact_set, "baz", my_lambda)
|
||||
expect(fact_set[:baz]).to eq("Make me new")
|
||||
end
|
||||
|
||||
it "Should error if a lambda with the wrong arguments is passed" do
|
||||
fact_set = { foo: "bar", level1: { level2: { level3: "level4" } } }
|
||||
my_lambda = lambda { |foo, value| "Make me new" }
|
||||
expect { described_class.set(fact_set, "baz", my_lambda) }.to raise_error(ArgumentError, /1 or 3 parameters, got 2/)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,68 @@
|
|||
require "spec_helper"
|
||||
require "yaml"
|
||||
|
||||
describe Octofacts::Manipulators::Set do
|
||||
before(:each) do
|
||||
fixture = File.join(Octofacts::Spec.fixture_root, "facts", "basic.yaml")
|
||||
@obj = Octofacts.from_file(fixture)
|
||||
end
|
||||
|
||||
it "should contain the expected facts" do
|
||||
expect(@obj.facts[:fqdn]).to eq("somenode.example.net")
|
||||
expect(@obj.facts[:hostname]).to eq("somenode")
|
||||
expect(@obj.facts[:processor0]).to eq("Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz")
|
||||
expect(@obj.facts[:os][:family]).to eq("Debian")
|
||||
end
|
||||
|
||||
it "should do nothing when called with no arguments" do
|
||||
@obj.replace
|
||||
expect(@obj.facts[:fqdn]).to eq("somenode.example.net")
|
||||
expect(@obj.facts[:hostname]).to eq("somenode")
|
||||
expect(@obj.facts[:processor0]).to eq("Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz")
|
||||
expect(@obj.facts[:os][:family]).to eq("Debian")
|
||||
end
|
||||
|
||||
it "should set a simple stringified fact at the top level, addressed by symbol" do
|
||||
@obj.replace(operatingsystem: "OctoAwesome OS")
|
||||
expect(@obj.facts[:operatingsystem]).to eq("OctoAwesome OS")
|
||||
end
|
||||
|
||||
it "should set a simple stringified fact at the top level, addressed by string" do
|
||||
@obj.replace(operatingsystem: "OctoAwesome OS")
|
||||
expect(@obj.facts[:operatingsystem]).to eq("OctoAwesome OS")
|
||||
end
|
||||
|
||||
it "should set two facts at the same time" do
|
||||
@obj.replace(operatingsystem: "OctoAwesome OS", hostname: "octoawesome")
|
||||
expect(@obj.facts[:hostname]).to eq("octoawesome")
|
||||
expect(@obj.facts[:operatingsystem]).to eq("OctoAwesome OS")
|
||||
end
|
||||
|
||||
it "should instantiate a fact that did not exist before" do
|
||||
@obj.replace(hats: "OctoAwesome OS", hostname: "octoawesome")
|
||||
expect(@obj.facts[:hostname]).to eq("octoawesome")
|
||||
expect(@obj.facts[:hats]).to eq("OctoAwesome OS")
|
||||
end
|
||||
|
||||
it "should set a nested fact" do
|
||||
@obj.replace("ec2_metadata::placement::availability-zone" => "the-moon-1a")
|
||||
expect(@obj.facts[:ec2_metadata][:placement][:"availability-zone"]).to eq("the-moon-1a")
|
||||
end
|
||||
|
||||
it "should be possible to chain replace operators" do
|
||||
@obj.replace(operatingsystem: "OctoAwesome OS").replace(hostname: "octoawesome")
|
||||
expect(@obj.facts[:hostname]).to eq("octoawesome")
|
||||
expect(@obj.facts[:operatingsystem]).to eq("OctoAwesome OS")
|
||||
end
|
||||
|
||||
it "should accept a single argument lambda" do
|
||||
@obj.replace(operatingsystem: lambda { |value| value.upcase })
|
||||
expect(@obj.facts[:hostname]).to eq("somenode")
|
||||
expect(@obj.facts[:operatingsystem]).to eq("DEBIAN")
|
||||
end
|
||||
|
||||
it "should accept a 3 argument lambda" do
|
||||
@obj.replace(operatingsystem: lambda { |fact_set, key, value| fact_set[:hostname] + value })
|
||||
expect(@obj.facts[:operatingsystem]).to eq("somenodeDebian")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Octofacts::Manipulators do
|
||||
describe "#self.run" do
|
||||
let(:backend) { Octofacts::Backends::Hash.new(foo: "bar") }
|
||||
|
||||
let(:facts_object) { Octofacts::Facts.new(backend: backend) }
|
||||
|
||||
it "should return false if no manipulator exists" do
|
||||
result = described_class.run(facts_object, "no_such_manipulator")
|
||||
expect(result).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
require "spec_helper"
|
||||
|
||||
# FIXME: Remove when real specs are added
|
||||
# This has only been checked in to test our CI job
|
||||
|
||||
describe Octofacts::VERSION do
|
||||
it "is set" do
|
||||
expect(Octofacts::VERSION).to_not be_nil
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
module Octofacts
|
||||
class Spec
|
||||
def self.fixture_root
|
||||
File.expand_path("../fixtures", File.dirname(__FILE__))
|
||||
end
|
||||
end
|
||||
|
||||
module Backends
|
||||
class Hash < Base
|
||||
attr_reader :facts, :select_called, :reject_called, :prefer_called
|
||||
|
||||
def initialize(hash_in)
|
||||
@facts = hash_in
|
||||
end
|
||||
|
||||
def select(_)
|
||||
@select_called = true
|
||||
self
|
||||
end
|
||||
|
||||
def reject(_)
|
||||
@reject_called = true
|
||||
self
|
||||
end
|
||||
|
||||
def prefer(_)
|
||||
@prefer_called = true
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Manipulators
|
||||
class Fake < Octofacts::Manipulators
|
||||
def self.execute(facts, *args)
|
||||
# noop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe Octofacts::Util::Config do
|
||||
before(:all) do
|
||||
RSpec.configure do |c|
|
||||
c.add_setting :fake_setting
|
||||
c.fake_setting = "kittens"
|
||||
end
|
||||
ENV["FAKE_SETTING"] = "hats"
|
||||
ENV["FAKE_SETTING_2"] = "cats"
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
RSpec.configure do |c|
|
||||
c.fake_setting = nil
|
||||
end
|
||||
ENV.delete("FAKE_SETTING")
|
||||
ENV.delete("FAKE_SETTING_2")
|
||||
end
|
||||
|
||||
describe "#fetch" do
|
||||
it "should return a value from the hash" do
|
||||
h = { fake_setting: "chickens" }
|
||||
expect(described_class.fetch(:fake_setting, h, "dogs")).to eq("chickens")
|
||||
end
|
||||
|
||||
it "should return a value from the rspec configuration" do
|
||||
expect(described_class.fetch(:fake_setting, {}, "dogs")).to eq("kittens")
|
||||
end
|
||||
|
||||
it "should return a value from the environment" do
|
||||
expect(described_class.fetch(:fake_setting_2, {}, "dogs")).to eq("cats")
|
||||
end
|
||||
|
||||
it "should return the default value" do
|
||||
expect(described_class.fetch(:fake_setting_3, {}, "dogs")).to eq("dogs")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
describe Octofacts::Util::Keys do
|
||||
describe "#downcase_keys!" do
|
||||
let(:facts) do
|
||||
{ "foo" => "foo-value", "Bar" => "bar-value", :baz => "baz-value", :buZZ => "buzz-value" }
|
||||
end
|
||||
|
||||
it "should work" do
|
||||
f = facts
|
||||
result = described_class.downcase_keys!(f)
|
||||
expect(f).to eq({"foo" => "foo-value", baz: "baz-value", "bar" => "bar-value", buzz: "buzz-value"})
|
||||
expect(result).to eq({"foo" => "foo-value", baz: "baz-value", "bar" => "bar-value", buzz: "buzz-value"})
|
||||
end
|
||||
end
|
||||
|
||||
describe "#symbolize_keys!" do
|
||||
let(:facts) do
|
||||
{ "foo" => "foo-value", "Bar" => "bar-value", :baz => "baz-value", :buZZ => "buzz-value" }
|
||||
end
|
||||
|
||||
it "should work" do
|
||||
f = facts
|
||||
result = described_class.symbolize_keys!(f)
|
||||
expect(f).to eq({foo: "foo-value", baz: "baz-value", Bar: "bar-value", buZZ: "buzz-value"})
|
||||
expect(result).to eq({foo: "foo-value", baz: "baz-value", Bar: "bar-value", buZZ: "buzz-value"})
|
||||
end
|
||||
end
|
||||
|
||||
describe "#desymbolize_keys!" do
|
||||
let(:facts) do
|
||||
{ "foo" => "foo-value", "Bar" => "bar-value", :baz => "baz-value", :buZZ => "buzz-value" }
|
||||
end
|
||||
|
||||
it "should work" do
|
||||
f = facts
|
||||
result = described_class.desymbolize_keys!(f)
|
||||
expect(f).to eq({"foo"=>"foo-value", "Bar"=>"bar-value", "baz"=>"baz-value", "buZZ"=>"buzz-value"})
|
||||
expect(result).to eq({"foo"=>"foo-value", "Bar"=>"bar-value", "baz"=>"baz-value", "buZZ"=>"buzz-value"})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,252 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe OctofactsUpdater::FactIndex do
|
||||
let(:fixture_path) { File.expand_path("../fixtures", File.dirname(__FILE__)) }
|
||||
|
||||
let(:fixture1) do
|
||||
OpenStruct.new(
|
||||
hostname: "host1",
|
||||
facts: {
|
||||
"fizz" => OpenStruct.new(value: { "buzz" => "zip" }),
|
||||
"foo" => OpenStruct.new(value: "bar")
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let(:fixture2) do
|
||||
OpenStruct.new(
|
||||
hostname: "host2",
|
||||
facts: {
|
||||
"foo" => OpenStruct.new(value: "bar")
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let(:fixture3) do
|
||||
OpenStruct.new(
|
||||
hostname: "host3",
|
||||
facts: {
|
||||
"foo" => OpenStruct.new(value: "baz")
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let(:fixture4) do
|
||||
OpenStruct.new(
|
||||
hostname: "host4",
|
||||
facts: {
|
||||
"fizz" => OpenStruct.new(value: { "buzz" => "baz" })
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
describe "#load_file" do
|
||||
it "should raise an error if the file does not exist" do
|
||||
fixture_file = File.join(fixture_path, "non-existing.yaml")
|
||||
expect { described_class.load_file(fixture_file) }.to raise_error(Errno::ENOENT)
|
||||
end
|
||||
|
||||
it "should return the object based on the YAML parsed file" do
|
||||
fixture_file = File.join(fixture_path, "index.yaml")
|
||||
result = described_class.load_file(fixture_file)
|
||||
expect(result).to be_a_kind_of(described_class)
|
||||
expect(result.nodes).to eq(["ops-consul-67890.dc1.example.com", "puppet-puppetserver-12345.dc1.example.com", "puppet-puppetserver-00decaf.dc1.example.com", "ops-consul-12345.dc2.example.com"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#add" do
|
||||
let(:subject) { described_class.new({}) }
|
||||
|
||||
it "should update index when fact did not exist before" do
|
||||
subject.add("foo", [fixture1, fixture2, fixture3, fixture4])
|
||||
expect(subject.index_data).to eq({"foo"=>{"bar"=>["host1", "host2"], "baz"=>["host3"]}})
|
||||
end
|
||||
|
||||
it "should update index when fact existed before but value did not" do
|
||||
subject.add("foo", [fixture1, fixture2])
|
||||
subject.add("foo", [fixture3, fixture4])
|
||||
expect(subject.index_data).to eq({"foo"=>{"bar"=>["host1", "host2"], "baz"=>["host3"]}})
|
||||
end
|
||||
|
||||
it "should update index when fact existed and value existed" do
|
||||
subject.add("foo", [fixture1, fixture4])
|
||||
subject.add("foo", [fixture3, fixture2])
|
||||
expect(subject.index_data).to eq({"foo"=>{"bar"=>["host1", "host2"], "baz"=>["host3"]}})
|
||||
end
|
||||
|
||||
it "should add a structured fact" do
|
||||
subject.add("fizz.buzz", [fixture1, fixture2, fixture3, fixture4])
|
||||
expect(subject.index_data).to eq({"fizz.buzz"=>{"zip"=>["host1"], "baz"=>["host4"]}})
|
||||
end
|
||||
end
|
||||
|
||||
describe "#nodes" do
|
||||
let(:fixture_file) { File.join(fixture_path, "index-no-nodes.yaml") }
|
||||
let(:subject) { described_class.load_file(fixture_file) }
|
||||
|
||||
context "in quick mode" do
|
||||
it "should blindly display the nodes from the fixture" do
|
||||
result = subject.nodes(true)
|
||||
expect(result).to eq(["broken.example.com"])
|
||||
end
|
||||
end
|
||||
|
||||
context "not in quick mode" do
|
||||
it "should re-compute and sort the nodes from the fixture" do
|
||||
result = subject.nodes(false)
|
||||
expect(result).to eq(["ops-consul-12345.dc2.example.com", "ops-consul-67890.dc1.example.com", "puppet-puppetserver-00decaf.dc1.example.com", "puppet-puppetserver-12345.dc1.example.com"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#reindex" do
|
||||
let(:subject) { described_class.new({}) }
|
||||
let(:answer) do
|
||||
{
|
||||
"foo"=>{
|
||||
"bar"=>["host1", "host2"],
|
||||
"baz"=>["host3"]
|
||||
},
|
||||
"bar"=>{},
|
||||
"fizz.buzz"=>{
|
||||
"zip"=>["host1"],
|
||||
"baz"=>["host4"]
|
||||
},
|
||||
"_nodes"=>["host1", "host2", "host3", "host4"]
|
||||
}
|
||||
end
|
||||
|
||||
it "should construct the index from the provided fixtures" do
|
||||
subject.reindex(["foo", "bar", "fizz.buzz"], [fixture1, fixture2, fixture3, fixture4])
|
||||
expect(subject.index_data).to eq(answer)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#set_top_level_nodes_fact" do
|
||||
let(:subject) { described_class.new({}) }
|
||||
|
||||
it "should set host names from fixtures (sorted)" do
|
||||
subject.set_top_level_nodes_fact([fixture3, fixture1, fixture2, fixture4])
|
||||
expect(subject.index_data["_nodes"]).to eq(["host1", "host2", "host3", "host4"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_yaml" do
|
||||
let(:subject) { described_class.new({ "foo" => "bar", "fizz" => "buzz" }) }
|
||||
|
||||
it "should return YAML representation with sorted keys" do
|
||||
expect(subject.to_yaml).to eq("---\nfizz: buzz\nfoo: bar\n")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#write_file" do
|
||||
let(:fixture_path) { File.expand_path("../fixtures", File.dirname(__FILE__)) }
|
||||
let(:fixture_file) { File.join(fixture_path, "index.yaml") }
|
||||
let(:sorted_fixture_file) { File.join(fixture_path, "sorted-index.yaml") }
|
||||
|
||||
before(:each) do
|
||||
@tempdir = Dir.mktmpdir
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
FileUtils.remove_entry_secure(@tempdir) if File.directory?(@tempdir)
|
||||
end
|
||||
|
||||
context "when filename is supplied" do
|
||||
it "should write out the YAML file with the data" do
|
||||
outfile = File.join(@tempdir, "foo.yaml")
|
||||
|
||||
obj = described_class.load_file(fixture_file)
|
||||
obj.write_file(outfile)
|
||||
|
||||
expect(File.file?(outfile)).to eq(true)
|
||||
expect(File.read(outfile)).to eq(File.read(sorted_fixture_file))
|
||||
end
|
||||
end
|
||||
|
||||
context "when filename is in the object" do
|
||||
it "should write out the YAML file with the data" do
|
||||
outfile = File.join(@tempdir, "foo.yaml")
|
||||
|
||||
obj = described_class.load_file(fixture_file)
|
||||
obj.instance_variable_set("@filename", outfile)
|
||||
obj.write_file
|
||||
|
||||
expect(File.file?(outfile)).to eq(true)
|
||||
expect(File.read(outfile)).to eq(File.read(sorted_fixture_file))
|
||||
end
|
||||
end
|
||||
|
||||
context "when filename is not supplied" do
|
||||
it "should raise ArgumentError" do
|
||||
obj = described_class.new({})
|
||||
expect { obj.write_file }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get_fact" do
|
||||
let(:subject) { described_class.allocate }
|
||||
let(:fixture) { OpenStruct.new(hostname: "foo1", facts: facts_hash) }
|
||||
|
||||
context "key not in hash" do
|
||||
let(:facts_hash) { {} }
|
||||
|
||||
it "should return nil" do
|
||||
expect(subject.send(:get_fact, fixture, "foo")).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "simple value (not structured)" do
|
||||
let(:facts_hash) { { "foo" => OpenStruct.new(value: "bar") } }
|
||||
|
||||
it "should return the correct value" do
|
||||
expect(subject.send(:get_fact, fixture, "foo")).to eq("bar")
|
||||
end
|
||||
end
|
||||
|
||||
context "structured value 2 levels" do
|
||||
let(:facts_hash) { { "foo" => OpenStruct.new(value: { "level1" => "bar" }) } }
|
||||
|
||||
it "should return the correct value" do
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1")).to eq("bar")
|
||||
end
|
||||
|
||||
it "should return nil when the structure is not present" do
|
||||
expect(subject.send(:get_fact, fixture, "foo.bar")).to be_nil
|
||||
expect(subject.send(:get_fact, fixture, "foo.missing")).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "structured value 3 levels" do
|
||||
let(:facts_hash) { { "foo" => OpenStruct.new(value: { "level1" => { "level2" => "bar" }}) } }
|
||||
|
||||
it "should return the correct value" do
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1.level2")).to eq("bar")
|
||||
end
|
||||
|
||||
it "should return nil when the structure is not present" do
|
||||
expect(subject.send(:get_fact, fixture, "foo.missing.level1")).to be_nil
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1.missing")).to be_nil
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1.level2.bar")).to be_nil
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1.level2.missing")).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "structured value 4 levels" do
|
||||
let(:facts_hash) { { "foo" => OpenStruct.new(value: { "level1" => { "level2" => { "level3" => "bar" }}}) } }
|
||||
|
||||
it "should return the correct value" do
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1.level2.level3")).to eq("bar")
|
||||
end
|
||||
|
||||
it "should return nil when the structure is not present" do
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1.level2.missing")).to be_nil
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1.missing.level3")).to be_nil
|
||||
expect(subject.send(:get_fact, fixture, "foo.missing.level2.level3")).to be_nil
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1.level2.level3.bar")).to be_nil
|
||||
expect(subject.send(:get_fact, fixture, "foo.level1.level2.level3.missing")).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,158 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe OctofactsUpdater::Fact do
|
||||
describe "#value" do
|
||||
it "should return value if structured value is not requested" do
|
||||
subject = described_class.new("foo", "bar")
|
||||
expect(subject.value).to eq("bar")
|
||||
end
|
||||
|
||||
it "should return nil if structured value is requested for non-structured fact" do
|
||||
subject = described_class.new("foo", "bar")
|
||||
expect(subject.value("foo")).to be_nil
|
||||
expect(subject.value("baz")).to be_nil
|
||||
end
|
||||
|
||||
it "should return nil if not all keys exist in the path to a value" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
expect(subject.value("hats::baz::fizz")).to be_nil
|
||||
expect(subject.value("bar::hats::fizz")).to be_nil
|
||||
end
|
||||
|
||||
it "should return nil if the last key does not exist" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
expect(subject.value("bar::baz::hats")).to be_nil
|
||||
expect(subject.value("bar::baz::fizz::buzz")).to be_nil
|
||||
end
|
||||
|
||||
it "should return the value from the structured fact" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
expect(subject.value("bar::baz::fizz")).to eq("buzz")
|
||||
expect(subject.value("bar::baz")).to eq("fizz" => "buzz")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#value=" do
|
||||
it "should set the overall fact value if structured value is not requested" do
|
||||
subject = described_class.new("foo", "bar")
|
||||
subject.value = "baz"
|
||||
expect(subject.value).to eq("baz")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#set_value" do
|
||||
it "should set the overall fact value if structured value is not requested" do
|
||||
subject = described_class.new("foo", "bar")
|
||||
subject.set_value("baz")
|
||||
expect(subject.value).to eq("baz")
|
||||
end
|
||||
|
||||
it "should call a block if provided instead of a static value" do
|
||||
subject = described_class.new("foo", "bar")
|
||||
blk = Proc.new { |val| val.upcase }
|
||||
subject.set_value(blk)
|
||||
expect(subject.value).to eq("BAR")
|
||||
end
|
||||
|
||||
it "should raise an error if structured value is specified for non-structured fact" do
|
||||
subject = described_class.new("foo", "bar")
|
||||
expect { subject.set_value("baz", "foo") }.to raise_error(ArgumentError, /Cannot set structured value at "foo"/)
|
||||
expect { subject.set_value("baz", "key") }.to raise_error(ArgumentError, /Cannot set structured value at "key"/)
|
||||
expect { subject.set_value("baz", "bar::baz") }.to raise_error(ArgumentError, /Cannot set structured value at "bar"/)
|
||||
end
|
||||
|
||||
it "should raise an error if it encounters a non-structured value in the path" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
expect { subject.set_value("baz", "bar::baz::fizz::buzz") }.to raise_error(ArgumentError, /Cannot set structured value at "buzz"/)
|
||||
end
|
||||
|
||||
it "should create all missing hashes in structure to set value" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
subject.set_value("baz", "bar::baz::hats::caps")
|
||||
expect(subject.value("bar::baz::hats::caps")).to eq("baz")
|
||||
expect(subject.value("bar::baz")).to eq({"fizz"=>"buzz", "hats"=>{"caps"=>"baz"}})
|
||||
end
|
||||
|
||||
it "should not create missing hashes if new value is nil" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
subject.set_value(nil, "bar::baz::hats::caps")
|
||||
expect(subject.value("bar::baz::hats::caps")).to be_nil
|
||||
expect(subject.value("bar::baz")).to eq("fizz" => "buzz")
|
||||
end
|
||||
|
||||
it "should delete the structured value if new value is nil (at end)" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
subject.set_value(nil, "bar::baz::fizz")
|
||||
expect(subject.value("bar::baz::fizz")).to be_nil
|
||||
expect(subject.value("bar::baz")).to eq({})
|
||||
end
|
||||
|
||||
it "should delete the structured value if new value is nil (in middle)" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
subject.set_value(nil, "bar::baz")
|
||||
expect(subject.value("bar::baz::fizz")).to be_nil
|
||||
expect(subject.value("bar")).to eq({})
|
||||
end
|
||||
|
||||
it "should set value to new value within the structure" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
subject.set_value("kittens", "bar::baz::fizz")
|
||||
expect(subject.value("bar::baz::fizz")).to eq("kittens")
|
||||
end
|
||||
|
||||
it "should accept an array of strings when describing the structure" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
subject.set_value("kittens", %w(bar baz fizz))
|
||||
expect(subject.value("bar::baz::fizz")).to eq("kittens")
|
||||
end
|
||||
|
||||
it "should handle a structure at the top level of a structured fact" do
|
||||
subject = described_class.new("foo", "bar" => "baz")
|
||||
subject.set_value("kittens", "bar")
|
||||
expect(subject.value).to eq({ "bar" => "kittens" })
|
||||
expect(subject.value("bar")).to eq("kittens")
|
||||
end
|
||||
|
||||
it "should handle regular expressions" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
subject.set_value("kittens", ["bar", { "regexp" => "^ba" }, { "regexp" => "zz" }])
|
||||
expect(subject.value("bar::baz::fizz")).to eq("kittens")
|
||||
end
|
||||
|
||||
it "should not auto-create keys based on regular expressions" do
|
||||
subject = described_class.new("foo", "bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
subject.set_value("kittens", ["bar", { "regexp" => "^boo" }, { "regexp" => "zz" }])
|
||||
expect(subject.value).to eq("bar" => { "baz" => { "fizz" => "buzz" } })
|
||||
end
|
||||
|
||||
it "should match multiple keys when using regular expressions" do
|
||||
subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" })
|
||||
subject.set_value("kittens", [{ "regexp" => "^ba" }])
|
||||
expect(subject.value).to eq({"bar"=>"kittens", "baz"=>"kittens", "fizz"=>"!fizz"})
|
||||
end
|
||||
|
||||
it "should call a Proc when matching multiple keys" do
|
||||
blk = Proc.new { |val| val.upcase }
|
||||
subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" })
|
||||
subject.set_value(blk, [{ "regexp" => "^ba" }])
|
||||
expect(subject.value).to eq({"bar"=>"!BAR", "baz"=>"!BAZ", "fizz"=>"!fizz"})
|
||||
end
|
||||
|
||||
it "should delete values from a Proc when matching multiple keys" do
|
||||
blk = Proc.new { |val| val == "!bar" ? val.upcase : nil }
|
||||
subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" })
|
||||
subject.set_value(blk, [{ "regexp" => "^ba" }])
|
||||
expect(subject.value).to eq({"bar"=>"!BAR", "fizz"=>"!fizz"})
|
||||
end
|
||||
|
||||
it "should raise an error if a part is not a string or regexp" do
|
||||
subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" })
|
||||
expect { subject.set_value("kittens", [:foo]) }.to raise_error(ArgumentError, /Unable to interpret structure item: :foo/)
|
||||
end
|
||||
|
||||
it "should raise an error if the structure cannot be interpreted" do
|
||||
subject = described_class.new("foo", { "bar" => "!bar", "baz" => "!baz", "fizz" => "!fizz" })
|
||||
expect { subject.set_value("kittens", :foo) }.to raise_error(ArgumentError, /Unable to interpret structure: :foo/)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,183 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe OctofactsUpdater::Fixture do
|
||||
describe "#make" do
|
||||
let(:hostname) { "HostName" }
|
||||
let(:config) {{ "enc" => { "path" => "/foo" }, "puppetdb" => { "url" => "https://puppetdb.example.com:8081" } }}
|
||||
let(:enc_return) {{ "parameters" => { "fizz" => "buzz" }, "classes" => ["class1", "class2"] }}
|
||||
let(:fact_hash_with_values) {{ "name" => hostname, "values" => { "foo" => "bar" } }}
|
||||
|
||||
it "should instantiate and return a fixture object with the given facts" do
|
||||
expect(OctofactsUpdater::Service::ENC).to receive(:run_enc).with(hostname, config).and_return(enc_return)
|
||||
expect(OctofactsUpdater::Service::PuppetDB).to receive(:facts).with(hostname, config).and_return(fact_hash_with_values)
|
||||
subject = described_class.make(hostname, config)
|
||||
expect(subject).to be_a_kind_of(OctofactsUpdater::Fixture)
|
||||
expect(subject.to_yaml).to eq("---\nfizz: buzz\nfoo: bar\n")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#facts_from_configured_datasource" do
|
||||
let(:custom_exception) { RuntimeError.new("custom exception for testing") }
|
||||
|
||||
context "with no fact sources configured" do
|
||||
it "should raise ArgumentError" do
|
||||
config = {}
|
||||
expect { described_class.facts_from_configured_datasource("foo.example.net", config) }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
context "with puppetdb configured but broken and SSH not configured" do
|
||||
it "should raise error from PuppetDB" do
|
||||
config = {"puppetdb" => {}}
|
||||
expect(OctofactsUpdater::Service::PuppetDB).to receive(:facts).and_raise(custom_exception)
|
||||
expect { described_class.facts_from_configured_datasource("foo.example.net", config) }.to raise_error(custom_exception)
|
||||
end
|
||||
end
|
||||
|
||||
context "with puppetdb configured and working" do
|
||||
it "should return parsed data" do
|
||||
config = {"puppetdb" => {}}
|
||||
expect(OctofactsUpdater::Service::PuppetDB).to receive(:facts).and_return("values" => {"foo" => "bar"})
|
||||
expect(described_class.facts_from_configured_datasource("foo.example.net", config)).to eq("foo" => "bar")
|
||||
end
|
||||
end
|
||||
|
||||
context "with puppetdb not configured and SSH configured and working" do
|
||||
it "should return parsed data" do
|
||||
config = {"ssh" => {}}
|
||||
expect(OctofactsUpdater::Service::SSH).to receive(:facts).and_return("values" => {"foo" => "bar"})
|
||||
expect(described_class.facts_from_configured_datasource("foo.example.net", config)).to eq("foo" => "bar")
|
||||
end
|
||||
end
|
||||
|
||||
context "with puppetdb not configured and SSH configured and working with facter-like output" do
|
||||
it "should return parsed data" do
|
||||
config = {"ssh" => {}}
|
||||
expect(OctofactsUpdater::Service::SSH).to receive(:facts).and_return({"foo" => "bar"})
|
||||
expect(described_class.facts_from_configured_datasource("foo.example.net", config)).to eq("foo" => "bar")
|
||||
end
|
||||
end
|
||||
|
||||
context "with puppetdb not configured and SSH configured but broken" do
|
||||
it "should raise error from SSH" do
|
||||
config = {"ssh" => {}}
|
||||
expect(OctofactsUpdater::Service::SSH).to receive(:facts).and_raise(custom_exception)
|
||||
expect { described_class.facts_from_configured_datasource("foo.example.net", config) }.to raise_error(custom_exception)
|
||||
end
|
||||
end
|
||||
|
||||
context "with puppetdb broken and SSH broken" do
|
||||
it "should raise error from SSH" do
|
||||
config = {"puppetdb" => {}, "ssh" => {}}
|
||||
expect(OctofactsUpdater::Service::PuppetDB).to receive(:facts).and_raise(ArgumentError)
|
||||
expect(OctofactsUpdater::Service::SSH).to receive(:facts).and_raise(custom_exception)
|
||||
expect { described_class.facts_from_configured_datasource("foo.example.net", config) }.to raise_error(custom_exception)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#load_file" do
|
||||
let(:fixture_path) { File.expand_path("../fixtures/facts", File.dirname(__FILE__)) }
|
||||
|
||||
it "should raise an error if the file does not exist" do
|
||||
fixture_file = File.join(fixture_path, "non-existing.yaml")
|
||||
expect { described_class.load_file("foo", fixture_file) }.to raise_error(Errno::ENOENT)
|
||||
end
|
||||
|
||||
it "should return the object based on the YAML parsed file" do
|
||||
fixture_file = File.join(fixture_path, "basic.yaml")
|
||||
result = described_class.load_file("foo", fixture_file)
|
||||
expect(result).to be_a_kind_of(described_class)
|
||||
expect(result.facts["bios_release_date"].value).to eq("02/16/2017")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#initialize" do
|
||||
let(:subject) { described_class.new("HostName", { "config" => "value" }, { "foo" => "bar" }) }
|
||||
|
||||
it "should instantiate hostname" do
|
||||
expect(subject.instance_variable_get("@hostname")).to eq("HostName")
|
||||
end
|
||||
|
||||
it "should instantiate config" do
|
||||
expect(subject.instance_variable_get("@config")).to eq({ "config" => "value" })
|
||||
end
|
||||
|
||||
it "should instantiate facts" do
|
||||
facts = subject.instance_variable_get("@facts")
|
||||
expect(facts["foo"]).to be_a_kind_of(OctofactsUpdater::Fact)
|
||||
expect(facts["foo"].value).to eq("bar")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#execute_plugins!" do
|
||||
let(:default_facts) { { "foo" => "bar", "fizz" => "buzz" } }
|
||||
|
||||
it "should return the object if the config has no fact modifications" do
|
||||
config = {}
|
||||
subject = described_class.new("HostName", config, default_facts)
|
||||
expect(subject.execute_plugins!).to be_a_kind_of(OctofactsUpdater::Fixture)
|
||||
expect(subject.facts["foo"].value).to eq("bar")
|
||||
expect(subject.facts["fizz"].value).to eq("buzz")
|
||||
end
|
||||
|
||||
it "should apply plugins as requested by fact configuration" do
|
||||
config = { "facts" => { "test" => { "plugin" => "foo" } } }
|
||||
expect(OctofactsUpdater::Plugin).to receive(:execute).with("foo", OctofactsUpdater::Fact, { "plugin" => "foo" }, Hash)
|
||||
subject = described_class.new("HostName", config, default_facts)
|
||||
expect(subject.execute_plugins!).to be_a_kind_of(OctofactsUpdater::Fixture)
|
||||
expect(subject.facts["foo"].value).to eq("bar")
|
||||
expect(subject.facts["fizz"].value).to eq("buzz")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#fact_names" do
|
||||
it "should return the YAML key when regexp and fact are not specified" do
|
||||
subject = described_class.new("HostName", {}, { "foo" => "bar", "fizz" => "buzz", "baz" => 42 })
|
||||
expect(subject.send(:fact_names, "foo", { "plugin" => "delete" })).to eq(["foo"])
|
||||
end
|
||||
|
||||
it "should return the fact name when the fact name is specified" do
|
||||
subject = described_class.new("HostName", {}, { "foo" => "bar", "fizz" => "buzz", "baz" => 42 })
|
||||
expect(subject.send(:fact_names, "foo", { "plugin" => "delete", "fact" => "baz" })).to eq(["baz"])
|
||||
end
|
||||
|
||||
it "should return all facts matching the regexp when a regexp is specified" do
|
||||
subject = described_class.new("HostName", {}, { "foo" => "bar", "fizz" => "buzz", "baz" => 42 })
|
||||
expect(subject.send(:fact_names, "foo", { "plugin" => "delete", "regexp" => "^f" })).to eq(["foo", "fizz"])
|
||||
expect(subject.send(:fact_names, "foo", { "plugin" => "delete", "regexp" => "z$" })).to eq(["fizz", "baz"])
|
||||
expect(subject.send(:fact_names, "foo", { "plugin" => "delete", "regexp" => "asdf" })).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_yaml" do
|
||||
let(:subject) { described_class.new("HostName", {}, { "foo" => "bar", "fizz" => "buzz" }) }
|
||||
|
||||
it "should return YAML representation with sorted keys" do
|
||||
expect(subject.to_yaml).to eq("---\nfizz: buzz\nfoo: bar\n")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#write_file" do
|
||||
let(:fixture_path) { File.expand_path("../fixtures/facts", File.dirname(__FILE__)) }
|
||||
|
||||
before(:each) do
|
||||
@tempdir = Dir.mktmpdir
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
FileUtils.remove_entry_secure(@tempdir) if File.directory?(@tempdir)
|
||||
end
|
||||
|
||||
it "should write out the YAML file with the data" do
|
||||
fixture_file = File.join(fixture_path, "basic.yaml")
|
||||
outfile = File.join(@tempdir, "foo.yaml")
|
||||
|
||||
obj = described_class.load_file("foo", fixture_file)
|
||||
obj.write_file(outfile)
|
||||
|
||||
expect(File.file?(outfile)).to eq(true)
|
||||
expect(File.read(outfile)).to eq(File.read(fixture_file))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe OctofactsUpdater::VERSION do
|
||||
it "is set" do
|
||||
expect(OctofactsUpdater::VERSION).to_not be_nil
|
||||
end
|
||||
end
|
|
@ -0,0 +1,55 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe OctofactsUpdater::Plugin do
|
||||
before(:each) do
|
||||
described_class.clear!(:test1)
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
described_class.clear!(:test1)
|
||||
end
|
||||
|
||||
describe "#register" do
|
||||
let(:blk) { Proc.new { |_fact, _args| true } }
|
||||
|
||||
it "should raise error upon attempting to register a plugin 2x" do
|
||||
expect do
|
||||
described_class.register(:test1, &blk)
|
||||
end.not_to raise_error
|
||||
|
||||
expect do
|
||||
described_class.register(:test1, &blk)
|
||||
end.to raise_error(ArgumentError, /A plugin named test1 is already registered/)
|
||||
end
|
||||
|
||||
it "should register a plugin such that it can be executed later" do
|
||||
described_class.register(:test1, &blk)
|
||||
expect(described_class.plugins.key?(:test1)).to eq(true)
|
||||
expect(described_class.plugins[:test1]).to eq(blk)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#execute" do
|
||||
it "should raise an error if the plugin method is not found" do
|
||||
dummy_fact = instance_double("OctofactsUpdater::Fact")
|
||||
expect { described_class.execute(:test1, dummy_fact, {}) }.to raise_error(NoMethodError, /A plugin named test1/)
|
||||
end
|
||||
|
||||
it "should execute the plugin code if the plugin method is found" do
|
||||
fact = OctofactsUpdater::Fact.new("foo", "bar")
|
||||
blk = Proc.new { |fact, args| fact.value = args["value"] }
|
||||
described_class.register(:test1, &blk)
|
||||
described_class.execute(:test1, fact, { "plugin" => "test1", "value" => "value1" })
|
||||
expect(fact.value).to eq("value1")
|
||||
described_class.execute(:test1, fact, { "plugin" => "test1", "value" => "value2" })
|
||||
expect(fact.value).to eq("value2")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#randomize_long_string" do
|
||||
it "should return the expected result" do
|
||||
result = described_class.randomize_long_string("abcdefghijklmnop")
|
||||
expect(result).to eq("MKf99Vml4egcfIIM")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,118 @@
|
|||
require "spec_helper"
|
||||
require "ipaddr"
|
||||
|
||||
describe "ipv4_anonymize plugin" do
|
||||
let(:plugin) { OctofactsUpdater::Plugin.plugins[:ipv4_anonymize] }
|
||||
let(:fact) { OctofactsUpdater::Fact.new("ipv4", "192.168.42.42") }
|
||||
let(:structured_fact) do
|
||||
OctofactsUpdater::Fact.new("networking",
|
||||
{
|
||||
"ip" => "192.168.42.42",
|
||||
"interfaces" => {
|
||||
"eth0" => {
|
||||
"ip" => "192.168.42.42"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it "should be defined" do
|
||||
expect(plugin).to be_a_kind_of(Proc)
|
||||
end
|
||||
|
||||
it "should raise an error if the subnet is not passed" do
|
||||
args = { "plugin" => "ipv4_anonymize" }
|
||||
expect(OctofactsUpdater::Plugin).to receive(:warn)
|
||||
.with("ArgumentError occurred executing ipv4_anonymize on ipv4 with value \"192.168.42.42\"")
|
||||
expect do
|
||||
OctofactsUpdater::Plugin.execute(:ipv4_anonymize, fact, args)
|
||||
end.to raise_error(ArgumentError, /ipv4_anonymize requires a subnet/)
|
||||
end
|
||||
|
||||
it "should change the IP to a given subnet" do
|
||||
args = { "plugin" => "ipv4_anonymize", "subnet" => "192.168.1.0/24" }
|
||||
OctofactsUpdater::Plugin.execute(:ipv4_anonymize, fact, args, { "hostname" => "myhostname" })
|
||||
expect(fact.value).to eq("192.168.1.60")
|
||||
end
|
||||
|
||||
it "should properly update a structured fact at the top level" do
|
||||
args = { "plugin" => "ipv4_anonymize", "subnet" => "192.168.1.0/24", "structure" => "ip" }
|
||||
OctofactsUpdater::Plugin.execute(:ipv4_anonymize, structured_fact, args, { "hostname" => "myhostname" })
|
||||
expect(structured_fact.value["ip"]).to eq("192.168.1.60")
|
||||
end
|
||||
|
||||
it "should properly update a structured fact nested within" do
|
||||
args = { "plugin" => "ipv4_anonymize", "subnet" => "192.168.1.0/24", "structure" => "interfaces::eth0::ip" }
|
||||
OctofactsUpdater::Plugin.execute(:ipv4_anonymize, structured_fact, args, { "hostname" => "myhostname" })
|
||||
expect(structured_fact.value["interfaces"]["eth0"]["ip"]).to eq("192.168.1.60")
|
||||
end
|
||||
|
||||
it "should be consistent" do
|
||||
args = { "plugin" => "ipv4_anonymize", "subnet" => "10.0.0.0/8" }
|
||||
original_fact = fact.dup
|
||||
OctofactsUpdater::Plugin.execute(:ipv4_anonymize, fact, args, { "hostname" => "myhostname" })
|
||||
expect(fact.value).to eq("10.67.98.60")
|
||||
fact = original_fact
|
||||
OctofactsUpdater::Plugin.execute(:ipv4_anonymize, fact, args, { "hostname" => "myhostname" })
|
||||
expect(fact.value).to eq("10.67.98.60")
|
||||
end
|
||||
end
|
||||
|
||||
describe "ipv6_anonymize plugin" do
|
||||
let(:plugin) { OctofactsUpdater::Plugin.plugins[:ipv6_anonymize] }
|
||||
let(:fact) { OctofactsUpdater::Fact.new("ipv6", "fd00::/8") }
|
||||
let(:structured_fact) do
|
||||
OctofactsUpdater::Fact.new("networking",
|
||||
{
|
||||
"ip6" => "fd00::/8",
|
||||
"interfaces" => {
|
||||
"eth0" => {
|
||||
"ip6" => "fd00::/8"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it "should be defined" do
|
||||
expect(plugin).to be_a_kind_of(Proc)
|
||||
end
|
||||
|
||||
it "should raise an error if the subnet is not passed" do
|
||||
args = { "plugin" => "ipv6_anonymize" }
|
||||
expect(OctofactsUpdater::Plugin).to receive(:warn)
|
||||
.with("ArgumentError occurred executing ipv6_anonymize on ipv6 with value \"fd00::/8\"")
|
||||
expect do
|
||||
OctofactsUpdater::Plugin.execute(:ipv6_anonymize, fact, args)
|
||||
end.to raise_error(ArgumentError, /ipv6_anonymize requires a subnet/)
|
||||
end
|
||||
|
||||
it "should change the IP to a given subnet" do
|
||||
args = { "plugin" => "ipv6_anonymize", "subnet" => "fd00::/8" }
|
||||
OctofactsUpdater::Plugin.execute(:ipv6_anonymize, fact, args, { "hostname" => "myhostname" })
|
||||
expect(fact.value).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364")
|
||||
end
|
||||
|
||||
it "should properly update a structured fact at the top level" do
|
||||
args = { "plugin" => "ipv4_anonymize", "subnet" => "fd00::/8", "structure" => "ip6" }
|
||||
OctofactsUpdater::Plugin.execute(:ipv6_anonymize, structured_fact, args, { "hostname" => "myhostname" })
|
||||
expect(structured_fact.value(args["structure"])).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364")
|
||||
end
|
||||
|
||||
it "should properly update a structured fact nested within" do
|
||||
args = { "plugin" => "ipv4_anonymize", "subnet" => "fd00::/8", "structure" => "interfaces::eth0::ip6" }
|
||||
OctofactsUpdater::Plugin.execute(:ipv6_anonymize, structured_fact, args, { "hostname" => "myhostname" })
|
||||
expect(structured_fact.value(args["structure"])).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364")
|
||||
end
|
||||
|
||||
it "should be consistent" do
|
||||
args = { "plugin" => "ipv6_anonymize", "subnet" => "fd00::/8" }
|
||||
original_fact = fact.dup
|
||||
OctofactsUpdater::Plugin.execute(:ipv6_anonymize, fact, args, { "hostname" => "myhostname" })
|
||||
expect(fact.value).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364")
|
||||
fact = original_fact
|
||||
OctofactsUpdater::Plugin.execute(:ipv6_anonymize, fact, args, { "hostname" => "myhostname" })
|
||||
expect(fact.value).to eq("fdcd:baee:2c4d:ab66:c3d5:2929:786a:9364")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe "sshfp_randomize plugin" do
|
||||
let(:plugin) { OctofactsUpdater::Plugin.plugins[:sshfp_randomize] }
|
||||
let(:value) { "SSHFP 1 1 0123456789abcdef0123456789abcdef01234567\nSSHFP 1 2 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" }
|
||||
let(:args) {{ "plugin" => "sshfp_randomize" }}
|
||||
|
||||
it "should be defined" do
|
||||
expect(plugin).to be_a_kind_of(Proc)
|
||||
end
|
||||
|
||||
it "should raise an error if the input is not a sshfp key" do
|
||||
fact = OctofactsUpdater::Fact.new("foo", "kittens123")
|
||||
expect(OctofactsUpdater::Plugin).to receive(:warn)
|
||||
.with("RuntimeError occurred executing sshfp_randomize on foo with value \"kittens123\"")
|
||||
expect do
|
||||
OctofactsUpdater::Plugin.execute(:sshfp_randomize, fact, args)
|
||||
end.to raise_error(/Unparseable pattern: kittens123/)
|
||||
end
|
||||
|
||||
it "should randomize a sshfp key" do
|
||||
allow(OctofactsUpdater::Plugin).to receive(:randomize_long_string) { |arg| "random:#{arg}" }
|
||||
fact = OctofactsUpdater::Fact.new("foo", value)
|
||||
OctofactsUpdater::Plugin.execute(:sshfp_randomize, fact, args)
|
||||
expect(fact.value).to eq("SSHFP 1 1 random:0123456789abcdef0123456789abcdef01234567\nSSHFP 1 2 random:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,127 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe "delete plugin" do
|
||||
let(:plugin) { OctofactsUpdater::Plugin.plugins[:delete] }
|
||||
|
||||
it "should be defined" do
|
||||
expect(plugin).to be_a_kind_of(Proc)
|
||||
end
|
||||
|
||||
it "should set the value of a fact to nil" do
|
||||
fact = OctofactsUpdater::Fact.new("foo", "bar")
|
||||
args = { "plugin" => "delete" }
|
||||
OctofactsUpdater::Plugin.execute(:delete, fact, args)
|
||||
expect(fact.value).to be_nil
|
||||
end
|
||||
|
||||
it "should remove a value within a structured fact" do
|
||||
value = { "one" => 1, "two" => 2 }
|
||||
fact = OctofactsUpdater::Fact.new("foo", value)
|
||||
args = { "plugin" => "delete", "structure" => "one" }
|
||||
OctofactsUpdater::Plugin.execute(:delete, fact, args)
|
||||
expect(fact.value).to eq({"two"=>2})
|
||||
end
|
||||
end
|
||||
|
||||
describe "set plugin" do
|
||||
let(:plugin) { OctofactsUpdater::Plugin.plugins[:set] }
|
||||
|
||||
it "should be defined" do
|
||||
expect(plugin).to be_a_kind_of(Proc)
|
||||
end
|
||||
|
||||
it "should set the value of a fact" do
|
||||
value = { "one" => 1, "two" => 2 }
|
||||
fact = OctofactsUpdater::Fact.new("foo", value)
|
||||
args = { "plugin" => "set", "value" => "kittens" }
|
||||
OctofactsUpdater::Plugin.execute(:set, fact, args)
|
||||
expect(fact.value).to eq("kittens")
|
||||
end
|
||||
|
||||
it "should set the value of a structured fact" do
|
||||
value = { "one" => 1, "two" => 2 }
|
||||
fact = OctofactsUpdater::Fact.new("foo", value)
|
||||
args = { "plugin" => "set", "value" => "kittens", "structure" => "one" }
|
||||
OctofactsUpdater::Plugin.execute(:set, fact, args)
|
||||
expect(fact.value).to eq({"one"=>"kittens", "two"=>2})
|
||||
end
|
||||
|
||||
it "should add the value to a structured fact" do
|
||||
value = { "one" => 1, "two" => 2 }
|
||||
fact = OctofactsUpdater::Fact.new("foo", value)
|
||||
args = { "plugin" => "set", "value" => "kittens", "structure" => "three" }
|
||||
OctofactsUpdater::Plugin.execute(:set, fact, args)
|
||||
expect(fact.value).to eq({"one"=>1, "two"=>2, "three"=>"kittens"})
|
||||
end
|
||||
end
|
||||
|
||||
describe "remove_from_delimited_string plugin" do
|
||||
let(:plugin) { OctofactsUpdater::Plugin.plugins[:remove_from_delimited_string] }
|
||||
let(:fact) { OctofactsUpdater::Fact.new("foo", "foo,bar,baz,fizz") }
|
||||
|
||||
it "should be defined" do
|
||||
expect(plugin).to be_a_kind_of(Proc)
|
||||
end
|
||||
|
||||
it "should raise ArgumentError if delimiter is not provided" do
|
||||
args = { "plugin" => "remove_from_delimited_string", "regexp" => ".*" }
|
||||
expect(OctofactsUpdater::Plugin).to receive(:warn)
|
||||
.with("ArgumentError occurred executing remove_from_delimited_string on foo with value \"foo,bar,baz,fizz\"")
|
||||
expect do
|
||||
OctofactsUpdater::Plugin.execute(:remove_from_delimited_string, fact, args)
|
||||
end.to raise_error(ArgumentError, /remove_from_delimited_string requires a delimiter/)
|
||||
end
|
||||
|
||||
it "should raise ArgumentError if regexp is not provided" do
|
||||
args = { "plugin" => "remove_from_delimited_string", "delimiter" => "," }
|
||||
expect(OctofactsUpdater::Plugin).to receive(:warn)
|
||||
.with("ArgumentError occurred executing remove_from_delimited_string on foo with value \"foo,bar,baz,fizz\"")
|
||||
expect do
|
||||
OctofactsUpdater::Plugin.execute(:remove_from_delimited_string, fact, args)
|
||||
end.to raise_error(ArgumentError, /remove_from_delimited_string requires a regexp/)
|
||||
end
|
||||
|
||||
it "should return joined string with elements matching regexp removed" do
|
||||
args = { "plugin" => "remove_from_delimited_string", "delimiter" => ",", "regexp" => "^b" }
|
||||
OctofactsUpdater::Plugin.execute(:remove_from_delimited_string, fact, args)
|
||||
expect(fact.value).to eq("foo,fizz")
|
||||
end
|
||||
|
||||
it "should be a no-op if no elements match the regexp" do
|
||||
args = { "plugin" => "remove_from_delimited_string", "delimiter" => ",", "regexp" => "does-not-match" }
|
||||
OctofactsUpdater::Plugin.execute(:remove_from_delimited_string, fact, args)
|
||||
expect(fact.value).to eq("foo,bar,baz,fizz")
|
||||
end
|
||||
end
|
||||
|
||||
describe "noop plugin" do
|
||||
let(:plugin) { OctofactsUpdater::Plugin.plugins[:noop] }
|
||||
|
||||
it "should be defined" do
|
||||
expect(plugin).to be_a_kind_of(Proc)
|
||||
end
|
||||
|
||||
it "should do nothing at all" do
|
||||
fact = OctofactsUpdater::Fact.new("foo", "kittens")
|
||||
args = { "plugin" => "noop" }
|
||||
OctofactsUpdater::Plugin.execute(:noop, fact, args)
|
||||
expect(fact.value).to eq("kittens")
|
||||
end
|
||||
end
|
||||
|
||||
describe "randomize_long_string plugin" do
|
||||
let(:plugin) { OctofactsUpdater::Plugin.plugins[:randomize_long_string] }
|
||||
let(:value) { "1234567890abcdef" }
|
||||
let(:args) {{ "plugin" => "randomize_long_string" }}
|
||||
|
||||
it "should be defined" do
|
||||
expect(plugin).to be_a_kind_of(Proc)
|
||||
end
|
||||
|
||||
it "should randomize a string" do
|
||||
allow(OctofactsUpdater::Plugin).to receive(:randomize_long_string) { |arg| "random:#{arg}" }
|
||||
fact = OctofactsUpdater::Fact.new("foo", value)
|
||||
OctofactsUpdater::Plugin.execute(:randomize_long_string, fact, args)
|
||||
expect(fact.value).to eq("random:#{value}")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,63 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe OctofactsUpdater::Service::Base do
|
||||
describe "#parse_yaml" do
|
||||
it "should convert first '--- (whatever)' to just '---'" do
|
||||
text = <<-EOF
|
||||
--- !ruby/object:Puppet::Node::Facts
|
||||
name: foo-bar.example.net
|
||||
values:
|
||||
agent_specified_environment: production
|
||||
EOF
|
||||
result = described_class.parse_yaml(text)
|
||||
expect(result).to eq({"agent_specified_environment"=>"production"})
|
||||
end
|
||||
|
||||
it "should convert first '--- (whatever)' to just '---' after comments" do
|
||||
text = <<-EOF
|
||||
# Facts for foo-bar.example.net
|
||||
--- !ruby/object:Puppet::Node::Facts
|
||||
name: foo-bar.example.net
|
||||
values:
|
||||
agent_specified_environment: production
|
||||
EOF
|
||||
result = described_class.parse_yaml(text)
|
||||
expect(result).to eq({"agent_specified_environment"=>"production"})
|
||||
end
|
||||
|
||||
it "should convert first '--- (whatever)' to just '---' after blank lines" do
|
||||
text = <<-EOF
|
||||
|
||||
|
||||
--- !ruby/object:Puppet::Node::Facts
|
||||
name: foo-bar.example.net
|
||||
values:
|
||||
agent_specified_environment: production
|
||||
EOF
|
||||
result = described_class.parse_yaml(text)
|
||||
expect(result).to eq({"agent_specified_environment"=>"production"})
|
||||
end
|
||||
|
||||
it "should work correctly when first non-comment line is not '---'" do
|
||||
text = <<-EOF
|
||||
# Test 123
|
||||
|
||||
# Test 456
|
||||
name: foo-bar.example.net
|
||||
values:
|
||||
agent_specified_environment: production
|
||||
EOF
|
||||
result = described_class.parse_yaml(text)
|
||||
expect(result).to eq({"agent_specified_environment"=>"production"})
|
||||
end
|
||||
|
||||
it "should convert a plain formatted fact file" do
|
||||
text = <<-EOF
|
||||
---
|
||||
agent_specified_environment: production
|
||||
EOF
|
||||
result = described_class.parse_yaml(text)
|
||||
expect(result).to eq({"agent_specified_environment"=>"production"})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
require "spec_helper"
|
||||
|
||||
require "ostruct"
|
||||
|
||||
describe OctofactsUpdater::Service::ENC do
|
||||
let(:config) {{ "enc" => { "path" => "/tmp/foo.enc" }}}
|
||||
|
||||
describe "#run_enc" do
|
||||
it "should raise ArgumentError if no configuration for the ENC is defined" do
|
||||
expect { described_class.run_enc("HostName", {}) }.to raise_error(ArgumentError, /The ENC configuration must be defined/)
|
||||
end
|
||||
|
||||
it "should raise ArgumentError if configuration does not have a path" do
|
||||
expect { described_class.run_enc("HostName", { "enc" => {} }) }.to raise_error(ArgumentError, /The ENC path must be defined/)
|
||||
end
|
||||
|
||||
it "should raise Errno::ENOENT if the script doesn't exist at the path" do
|
||||
allow(File).to receive(:"file?").and_call_original
|
||||
allow(File).to receive(:"file?").with("/tmp/foo.enc").and_return(false)
|
||||
expect { described_class.run_enc("HostName", config) }.to raise_error(Errno::ENOENT, /The ENC script could not be found/)
|
||||
end
|
||||
|
||||
it "should raise RuntimeError if the exit status from the ENC is nonzero" do
|
||||
allow(File).to receive(:"file?").and_call_original
|
||||
allow(File).to receive(:"file?").with("/tmp/foo.enc").and_return(true)
|
||||
open3_response = ["", "Whoopsie", OpenStruct.new(exitstatus: 1)]
|
||||
allow(Open3).to receive(:capture3).with("/tmp/foo.enc HostName").and_return(open3_response)
|
||||
expect { described_class.run_enc("HostName", config) }.to raise_error(%r{Error executing "/tmp/foo.enc HostName"})
|
||||
end
|
||||
|
||||
it "should return the parsed YAML output from the ENC" do
|
||||
allow(File).to receive(:"file?").and_call_original
|
||||
allow(File).to receive(:"file?").with("/tmp/foo.enc").and_return(true)
|
||||
yaml_out = { "parameters" => { "foo" => "bar" } }.to_yaml
|
||||
open3_response = [yaml_out, "", OpenStruct.new(exitstatus: 0)]
|
||||
allow(Open3).to receive(:capture3).with("/tmp/foo.enc HostName").and_return(open3_response)
|
||||
expect(described_class.run_enc("HostName", config)).to eq("parameters" => { "foo" => "bar" })
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,313 @@
|
|||
require "spec_helper"
|
||||
|
||||
require "ostruct"
|
||||
|
||||
describe OctofactsUpdater::Service::GitHub do
|
||||
before(:each) do
|
||||
ENV["OCTOKIT_TOKEN"] = "00decaf"
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
ENV.delete("OCTOKIT_TOKEN")
|
||||
end
|
||||
|
||||
describe "#run" do
|
||||
it "should raise error if base directory is not specified" do
|
||||
paths = %w{/tmp/foo/spec/fixtures/nodes/foo.yaml /tmp/foo/spec/fixtures/index.yaml}
|
||||
options = {}
|
||||
obj = instance_double(described_class)
|
||||
expect { described_class.run(nil, paths, options) }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "should raise error if base directory is not found" do
|
||||
root = "/tmp/foo"
|
||||
paths = %w{/tmp/foo/spec/fixtures/nodes/foo.yaml /tmp/foo/spec/fixtures/index.yaml}
|
||||
options = {}
|
||||
obj = instance_double(described_class)
|
||||
allow(File).to receive(:directory?).and_call_original
|
||||
allow(File).to receive(:directory?).with("/tmp/foo").and_return(false)
|
||||
expect { described_class.run(root, paths, options) }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "should call the appropriate methods from the workflow" do
|
||||
root = "/tmp/foo"
|
||||
paths = %w{/tmp/foo/spec/fixtures/nodes/foo.yaml /tmp/foo/spec/fixtures/index.yaml}
|
||||
options = {}
|
||||
|
||||
obj = instance_double(described_class)
|
||||
expect(described_class).to receive(:new).with(options).and_return(obj)
|
||||
|
||||
allow(File).to receive(:read).and_call_original
|
||||
allow(File).to receive(:read).with("/tmp/foo/spec/fixtures/nodes/foo.yaml").and_return("foo!")
|
||||
allow(File).to receive(:read).with("/tmp/foo/spec/fixtures/index.yaml").and_return("index!")
|
||||
|
||||
allow(File).to receive(:directory?).and_call_original
|
||||
allow(File).to receive(:directory?).with("/tmp/foo").and_return(true)
|
||||
|
||||
expect(obj).to receive(:commit_data).with("spec/fixtures/nodes/foo.yaml", "foo!")
|
||||
expect(obj).to receive(:commit_data).with("spec/fixtures/index.yaml", "index!")
|
||||
|
||||
expect(obj).to receive(:finalize_commit)
|
||||
|
||||
described_class.run(root, paths, options)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#initialize" do
|
||||
it "should construct octokit object when appropriate configuration is supplied" do
|
||||
config = { "github" => { "token" => "beefbeef" } }
|
||||
subject = described_class.new(config)
|
||||
expect(subject.send(:octokit)).to be_a_kind_of(Octokit::Client)
|
||||
expect(subject.send(:octokit).access_token).to eq("beefbeef")
|
||||
end
|
||||
|
||||
it "should use octokit token from the enviroment" do
|
||||
subject = described_class.new({})
|
||||
expect(subject.send(:octokit)).to be_a_kind_of(Octokit::Client)
|
||||
expect(subject.send(:octokit).access_token).to eq("00decaf")
|
||||
end
|
||||
|
||||
it "should raise an error when token is missing from configuration" do
|
||||
ENV.delete("OCTOKIT_TOKEN")
|
||||
config = { "github" => {} }
|
||||
obj = described_class.new(config)
|
||||
expect { obj.send(:octokit) }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#commit_data" do
|
||||
let(:cfg) { { "github" => { "branch" => "foo", "repository" => "example/repo" } } }
|
||||
|
||||
context "when the file is found in the repo and it has not changed" do
|
||||
let(:old_content) { Base64.encode64 "this is the content" }
|
||||
let(:new_content) { "this is the content" }
|
||||
let(:octokit_args) { {path: "foo/bar/baz.yaml", ref: "foo"} }
|
||||
|
||||
before(:each) do
|
||||
octokit = double("Octokit")
|
||||
allow(octokit).to receive(:contents).with("example/repo", octokit_args).and_return(OpenStruct.new(content: old_content))
|
||||
@subject = described_class.new(cfg)
|
||||
allow(@subject).to receive(:octokit).and_return(octokit)
|
||||
expect(@subject).to receive(:ensure_branch_exists)
|
||||
expect(@subject).to receive(:verbose).with("Content of foo/bar/baz.yaml matches, no commit needed")
|
||||
@result = @subject.commit_data("foo/bar/baz.yaml", new_content)
|
||||
end
|
||||
|
||||
it "should return false" do
|
||||
expect(@result).to eq(false)
|
||||
end
|
||||
|
||||
it "should not appear in the @changes array" do
|
||||
expect(@subject.instance_variable_get("@changes")).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "when the file is found in the repo and it has changed" do
|
||||
let(:old_content) { Base64.encode64 "this is the old content" }
|
||||
let(:new_content) { "this is the new content" }
|
||||
let(:octokit_args) { {path: "foo/bar/baz.yaml", ref: "foo"} }
|
||||
|
||||
before(:each) do
|
||||
octokit = double("Octokit")
|
||||
allow(octokit).to receive(:contents).with("example/repo", octokit_args).and_return(OpenStruct.new(content: old_content))
|
||||
allow(octokit).to receive(:create_blob).with("example/repo", new_content)
|
||||
@subject = described_class.new(cfg)
|
||||
allow(@subject).to receive(:octokit).and_return(octokit)
|
||||
expect(@subject).to receive(:ensure_branch_exists)
|
||||
expect(@subject).to receive(:verbose).with("Content of foo/bar/baz.yaml does not match. A commit is needed.")
|
||||
expect(@subject).to receive(:verbose).with("Batched update of foo/bar/baz.yaml")
|
||||
expect(@subject).to receive(:verbose).with(Diffy::Diff)
|
||||
@result = @subject.commit_data("foo/bar/baz.yaml", new_content)
|
||||
end
|
||||
|
||||
it "should return true" do
|
||||
expect(@result).to eq(true)
|
||||
end
|
||||
|
||||
it "should appear in the @changes array" do
|
||||
expect(@subject.instance_variable_get("@changes")).to eq([{path: "foo/bar/baz.yaml", mode: "100644", type: "blob", sha: nil}])
|
||||
end
|
||||
end
|
||||
|
||||
context "when the file is not found in the repo" do
|
||||
let(:new_content) { "this is the new content" }
|
||||
let(:octokit_args) { {path: "foo/bar/baz.yaml", ref: "foo"} }
|
||||
|
||||
before(:each) do
|
||||
octokit = double("Octokit")
|
||||
allow(octokit).to receive(:contents).with("example/repo", octokit_args).and_raise(Octokit::NotFound)
|
||||
allow(octokit).to receive(:create_blob).with("example/repo", new_content)
|
||||
@subject = described_class.new(cfg)
|
||||
allow(@subject).to receive(:octokit).and_return(octokit)
|
||||
expect(@subject).to receive(:ensure_branch_exists)
|
||||
expect(@subject).to receive(:verbose).with("No old content found in \"example/repo\" at \"foo/bar/baz.yaml\" in \"foo\"")
|
||||
expect(@subject).to receive(:verbose).with("Content of foo/bar/baz.yaml does not match. A commit is needed.")
|
||||
expect(@subject).to receive(:verbose).with("Batched update of foo/bar/baz.yaml")
|
||||
expect(@subject).to receive(:verbose).with(Diffy::Diff)
|
||||
@result = @subject.commit_data("foo/bar/baz.yaml", new_content)
|
||||
end
|
||||
|
||||
it "should return true" do
|
||||
expect(@result).to eq(true)
|
||||
end
|
||||
|
||||
it "should appear in the @changes array" do
|
||||
expect(@subject.instance_variable_get("@changes")).to eq([{path: "foo/bar/baz.yaml", mode: "100644", type: "blob", sha: nil}])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#finalize_commit" do
|
||||
let(:cfg) { { "github" => { "branch" => "foo", "repository" => "example/repo", "commit_message" => "Hi" } } }
|
||||
let(:octokit) { double("Octokit") }
|
||||
let(:subject) { described_class.new(cfg) }
|
||||
|
||||
context "with no changes" do
|
||||
it "should do nothing" do
|
||||
subject.instance_variable_set("@changes", [])
|
||||
expect(subject).not_to receive(:ensure_branch_exists)
|
||||
subject.finalize_commit
|
||||
end
|
||||
end
|
||||
|
||||
context "with changes" do
|
||||
it "should make the expected octokit calls" do
|
||||
subject.instance_variable_set("@changes", [{path: "foo/bar/baz.yaml", mode: "100644", type: "blob", sha: nil}])
|
||||
expect(subject).to receive(:ensure_branch_exists).and_return(true)
|
||||
allow(subject).to receive(:octokit).and_return(octokit)
|
||||
expect(octokit).to receive(:branch).with("example/repo", "foo").and_return(commit: { sha: "00abcdef" })
|
||||
expect(octokit).to receive(:git_commit)
|
||||
.with("example/repo", "00abcdef")
|
||||
.and_return("sha" => "abcdef00", "tree" => { "sha" => "abcdef00" })
|
||||
expect(octokit).to receive(:create_tree)
|
||||
.with("example/repo", [{path: "foo/bar/baz.yaml", mode: "100644", type: "blob", sha: nil}], {base_tree: "abcdef00"})
|
||||
.and_return("sha" => "abcdef00")
|
||||
expect(octokit).to receive(:create_commit)
|
||||
.with("example/repo", "Hi", "abcdef00", "abcdef00")
|
||||
.and_return("sha" => "abcdef00")
|
||||
expect(octokit).to receive(:update_ref)
|
||||
.with("example/repo", "heads/foo", "abcdef00")
|
||||
expect(subject).to receive(:verbose).with("Committed 1 change(s) to GitHub")
|
||||
expect(subject).to receive(:find_or_create_pull_request)
|
||||
subject.finalize_commit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#delete_file" do
|
||||
let(:cfg) { { "github" => { "branch" => "foo", "repository" => "example/repo", "commit_message" => "Hi" } } }
|
||||
let(:octokit) { double("Octokit") }
|
||||
let(:subject) { described_class.new(cfg) }
|
||||
|
||||
context "when the file exists" do
|
||||
it "should send an octokit commit to delete the file" do
|
||||
expect(subject).to receive(:ensure_branch_exists)
|
||||
allow(subject).to receive(:octokit).and_return(octokit)
|
||||
expect(octokit).to receive(:contents)
|
||||
.with("example/repo", {path: "foo/bar/baz.yaml", ref: "foo"})
|
||||
.and_return(OpenStruct.new(sha: "00abcdef"))
|
||||
expect(octokit).to receive(:delete_contents)
|
||||
.with("example/repo", "foo/bar/baz.yaml", "Hi", "00abcdef", {branch: "foo"})
|
||||
expect(subject).to receive(:verbose).with("Deleted foo/bar/baz.yaml")
|
||||
expect(subject).to receive(:find_or_create_pull_request)
|
||||
expect(subject.delete_file("foo/bar/baz.yaml")).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "with the file does not exist" do
|
||||
it "should do nothing" do
|
||||
allow(subject).to receive(:octokit).and_return(octokit)
|
||||
expect(subject).to receive(:ensure_branch_exists)
|
||||
expect(octokit).to receive(:contents)
|
||||
.with("example/repo", {path: "foo/bar/baz.yaml", ref: "foo"})
|
||||
.and_raise(Octokit::NotFound)
|
||||
expect(subject).to receive(:verbose).with("Deleted foo/bar/baz.yaml (already gone)")
|
||||
expect(subject).not_to receive(:find_or_create_pull_request)
|
||||
expect(subject.delete_file("foo/bar/baz.yaml")).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#default_branch" do
|
||||
it "should return the branch from the options" do
|
||||
opts = { "github" => { "default_branch" => "cuddly-kittens", "repository" => "example/repo-name", "token" => "00decaf" } }
|
||||
subject = described_class.new(opts)
|
||||
expect(subject.send(:default_branch)).to eq("cuddly-kittens")
|
||||
end
|
||||
|
||||
it "should return the branch from octokit" do
|
||||
opts = { "github" => { "repository" => "example/repo-name", "token" => "00decaf" } }
|
||||
fake_octokit = double("Octokit")
|
||||
allow(fake_octokit).to receive(:repo).with("example/repo-name").and_return(default_branch: "adorable-kittens")
|
||||
subject = described_class.new(opts)
|
||||
allow(subject).to receive(:octokit).and_return(fake_octokit)
|
||||
expect(subject.send(:default_branch)).to eq("adorable-kittens")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#ensure_branch_exists" do
|
||||
let(:cfg) { { "github" => { "branch" => "foo", "repository" => "example/repo", "default_branch" => "master" } } }
|
||||
let(:octokit) { double("Octokit") }
|
||||
let(:subject) { described_class.new(cfg) }
|
||||
|
||||
context "when branch exists" do
|
||||
it "should not create branch" do
|
||||
allow(octokit).to receive(:branch).with("example/repo", "foo").and_return(true)
|
||||
allow(subject).to receive(:octokit).and_return(octokit)
|
||||
expect(subject).to receive(:verbose).with("Branch foo already exists in example/repo.")
|
||||
expect(subject.send(:ensure_branch_exists)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when branch does not exist" do
|
||||
it "should create branch of Octokit::NotFound is raised" do
|
||||
allow(octokit).to receive(:branch).with("example/repo", "foo").and_raise(Octokit::NotFound)
|
||||
allow(octokit).to receive(:branch).with("example/repo", "master").and_return(commit: { sha: "00abcdef" })
|
||||
expect(octokit).to receive(:create_ref).with("example/repo", "heads/foo", "00abcdef")
|
||||
expect(subject).to receive(:verbose).with("Created branch foo based on master 00abcdef.")
|
||||
allow(subject).to receive(:octokit).and_return(octokit)
|
||||
expect(subject.send(:ensure_branch_exists)).to eq(true)
|
||||
end
|
||||
|
||||
it "should create branch if .branch call returns nil" do
|
||||
allow(octokit).to receive(:branch).with("example/repo", "foo").and_return(nil)
|
||||
allow(octokit).to receive(:branch).with("example/repo", "master").and_return(commit: { sha: "00abcdef" })
|
||||
expect(octokit).to receive(:create_ref).with("example/repo", "heads/foo", "00abcdef")
|
||||
expect(subject).to receive(:verbose).with("Created branch foo based on master 00abcdef.")
|
||||
allow(subject).to receive(:octokit).and_return(octokit)
|
||||
expect(subject.send(:ensure_branch_exists)).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#find_or_create_pull_request" do
|
||||
let(:cfg) { { "github" => { "branch" => "foo", "repository" => "example/repo", "default_branch" => "master" } } }
|
||||
let(:subject) { described_class.new(cfg) }
|
||||
let(:pr) { OpenStruct.new(html_url: "https://github.com/example/repo/pull/12345") }
|
||||
|
||||
context "when PRs are returned" do
|
||||
it "should return the first matching PR" do
|
||||
octokit = double("Octokit")
|
||||
allow(subject).to receive(:octokit).and_return(octokit)
|
||||
expect(octokit).to receive(:pull_requests).with("example/repo", {head: "github:foo", state: "open"}).and_return([pr])
|
||||
expect(subject).to receive(:verbose).with("Found existing PR https://github.com/example/repo/pull/12345")
|
||||
result = subject.send(:find_or_create_pull_request)
|
||||
expect(result).to eq(pr)
|
||||
end
|
||||
end
|
||||
|
||||
context "when PRs are not returned" do
|
||||
it "should create a new PR and return it" do
|
||||
octokit = double("Octokit")
|
||||
allow(subject).to receive(:octokit).and_return(octokit)
|
||||
expect(octokit).to receive(:pull_requests).with("example/repo", {head: "github:foo", state: "open"}).and_return([])
|
||||
expect(octokit).to receive(:create_pull_request).with("example/repo", "master", "foo", "PR_Subject", "PR_Body").and_return(pr)
|
||||
expect(subject).to receive(:verbose).with("Created a new PR https://github.com/example/repo/pull/12345")
|
||||
expect(subject).to receive(:pr_subject).and_return("PR_Subject")
|
||||
expect(subject).to receive(:pr_body).and_return("PR_Body")
|
||||
result = subject.send(:find_or_create_pull_request)
|
||||
expect(result).to eq(pr)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe OctofactsUpdater::Service::LocalFile do
|
||||
describe "#facts" do
|
||||
let(:node) { "foo.example.net" }
|
||||
let(:custom_exception) { RuntimeError.new("custom exception for testing") }
|
||||
|
||||
context "when localfile is not configured" do
|
||||
it "should raise ArgumentError when localfile is undefined" do
|
||||
config = {}
|
||||
expect{ described_class.facts(node, config) }.to raise_error(ArgumentError, /requires localfile section/)
|
||||
end
|
||||
|
||||
it "should raise ArgumentError when localfile is not a hash" do
|
||||
config = {"localfile" => :do_it}
|
||||
expect{ described_class.facts(node, config) }.to raise_error(ArgumentError, /requires localfile section/)
|
||||
end
|
||||
end
|
||||
|
||||
context "when localfile is configured" do
|
||||
let(:file_path) { File.expand_path("../../fixtures/facts", File.dirname(__FILE__)) }
|
||||
|
||||
it "should raise error if the path is undefined" do
|
||||
config = { "localfile" => {} }
|
||||
expect{ described_class.facts(node, config) }.to raise_error(ArgumentError, /requires 'path' in the localfile section/)
|
||||
end
|
||||
|
||||
it "should raise error if the path does not exist" do
|
||||
config = { "localfile" => { "path" => File.join(file_path, "missing.yaml") } }
|
||||
expect{ described_class.facts(node, config) }.to raise_error(Errno::ENOENT, /LocalFile cannot find a file at/)
|
||||
end
|
||||
|
||||
it "should return the proper object from the parsed file" do
|
||||
config = { "localfile" => { "path" => File.join(file_path, "basic.yaml") } }
|
||||
result = described_class.facts(node, config)
|
||||
desired_result = YAML.safe_load(File.read(File.join(file_path, "basic.yaml")))
|
||||
expect(result).to eq(desired_result)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,50 @@
|
|||
require "spec_helper"
|
||||
|
||||
require "octocatalog-diff"
|
||||
|
||||
describe OctofactsUpdater::Service::PuppetDB do
|
||||
before(:each) do
|
||||
ENV.delete("PUPPETDB_URL")
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
ENV.delete("PUPPETDB_URL")
|
||||
end
|
||||
|
||||
describe "#facts" do
|
||||
it "should return facts from octocatalog-diff" do
|
||||
facts_double = instance_double(OctocatalogDiff::Facts)
|
||||
facts_answer = { "foo" => "bar" }
|
||||
expected_args = {node: "foo.bar.node", backend: :puppetdb, puppetdb_url: "https://puppetdb.fake:8443"}
|
||||
expect(described_class).to receive(:puppetdb_url).and_return("https://puppetdb.fake:8443")
|
||||
expect(OctocatalogDiff::Facts).to receive(:new).with(expected_args).and_return(facts_double)
|
||||
expect(facts_double).to receive(:facts).and_return(facts_answer)
|
||||
expect(described_class.facts("foo.bar.node", {})).to eq(facts_answer)
|
||||
end
|
||||
|
||||
it "should raise an error if facts cannot be determined" do
|
||||
facts_double = instance_double(OctocatalogDiff::Facts)
|
||||
expected_args = {node: "foo.bar.node", backend: :puppetdb, puppetdb_url: "https://puppetdb.fake:8443"}
|
||||
expect(described_class).to receive(:puppetdb_url).and_return("https://puppetdb.fake:8443")
|
||||
expect(OctocatalogDiff::Facts).to receive(:new).with(expected_args).and_return(facts_double)
|
||||
expect(facts_double).to receive(:facts).and_return(nil)
|
||||
expect { described_class.facts("foo.bar.node", {}) }.to raise_error(OctocatalogDiff::Errors::FactSourceError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#puppetdb_url" do
|
||||
let(:fake_url) { "https://puppetdb.fake:8443" }
|
||||
it "should return puppetdb_url from configuration" do
|
||||
expect(described_class.puppetdb_url("puppetdb" => { "url" => fake_url })).to eq(fake_url)
|
||||
end
|
||||
|
||||
it "should return PUPPETDB_URL from environment" do
|
||||
ENV["PUPPETDB_URL"] = fake_url
|
||||
expect(described_class.puppetdb_url).to eq(fake_url)
|
||||
end
|
||||
|
||||
it "should raise an error if puppetdb URL cannot be determined" do
|
||||
expect { described_class.puppetdb_url }.to raise_error(/PuppetDB URL not configured or set in environment/)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,75 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe OctofactsUpdater::Service::SSH do
|
||||
describe "#facts" do
|
||||
let(:node) { "foo.example.net" }
|
||||
let(:custom_exception) { RuntimeError.new("custom exception for testing") }
|
||||
|
||||
context "when ssh is not configured" do
|
||||
it "should raise ArgumentError when ssh is undefined" do
|
||||
config = {}
|
||||
expect{ described_class.facts(node, config) }.to raise_error(ArgumentError, /requires ssh section/)
|
||||
end
|
||||
|
||||
it "should raise ArgumentError when ssh is not a hash" do
|
||||
config = {"ssh" => :do_it}
|
||||
expect{ described_class.facts(node, config) }.to raise_error(ArgumentError, /requires ssh section/)
|
||||
end
|
||||
end
|
||||
|
||||
context "when ssh is configured" do
|
||||
it "should raise error if no server is configured" do
|
||||
config = { "ssh" => {} }
|
||||
expect{ described_class.facts(node, config) }.to raise_error(ArgumentError, /requires 'server' in the ssh section/)
|
||||
end
|
||||
|
||||
context "when user is unspecified" do
|
||||
before(:each) do
|
||||
@user_save = ENV.delete("USER")
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
if @user_save
|
||||
ENV["USER"] = @user_save
|
||||
else
|
||||
ENV.delete("USER")
|
||||
end
|
||||
end
|
||||
|
||||
it "should raise error if no user is configured" do
|
||||
config = { "ssh" => { "server" => "puppetserver.example.net" } }
|
||||
expect{ described_class.facts(node, config) }.to raise_error(ArgumentError, /requires 'user' in the ssh section/)
|
||||
end
|
||||
|
||||
it "should use USER from environment if no user is configured" do
|
||||
ENV["USER"] = "ssh-user-from-env"
|
||||
config = { "ssh" => { "server" => "puppetserver.example.net" } }
|
||||
expect(Net::SSH).to receive(:start).with("puppetserver.example.net", "ssh-user-from-env", {}).and_raise(custom_exception)
|
||||
expect{ described_class.facts(node, config) }.to raise_error(custom_exception)
|
||||
end
|
||||
end
|
||||
|
||||
it "should raise error if SSH call fails" do
|
||||
config = { "ssh" => { "server" => "puppetserver.example.net", "user" => "foo", "extra" => "bar" } }
|
||||
ssh = double
|
||||
ssh_result = double
|
||||
allow(ssh_result).to receive(:exitstatus).and_return(1)
|
||||
allow(ssh_result).to receive(:to_s).and_return("Failed to cat foo: no such file or directory")
|
||||
expect(ssh).to receive(:"exec!").and_return(ssh_result)
|
||||
expect(Net::SSH).to receive(:start).with("puppetserver.example.net", "foo", extra: "bar").and_yield(ssh)
|
||||
expect { described_class.facts(node, config) }.to raise_error(/ssh failed with exitcode=1: Failed to cat foo/)
|
||||
end
|
||||
|
||||
it "should return data if SSH call succeeds" do
|
||||
config = { "ssh" => { "server" => "puppetserver.example.net", "user" => "foo", "extra" => "bar" } }
|
||||
ssh = double
|
||||
ssh_result = double
|
||||
allow(ssh_result).to receive(:exitstatus).and_return(0)
|
||||
allow(ssh_result).to receive(:to_s).and_return("---\nname: #{node}\nvalues:\n foo: bar\n")
|
||||
expect(ssh).to receive(:"exec!").and_return(ssh_result)
|
||||
expect(Net::SSH).to receive(:start).with("puppetserver.example.net", "foo", extra: "bar").and_yield(ssh)
|
||||
expect(described_class.facts(node, config)).to eq("name" => node, "values" => { "foo" => "bar" })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
if ENV["SPEC_NAME"]
|
||||
require "simplecov"
|
||||
require "simplecov-json"
|
||||
|
||||
SimpleCov.root File.expand_path("..", File.dirname(__FILE__))
|
||||
SimpleCov.coverage_dir File.expand_path("../lib/#{ENV['SPEC_NAME']}/coverage", File.dirname(__FILE__))
|
||||
|
||||
if ENV["JOB_NAME"]
|
||||
SimpleCov.formatters = [SimpleCov::Formatter::JSONFormatter]
|
||||
else
|
||||
SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::JSONFormatter]
|
||||
end
|
||||
|
||||
SimpleCov.start do
|
||||
add_filter "spec/"
|
||||
if ENV["SPEC_NAME"] == "octofacts"
|
||||
add_filter "lib/octofacts_updater.rb"
|
||||
add_filter "lib/octofacts_updater/"
|
||||
elsif ENV["SPEC_NAME"] == "octofacts_updater"
|
||||
add_filter "lib/octofacts.rb"
|
||||
add_filter "lib/octofacts/"
|
||||
end
|
||||
end
|
||||
|
||||
require ENV["SPEC_NAME"]
|
||||
require_relative "octofacts/octofacts_spec_helper" if ENV["SPEC_NAME"] == "octofacts"
|
||||
else
|
||||
require "octofacts"
|
||||
require_relative "octofacts/octofacts_spec_helper"
|
||||
require "octofacts_updater"
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
# Prohibit using the should syntax
|
||||
config.expect_with :rspec do |spec|
|
||||
spec.syntax = :expect
|
||||
end
|
||||
|
||||
config.before(:each) do
|
||||
ENV.delete("OCTOFACTS_INDEX_PATH")
|
||||
ENV.delete("OCTOFACTS_FIXTURE_PATH")
|
||||
end
|
||||
end
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче