knife-inspect 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ !.gitkeep
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in health_inspector.gemspec
4
+ gemspec
data/HISTORY.md ADDED
@@ -0,0 +1,88 @@
1
+ ## 0.6.0 ( 2012-11-1 )
2
+
3
+ * Add knife plugins for all existing functionality:
4
+ - knife inspect
5
+ - knife cookbook inspect [COOKBOOK]
6
+ - knife data bag inspect [BAG] [ITEM]
7
+ - knife environment inspect [ENVIRONMENT]
8
+ - knife role inspect [ROLE]
9
+
10
+ * Lost support for quiet-sucess option (We can add that back, or make a quiet
11
+ options that just returns exit status).
12
+
13
+ ## 0.5.2 ( 2012-10-19 )
14
+
15
+ * Make loading of Chef Config a little more robust.
16
+
17
+ ## 0.5.1 ( 2012-10-15 )
18
+
19
+ * Ignore _default environment if it only exists on the server.
20
+
21
+ ## 0.5.0 ( 2012-10-14 )
22
+
23
+ * Switch to RSpec
24
+ * Add some test coverage (still needs much more).
25
+ * Add option to suppress terminal output on successful checks.
26
+ * Add option to not use ansi color output.
27
+ * Make cookbook version comparison use Chef's native version class.
28
+
29
+ ## 0.4.1 ( 2012-09-28 )
30
+
31
+ * Fix a bug I created in last release when passing no component.
32
+
33
+ ## 0.4.0 ( 2012-09-28 )
34
+
35
+ * Make `inspect` the default task
36
+ * Add ability to specify individual components:
37
+
38
+ health_inspector inspect cookbooks
39
+
40
+ ## 0.3.1 ( 2012-09-27 )
41
+
42
+ * Stop shelling out for knife commands, use Chef API directly for everything.
43
+
44
+ ## 0.3.0 ( 2012-09-26 )
45
+
46
+ * Add new check for cookbooks: checksum comparison for each file.
47
+
48
+ ## 0.2.1 ( 2012-09-26 )
49
+
50
+ * Fix 1.8.7 incompatibility introduced in last release (String#prepend).
51
+
52
+ ## 0.2.0 ( 2012-09-25 )
53
+
54
+ * Add a better diff output.
55
+ * Add diff output to data bag items.
56
+ * Switch to yajl-ruby to fix JSON parsing issues (Chef uses this also).
57
+
58
+ ## 0.1.0 ( 2012-09-24 )
59
+
60
+ * Bump Chef dependency version up to 10.14
61
+ * Add support for JSON environments.
62
+ * Add support for JSON roles.
63
+ * Display the diff between JSONs when JSON data doesn't match.
64
+
65
+ ## 0.0.6 ( 2012-05-23 )
66
+
67
+ * Depend on Chef 0.10.8, since it depends on a later version of the json gem.
68
+ An earlier version of the json gem was throwing incorrect parse errors.
69
+
70
+ ## 0.0.5 ( 2012-04-13 )
71
+
72
+ * Fix #2, exception when a data bag item json file doesn't exist locally.
73
+
74
+ ## 0.0.4 ( 2012-04-09 )
75
+
76
+ * Add checks for data bags, data bag items, environments, and roles.
77
+
78
+ ## 0.0.3 ( 2012-03-27 )
79
+
80
+ * Read cookbook paths from knife config file instead of hardcoding /cookbooks.
81
+
82
+ ## 0.0.2 ( 2012-03-27 )
83
+
84
+ * Make sure we iterate over actual cookbooks in cookbooks folder.
85
+
86
+ ## 0.0.1 ( 2012-03-27 )
87
+
88
+ * Initial release.
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2012 Ben Marini
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ [![Build Status](https://secure.travis-ci.org/bmarini/health_inspector.png)](http://travis-ci.org/bmarini/health_inspector)
2
+
3
+ ## Summary
4
+
5
+ `health_inspector` is a knife plugin that inspects your chef repo as it
6
+ compares to what is on your chef server. You can inspect your entire repo,
7
+ or individual components.
8
+
9
+ ## Usage
10
+
11
+ $ gem install health_inspector
12
+ $ cd [chef repo]
13
+
14
+ ## Knife Commands
15
+
16
+ knife inspect
17
+
18
+ knife cookbook inspect
19
+ knife cookbook inspect [COOKBOOK]
20
+
21
+ knife data bag inspect
22
+ knife data bag inspect [BAG]
23
+ knife data bag inspect [BAG] [ITEM]
24
+
25
+ knife environment inspect
26
+ knife environment inspect [ENVIRONMENT]
27
+
28
+ knife role inspect
29
+ knife role inspect [ROLE]
30
+
31
+ ## What it does
32
+
33
+ So far it checks if...
34
+
35
+ * your cookbooks are in sync
36
+ * you have uncommitted changes in a cookbook (assuming your cookbooks are in
37
+ their own git repos)
38
+ * you have commits in a cookbook that haven't been pushed to your remote
39
+ (assuming your cookbooks are in their own git repos)
40
+ * your data bags are in sync
41
+ * your data bag items are in sync
42
+ * your environments are in sync
43
+ * your roles are in sync
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
6
+ t.pattern = 'spec/**/*_spec.rb'
7
+ end
8
+
9
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "health_inspector/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "knife-inspect"
7
+ s.version = HealthInspector::VERSION
8
+ s.authors = ["Ben Marini"]
9
+ s.email = ["bmarini@gmail.com"]
10
+ s.homepage = "https://github.com/bmarini/knife-inspect"
11
+ s.summary = %q{Inspect your chef repo as is compares to what is on your chef server}
12
+ s.description = %q{Inspect your chef repo as is compares to what is on your chef server}
13
+
14
+ s.rubyforge_project = "knife-inspect"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "rake"
22
+ s.add_development_dependency "rspec"
23
+
24
+ s.add_runtime_dependency "thor"
25
+ s.add_runtime_dependency "chef", "~> 10.14"
26
+ s.add_runtime_dependency "yajl-ruby"
27
+ end
@@ -0,0 +1,36 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class CookbookInspect < Knife
6
+
7
+ deps do
8
+ require 'health_inspector'
9
+ require 'chef/json_compat'
10
+ require 'uri'
11
+ require 'chef/cookbook_version'
12
+ end
13
+
14
+ banner "knife cookbook inspect [COOKBOOK] (options)"
15
+
16
+ def run
17
+ case @name_args.length
18
+ when 1 # We are inspecting a cookbook
19
+ cookbook_name = @name_args[0]
20
+ # TODO: Support environments
21
+ # env = config[:environment]
22
+ # api_endpoint = env ? "environments/#{env}/cookbooks/#{cookbook_name}" : "cookbooks/#{cookbook_name}"
23
+
24
+ validator = HealthInspector::Checklists::Cookbooks.new(self)
25
+ validator.validate_item( validator.load_item(cookbook_name) )
26
+ when 0 # We are inspecting all the cookbooks
27
+ HealthInspector::Checklists::Cookbooks.run(self)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+
36
+
@@ -0,0 +1,35 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class DataBagInspect < Knife
6
+
7
+ deps do
8
+ require 'health_inspector'
9
+ end
10
+
11
+ banner "knife data bag inspect [BAG] [ITEM] (options)"
12
+
13
+ def run
14
+ case @name_args.length
15
+ when 2 # We are inspecting a data bag item
16
+ bag_name = @name_args[0]
17
+ item_name = @name_args[1]
18
+
19
+ validator = HealthInspector::Checklists::DataBagItems.new(self)
20
+ validator.validate_item( validator.load_item("#{bag_name}/#{item_name}") )
21
+
22
+ when 1 # We are inspecting a data bag
23
+ bag_name = @name_args[0]
24
+
25
+ validator = HealthInspector::Checklists::DataBags.new(self)
26
+ validator.validate_item( validator.load_item(bag_name) )
27
+
28
+ when 0 # We are inspecting all the data bags
29
+ HealthInspector::Checklists::DataBags.run(self)
30
+ HealthInspector::Checklists::DataBagItems.run(self)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class EnvironmentInspect < Knife
6
+
7
+ deps do
8
+ require 'health_inspector'
9
+ end
10
+
11
+ banner "knife environment inspect [ENVIRONMENT] (options)"
12
+
13
+ def run
14
+ case @name_args.length
15
+ when 1 # We are inspecting a environment
16
+ environment_name = @name_args[0]
17
+ validator = HealthInspector::Checklists::Environments.new(self)
18
+ validator.validate_item( validator.load_item(environment_name) )
19
+ when 0 # We are inspecting all the environments
20
+ HealthInspector::Checklists::Environments.run(self)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class Inspect < Knife
6
+
7
+ deps do
8
+ require "health_inspector"
9
+ end
10
+
11
+ banner "knife inspect"
12
+
13
+ def run
14
+ %w[ Cookbooks DataBags DataBagItems Environments Roles ].each do |checklist|
15
+ HealthInspector::Checklists.const_get(checklist).run(self)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class RoleInspect < Knife
6
+
7
+ deps do
8
+ require 'health_inspector'
9
+ end
10
+
11
+ banner "knife role inspect [ROLE] (options)"
12
+
13
+ def run
14
+ case @name_args.length
15
+ when 1 # We are inspecting a role
16
+ role_name = @name_args[0]
17
+ validator = HealthInspector::Checklists::Roles.new(self)
18
+ validator.validate_item( validator.load_item(role_name) )
19
+ when 0 # We are inspecting all the roles
20
+ HealthInspector::Checklists::Roles.run(self)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+ require "health_inspector/version"
3
+ require "health_inspector/color"
4
+ require "health_inspector/context"
5
+ require "health_inspector/pairing"
6
+ require "health_inspector/inspector"
7
+ require "health_inspector/checklists/base"
8
+ require "health_inspector/checklists/cookbooks"
9
+ require "health_inspector/checklists/data_bags"
10
+ require "health_inspector/checklists/data_bag_items"
11
+ require "health_inspector/checklists/environments"
12
+ require "health_inspector/checklists/roles"
13
+ require 'chef/rest'
14
+ require 'chef/checksum_cache'
15
+ require 'chef/version'
16
+
17
+ module HealthInspector
18
+ end
@@ -0,0 +1,136 @@
1
+ # encoding: UTF-8
2
+ require "pathname"
3
+
4
+ module HealthInspector
5
+ module Checklists
6
+ class Base
7
+ include Color
8
+
9
+ class << self
10
+ attr_reader :title
11
+
12
+ def title(val=nil)
13
+ val.nil? ? @title : @title = val
14
+ end
15
+ end
16
+
17
+ def self.run(knife)
18
+ new(knife).run
19
+ end
20
+
21
+ def initialize(knife)
22
+ @context = Context.new(knife)
23
+ end
24
+
25
+ def ui
26
+ @context.knife.ui
27
+ end
28
+
29
+ def all_item_names
30
+ ( server_items + local_items ).uniq.sort
31
+ end
32
+
33
+ # Subclasses should collect all items from the server and the local repo,
34
+ # and for each item pair, yield an object that contains a reference to
35
+ # the server item, and the local repo item. A reference can be nil if it does
36
+ # not exist in one of the locations.
37
+ def each_item
38
+ raise NotImplementedError, "You must implement this method in a subclass"
39
+ end
40
+
41
+ def run
42
+ banner "Inspecting #{self.class.title}"
43
+
44
+ each_item do |item|
45
+ validate_item(item)
46
+ end
47
+ end
48
+
49
+ def validate_item(item)
50
+ item.validate
51
+ failures = item.errors
52
+
53
+ if failures.empty?
54
+ print_success(item.name) # unless @context.quiet_success
55
+ else
56
+ print_failures(item.name, failures)
57
+ end
58
+ end
59
+
60
+ def banner(message)
61
+ ui.msg ""
62
+ ui.msg message
63
+ ui.msg "-" * 80
64
+ end
65
+
66
+ def print_success(subject)
67
+ ui.msg color('bright pass', "✓") + " #{subject}"
68
+ end
69
+
70
+ def print_failures(subject, failures)
71
+ ui.msg color('bright fail', "- #{subject}")
72
+
73
+ failures.each do |message|
74
+ if message.kind_of? Hash
75
+ puts color('bright yellow'," has the following values mismatched on the server and repo\n")
76
+ print_failures_from_hash(message)
77
+ else
78
+ puts color('bright yellow', " #{message}")
79
+ end
80
+ end
81
+ end
82
+
83
+ def print_failures_from_hash(message, depth=2)
84
+ message.keys.each do |key|
85
+ print_key(key,depth)
86
+
87
+ if message[key].include? "server"
88
+ print_value_diff(message[key],depth)
89
+ message[key].delete_if { |k,v| k == "server" || "local" }
90
+ print_failures_from_hash(message[key], depth + 1) unless message[key].empty?
91
+ else
92
+ print_failures_from_hash(message[key], depth + 1)
93
+ end
94
+ end
95
+ end
96
+
97
+ def print_key(key, depth)
98
+ ui.msg indent( color('bright yellow',"#{key} : "), depth )
99
+ end
100
+
101
+ def print_value_diff(value, depth)
102
+ print indent( color('bright fail',"server value = "), depth + 1 )
103
+ print value["server"]
104
+ print "\n"
105
+ print indent( color('bright fail',"local value = "), depth + 1 )
106
+ print value["local"]
107
+ print "\n\n"
108
+ end
109
+
110
+ def load_ruby_or_json_from_local(chef_class, folder, name)
111
+ path_template = "#{@context.repo_path}/#{folder}/#{name}.%s"
112
+ ruby_pathname = Pathname.new(path_template % "rb")
113
+ json_pathname = Pathname.new(path_template % "json")
114
+ js_pathname = Pathname.new(path_template % "js")
115
+
116
+ if ruby_pathname.exist?
117
+ instance = chef_class.new
118
+ instance.from_file(ruby_pathname.to_s)
119
+ elsif json_pathname.exist?
120
+ instance = chef_class.json_create( Yajl::Parser.parse( json_pathname.read ) )
121
+ elsif js_pathname.exist?
122
+ instance = chef_class.json_create( Yajl::Parser.parse( js_pathname.read ) )
123
+ end
124
+
125
+ instance ? instance.to_hash : nil
126
+ rescue IOError
127
+ nil
128
+ end
129
+
130
+ def indent(string, depth)
131
+ (' ' * 2 * depth) + string
132
+ end
133
+
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,134 @@
1
+ module HealthInspector
2
+ module Checklists
3
+
4
+ class Cookbook < Pairing
5
+ include ExistenceValidations
6
+
7
+ def validate_versions
8
+ if versions_exist? && !versions_match?
9
+ errors.add "chef server has #{server} but local version is #{local}"
10
+ end
11
+ end
12
+
13
+ def validate_uncommited_changes
14
+ if git_repo?
15
+ result = `cd #{cookbook_path} && git status -s`
16
+
17
+ unless result.empty?
18
+ errors.add "Uncommitted changes:\n#{result.chomp}"
19
+ end
20
+ end
21
+ end
22
+
23
+ def validate_commits_not_pushed_to_remote
24
+ if git_repo?
25
+ result = `cd #{cookbook_path} && git status`
26
+
27
+ if result =~ /Your branch is ahead of (.+)/
28
+ errors.add "ahead of #{$1}"
29
+ end
30
+ end
31
+ end
32
+
33
+ # TODO: Check files that exist locally but not in manifest on server
34
+ def validate_changes_on_the_server_not_in_the_repo
35
+ if versions_exist? && versions_match?
36
+
37
+ begin
38
+ cookbook = context.rest.get_rest("/cookbooks/#{name}/#{local}")
39
+ messages = []
40
+
41
+ Chef::CookbookVersion::COOKBOOK_SEGMENTS.each do |segment|
42
+ cookbook.manifest[segment].each do |manifest_record|
43
+ path = cookbook_path.join("#{manifest_record["path"]}")
44
+
45
+ if path.exist?
46
+ checksum = checksum_cookbook_file(path)
47
+ messages << "#{manifest_record['path']}" if checksum != manifest_record['checksum']
48
+ else
49
+ messages << "#{manifest_record['path']} does not exist in the repo"
50
+ end
51
+ end
52
+ end
53
+
54
+ unless messages.empty?
55
+ message = "has a checksum mismatch between server and repo in\n"
56
+ message << messages.map { |f| " #{f}" }.join("\n")
57
+ errors.add message
58
+ end
59
+
60
+ rescue Net::HTTPServerException => e
61
+ errors.add "Could not find cookbook #{name} on the server"
62
+ end
63
+
64
+ end
65
+ end
66
+
67
+ def versions_exist?
68
+ local && server
69
+ end
70
+
71
+ def versions_match?
72
+ local == server
73
+ end
74
+
75
+ def git_repo?
76
+ cookbook_path && File.exist?("#{cookbook_path}/.git")
77
+ end
78
+
79
+ def cookbook_path
80
+ path = context.cookbook_path.find { |f| File.exist?("#{f}/#{name}") }
81
+ path ? Pathname.new(path).join(name) : nil
82
+ end
83
+
84
+ def checksum_cookbook_file(filepath)
85
+ Chef::CookbookVersion.checksum_cookbook_file(filepath)
86
+ end
87
+
88
+ end
89
+
90
+ class Cookbooks < Base
91
+
92
+ title "cookbooks"
93
+
94
+ def each_item
95
+ all_cookbook_names = ( server_cookbooks.keys + local_cookbooks.keys ).uniq.sort
96
+
97
+ all_cookbook_names.each do |name|
98
+ yield load_item(name)
99
+ end
100
+ end
101
+
102
+ def load_item(name)
103
+ Cookbook.new(@context,
104
+ :name => name,
105
+ :server => server_cookbooks[name],
106
+ :local => local_cookbooks[name]
107
+ )
108
+ end
109
+
110
+ def server_cookbooks
111
+ @context.rest.get_rest("/cookbooks").inject({}) do |hsh, (name,version)|
112
+ hsh[name] = Chef::Version.new(version["versions"].first["version"])
113
+ hsh
114
+ end
115
+ end
116
+
117
+ def local_cookbooks
118
+ @context.cookbook_path.
119
+ map { |path| Dir["#{path}/*"] }.
120
+ flatten.
121
+ select { |path| File.exists?("#{path}/metadata.rb") }.
122
+ inject({}) do |hsh, path|
123
+
124
+ name = File.basename(path)
125
+ version = (`grep '^version' #{path}/metadata.rb`).split.last[1...-1]
126
+
127
+ hsh[name] = Chef::Version.new(version)
128
+ hsh
129
+ end
130
+ end
131
+
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,60 @@
1
+ require "chef/data_bag"
2
+
3
+ module HealthInspector
4
+ module Checklists
5
+ class DataBagItem < Pairing
6
+ include ExistenceValidations
7
+ include JsonValidations
8
+ end
9
+
10
+ class DataBagItems < Base
11
+ title "data bag items"
12
+
13
+ def each_item
14
+ all_item_names.each do |name|
15
+ yield load_item(name)
16
+ end
17
+ end
18
+
19
+ def load_item(name)
20
+ DataBagItem.new(@context,
21
+ :name => name,
22
+ :server => load_item_from_server(name),
23
+ :local => load_item_from_local(name)
24
+ )
25
+ end
26
+
27
+ def server_items
28
+ @server_items ||= Chef::DataBag.list.keys.map do |bag_name|
29
+ [ bag_name, Chef::DataBag.load(bag_name) ]
30
+ end.inject([]) do |arr, (bag_name, data_bag)|
31
+ arr += data_bag.keys.map { |item_name| "#{bag_name}/#{item_name}"}
32
+ end
33
+ end
34
+
35
+ def local_items
36
+ entries = nil
37
+
38
+ Dir.chdir("#{@context.repo_path}/data_bags") do
39
+ entries = Dir["**/*.json"].map { |entry| entry.gsub('.json', '') }
40
+ end
41
+
42
+ return entries
43
+ end
44
+
45
+ def load_item_from_server(name)
46
+ bag_name, item_name = name.split("/")
47
+ Chef::DataBagItem.load(bag_name, item_name).raw_data
48
+ rescue
49
+ nil
50
+ end
51
+
52
+ def load_item_from_local(name)
53
+ Yajl::Parser.parse( File.read("#{@context.repo_path}/data_bags/#{name}.json") )
54
+ rescue IOError, Errno::ENOENT
55
+ nil
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,38 @@
1
+ require "chef/data_bag"
2
+
3
+ module HealthInspector
4
+ module Checklists
5
+ class DataBag < Pairing
6
+ include ExistenceValidations
7
+ end
8
+
9
+ class DataBags < Base
10
+ title "data bags"
11
+
12
+ def each_item
13
+ all_item_names.each do |name|
14
+ yield load_item(name)
15
+ end
16
+ end
17
+
18
+ def load_item(name)
19
+ DataBag.new(@context,
20
+ :name => name,
21
+ :server => server_items.include?(name),
22
+ :local => local_items.include?(name)
23
+ )
24
+ end
25
+
26
+ def server_items
27
+ @server_items ||= Chef::DataBag.list.keys
28
+ end
29
+
30
+ def local_items
31
+ @local_items ||= Dir["#{@context.repo_path}/data_bags/*"].entries.
32
+ select { |e| File.directory?(e) }.
33
+ map { |e| File.basename(e) }
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ require "chef/environment"
2
+
3
+ module HealthInspector
4
+ module Checklists
5
+ class Environment < Pairing
6
+ include ExistenceValidations
7
+ include JsonValidations
8
+
9
+ # Override to ignore _default environment if it is missing locally
10
+ def validate_local_copy_exists
11
+ super unless name == '_default'
12
+ end
13
+ end
14
+
15
+ class Environments < Base
16
+ title "environments"
17
+
18
+ def each_item
19
+ all_item_names.each do |name|
20
+ yield load_item(name)
21
+ end
22
+ end
23
+
24
+ def load_item(name)
25
+ Environment.new(@context,
26
+ :name => name,
27
+ :server => load_item_from_server(name),
28
+ :local => load_item_from_local(name)
29
+ )
30
+ end
31
+
32
+ def server_items
33
+ @server_items ||= Chef::Environment.list.keys
34
+ end
35
+
36
+ def local_items
37
+ Dir.chdir("#{@context.repo_path}/environments") do
38
+ Dir["*.{rb,json,js}"].map { |e| e.gsub(/\.(rb|json|js)/,"") }
39
+ end
40
+ end
41
+
42
+ def load_item_from_server(name)
43
+ env = Chef::Environment.load(name)
44
+ env.to_hash
45
+ rescue
46
+ nil
47
+ end
48
+
49
+ def load_item_from_local(name)
50
+ load_ruby_or_json_from_local(Chef::Environment, "environments", name)
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,51 @@
1
+ require "chef/role"
2
+ require 'yajl'
3
+
4
+ module HealthInspector
5
+ module Checklists
6
+ class Role < Pairing
7
+ include ExistenceValidations
8
+ include JsonValidations
9
+ end
10
+
11
+ class Roles < Base
12
+ title "roles"
13
+
14
+ def each_item
15
+ all_item_names.each do |name|
16
+ yield load_item(name)
17
+ end
18
+ end
19
+
20
+ def load_item(name)
21
+ Role.new(@context,
22
+ :name => name,
23
+ :server => load_item_from_server(name),
24
+ :local => load_item_from_local(name)
25
+ )
26
+ end
27
+
28
+ def server_items
29
+ @server_items ||= Chef::Role.list.keys
30
+ end
31
+
32
+ def local_items
33
+ Dir.chdir("#{@context.repo_path}/roles") do
34
+ Dir["*.{rb,json,js}"].map { |e| e.gsub(/\.(rb|json|js)/, '') }
35
+ end
36
+ end
37
+
38
+ def load_item_from_server(name)
39
+ role = Chef::Role.load(name)
40
+ role.to_hash
41
+ rescue
42
+ nil
43
+ end
44
+
45
+ def load_item_from_local(name)
46
+ load_ruby_or_json_from_local(Chef::Role, "roles", name)
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ module HealthInspector
2
+ module Color
3
+
4
+ # TODO: Use a highline color scheme here instead
5
+ def color(type, str)
6
+ colors = {
7
+ 'pass' => [:green],# 90,
8
+ 'fail' => [:red],# 31,
9
+ 'bright pass' => [:bold, :green],# 92,
10
+ 'bright fail' => [:bold, :red],# 91,
11
+ 'bright yellow' => [:bold, :yellow],# 93,
12
+ 'pending' => [:yellow],# 36,
13
+ 'suite' => [],# 0,
14
+ 'error title' => [],# 0,
15
+ 'error message' => [:red],# 31,
16
+ 'error stack' => [:green],# 90,
17
+ 'checkmark' => [:green],# 32,
18
+ 'fast' => [:green],# 90,
19
+ 'medium' => [:green],# 33,
20
+ 'slow' => [:red],# 31,
21
+ 'green' => [:green],# 32,
22
+ 'light' => [:green],# 90,
23
+ 'diff gutter' => [:green],# 90,
24
+ 'diff added' => [:green],# 42,
25
+ 'diff removed' => [:red]# 41
26
+ }
27
+
28
+ @context.knife.ui.color( str, *colors[type] )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ require "chef/config"
2
+
3
+ module HealthInspector
4
+ class Context
5
+ attr_accessor :knife
6
+
7
+ def initialize(knife)
8
+ @knife = knife
9
+ end
10
+
11
+ def cookbook_path
12
+ Array( config.cookbook_path )
13
+ end
14
+
15
+ def config
16
+ Chef::Config
17
+ end
18
+
19
+ def rest
20
+ @knife.rest
21
+ end
22
+
23
+ def repo_path
24
+ ENV['PWD']
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ module HealthInspector
2
+ class Inspector
3
+ def self.inspect(checklists, options)
4
+ new(options).inspect( checklists )
5
+ end
6
+
7
+ def initialize(options)
8
+ @context = Context.new( options[:repopath], options[:configpath] )
9
+ @context.quiet_success = options[:'quiet-success']
10
+ @context.no_color = options[:'no-color']
11
+ @context.configure
12
+ end
13
+
14
+ def inspect(checklists)
15
+ checklists.each do |checklist|
16
+ Checklists.const_get(checklist).run(@context) if Checklists.const_defined?(checklist)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,73 @@
1
+ module HealthInspector
2
+ class Errors
3
+ include Enumerable
4
+
5
+ def initialize
6
+ @errors = []
7
+ end
8
+
9
+ def add(message)
10
+ @errors << message
11
+ end
12
+
13
+ def each
14
+ @errors.each { |e| yield(e) }
15
+ end
16
+
17
+ def empty?
18
+ @errors.empty?
19
+ end
20
+ end
21
+
22
+ class Pairing
23
+ attr_accessor :name, :local, :server
24
+ attr_reader :context, :errors
25
+
26
+ def initialize(context, opts={})
27
+ @context = context
28
+ @name = opts[:name]
29
+ @local = opts[:local]
30
+ @server = opts[:server]
31
+
32
+ @validations = []
33
+ @errors = Errors.new
34
+ end
35
+
36
+ def validate
37
+ self.methods.grep(/^validate_/).each { |meth| send(meth) }
38
+ end
39
+
40
+ def hash_diff(original, other)
41
+ (original.keys + other.keys).uniq.inject({}) do |memo, key|
42
+ unless original[key] == other[key]
43
+ if original[key].kind_of?(Hash) && other[key].kind_of?(Hash)
44
+ memo[key] = hash_diff(original[key], other[key])
45
+ else
46
+ memo[key] = {"server" => original[key],"local" => other[key]}
47
+ end
48
+ end
49
+ memo
50
+ end
51
+ end
52
+ end
53
+
54
+ # Mixins for common validations across pairings
55
+ module ExistenceValidations
56
+ def validate_local_copy_exists
57
+ errors.add "exists on server but not locally" if local.nil?
58
+ end
59
+
60
+ def validate_server_copy_exists
61
+ errors.add "exists locally but not on server" if server.nil?
62
+ end
63
+ end
64
+
65
+ module JsonValidations
66
+ def validate_items_are_the_same
67
+ if server && local
68
+ differences = hash_diff(server, local)
69
+ errors.add differences unless differences.empty?
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,3 @@
1
+ module HealthInspector
2
+ VERSION = "0.6.0"
3
+ end
File without changes
@@ -0,0 +1,7 @@
1
+ chef_repo_dir = File.expand_path( "../", File.dirname(__FILE__) )
2
+
3
+ client_key "#{chef_repo_dir}/.chef/client.pem"
4
+ log_level :info
5
+ log_location STDOUT
6
+ cache_type "BasicFile"
7
+ cookbook_path [ "#{chef_repo_dir}/cookbooks" ]
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::Cookbook do
4
+ let(:pairing) { described_class.new(health_inspector_context, :name => "dummy") }
5
+
6
+ it "should detect if an item does not exist locally" do
7
+ pairing.server = "0.0.1"
8
+ pairing.local = nil
9
+ pairing.validate
10
+
11
+ pairing.errors.should_not be_empty
12
+ pairing.errors.first.should == "exists on server but not locally"
13
+ end
14
+
15
+ it "should detect if an item does not exist on server" do
16
+ pairing.server = nil
17
+ pairing.local = "0.0.1"
18
+ pairing.validate
19
+
20
+ pairing.errors.should_not be_empty
21
+ pairing.errors.first.should == "exists locally but not on server"
22
+ end
23
+
24
+ it "should detect if an item is different" do
25
+ pairing.server = "0.0.1"
26
+ pairing.local = "0.0.2"
27
+ pairing.validate
28
+
29
+ pairing.errors.should_not be_empty
30
+ pairing.errors.first.should == "chef server has 0.0.1 but local version is 0.0.2"
31
+ end
32
+
33
+ it "should detect if an item is the same" do
34
+ pairing.should_receive(:validate_changes_on_the_server_not_in_the_repo)
35
+ pairing.server = "0.0.1"
36
+ pairing.local = "0.0.1"
37
+ pairing.validate
38
+
39
+ pairing.errors.should be_empty
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::DataBagItem do
4
+ it_behaves_like "a chef model"
5
+ it_behaves_like "a chef model that can be respresented in json"
6
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::DataBag do
4
+ it_behaves_like "a chef model"
5
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::Environment do
4
+ let(:pairing) { described_class.new(health_inspector_context) }
5
+
6
+ it_behaves_like "a chef model"
7
+ it_behaves_like "a chef model that can be respresented in json"
8
+
9
+ it "should ignore _default environment if it only exists on server" do
10
+ pairing.name = "_default"
11
+ pairing.server = {}
12
+ pairing.local = nil
13
+ pairing.validate
14
+
15
+ pairing.errors.should be_empty
16
+ end
17
+
18
+ end
data/spec/role_spec.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+
3
+ describe HealthInspector::Checklists::Role do
4
+ it_behaves_like "a chef model"
5
+ it_behaves_like "a chef model that can be respresented in json"
6
+ end
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'health_inspector'
4
+
5
+ module HealthInspector::SpecHelpers
6
+ def health_inspector_context
7
+ @health_inspector_context ||= HealthInspector::Context.new(nil)
8
+ end
9
+ end
10
+
11
+ RSpec.configure do |c|
12
+ c.include HealthInspector::SpecHelpers
13
+ end
14
+
15
+ shared_examples "a chef model" do
16
+ let(:pairing) { described_class.new(health_inspector_context, :name => "dummy") }
17
+
18
+ it "should detect if an item does not exist locally" do
19
+ pairing.server = {}
20
+ pairing.local = nil
21
+ pairing.validate
22
+
23
+ pairing.errors.should_not be_empty
24
+ pairing.errors.first.should == "exists on server but not locally"
25
+ end
26
+
27
+ it "should detect if an item does not exist on server" do
28
+ pairing.server = nil
29
+ pairing.local = {}
30
+ pairing.validate
31
+
32
+ pairing.errors.should_not be_empty
33
+ pairing.errors.first.should == "exists locally but not on server"
34
+ end
35
+ end
36
+
37
+ shared_examples "a chef model that can be respresented in json" do
38
+ let(:pairing) { described_class.new(health_inspector_context, :name => "dummy") }
39
+
40
+ it "should detect if an item is different" do
41
+ pairing.server = {"foo" => "bar"}
42
+ pairing.local = {"foo" => "baz"}
43
+ pairing.validate
44
+
45
+ pairing.errors.should_not be_empty
46
+ pairing.errors.first.should == {"foo"=>{"server"=>"bar", "local"=>"baz"}}
47
+ end
48
+
49
+ it "should detect if an item is the same" do
50
+ pairing.server = {"foo" => "bar"}
51
+ pairing.local = {"foo" => "bar"}
52
+ pairing.validate
53
+
54
+ pairing.errors.should be_empty
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knife-inspect
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ben Marini
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70274490577920 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70274490577920
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70274490573900 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70274490573900
36
+ - !ruby/object:Gem::Dependency
37
+ name: thor
38
+ requirement: &70274490581800 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70274490581800
47
+ - !ruby/object:Gem::Dependency
48
+ name: chef
49
+ requirement: &70274490608360 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '10.14'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70274490608360
58
+ - !ruby/object:Gem::Dependency
59
+ name: yajl-ruby
60
+ requirement: &70274490625560 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70274490625560
69
+ description: Inspect your chef repo as is compares to what is on your chef server
70
+ email:
71
+ - bmarini@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - Gemfile
78
+ - HISTORY.md
79
+ - MIT-LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - knife-inspect.gemspec
83
+ - lib/chef/knife/cookbook_inspect.rb
84
+ - lib/chef/knife/data_bag_inspect.rb
85
+ - lib/chef/knife/environment_inspect.rb
86
+ - lib/chef/knife/inspect.rb
87
+ - lib/chef/knife/role_inspect.rb
88
+ - lib/health_inspector.rb
89
+ - lib/health_inspector/checklists/base.rb
90
+ - lib/health_inspector/checklists/cookbooks.rb
91
+ - lib/health_inspector/checklists/data_bag_items.rb
92
+ - lib/health_inspector/checklists/data_bags.rb
93
+ - lib/health_inspector/checklists/environments.rb
94
+ - lib/health_inspector/checklists/roles.rb
95
+ - lib/health_inspector/color.rb
96
+ - lib/health_inspector/context.rb
97
+ - lib/health_inspector/inspector.rb
98
+ - lib/health_inspector/pairing.rb
99
+ - lib/health_inspector/version.rb
100
+ - spec/chef-repo/.chef/client.pem
101
+ - spec/chef-repo/.chef/knife.rb
102
+ - spec/cookbook_spec.rb
103
+ - spec/data_bag_item_spec.rb
104
+ - spec/data_bag_spec.rb
105
+ - spec/environment_spec.rb
106
+ - spec/role_spec.rb
107
+ - spec/spec_helper.rb
108
+ homepage: https://github.com/bmarini/knife-inspect
109
+ licenses: []
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project: knife-inspect
128
+ rubygems_version: 1.8.11
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: Inspect your chef repo as is compares to what is on your chef server
132
+ test_files:
133
+ - spec/chef-repo/.chef/client.pem
134
+ - spec/chef-repo/.chef/knife.rb
135
+ - spec/cookbook_spec.rb
136
+ - spec/data_bag_item_spec.rb
137
+ - spec/data_bag_spec.rb
138
+ - spec/environment_spec.rb
139
+ - spec/role_spec.rb
140
+ - spec/spec_helper.rb