├── lib ├── knife-preflight.rb └── chef │ └── knife │ └── preflight.rb ├── CHANGELOG.md ├── README.md ├── knife-preflight.gemspec └── Rakefile /lib/knife-preflight.rb: -------------------------------------------------------------------------------- 1 | module KnifePreflight 2 | VERSION = "0.1.3" 3 | end 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.8 (26th February, 2015) 2 | 3 | Bugfixes: 4 | 5 | - Remove '*' prefix in search queries to avoid false positives (Thanks to @8la https://github.com/jonlives/knife-preflight/pull/9) 6 | 7 | 8 | ## 0.1.7 (13th October, 2014) 9 | 10 | Features: 11 | 12 | - Adding back in logic to allow last_seen_recipes from https://github.com/jgoulah/knife-lastrun to be searched too 13 | 14 | 15 | ## 0.1.5 (25th January, 2013) 16 | 17 | Features: 18 | 19 | - Warns if nodes are in environments lacking a constraint for the cookbook being searched for 20 | 21 | ## 0.1.4 (25th January, 2013) 22 | 23 | Yanked 24 | 25 | ## 0.1.3 (June 29, 2012) 26 | 27 | Initial version. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # knife-preflight 2 | 3 | A preflight plugin for Chef::Knife which lets you see which nodes and roles use a particular cookbook before you upload it. 4 | 5 | # Installation 6 | 7 | ## SCRIPT INSTALL 8 | 9 | Copy preflight.rb script from lib/chef/knife to your ~/.chef/plugins/knife directory. 10 | 11 | ## GEM INSTALL 12 | knife-prelight is available on rubygems.org - if you have that source in your gemrc, you can simply use: 13 | 14 | ```` 15 | gem install knife-preflight 16 | ```` 17 | 18 | ## Preface 19 | 20 | Searches the expanded run_lists of all nodes along with the run_list of all roles for the specified cookbook 21 | 22 | ## What it does 23 | 24 | knife preflight apache2::default 25 | will return a list of all nodes containing this cookbook in their expanded run_list followed by all roles with the cookbook in their expanded run_list. It will warn if any nodes are in an environment which does not contain a version constraint for the cookbook being searched for. 26 | 27 | ## Notes 28 | This will currently only search for cookbooks. It won't work if you specify a role on the command line because I've tried to avoid duplication of functionality which knife makes obvious. 29 | 30 | -------------------------------------------------------------------------------- /knife-preflight.gemspec: -------------------------------------------------------------------------------- 1 | ## This is the rakegem gemspec template. Make sure you read and understand 2 | ## all of the comments. Some sections require modification, and others can 3 | ## be deleted if you don't need them. Once you understand the contents of 4 | ## this file, feel free to delete any comments that begin with two hash marks. 5 | ## You can find comprehensive Gem::Specification documentation, at 6 | ## http://docs.rubygems.org/read/chapter/20 7 | Gem::Specification.new do |s| 8 | s.specification_version = 2 if s.respond_to? :specification_version= 9 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 10 | s.rubygems_version = '1.3.5' 11 | 12 | ## Leave these as is they will be modified for you by the rake gemspec task. 13 | ## If your rubyforge_project name is different, then edit it and comment out 14 | ## the sub! line in the Rakefile 15 | s.name = 'knife-preflight' 16 | s.version = '0.1.8' 17 | s.date = '2015-02-25' 18 | s.rubyforge_project = 'knife-preflight' 19 | 20 | ## Make sure your summary is short. The description may be as long 21 | ## as you like. 22 | s.summary = "Knife plugin for checking what your cookbook changes will affect" 23 | s.description = "Knife plugin for checking what your cookbook changes will affect" 24 | 25 | ## List the primary authors. If there are a bunch of authors, it's probably 26 | ## better to set the email to an email list or something. If you don't have 27 | ## a custom homepage, consider using your GitHub URL or the like. 28 | s.authors = ["Jon Cowie"] 29 | s.email = 'jonlives@gmail.com' 30 | s.homepage = 'https://github.com/jonlives/knife-preflight' 31 | 32 | ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as 33 | ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb' 34 | s.require_paths = %w[lib] 35 | 36 | ## Specify any RDoc options here. You'll want to add your README and 37 | ## LICENSE files to the extra_rdoc_files list. 38 | s.rdoc_options = ["--charset=UTF-8"] 39 | s.extra_rdoc_files = %w[README.md] 40 | 41 | ## List your runtime dependencies here. Runtime dependencies are those 42 | ## that are needed for an end user to actually USE your code. 43 | s.add_dependency('chef', [">= 0.10.4"]) 44 | 45 | ## Leave this section as-is. It will be automatically generated from the 46 | ## contents of your Git repository via the gemspec task. DO NOT REMOVE 47 | ## THE MANIFEST COMMENTS, they are used as delimiters by the task. 48 | # = MANIFEST = 49 | s.files = %w[ 50 | README.md 51 | lib/chef/knife/preflight.rb 52 | ] 53 | # = MANIFEST = 54 | 55 | ## Test files will be grabbed from the file list. Make sure the path glob 56 | ## matches what you actually use. 57 | s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ } 58 | end 59 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'date' 4 | 5 | ############################################################################# 6 | # 7 | # Helper functions 8 | # 9 | ############################################################################# 10 | 11 | def name 12 | @name ||= Dir['*.gemspec'].first.split('.').first 13 | end 14 | 15 | def version 16 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/] 17 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 18 | end 19 | 20 | def date 21 | Date.today.to_s 22 | end 23 | 24 | def rubyforge_project 25 | name 26 | end 27 | 28 | def gemspec_file 29 | "#{name}.gemspec" 30 | end 31 | 32 | def gem_file 33 | "#{name}-#{version}.gem" 34 | end 35 | 36 | def replace_header(head, header_name) 37 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 38 | end 39 | 40 | ############################################################################# 41 | # 42 | # Standard tasks 43 | # 44 | ############################################################################# 45 | 46 | task :default => :validate 47 | 48 | desc "Open an irb session preloaded with this library" 49 | task :console do 50 | sh "irb -rubygems -r ./lib/#{name}.rb" 51 | end 52 | 53 | ############################################################################# 54 | # 55 | # Custom tasks (add your own tasks here) 56 | # 57 | ############################################################################# 58 | 59 | 60 | 61 | ############################################################################# 62 | # 63 | # Packaging tasks 64 | # 65 | ############################################################################# 66 | 67 | desc "Create tag v#{version} and build and push #{gem_file} to Rubygems" 68 | task :release => :build do 69 | unless `git branch` =~ /^\* master$/ 70 | puts "You must be on the master branch to release!" 71 | exit! 72 | end 73 | sh "git commit --allow-empty -a -m 'Release #{version}'" 74 | sh "git tag v#{version}" 75 | sh "git push origin master" 76 | sh "git push origin v#{version}" 77 | sh "gem push pkg/#{name}-#{version}.gem" 78 | end 79 | 80 | desc "Build #{gem_file} into the pkg directory" 81 | task :build => :gemspec do 82 | sh "mkdir -p pkg" 83 | sh "gem build #{gemspec_file}" 84 | sh "mv #{gem_file} pkg" 85 | end 86 | 87 | desc "Generate #{gemspec_file}" 88 | task :gemspec => :validate do 89 | # read spec file and split out manifest section 90 | spec = File.read(gemspec_file) 91 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 92 | 93 | # replace name version and date 94 | replace_header(head, :name) 95 | replace_header(head, :version) 96 | replace_header(head, :date) 97 | #comment this out if your rubyforge_project has a different name 98 | replace_header(head, :rubyforge_project) 99 | 100 | # determine file list from git ls-files 101 | files = `git ls-files`. 102 | split("\n"). 103 | sort. 104 | reject { |file| file =~ /^\./ }. 105 | reject { |file| file =~ /^(rdoc|pkg)/ }. 106 | map { |file| " #{file}" }. 107 | join("\n") 108 | 109 | # piece file back together and write 110 | manifest = " s.files = %w[\n#{files}\n ]\n" 111 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 112 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 113 | puts "Updated #{gemspec_file}" 114 | end 115 | 116 | desc "Validate #{gemspec_file}" 117 | task :validate do 118 | unless Dir['VERSION*'].empty? 119 | puts "A `VERSION` file at root level violates Gem best practices." 120 | exit! 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/chef/knife/preflight.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Author:: Jon Cowie () 3 | # Copyright:: Copyright (c) 2011 Jon Cowie 4 | # License:: GPL 5 | 6 | # Based on the knife chef plugin by: 7 | # Adam Jacob () 8 | # Copyright:: Copyright (c) 2009 Opscode, Inc. 9 | 10 | require 'chef/knife' 11 | require 'chef/knife/core/node_presenter' 12 | 13 | module KnifePreflight 14 | class Preflight < Chef::Knife 15 | 16 | deps do 17 | require 'chef/node' 18 | require 'chef/environment' 19 | require 'chef/api_client' 20 | require 'chef/search/query' 21 | end 22 | 23 | include Chef::Knife::Core::NodeFormattingOptions 24 | 25 | banner "knife preflight QUERY (options)" 26 | 27 | option :sort, 28 | :short => "-o SORT", 29 | :long => "--sort SORT", 30 | :description => "The order to sort the results in", 31 | :default => nil 32 | 33 | option :start, 34 | :short => "-b ROW", 35 | :long => "--start ROW", 36 | :description => "The row to start returning results at", 37 | :default => 0, 38 | :proc => lambda { |i| i.to_i } 39 | 40 | option :rows, 41 | :short => "-R INT", 42 | :long => "--rows INT", 43 | :description => "The number of rows to return", 44 | :default => 1000, 45 | :proc => lambda { |i| i.to_i } 46 | 47 | 48 | def run 49 | if config[:query] && @name_args[0] 50 | ui.error "please specify query as an argument or an option via -q, not both" 51 | ui.msg opt_parser 52 | exit 1 53 | end 54 | raw_query = config[:query] || @name_args[0] 55 | if !raw_query || raw_query.empty? 56 | ui.error "no query specified" 57 | ui.msg opt_parser 58 | exit 1 59 | end 60 | 61 | result_count_nodes = perform_query(raw_query, 'node') 62 | 63 | result_count_roles = perform_query(raw_query, 'role') 64 | 65 | ui.msg("Found #{result_count_nodes} nodes and #{result_count_roles} roles using the specified search criteria") 66 | end 67 | 68 | def perform_query(raw_query, type='node') 69 | q = Chef::Search::Query.new 70 | 71 | if type == 'node' 72 | cookbook = raw_query.split("::").first 73 | unconstrained_env_search = Chef::Search::Query.new 74 | unconstrained_envs = unconstrained_env_search.search('environment', "NOT cookbook_versions:#{cookbook}").first.map{|e|e.name} 75 | end 76 | 77 | # strip default if it exists to simplify logic 78 | raw_query = raw_query.sub("::default", "") 79 | escaped_query = raw_query.sub( "::", "\\:\\:") 80 | 81 | if !raw_query.include? "::" 82 | if type == 'node' 83 | search_query = "recipes:#{escaped_query} OR recipes:#{escaped_query}\\:\\:default" 84 | search_query += " OR " + search_query.gsub("recipes", "last_seen_recipes") 85 | else 86 | search_query = "run_list:recipe\\[#{escaped_query}\\] OR run_list:recipe\\[#{escaped_query}\\:\\:default\\]" 87 | end 88 | ui.msg("Searching for #{type}s containing #{raw_query} OR #{raw_query}::default in their expanded run_list or added via include_recipe...\n") 89 | else 90 | if type == 'node' 91 | search_query = "recipes:#{escaped_query}" 92 | search_query += " OR " + search_query.gsub("recipes", "last_seen_recipes") 93 | else 94 | search_query = "run_list:recipe\\[#{escaped_query}\\]" 95 | end 96 | ui.msg("Searching for #{type}s containing #{raw_query} in their expanded run_list or added via include_recipe......\n") 97 | end 98 | 99 | query = URI.escape(search_query, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) 100 | 101 | result_items = [] 102 | result_count = 0 103 | 104 | rows = config[:rows] 105 | start = config[:start] 106 | begin 107 | q.search(type, query, config[:sort], start, rows) do |item| 108 | formatted_item = format_for_display(item) 109 | if formatted_item.respond_to?(:has_key?) && !formatted_item.has_key?('id') 110 | formatted_item.normal['id'] = item.has_key?('id') ? item['id'] : item.name 111 | end 112 | result_items << formatted_item 113 | result_count += 1 114 | end 115 | rescue Net::HTTPServerException => e 116 | msg = Chef::JSONCompat.from_json(e.response.body)["error"].first 117 | ui.error("knife preflight failed: #{msg}") 118 | exit 1 119 | end 120 | 121 | if ui.interchange? 122 | output({:results => result_count, :rows => result_items}) 123 | else 124 | ui.msg "#{result_count} #{type.capitalize}s found" 125 | ui.msg("\n") 126 | result_items.each do |item| 127 | if item.instance_of?(Chef::Node) 128 | output("#{item.name}#{unconstrained_envs.include?(item.chef_environment) ? " - in environment #{item.chef_environment}, no version constraint for #{cookbook} cookbook" : nil}") 129 | else 130 | output(item.name) 131 | end 132 | end 133 | end 134 | 135 | ui.msg("\n") 136 | ui.msg("\n") 137 | return result_count 138 | end 139 | end 140 | end 141 | --------------------------------------------------------------------------------