This commit is contained in:
Kevin Paulisse 2017-10-06 08:15:17 -05:00
Коммит 88e4a98fb7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 66DA91D838188671
158 изменённых файлов: 6702 добавлений и 0 удалений

31
.github/ISSUE_TEMPLATE.md поставляемый Normal file
Просмотреть файл

@ -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?

26
.github/PULL_REQUEST_TEMPLATE.md поставляемый Normal file
Просмотреть файл

@ -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]

54
.gitignore поставляемый Normal file
Просмотреть файл

@ -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

9
.rubocop.yml Normal file
Просмотреть файл

@ -0,0 +1,9 @@
inherit_gem:
rubocop-github:
- config/default.yml
AllCops:
DisplayCopNames: true
AllCops:
TargetRubyVersion: 2.1

1
.ruby-version Normal file
Просмотреть файл

@ -0,0 +1 @@
2.1.9

1
.version Normal file
Просмотреть файл

@ -0,0 +1 @@
0.2.0

46
CODE_OF_CONDUCT.md Normal file
Просмотреть файл

@ -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/

33
CONTRIBUTING.md Normal file
Просмотреть файл

@ -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)

17
Gemfile Normal file
Просмотреть файл

@ -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

125
Gemfile.lock Normal file
Просмотреть файл

@ -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

21
LICENSE Normal file
Просмотреть файл

@ -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.

69
README.md Normal file
Просмотреть файл

@ -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)

26
Rakefile Normal file
Просмотреть файл

@ -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"

6
bin/octofacts-updater Executable file
Просмотреть файл

@ -0,0 +1,6 @@
#!/usr/bin/env ruby
require "bundler/setup"
require "octofacts_updater"
cli = OctofactsUpdater::CLI.new(ARGV)
cli.run

0
contrib/plugins/.gitkeep Normal file
Просмотреть файл

99
doc/manipulators.md Normal file
Просмотреть файл

@ -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
```

67
doc/more-examples.md Normal file
Просмотреть файл

@ -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
```

296
doc/octofacts-updater.md Normal file
Просмотреть файл

@ -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.

245
doc/plugin-reference.md Normal file
Просмотреть файл

@ -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
```

141
doc/tutorial.md Normal file
Просмотреть файл

@ -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
examples/code/.gitkeep Normal file
Просмотреть файл

Просмотреть файл

@ -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

15
lib/octofacts.rb Normal file
Просмотреть файл

@ -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

7
lib/octofacts/errors.rb Normal file
Просмотреть файл

@ -0,0 +1,7 @@
module Octofacts
class Errors
class FactNotIndexed < RuntimeError; end
class OperationNotPermitted < RuntimeError; end
class NoFactsError < RuntimeError; end
end
end

121
lib/octofacts/facts.rb Normal file
Просмотреть файл

@ -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

3
lib/octofacts/version.rb Normal file
Просмотреть файл

@ -0,0 +1,3 @@
module Octofacts
VERSION = File.read(File.expand_path("../../.version", File.dirname(__FILE__))).strip
end

19
lib/octofacts_updater.rb Normal file
Просмотреть файл

@ -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

29
octofacts-updater.gemspec Normal file
Просмотреть файл

@ -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

20
octofacts.gemspec Normal file
Просмотреть файл

@ -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

19
script/bootstrap Executable file
Просмотреть файл

@ -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

77
script/cibuild Executable file
Просмотреть файл

@ -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

7
script/console Executable file
Просмотреть файл

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
require "bundler/setup"
require "octofacts"
require "pry"
Pry.start

46
script/git-pre-commit Executable file
Просмотреть файл

@ -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

207
spec/fixtures/facts/basic.yaml поставляемый Normal file
Просмотреть файл

@ -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

8
spec/fixtures/facts/ops-consul-12345.dc2.example.com.yaml поставляемый Normal file
Просмотреть файл

@ -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

8
spec/fixtures/facts/ops-consul-67890.dc1.example.com.yaml поставляемый Normal file
Просмотреть файл

@ -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

38
spec/fixtures/index-no-nodes.yaml поставляемый Normal file
Просмотреть файл

@ -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

41
spec/fixtures/index.yaml поставляемый Normal file
Просмотреть файл

@ -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

41
spec/fixtures/sorted-index.yaml поставляемый Normal file
Просмотреть файл

@ -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

43
spec/spec_helper.rb Normal file
Просмотреть файл

@ -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

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше