├── test ├── support │ ├── knife.rb │ ├── issue_files │ │ ├── ubuntu │ │ ├── gentoo2011 │ │ └── sles11-sp1 │ ├── ssh_config │ ├── environment_cookbook │ │ ├── attributes │ │ │ └── default.rb │ │ ├── recipes │ │ │ └── default.rb │ │ └── metadata.rb │ ├── config.yml.example │ ├── secret_cookbook │ │ ├── metadata.rb │ │ └── recipes │ │ │ └── default.rb │ ├── test_environment.json │ ├── cache_using_cookbook │ │ ├── metadata.rb │ │ └── recipes │ │ │ └── default.rb │ ├── kitchen_helper.rb │ ├── validation_helper.rb │ ├── loggable.rb │ ├── data_bag_key │ ├── test_case.rb │ ├── integration_test.rb │ └── ec2_runner.rb ├── gemfiles │ ├── Gemfile.chef-12 │ ├── Gemfile.chef-13 │ └── Gemfile.chef-master ├── integration │ ├── cases │ │ ├── empty_cook.rb │ │ ├── knife_bootstrap.rb │ │ ├── cache_path_usage.rb │ │ ├── apache2_bootstrap.rb │ │ ├── apache2_cook.rb │ │ ├── ohai_hints.rb │ │ ├── environment.rb │ │ └── encrypted_data_bag.rb │ ├── sles_11_test.rb │ ├── omnios_r151014_test.rb │ ├── centos6_3_test.rb │ ├── scientific_linux_63_test.rb │ ├── ubuntu12_04_test.rb │ ├── ubuntu12_04_ohai_hints_test.rb │ ├── ubuntu12_04_bootstrap_test.rb │ ├── centos7_test.rb │ ├── amazon_linux_2016_03_bootstrap_test.rb │ └── debian7_knife_bootstrap_test.rb ├── test_helper.rb ├── integration_helper.rb ├── solo_clean_test.rb ├── performance │ └── ssh_performance_test.rb ├── solo_bootstrap_test.rb ├── minitest │ └── parallel.rb ├── deprecated_command_test.rb ├── gitignore_test.rb ├── solo_prepare_test.rb ├── knife_bootstrap_test.rb ├── tools_test.rb ├── bootstraps_test.rb ├── node_config_command_test.rb ├── solo_init_test.rb ├── ssh_command_test.rb └── solo_cook_test.rb ├── Gemfile ├── script ├── test └── newb ├── .gitmodules ├── lib ├── knife-solo.rb ├── chef │ └── knife │ │ ├── cook.rb │ │ ├── kitchen.rb │ │ ├── wash_up.rb │ │ ├── prepare.rb │ │ ├── solo_clean.rb │ │ ├── bootstrap_solo.rb │ │ ├── solo_bootstrap.rb │ │ ├── solo_init.rb │ │ ├── solo_prepare.rb │ │ └── solo_cook.rb └── knife-solo │ ├── bootstraps │ ├── sun_os.rb │ ├── darwin.rb │ ├── freebsd.rb │ └── linux.rb │ ├── resources │ ├── knife.rb │ └── solo.rb.erb │ ├── deprecated_command.rb │ ├── gitignore.rb │ ├── info.rb │ ├── librarian.rb │ ├── tools.rb │ ├── berkshelf.rb │ ├── cookbook_manager_selector.rb │ ├── ssh_connection.rb │ ├── node_config_command.rb │ ├── cookbook_manager.rb │ ├── bootstraps.rb │ └── ssh_command.rb ├── .gitignore ├── README.md ├── .travis.yml ├── LICENSE ├── knife-solo.gemspec ├── Rakefile ├── Manifest.txt ├── README.rdoc └── CHANGELOG.md /test/support/knife.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /test/support/issue_files/ubuntu: -------------------------------------------------------------------------------- 1 | Ubuntu 10.04.3 LTS \n \l 2 | 3 | -------------------------------------------------------------------------------- /test/support/issue_files/gentoo2011: -------------------------------------------------------------------------------- 1 | 2 | This is \n.\O (\s \m \r) \t 3 | 4 | -------------------------------------------------------------------------------- /test/support/ssh_config: -------------------------------------------------------------------------------- 1 | Host 10.0.0.1 2 | User bob 3 | IdentityFile id_rsa_bob 4 | -------------------------------------------------------------------------------- /test/support/environment_cookbook/attributes/default.rb: -------------------------------------------------------------------------------- 1 | default['environment']['test_attribute'] = "untouched" 2 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.chef-12: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '../..' 4 | 5 | gem 'chef', '~> 12' 6 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.chef-13: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '../..' 4 | 5 | gem 'chef', '~> 13' 6 | -------------------------------------------------------------------------------- /test/support/config.yml.example: -------------------------------------------------------------------------------- 1 | aws: 2 | key_name: knife-solo 3 | access_key: YOUR_ACCESS_KEY 4 | secret: YOUR_SECRET 5 | -------------------------------------------------------------------------------- /test/support/issue_files/sles11-sp1: -------------------------------------------------------------------------------- 1 | 2 | Welcome to SUSE Linux Enterprise Server 11 SP1 (x86_64) - Kernel \r (\l). 3 | 4 | 5 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ -z "$1" ]]; then 4 | echo "Usage: $0 test/unit/some_test.rb" 5 | exit 1 6 | fi 7 | 8 | bundle exec ruby -Itest "$@" 9 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.chef-master: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec :path => '../..' 4 | 5 | gem 'chef', github: 'opscode/chef' 6 | 7 | gem "ohai", github: 'chef/ohai' 8 | -------------------------------------------------------------------------------- /test/support/environment_cookbook/recipes/default.rb: -------------------------------------------------------------------------------- 1 | file "/etc/chef_environment" do 2 | mode 0644 3 | content "#{node.chef_environment}/#{node['environment']['test_attribute']}" 4 | end 5 | -------------------------------------------------------------------------------- /test/integration/cases/empty_cook.rb: -------------------------------------------------------------------------------- 1 | # Tries to run cook on the box 2 | module EmptyCook 3 | def test_empty_cook 4 | write_nodefile(run_list: []) 5 | assert_subcommand "cook" 6 | end 7 | end 8 | 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/knife-solo/resources/patch_cookbooks/chef-solo-search"] 2 | path = lib/knife-solo/resources/patch_cookbooks/chef-solo-search 3 | url = https://github.com/edelight/chef-solo-search.git 4 | -------------------------------------------------------------------------------- /lib/knife-solo.rb: -------------------------------------------------------------------------------- 1 | require 'knife-solo/info' 2 | require 'pathname' 3 | 4 | module KnifeSolo 5 | def self.resource(name) 6 | Pathname.new(__FILE__).dirname.join('knife-solo/resources', name) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'coveralls' 5 | Coveralls.wear! 6 | 7 | require 'minitest/autorun' 8 | require 'mocha/setup' 9 | 10 | require 'support/test_case' 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | /b 3 | /.bundle 4 | /mykitchen 5 | /doc 6 | /.yardoc 7 | /log 8 | /test/support/config.yml 9 | /test/support/knife-solo.pem 10 | /test/support/kitchens 11 | /Gemfile.lock 12 | /test/gemfiles/*.lock 13 | /coverage 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This gem has officially fallen too far behind the curve of current Chef releases & workflows. 4 | 5 | Please consider using https://knife-zero.github.io/, ansible, or visit https://www.chef.io/ for other ideas. 6 | -------------------------------------------------------------------------------- /lib/chef/knife/cook.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife/solo_cook' 2 | require 'knife-solo/deprecated_command' 3 | 4 | class Chef 5 | class Knife 6 | class Cook < SoloCook 7 | include KnifeSolo::DeprecatedCommand 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/chef/knife/kitchen.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife/solo_init' 2 | require 'knife-solo/deprecated_command' 3 | 4 | class Chef 5 | class Knife 6 | class Kitchen < SoloInit 7 | include KnifeSolo::DeprecatedCommand 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/chef/knife/wash_up.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife/solo_clean' 2 | require 'knife-solo/deprecated_command' 3 | 4 | class Chef 5 | class Knife 6 | class WashUp < SoloClean 7 | include KnifeSolo::DeprecatedCommand 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/chef/knife/prepare.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife/solo_prepare' 2 | require 'knife-solo/deprecated_command' 3 | 4 | class Chef 5 | class Knife 6 | class Prepare < SoloPrepare 7 | include KnifeSolo::DeprecatedCommand 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/support/secret_cookbook/metadata.rb: -------------------------------------------------------------------------------- 1 | name "secret_cookbook" 2 | maintainer "Mat Schaffer" 3 | maintainer_email "mat@schaffer.me" 4 | license "MIT" 5 | description "Spits out a file containing a secret data bag value" 6 | version "0.0.1" 7 | -------------------------------------------------------------------------------- /test/integration/sles_11_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class Sles11_Test < IntegrationTest 4 | def user 5 | "root" 6 | end 7 | 8 | def image_id 9 | "ami-ca32efa3" 10 | end 11 | 12 | include EmptyCook 13 | include EncryptedDataBag 14 | end 15 | -------------------------------------------------------------------------------- /test/integration/omnios_r151014_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class OmniOSr151014Test < IntegrationTest 4 | def user 5 | "root" 6 | end 7 | 8 | def image_id 9 | "ami-79ee1b14" 10 | end 11 | 12 | include EmptyCook 13 | include EncryptedDataBag 14 | end 15 | -------------------------------------------------------------------------------- /test/support/secret_cookbook/recipes/default.rb: -------------------------------------------------------------------------------- 1 | passwords = Chef::EncryptedDataBagItem.load("dev", "passwords") 2 | 3 | file "/etc/admin_password" do 4 | # This mode is a terrible idea for passwords 5 | # but makes verification easier 6 | mode 0644 7 | content passwords["admin"] 8 | end 9 | -------------------------------------------------------------------------------- /test/support/environment_cookbook/metadata.rb: -------------------------------------------------------------------------------- 1 | name "environment_cookbook" 2 | maintainer "Mat Schaffer" 3 | maintainer_email "mat@schaffer.me" 4 | license "MIT" 5 | description "Spits out a file containing the current chef environment and a test attribute" 6 | version "0.0.1" 7 | -------------------------------------------------------------------------------- /lib/knife-solo/bootstraps/sun_os.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo::Bootstraps 2 | class SunOS < Base 3 | def bootstrap! 4 | stream_command "sudo pkg install network/rsync web/ca-bundle" 5 | stream_command "sudo bash -c 'echo ca_certificate=/etc/cacert.pem >> /etc/wgetrc'" 6 | omnibus_install 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/support/test_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_environment", 3 | "description": "An environment for integration tests", 4 | "override_attributes": { 5 | "environment": { 6 | "test_attribute": "test_env_was_here" 7 | } 8 | }, 9 | "json_class": "Chef::Environment", 10 | "chef_type": "environment" 11 | } -------------------------------------------------------------------------------- /test/integration/centos6_3_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class Centos6_3Test < IntegrationTest 4 | disable_firewall 5 | 6 | def user 7 | "root" 8 | end 9 | 10 | def image_id 11 | "ami-86e15bef" 12 | end 13 | 14 | include EmptyCook 15 | include Apache2Cook 16 | include EncryptedDataBag 17 | end 18 | -------------------------------------------------------------------------------- /test/integration/scientific_linux_63_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class ScientificLinux63Test < IntegrationTest 4 | disable_firewall 5 | 6 | def user 7 | "root" 8 | end 9 | 10 | def image_id 11 | "ami-313b8e58" 12 | end 13 | 14 | include EmptyCook 15 | include Apache2Cook 16 | include EncryptedDataBag 17 | end 18 | -------------------------------------------------------------------------------- /lib/knife-solo/resources/knife.rb: -------------------------------------------------------------------------------- 1 | cookbook_path ["cookbooks", "site-cookbooks"] 2 | node_path "nodes" 3 | role_path "roles" 4 | environment_path "environments" 5 | data_bag_path "data_bags" 6 | #encrypted_data_bag_secret "data_bag_key" 7 | 8 | knife[:berkshelf_path] = "cookbooks" 9 | Chef::Config[:ssl_verify_mode] = :verify_peer if defined? ::Chef 10 | -------------------------------------------------------------------------------- /test/integration/ubuntu12_04_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class Ubuntu12_04Test < IntegrationTest 4 | def user 5 | "ubuntu" 6 | end 7 | 8 | def image_id 9 | "ami-9a873ff3" 10 | end 11 | 12 | include EmptyCook 13 | include Apache2Cook 14 | include EncryptedDataBag 15 | include CachePathUsage 16 | include Environment 17 | end 18 | -------------------------------------------------------------------------------- /test/integration/ubuntu12_04_ohai_hints_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class Ubuntu12_04OhaiHintsTest < IntegrationTest 4 | def user 5 | "ubuntu" 6 | end 7 | 8 | def image_id 9 | "ami-9a873ff3" 10 | end 11 | 12 | def prepare_server 13 | # Do nothing as `solo bootstrap` will do everything 14 | end 15 | 16 | include OhaiHints 17 | end 18 | -------------------------------------------------------------------------------- /test/integration/ubuntu12_04_bootstrap_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class Ubuntu12_04BootstrapTest < IntegrationTest 4 | def user 5 | "ubuntu" 6 | end 7 | 8 | def image_id 9 | "ami-9a873ff3" 10 | end 11 | 12 | def prepare_server 13 | # Do nothing as `solo bootstrap` will do everything 14 | end 15 | 16 | include Apache2Bootstrap 17 | end 18 | -------------------------------------------------------------------------------- /test/support/cache_using_cookbook/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'cache_using_cookbook' 2 | maintainer 'Mat Schaffer' 3 | maintainer_email 'mat@schaffer.me' 4 | license 'MIT' 5 | description 'Writes some data to the cache path.' 6 | long_description 'This helps ensure that we can do this multiple times regardless of what user initiates the run.' 7 | version '0.1.0' 8 | -------------------------------------------------------------------------------- /test/integration/centos7_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class Centos7Test < IntegrationTest 4 | # disable_firewall 5 | 6 | def flavor_id 7 | "m3.medium" 8 | end 9 | 10 | def user 11 | "centos" 12 | end 13 | 14 | def image_id 15 | "ami-6d1c2007" 16 | end 17 | 18 | include EmptyCook 19 | include Apache2Cook 20 | include EncryptedDataBag 21 | end 22 | -------------------------------------------------------------------------------- /test/integration/amazon_linux_2016_03_bootstrap_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class AmazonLinux2016_03BootstrapTest < IntegrationTest 4 | def user 5 | "ec2-user" 6 | end 7 | 8 | def image_id 9 | "ami-4f111125" 10 | end 11 | 12 | def prepare_server 13 | # Do nothing as `solo bootstrap` will do everything 14 | end 15 | 16 | include Apache2Bootstrap 17 | end 18 | -------------------------------------------------------------------------------- /lib/knife-solo/bootstraps/darwin.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo::Bootstraps 2 | class Darwin < Base 3 | 4 | def issue 5 | @issue ||= run_command("sw_vers -productVersion").stdout.strip 6 | end 7 | 8 | def distro 9 | case issue 10 | when %r{10.(?:[6-9]|10)} 11 | {:type => 'omnibus'} 12 | else 13 | raise "OS X version #{issue} not supported" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/integration_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'minitest/parallel' 5 | require 'minitest/autorun' 6 | 7 | require 'pathname' 8 | $base_dir = Pathname.new(__FILE__).dirname 9 | 10 | require 'support/loggable' 11 | require 'support/ec2_runner' 12 | require 'support/integration_test' 13 | 14 | MiniTest::Parallel.processor_count = Dir[$base_dir.join('integration', '*.rb')].size 15 | MiniTest::Unit.runner = EC2Runner.new 16 | -------------------------------------------------------------------------------- /test/support/kitchen_helper.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | 3 | require 'chef/knife/solo_init' 4 | 5 | module KitchenHelper 6 | 7 | def in_kitchen 8 | outside_kitchen do 9 | knife_command(Chef::Knife::SoloInit, ".", "--no-berkshelf", "--no-librarian").run 10 | yield 11 | end 12 | end 13 | 14 | def outside_kitchen 15 | Dir.mktmpdir do |dir| 16 | Dir.chdir(dir) do 17 | yield 18 | end 19 | end 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /test/integration/cases/knife_bootstrap.rb: -------------------------------------------------------------------------------- 1 | # Tries to bootstrap with apache2 cookbook and 2 | # verifies the "It Works!" page is present. 3 | 4 | require $base_dir.join('integration', 'cases', 'apache2_cook') 5 | 6 | module KnifeBootstrap 7 | include Apache2Cook 8 | 9 | def test_apache2 10 | write_cheffile 11 | assert_knife_command "bootstrap --solo --run-list=recipe[apache2]" 12 | assert_match default_apache_message, http_response 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/integration/cases/cache_path_usage.rb: -------------------------------------------------------------------------------- 1 | module CachePathUsage 2 | def setup 3 | super 4 | FileUtils.cp_r $base_dir.join('support', 'cache_using_cookbook'), 'site-cookbooks/cache_using_cookbook' 5 | end 6 | 7 | def test_changing_a_cached_directory_between_cooks 8 | write_nodefile(run_count: 1, run_list: ["cache_using_cookbook"]) 9 | assert_subcommand "cook" 10 | write_nodefile(run_count: 2, run_list: ["cache_using_cookbook"]) 11 | assert_subcommand "cook" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/support/validation_helper.rb: -------------------------------------------------------------------------------- 1 | require 'support/kitchen_helper' 2 | 3 | module ValidationHelper 4 | 5 | module SshCommandTests 6 | include KitchenHelper 7 | 8 | def test_barks_without_atleast_a_hostname 9 | cmd = command 10 | cmd.ui.expects(:fatal).with(regexp_matches(/hostname.*argument/)) 11 | $stdout.stubs(:puts) 12 | in_kitchen do 13 | assert_exits cmd 14 | end 15 | end 16 | end 17 | 18 | module ValidationTests 19 | include SshCommandTests 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/integration/debian7_knife_bootstrap_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | 3 | class Debian7KnifeBootstrapTest < IntegrationTest 4 | def user 5 | "admin" 6 | end 7 | 8 | def image_id 9 | # PVM instance store 10 | # From https://wiki.debian.org/Cloud/AmazonEC2Image/Wheezy 11 | "ami-74efab1c" 12 | end 13 | 14 | def prepare_server 15 | # Do nothing as `knife bootstrap --solo` will do everything 16 | end 17 | 18 | def default_apache_message 19 | /It works!/ 20 | end 21 | 22 | include KnifeBootstrap 23 | end 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 2.5.0 6 | - 2.4.3 7 | - 2.3.6 8 | - 2.2.9 9 | gemfile: 10 | - test/gemfiles/Gemfile.chef-12 11 | - test/gemfiles/Gemfile.chef-13 12 | - test/gemfiles/Gemfile.chef-master 13 | matrix: 14 | allow_failures: 15 | - gemfile: test/gemfiles/Gemfile.chef-master 16 | exclude: 17 | # Chef ~> 13 requires Ruby version >= 2.3.0 18 | - rvm: 2.2.9 19 | gemfile: test/gemfiles/Gemfile.chef-13 20 | - rvm: 2.2.9 21 | gemfile: test/gemfiles/Gemfile.chef-master 22 | -------------------------------------------------------------------------------- /test/support/loggable.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | # A module that provides logger and log_file attached to an integration log file 4 | module Loggable 5 | # The file that logs will go do, using the class name as a differentiator 6 | def log_file 7 | return @log_file if @log_file 8 | @log_file = $base_dir.join('..', 'log', "#{self.class}-integration.log") 9 | @log_file.dirname.mkpath 10 | @log_file 11 | end 12 | 13 | # The logger object so you can say logger.info to log messages 14 | def logger 15 | @logger ||= Logger.new(log_file) 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /lib/knife-solo/deprecated_command.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo 2 | module DeprecatedCommand 3 | 4 | def self.included(other) 5 | other.class_eval do 6 | def self.deprecated 7 | "`knife #{common_name}` is deprecated! Please use:\n #{superclass.banner}" 8 | end 9 | 10 | banner deprecated 11 | self.options = superclass.options 12 | 13 | def self.load_deps 14 | superclass.load_deps 15 | end 16 | end 17 | end 18 | 19 | def run 20 | ui.warn self.class.deprecated 21 | super 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/knife-solo/gitignore.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo 2 | class Gitignore 3 | include Enumerable 4 | 5 | attr_accessor :ignore_file 6 | 7 | def initialize(dir) 8 | @ignore_file = File.join(dir, '.gitignore') 9 | end 10 | 11 | def each 12 | if File.exist? ignore_file 13 | File.new(ignore_file).each do |line| 14 | yield line.chomp 15 | end 16 | end 17 | end 18 | 19 | def add(*new_entries) 20 | new_entries = (entries + new_entries.flatten).uniq 21 | File.open(ignore_file, 'w') do |f| 22 | f.puts new_entries 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/integration/cases/apache2_bootstrap.rb: -------------------------------------------------------------------------------- 1 | # Tries to bootstrap with apache2 cookbook and 2 | # verifies the "It Works!" page is present. 3 | 4 | require $base_dir.join('integration', 'cases', 'apache2_cook') 5 | 6 | module Apache2Bootstrap 7 | include Apache2Cook 8 | 9 | def write_berksfile 10 | File.open('Berksfile', 'w') do |f| 11 | f.puts 'source "https://supermarket.chef.io"' 12 | f.puts "cookbook 'apache2'" 13 | end 14 | end 15 | 16 | def test_apache2 17 | write_berksfile 18 | assert_subcommand "bootstrap --run-list=recipe[apache2]" 19 | assert_match default_apache_message, http_response 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/support/data_bag_key: -------------------------------------------------------------------------------- 1 | dZkd2eosIBc+4dWfDjFgwyc+sj0XlCBRR5/nVbA23sA4RAlg0pd+RIh4GiPpwp3IGPslJhapNX36bkPbjXjsyGzxKt2kZth9Z5Ucw9tC8jEnWtfxs66xABmZQvE/WGEdBZ3UDEbBaEBSjznVpHlXI//oLd1bU83nyV21i4OYW6eDc7h47mdbu59tvY8q4Doks8nwmoTMbTqzWkD2CRxx2Z5x2kqBb+HAXRI6KZK7vqvhAGYcm1d/vLkCGp/2ntDvRHEGmjbtJHDVD+0qJIf7w5W9jumhVkFTOPBym7P5i3OkAbO3RSsKHb2iRUlAw4Sld21pZBtBc5sDXJ/AUVzV+c/rsmvZDf01TWg9RoIN82928qnlCfJohmJGHTVt0Q5rf/DYOtk7DBCSsOkOChYdX8pRJG2+A5ACvALjXmd/5qUYUGuHe9m7PzNHV+37GOnlHBYqkZ5EU0LGjLMqq0Tgs/KzWpeotRK/VPNwmCrYCMwYwJtRKi1oMexUwDLrBcTilivp+62e9HFhSg0BRV0JlJcPgYRWqJ2RyBkrmOXv7lD10tPVnMzbwi+5yW131e9JbvgdBreJ2Ph314XyxjQvFNkQ/vny6iiisJEquwVBJTnqW+HdjIH07MeqkdaVytdMLlTSdgqGr/rIf1ZNS37jeq0AT8Y8YeihQNsnx559hu4= -------------------------------------------------------------------------------- /lib/knife-solo/info.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo 2 | def self.version 3 | '0.8.0.pre1' 4 | end 5 | 6 | def self.post_install_message 7 | <<-TXT.gsub(/^ {6}/, '').strip 8 | Thanks for installing knife-solo! 9 | 10 | If you run into any issues please let us know at: 11 | https://github.com/matschaffer/knife-solo/issues 12 | 13 | If you are upgrading knife-solo please uninstall any old versions by 14 | running `gem clean knife-solo` to avoid any errors. 15 | 16 | See http://bit.ly/CHEF-3255 for more information on the knife bug 17 | that causes this. 18 | TXT 19 | end 20 | 21 | def self.prerelease? 22 | Gem::Version.new(self.version).prerelease? 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/integration/cases/apache2_cook.rb: -------------------------------------------------------------------------------- 1 | # Tries to cook with apache2 cookbook and 2 | # verifies the "It Works!" page is present. 3 | module Apache2Cook 4 | def write_cheffile 5 | File.open('Cheffile', 'w') do |f| 6 | f.puts "site 'http://community.opscode.com/api/v1'" 7 | f.puts "cookbook 'apache2'" 8 | end 9 | end 10 | 11 | def http_response 12 | Net::HTTP.get(URI.parse("http://#{server.public_ip_address}")) 13 | end 14 | 15 | def default_apache_message 16 | /Apache Server/ 17 | end 18 | 19 | def test_apache2 20 | write_cheffile 21 | write_nodefile(run_list: ["recipe[apache2]"]) 22 | assert_subcommand "cook" 23 | assert_match default_apache_message, http_response 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/solo_clean_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/validation_helper' 3 | 4 | require 'chef/knife/solo_clean' 5 | 6 | class SoloCleanTest < TestCase 7 | include ValidationHelper::ValidationTests 8 | 9 | def test_removes_default_directory 10 | cmd = command('somehost') 11 | cmd.expects(:run_command).with('sudo rm -rf ~/chef-solo').returns(SuccessfulResult.new) 12 | cmd.run 13 | end 14 | 15 | def test_removes_provision_path 16 | cmd = command('somehost', '--provisioning-path=/foo/bar') 17 | cmd.expects(:run_command).with('sudo rm -rf /foo/bar').returns(SuccessfulResult.new) 18 | cmd.run 19 | end 20 | 21 | def command(*args) 22 | knife_command(Chef::Knife::SoloClean, *args) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/support/test_case.rb: -------------------------------------------------------------------------------- 1 | class TestCase < MiniTest::Unit::TestCase 2 | def default_test 3 | super unless self.class == TestCase 4 | end 5 | 6 | def write_file(file, contents) 7 | FileUtils.mkpath(File.dirname(file)) 8 | File.open(file, 'w') { |f| f.print contents } 9 | end 10 | 11 | def knife_command(cmd_class, *args) 12 | cmd_class.load_deps 13 | command = cmd_class.new(args) 14 | command.ui.stubs(:msg) 15 | command.ui.stubs(:warn) 16 | Chef::Config[:verbosity] = 0 17 | command.config[:config_file] = "#{File.dirname(__FILE__)}/knife.rb" 18 | command.configure_chef 19 | command 20 | end 21 | 22 | # Assert that the specified command or block raises SystemExit 23 | def assert_exits(command = nil) 24 | assert_raises SystemExit do 25 | if command 26 | command.run 27 | else 28 | yield 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/knife-solo/librarian.rb: -------------------------------------------------------------------------------- 1 | require 'knife-solo/cookbook_manager' 2 | 3 | module KnifeSolo 4 | class Librarian 5 | include CookbookManager 6 | 7 | def self.gem_libraries 8 | %w[librarian/action librarian/chef] 9 | end 10 | 11 | def self.conf_file_name 12 | 'Cheffile' 13 | end 14 | 15 | def self.gem_name 16 | 'librarian-chef' 17 | end 18 | 19 | def install! 20 | ui.msg "Installing Librarian cookbooks..." 21 | ::Librarian::Action::Resolve.new(env).run 22 | ::Librarian::Action::Install.new(env).run 23 | env.install_path 24 | end 25 | 26 | def env 27 | @env ||= ::Librarian::Chef::Environment.new 28 | end 29 | 30 | def initial_config 31 | "site 'https://supermarket.chef.io/api/v1'" 32 | end 33 | 34 | # Returns an array of strings to gitignore when bootstrapping 35 | def gitignores 36 | %w[/tmp/librarian/] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/integration/cases/ohai_hints.rb: -------------------------------------------------------------------------------- 1 | # Tries to bootstrap with --hint option and 2 | # verifies ohai hints get written properly. 3 | 4 | module OhaiHints 5 | def prepare_hints(hints) 6 | hints.map { |name, data| 7 | if data.nil? 8 | "--hint #{name}" 9 | else 10 | File.open("#{name}.json", "wb") { |f| f.write(data) } 11 | "--hint #{name}=#{name}.json" 12 | end 13 | }.join(' ') 14 | end 15 | 16 | def check_hints(hints) 17 | hints.each do |name, data| 18 | actual = `ssh #{connection_string} cat /etc/chef/ohai/hints/#{name}.json` 19 | assert_match actual.strip, data.nil? ? '{}' : data 20 | end 21 | end 22 | 23 | def test_ohai_hints 24 | hints = { 25 | 'test_hint_1' => '{"foo":"bar"}', 26 | 'test_hint_2' => nil 27 | } 28 | 29 | hint_opts = prepare_hints(hints) 30 | assert_subcommand "bootstrap #{hint_opts}" 31 | check_hints(hints) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/knife-solo/resources/solo.rb.erb: -------------------------------------------------------------------------------- 1 | node_name <%= node_name.inspect %> 2 | 3 | base = File.expand_path('..', __FILE__) 4 | 5 | nodes_path File.join(base, 'nodes') 6 | role_path File.join(base, 'roles') 7 | data_bag_path File.join(base, 'data_bags') 8 | encrypted_data_bag_secret File.join(base, 'data_bag_key') 9 | environment_path File.join(base, 'environments') 10 | environment <%= node_environment.inspect %> 11 | ssl_verify_mode <%= ssl_verify_mode.inspect %> 12 | solo_legacy_mode <%= solo_legacy_mode.inspect %> 13 | log_level <%= log_level.inspect %> 14 | enable_reporting <%= enable_reporting.inspect %> 15 | 16 | cookbook_path [] 17 | <% cookbook_paths.each_with_index do |path, i| -%> 18 | cookbook_path << File.join(base, 'cookbooks-<%= i+1 %>') # <%= path %> 19 | <% end -%> 20 | 21 | <% proxy_settings.each_pair do |k, v| -%> 22 | <%= k %> "<%= v %>" 23 | <% end -%> 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Mat Schaffer 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /lib/chef/knife/solo_clean.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | 3 | require 'knife-solo/ssh_command' 4 | 5 | class Chef 6 | class Knife 7 | class SoloClean < Knife 8 | include KnifeSolo::SshCommand 9 | 10 | deps do 11 | require 'knife-solo/tools' 12 | KnifeSolo::SshCommand.load_deps 13 | end 14 | 15 | banner "knife solo clean [USER@]HOSTNAME" 16 | 17 | option :provisioning_path, 18 | :long => '--provisioning-path path', 19 | :description => 'Where to store kitchen data on the node' 20 | # TODO de-duplicate this option with solo cook 21 | 22 | def run 23 | validate! 24 | ui.msg "Cleaning up #{host}..." 25 | run_command "sudo rm -rf #{provisioning_path}" 26 | end 27 | 28 | def validate! 29 | validate_ssh_options! 30 | end 31 | 32 | def provisioning_path 33 | # TODO de-duplicate this method with solo cook 34 | KnifeSolo::Tools.config_value(config, :provisioning_path, '~/chef-solo') 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/chef/knife/bootstrap_solo.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife/bootstrap' 2 | 3 | class Chef 4 | class Knife 5 | class Bootstrap 6 | 7 | def self.load_deps 8 | super 9 | require 'chef/knife/solo_bootstrap' 10 | require 'knife-solo/tools' 11 | SoloBootstrap.load_deps 12 | end 13 | 14 | option :solo, 15 | :long => '--[no-]solo', 16 | :description => 'Bootstrap using knife-solo' 17 | 18 | # Rename and override run method 19 | alias_method :run_with_chef_client, :run 20 | 21 | def run 22 | if KnifeSolo::Tools.config_value(config, :solo) 23 | run_with_knife_solo 24 | else 25 | run_with_chef_client 26 | end 27 | end 28 | 29 | # Bootstraps Chef on the node using knife-solo 30 | def run_with_knife_solo 31 | validate_name_args! 32 | 33 | bootstrap = SoloBootstrap.new 34 | bootstrap.name_args = @name_args 35 | bootstrap.config.merge! config 36 | bootstrap.run 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/performance/ssh_performance_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/kitchen_helper' 3 | require 'chef/knife/solo_prepare' 4 | 5 | require 'knife-solo/bootstraps' 6 | require 'knife-solo/bootstraps/linux' 7 | 8 | require 'knife-solo/ssh_connection' 9 | 10 | require 'benchmark' 11 | 12 | class KnifeSolo::Bootstraps::Linux 13 | def debianoid_omnibus_install 14 | run_command("echo apt-get update") 15 | run_command("echo apt-get install") 16 | run_command("echo curl omnibus") 17 | run_command("echo run omnibus") 18 | end 19 | end 20 | 21 | class SshPerformanceTest < TestCase 22 | include KitchenHelper 23 | 24 | def do_it 25 | # NOTE: Assumes user & host on @matschaffer's machine. Modify or paramaterize if needed. 26 | 10.times { knife_command(Chef::Knife::SoloPrepare, "ubuntu@172.16.20.133").run } 27 | end 28 | 29 | def test_ssh_performance_of_prepare 30 | in_kitchen do 31 | Benchmark.bmbm do |b| 32 | b.report("cached attributes: ") do 33 | do_it 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/integration/cases/environment.rb: -------------------------------------------------------------------------------- 1 | module Environment 2 | def setup 3 | super 4 | FileUtils.cp_r $base_dir.join('support', 'environment_cookbook'), 'site-cookbooks/environment_cookbook' 5 | FileUtils.cp $base_dir.join('support', 'test_environment.json'), 'environments/test_environment.json' 6 | end 7 | 8 | def cook_environment(node) 9 | write_nodefile(node) 10 | assert_subcommand "cook" 11 | `ssh #{connection_string} cat /etc/chef_environment` 12 | end 13 | 14 | # Test that chef picks up environments properly 15 | # NOTE: This shells out to ssh, so may not be windows-compatible 16 | def test_chef_environment 17 | # If no environment is specified chef needs to use "_default" and attribute from cookbook 18 | actual = cook_environment(run_list: ["recipe[environment_cookbook]"]) 19 | assert_equal "_default/untouched", actual 20 | 21 | # If one is specified chef needs to pick it up and get override attibute 22 | actual = cook_environment(run_list: ["recipe[environment_cookbook]"], environment: 'test_environment') 23 | assert_equal "test_environment/test_env_was_here", actual 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/solo_bootstrap_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/kitchen_helper' 3 | require 'support/validation_helper' 4 | 5 | require 'chef/knife/solo_bootstrap' 6 | require 'chef/knife/solo_cook' 7 | require 'chef/knife/solo_prepare' 8 | 9 | class SoloBootstrapTest < TestCase 10 | include KitchenHelper 11 | include ValidationHelper::ValidationTests 12 | 13 | def test_includes_all_prepare_options 14 | bootstrap_options = Chef::Knife::SoloBootstrap.options 15 | Chef::Knife::SoloPrepare.new.options.keys.each do |opt_key| 16 | assert bootstrap_options.include?(opt_key), "Should support option :#{opt_key}" 17 | end 18 | end 19 | 20 | def test_includes_clean_up_cook_option 21 | assert Chef::Knife::SoloBootstrap.options.include?(:clean_up), "Should support option :clean_up" 22 | end 23 | 24 | def test_runs_prepare_and_cook 25 | Chef::Knife::SoloPrepare.any_instance.expects(:run) 26 | Chef::Knife::SoloCook.any_instance.expects(:run) 27 | 28 | in_kitchen do 29 | command("somehost").run 30 | end 31 | end 32 | 33 | def command(*args) 34 | knife_command(Chef::Knife::SoloBootstrap, *args) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /script/newb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | help() { 6 | cat < "passwords", "admin" => @password} 18 | encrypted_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(data, secret) 19 | write_json_file('data_bags/dev/passwords.json', encrypted_data) 20 | end 21 | 22 | # Test that we can read an encrypted data bag value 23 | # NOTE: This shells out to ssh, so may not be windows-compatible 24 | def test_reading_encrypted_data_bag 25 | write_nodefile(run_list: ["recipe[secret_cookbook]"]) 26 | assert_subcommand "cook" 27 | actual = `ssh #{connection_string} cat /etc/admin_password` 28 | assert_equal @password, actual 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/knife-solo/tools.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo 2 | module Tools 3 | def system!(*command) 4 | raise "Failed to launch command #{command}" unless system(*command) 5 | end 6 | 7 | def windows_client? 8 | RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ 9 | end 10 | 11 | def self.cygwin_client? 12 | RbConfig::CONFIG['host_os'] =~ /cygwin/ 13 | end 14 | 15 | def config_value(key, default = nil) 16 | Tools.config_value(config, key, default) 17 | end 18 | 19 | # Chef 10 compatible way of getting correct precedence for command line 20 | # and configuration file options. Adds correct handling of `false` values 21 | # to the original example in 22 | # http://docs.opscode.com/breaking_changes_chef_11.html#knife-configuration-parameter-changes 23 | def self.config_value(config, key, default = nil) 24 | key = key.to_sym 25 | if !config[key].nil? 26 | config[key] 27 | elsif !Chef::Config[:knife][key].nil? 28 | # when Chef 10 support is dropped, this branch can be removed 29 | # as Chef 11 automatically merges the values to the `config` hash 30 | Chef::Config[:knife][key] 31 | else 32 | default 33 | end 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/chef/knife/solo_bootstrap.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | require 'chef/knife/solo_cook' 3 | require 'chef/knife/solo_prepare' 4 | 5 | require 'knife-solo/ssh_command' 6 | 7 | class Chef 8 | class Knife 9 | class SoloBootstrap < Knife 10 | include KnifeSolo::SshCommand 11 | 12 | deps do 13 | KnifeSolo::SshCommand.load_deps 14 | SoloPrepare.load_deps 15 | SoloCook.load_deps 16 | end 17 | 18 | banner "knife solo bootstrap [USER@]HOSTNAME [JSON] (options)" 19 | 20 | # Use (some) options from prepare and cook commands 21 | self.options = SoloPrepare.options 22 | [:berkshelf, :librarian, :sync_only, :why_run, :clean_up].each { |opt| option opt, SoloCook.options[opt] } 23 | 24 | def run 25 | validate! 26 | 27 | prepare = command_with_same_args(SoloPrepare) 28 | prepare.run 29 | 30 | cook = command_with_same_args(SoloCook) 31 | cook.config[:chef_check] = false 32 | cook.run 33 | end 34 | 35 | def validate! 36 | validate_ssh_options! 37 | end 38 | 39 | def command_with_same_args(klass) 40 | cmd = klass.new 41 | cmd.ui = ui 42 | cmd.name_args = @name_args 43 | cmd.config.merge! config 44 | cmd 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/minitest/parallel.rb: -------------------------------------------------------------------------------- 1 | # https://github.com/ngauthier/minitest-parallel 2 | if defined?(MiniTest) 3 | raise "Do not require minitest before minitest/parallel\n" 4 | end 5 | require 'parallel' 6 | require 'minitest/unit' 7 | 8 | module MiniTest::Parallel 9 | def self.included(base) 10 | base.class_eval do 11 | alias_method :_run_suites_in_series, :_run_suites 12 | alias_method :_run_suites, :_run_suites_in_parallel 13 | end 14 | end 15 | 16 | def self.processor_count=(procs) 17 | @processor_count = procs 18 | end 19 | 20 | def self.processor_count 21 | @processor_count ||= Parallel.processor_count 22 | end 23 | 24 | def _run_suites_in_parallel(suites, type) 25 | result = Parallel.map(suites, :in_processes => MiniTest::Parallel.processor_count) do |suite| 26 | ret = _run_suite(suite, type) 27 | { 28 | :failures => failures, 29 | :errors => errors, 30 | :report => report, 31 | :run_suite_return => ret 32 | } 33 | end 34 | self.failures = result.inject(0) {|sum, x| sum + x[:failures] } 35 | self.errors = result.inject(0) {|sum, x| sum + x[:errors] } 36 | self.report = result.inject([]) {|sum, x| sum + x[:report] } 37 | result.map {|x| x[:run_suite_return] } 38 | end 39 | end 40 | 41 | MiniTest::Unit.send(:include, MiniTest::Parallel) 42 | -------------------------------------------------------------------------------- /test/support/cache_using_cookbook/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: cache_using_cookbook 3 | # Recipe:: default 4 | # 5 | # Copyright 2013, Mat Schaffer 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining 8 | # a copy of this software and associated documentation files (the 9 | # "Software"), to deal in the Software without restriction, including 10 | # without limitation the rights to use, copy, modify, merge, publish, 11 | # distribute, sublicense, and/or sell copies of the Software, and to 12 | # permit persons to whom the Software is furnished to do so, subject to 13 | # the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | # 26 | 27 | container = Chef::Config[:file_cache_path] + "/test_dir" 28 | 29 | directory container 30 | 31 | file container + "/test_file" do 32 | content "This was generated from run #{node['run_count']}" 33 | end 34 | -------------------------------------------------------------------------------- /test/deprecated_command_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require 'chef/knife' 4 | require 'knife-solo/deprecated_command' 5 | 6 | class DummyNewCommand < Chef::Knife 7 | banner "knife dummy_new_command" 8 | 9 | option :foo, 10 | :long => '--foo', 11 | :description => 'Foo option' 12 | 13 | def run 14 | # calls #new_run so we can be sure this gets called 15 | new_run 16 | end 17 | 18 | def new_run 19 | # dummy 20 | end 21 | end 22 | 23 | class DummyDeprecatedCommand < DummyNewCommand 24 | include KnifeSolo::DeprecatedCommand 25 | end 26 | 27 | class DeprecatedCommandTest < TestCase 28 | def test_help_warns_about_deprecation 29 | $stdout.expects(:puts).with(regexp_matches(/deprecated!/)) 30 | assert_exits { command("--help") } 31 | end 32 | 33 | def test_warns_about_deprecation 34 | cmd = command 35 | cmd.ui.expects(:warn).with(regexp_matches(/deprecated!/)) 36 | cmd.run 37 | end 38 | 39 | def test_runs_new_command 40 | cmd = command 41 | cmd.ui.stubs(:warn) 42 | cmd.expects(:new_run) 43 | cmd.run 44 | end 45 | 46 | def test_includes_options_from_new_command 47 | assert DummyDeprecatedCommand.options.include?(:foo) 48 | end 49 | 50 | def test_loads_dependencies_from_new_command 51 | DummyNewCommand.expects(:load_deps) 52 | DummyDeprecatedCommand.load_deps 53 | end 54 | 55 | def command(*args) 56 | DummyDeprecatedCommand.load_deps 57 | DummyDeprecatedCommand.new(args) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/knife-solo/bootstraps/freebsd.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo::Bootstraps 2 | class FreeBSD < Base 3 | def issue 4 | run_command("uname -sr").stdout.strip 5 | end 6 | 7 | def prepare_make_conf 8 | ui.msg "Preparing make.conf" 9 | run_command <<-EOF 10 | echo 'RUBY_DEFAULT_VER=1.9' >> /etc/make.conf 11 | EOF 12 | end 13 | 14 | def freebsd_port_install 15 | ui.msg "Updating ports tree..." 16 | 17 | if Dir["/usr/ports/*"].empty? 18 | run_command("portsnap fetch extract") 19 | else 20 | run_command("portsnap update") 21 | end 22 | 23 | prepare_make_conf 24 | 25 | ui.msg "Installing required ports..." 26 | packages = %w(net/rsync ftp/curl lang/ruby19 devel/ruby-gems 27 | converters/ruby-iconv devel/rubygem-rake 28 | shells/bash) 29 | 30 | packages.each do |p| 31 | ui.msg "Installing #{p}..." 32 | result = run_command <<-SH 33 | cd /usr/ports/#{p} && make -DBATCH -DFORCE_PKG_REGISTER install clean 34 | SH 35 | raise "Couldn't install #{p} from ports." unless result.success? 36 | end 37 | 38 | ui.msg "...done installing ports." 39 | 40 | gem_install # chef 41 | end 42 | 43 | def distro 44 | return @distro if @distro 45 | case issue 46 | when %r{FreeBSD 9\.[01]} 47 | {:type => 'freebsd_port'} 48 | else 49 | raise "#{issue} not supported" 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /knife-solo.gemspec: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'lib', 'knife-solo', 'info') 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'knife-solo' 5 | s.version = KnifeSolo.version 6 | s.summary = 'A collection of knife plugins for dealing with chef solo' 7 | s.description = 'Handles bootstrapping, running chef solo, rsyncing cookbooks etc' 8 | 9 | s.author = 'Mat Schaffer' 10 | s.email = 'mat@schaffer.me' 11 | s.homepage = 'http://matschaffer.github.io/knife-solo/' 12 | 13 | manifest = File.readlines("Manifest.txt").map(&:chomp) 14 | s.files = manifest 15 | s.executables = manifest.grep(%r{^bin/}).map{ |f| File.basename(f) } 16 | s.test_files = manifest.grep(%r{^(test|spec|features)/}) 17 | s.require_paths = ["lib"] 18 | 19 | s.post_install_message = KnifeSolo.post_install_message 20 | 21 | s.add_development_dependency 'berkshelf', '>= 3.0.0.beta.2' 22 | s.add_development_dependency 'bundler' 23 | s.add_development_dependency 'ffi', '< 1.9.1' # transitional dependency of berkshelf 24 | s.add_development_dependency 'fog' 25 | s.add_development_dependency 'librarian-chef' 26 | s.add_development_dependency 'minitest', '~> 4.7' 27 | s.add_development_dependency 'mocha' 28 | s.add_development_dependency 'parallel' 29 | s.add_development_dependency 'rake' 30 | s.add_development_dependency 'rdoc' 31 | s.add_development_dependency 'coveralls' 32 | 33 | s.add_dependency 'chef', '>= 10.20' 34 | s.add_dependency 'net-ssh', '>= 2.7' 35 | s.add_dependency 'erubis', '~> 2.7.0' 36 | end 37 | -------------------------------------------------------------------------------- /lib/knife-solo/berkshelf.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | require 'fileutils' 3 | require 'knife-solo/cookbook_manager' 4 | require 'knife-solo/tools' 5 | 6 | module KnifeSolo 7 | class Berkshelf 8 | include CookbookManager 9 | 10 | def self.gem_libraries 11 | %w[berkshelf] 12 | end 13 | 14 | def self.conf_file_name 15 | 'Berksfile' 16 | end 17 | 18 | def install! 19 | path = berkshelf_path 20 | ui.msg "Installing Berkshelf cookbooks to '#{path}'..." 21 | 22 | if Gem::Version.new(::Berkshelf::VERSION) >= Gem::Version.new("3.0.0") 23 | berkshelf_options = KnifeSolo::Tools.config_value(config, :berkshelf_options) || {} 24 | berksfile = ::Berkshelf::Berksfile.from_file('Berksfile',berkshelf_options) 25 | else 26 | berksfile = ::Berkshelf::Berksfile.from_file('Berksfile') 27 | end 28 | if berksfile.respond_to?(:vendor) 29 | FileUtils.rm_rf(path) 30 | berksfile.vendor(path) 31 | else 32 | berksfile.install(:path => path) 33 | end 34 | 35 | path 36 | end 37 | 38 | def berkshelf_path 39 | KnifeSolo::Tools.config_value(config, :berkshelf_path) || default_path 40 | end 41 | 42 | def default_path 43 | File.join(::Berkshelf.berkshelf_path, 'knife-solo', 44 | Digest::SHA1.hexdigest(File.expand_path('.'))) 45 | end 46 | 47 | def initial_config 48 | if defined?(::Berkshelf) && Gem::Version.new(::Berkshelf::VERSION) >= Gem::Version.new("3.0.0") 49 | 'source "https://api.berkshelf.com"' 50 | else 51 | 'site :opscode' 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/gitignore_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/kitchen_helper' 3 | 4 | require 'knife-solo/gitignore' 5 | 6 | class GitignoreTest < TestCase 7 | include KitchenHelper 8 | 9 | def test_creates_with_one_entry 10 | outside_kitchen do 11 | KnifeSolo::Gitignore.new('.').add("foo") 12 | assert_equal "foo\n", IO.read('.gitignore') 13 | end 14 | end 15 | 16 | def test_creates_with_multiple_entries 17 | outside_kitchen do 18 | KnifeSolo::Gitignore.new('.').add("foo", "/bar") 19 | assert_equal "foo\n/bar\n", IO.read('.gitignore') 20 | end 21 | end 22 | 23 | def test_creates_with_array 24 | outside_kitchen do 25 | KnifeSolo::Gitignore.new('.').add(%w[foo/ bar]) 26 | assert_equal "foo/\nbar\n", IO.read('.gitignore') 27 | end 28 | end 29 | 30 | def test_appends_new_entries 31 | outside_kitchen do 32 | File.open(".gitignore", "w") do |f| 33 | f.puts "foo" 34 | end 35 | KnifeSolo::Gitignore.new('.').add(["bar.*"]) 36 | assert_equal "foo\nbar.*\n", IO.read('.gitignore') 37 | end 38 | end 39 | 40 | def test_appends_only_new_entries 41 | outside_kitchen do 42 | File.open(".gitignore", "w") do |f| 43 | f.puts "*.foo" 44 | end 45 | KnifeSolo::Gitignore.new('.').add("!foo", "*.foo") 46 | assert_equal "*.foo\n!foo\n", IO.read('.gitignore') 47 | end 48 | end 49 | 50 | def test_appends_only_if_any_new_entries 51 | outside_kitchen do 52 | File.open(".gitignore", "w") do |f| 53 | f.puts "!foo" 54 | f.puts "/bar/*.baz" 55 | end 56 | KnifeSolo::Gitignore.new('.').add(["!foo", "/bar/*.baz"]) 57 | assert_equal "!foo\n/bar/*.baz\n", IO.read('.gitignore') 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/solo_prepare_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/kitchen_helper' 3 | require 'support/validation_helper' 4 | 5 | require 'chef/knife/solo_prepare' 6 | 7 | class SoloPrepareTest < TestCase 8 | include KitchenHelper 9 | include ValidationHelper::ValidationTests 10 | 11 | def setup 12 | @host = 'someuser@somehost.domain.com' 13 | end 14 | 15 | def test_uses_local_chef_version_by_default 16 | Chef::Config[:knife][:bootstrap_version] = nil 17 | assert_equal Chef::VERSION, command.chef_version 18 | end 19 | 20 | def test_uses_chef_version_from_knife_config 21 | Chef::Config[:knife][:bootstrap_version] = "10.12.2" 22 | assert_equal "10.12.2", command.chef_version 23 | end 24 | 25 | def test_uses_chef_version_from_command_line_option 26 | Chef::Config[:knife][:bootstrap_version] = "10.16.2" 27 | assert_equal "0.4.2", command("--bootstrap-version", "0.4.2").chef_version 28 | end 29 | 30 | def test_chef_version_returns_nil_if_empty 31 | Chef::Config[:knife][:bootstrap_version] = "10.12.2" 32 | assert_nil command("--bootstrap-version", "").chef_version 33 | end 34 | 35 | def test_run_raises_if_operating_system_is_not_supported 36 | in_kitchen do 37 | run_command = command(@host) 38 | run_command.stubs(:operating_system).returns('MythicalOS') 39 | assert_raises KnifeSolo::Bootstraps::OperatingSystemNotImplementedError do 40 | run_command.run 41 | end 42 | end 43 | end 44 | 45 | def test_run_calls_bootstrap 46 | in_kitchen do 47 | bootstrap_instance = mock('mock OS bootstrap instance') 48 | bootstrap_instance.expects(:bootstrap!) 49 | 50 | run_command = command(@host) 51 | run_command.stubs(:bootstrap).returns(bootstrap_instance) 52 | run_command.run 53 | end 54 | end 55 | 56 | def command(*args) 57 | knife_command(Chef::Knife::SoloPrepare, *args) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/knife_bootstrap_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/kitchen_helper' 3 | 4 | require 'chef/knife/bootstrap_solo' 5 | require 'chef/knife/solo_bootstrap' 6 | 7 | class KnifeBootstrapTest < TestCase 8 | include KitchenHelper 9 | 10 | def test_includes_solo_options 11 | assert Chef::Knife::Bootstrap.options.include?(:solo) 12 | end 13 | 14 | def test_runs_solo_bootstrap_if_specified_as_option 15 | Chef::Config.knife[:solo] = false 16 | Chef::Knife::SoloBootstrap.any_instance.expects(:run) 17 | Chef::Knife::Bootstrap.any_instance.expects(:run_with_chef_client).never 18 | in_kitchen do 19 | command("somehost", "--solo").run 20 | end 21 | end 22 | 23 | def test_runs_solo_bootstrap_if_specified_as_chef_configuration 24 | Chef::Config.knife[:solo] = true 25 | Chef::Knife::SoloBootstrap.any_instance.expects(:run) 26 | Chef::Knife::Bootstrap.any_instance.expects(:run_with_chef_client).never 27 | in_kitchen do 28 | command("somehost").run 29 | end 30 | end 31 | 32 | def test_runs_original_bootstrap_by_default 33 | Chef::Config.knife[:solo] = false 34 | Chef::Knife::SoloBootstrap.any_instance.expects(:run).never 35 | Chef::Knife::Bootstrap.any_instance.expects(:run_with_chef_client) 36 | in_kitchen do 37 | command("somehost").run 38 | end 39 | end 40 | 41 | def test_runs_original_bootstrap_if_specified_as_option 42 | Chef::Config.knife[:solo] = true 43 | Chef::Knife::SoloBootstrap.any_instance.expects(:run).never 44 | Chef::Knife::Bootstrap.any_instance.expects(:run_with_chef_client) 45 | in_kitchen do 46 | command("somehost", "--no-solo").run 47 | end 48 | end 49 | 50 | def test_barks_without_atleast_a_hostname 51 | cmd = command("--solo") 52 | cmd.ui.expects(:error) 53 | in_kitchen do 54 | assert_exits cmd 55 | end 56 | end 57 | 58 | def command(*args) 59 | knife_command(Chef::Knife::Bootstrap, *args) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/knife-solo/cookbook_manager_selector.rb: -------------------------------------------------------------------------------- 1 | require 'knife-solo/berkshelf' 2 | require 'knife-solo/librarian' 3 | 4 | module KnifeSolo 5 | class CookbookManagerSelector 6 | attr_reader :config, :ui 7 | 8 | def initialize(config, ui) 9 | @config = config 10 | @ui = ui 11 | end 12 | 13 | def select(base) 14 | Chef::Log.debug "Selecting cookbook manager..." 15 | 16 | if (selected = select_or_disable_by_chef_config!) 17 | return selected 18 | elsif managers.empty? 19 | Chef::Log.debug "All disabled by configuration" 20 | return nil 21 | end 22 | 23 | selected = select_by_existing_conf_file(base) || select_by_installed_gem 24 | if selected.nil? 25 | Chef::Log.debug "Nothing selected" 26 | # TODO: ui.msg "Recommended to use a cookbook manager" 27 | end 28 | selected 29 | end 30 | 31 | private 32 | 33 | def managers 34 | @managers ||= [ 35 | KnifeSolo::Berkshelf.new(config, ui), 36 | KnifeSolo::Librarian.new(config, ui) 37 | ] 38 | end 39 | 40 | def select_or_disable_by_chef_config! 41 | @managers = managers.select do |manager| 42 | if (conf = manager.enabled_by_chef_config?) 43 | Chef::Log.debug "#{manager} selected by configuration" 44 | return manager 45 | elsif conf == false 46 | Chef::Log.debug "#{manager} disabled by configuration" 47 | false 48 | else # conf == nil 49 | true 50 | end 51 | end 52 | nil 53 | end 54 | 55 | def select_by_existing_conf_file(base) 56 | managers.each do |manager| 57 | if manager.conf_file_exists?(base) 58 | Chef::Log.debug "#{manager} selected because of existing #{manager.conf_file}" 59 | return manager 60 | end 61 | end 62 | nil 63 | end 64 | 65 | def select_by_installed_gem 66 | managers.each do |manager| 67 | if manager.gem_installed? 68 | Chef::Log.debug "#{manager} selected because of installed gem" 69 | return manager 70 | end 71 | end 72 | nil 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/knife-solo/ssh_connection.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh' 2 | 3 | module KnifeSolo 4 | class SshConnection 5 | class ExecResult 6 | attr_accessor :stdout, :stderr, :exit_code 7 | 8 | def initialize(exit_code = nil) 9 | @exit_code = exit_code 10 | @stdout = "" 11 | @stderr = "" 12 | end 13 | 14 | def success? 15 | exit_code == 0 16 | end 17 | 18 | # Helper to use when raising exceptions since some operations 19 | # (e.g., command not found) error on stdout 20 | def stderr_or_stdout 21 | return stderr unless stderr.empty? 22 | stdout 23 | end 24 | end 25 | 26 | def initialize(host, user, connection_options, sudo_password_hook) 27 | @host = host 28 | @user = user 29 | @connection_options = connection_options 30 | @password_hook = sudo_password_hook 31 | end 32 | 33 | attr_reader :host, :user, :connection_options 34 | 35 | def session(&block) 36 | @session ||= begin 37 | if connection_options[:gateway] 38 | co = connection_options 39 | gw_user,gw = co.delete(:gateway).split '@' 40 | Net::SSH::Gateway.new(gw, gw_user).ssh(host, user, co, &block) 41 | else 42 | Net::SSH.start(host, user, connection_options, &block) 43 | end 44 | end 45 | end 46 | 47 | def password 48 | @password ||= @password_hook.call 49 | end 50 | 51 | def run_command(command, output = nil) 52 | result = ExecResult.new 53 | 54 | session.open_channel do |channel| 55 | channel.request_pty 56 | channel.exec(command) do |_, success| 57 | raise "ssh.channel.exec failure" unless success 58 | 59 | channel.on_data do |ch, data| # stdout 60 | if data =~ /^knife sudo password: / 61 | ch.send_data("#{password}\n") 62 | else 63 | Chef::Log.debug("#{command} stdout: #{data}") 64 | output << data if output 65 | result.stdout << data 66 | end 67 | end 68 | 69 | channel.on_extended_data do |ch, type, data| 70 | next unless type == 1 71 | Chef::Log.debug("#{command} stderr: #{data}") 72 | output << data if output 73 | result.stderr << data 74 | end 75 | 76 | channel.on_request("exit-status") do |ch, data| 77 | result.exit_code = data.read_long 78 | end 79 | 80 | end 81 | end.wait 82 | 83 | result 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/chef/knife/solo_init.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | require 'fileutils' 3 | 4 | class Chef 5 | class Knife 6 | class SoloInit < Knife 7 | include FileUtils 8 | 9 | deps do 10 | require 'knife-solo' 11 | require 'knife-solo/cookbook_manager_selector' 12 | require 'knife-solo/gitignore' 13 | require 'knife-solo/tools' 14 | end 15 | 16 | banner "knife solo init DIRECTORY" 17 | 18 | option :git, 19 | :long => '--no-git', 20 | :description => 'Do not generate .gitignore' 21 | 22 | option :berkshelf, 23 | :long => '--[no-]berkshelf', 24 | :description => 'Generate files for Berkshelf support' 25 | 26 | option :librarian, 27 | :long => '--[no-]librarian', 28 | :description => 'Generate files for Librarian support' 29 | 30 | def run 31 | @base = @name_args.first 32 | validate! 33 | create_kitchen 34 | create_config 35 | create_cupboards %w[nodes roles data_bags environments site-cookbooks cookbooks] 36 | gitignore %w[/cookbooks/] 37 | if (cm = cookbook_manager) 38 | cm.bootstrap(@base) 39 | end 40 | end 41 | 42 | def validate! 43 | unless @base 44 | show_usage 45 | ui.fatal "You must specify a directory. Use '.' to initialize the current one." 46 | exit 1 47 | end 48 | end 49 | 50 | def config_value(key, default = nil) 51 | KnifeSolo::Tools.config_value(config, key, default) 52 | end 53 | 54 | def create_cupboards(dirs) 55 | ui.msg "Creating cupboards..." 56 | dirs.each do |dir| 57 | cupboard_dir = File.join(@base, dir) 58 | unless File.exist?(cupboard_dir) 59 | mkdir cupboard_dir 60 | touch File.join(cupboard_dir, '.gitkeep') 61 | end 62 | end 63 | end 64 | 65 | def create_kitchen 66 | ui.msg "Creating kitchen..." 67 | mkdir @base unless @base == '.' 68 | end 69 | 70 | def create_config 71 | ui.msg "Creating knife.rb in kitchen..." 72 | mkdir_p File.join(@base, '.chef') 73 | knife_rb = File.join(@base, '.chef', 'knife.rb') 74 | unless File.exist?(knife_rb) 75 | cp KnifeSolo.resource('knife.rb'), knife_rb 76 | end 77 | end 78 | 79 | def cookbook_manager 80 | KnifeSolo::CookbookManagerSelector.new(config, ui).select(@base) 81 | end 82 | 83 | def gitignore(*entries) 84 | if config_value(:git, true) 85 | KnifeSolo::Gitignore.new(@base).add(*entries) 86 | end 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/tools_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/kitchen_helper' 3 | 4 | require 'chef/config' 5 | require 'knife-solo/tools' 6 | 7 | class DummyToolsCommand < Chef::Knife 8 | include KnifeSolo::Tools 9 | 10 | option :foo, 11 | :long => '--foo FOO' 12 | 13 | option :boo, 14 | :long => '--[no-]boo' 15 | end 16 | 17 | class ToolsTest < TestCase 18 | include KitchenHelper 19 | 20 | def setup 21 | Chef::Config[:knife][:foo] = nil 22 | Chef::Config[:knife][:boo] = nil 23 | end 24 | 25 | def test_config_value_defaults_to_nil 26 | assert_nil command.config_value(:foo) 27 | assert_nil command.config_value(:boo) 28 | end 29 | 30 | def test_config_value_returns_the_default 31 | assert_equal 'bar', command.config_value(:foo, 'bar') 32 | 33 | assert_equal true, command.config_value(:boo, true) 34 | assert_equal false, command.config_value(:boo, false) 35 | end 36 | 37 | def test_config_value_uses_cli_option 38 | assert_equal 'bar', command('--foo=bar').config_value(:foo) 39 | assert_equal 'bar', command('--foo=bar').config_value(:foo, 'baz') 40 | 41 | assert_equal true, command('--boo').config_value(:boo) 42 | assert_equal true, command('--boo').config_value(:boo, false) 43 | assert_equal false, command('--no-boo').config_value(:boo) 44 | assert_equal false, command('--no-boo').config_value(:boo, true) 45 | end 46 | 47 | def test_config_value_uses_configuration 48 | Chef::Config[:knife][:foo] = 'bar' 49 | assert_equal 'bar', command.config_value(:foo) 50 | assert_equal 'bar', command.config_value(:foo, 'baz') 51 | 52 | Chef::Config[:knife][:boo] = true 53 | assert_equal true, command.config_value(:boo) 54 | assert_equal true, command.config_value(:boo, false) 55 | 56 | Chef::Config[:knife][:boo] = false 57 | assert_equal false, command.config_value(:boo) 58 | assert_equal false, command.config_value(:boo, true) 59 | end 60 | 61 | def test_config_value_prefers_cli_option 62 | Chef::Config[:knife][:foo] = 'foo' 63 | assert_equal 'bar', command('--foo=bar').config_value(:foo) 64 | assert_equal 'bar', command('--foo=bar').config_value(:foo, 'baz') 65 | 66 | Chef::Config[:knife][:boo] = true 67 | assert_equal true, command('--boo').config_value(:boo) 68 | assert_equal true, command('--boo').config_value(:boo, false) 69 | assert_equal false, command('--no-boo').config_value(:boo) 70 | assert_equal false, command('--no-boo').config_value(:boo, true) 71 | 72 | Chef::Config[:knife][:boo] = false 73 | assert_equal true, command('--boo').config_value(:boo) 74 | assert_equal true, command('--boo').config_value(:boo, false) 75 | assert_equal false, command('--no-boo').config_value(:boo) 76 | assert_equal false, command('--no-boo').config_value(:boo, true) 77 | end 78 | 79 | def command(*args) 80 | knife_command(DummyToolsCommand, *args) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/chef/knife/solo_prepare.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | require 'knife-solo/ssh_command' 3 | require 'knife-solo/node_config_command' 4 | 5 | class Chef 6 | class Knife 7 | # Approach ported from littlechef (https://github.com/tobami/littlechef) 8 | # Copyright 2010, 2011, Miquel Torres 9 | class SoloPrepare < Knife 10 | include KnifeSolo::SshCommand 11 | include KnifeSolo::NodeConfigCommand 12 | 13 | deps do 14 | require 'knife-solo/bootstraps' 15 | KnifeSolo::SshCommand.load_deps 16 | KnifeSolo::NodeConfigCommand.load_deps 17 | end 18 | 19 | banner "knife solo prepare [USER@]HOSTNAME [JSON] (options)" 20 | 21 | option :bootstrap_version, 22 | :long => '--bootstrap-version VERSION', 23 | :description => 'The version of Chef to install', 24 | :proc => lambda {|v| Chef::Config[:knife][:bootstrap_version] = v} 25 | 26 | option :prerelease, 27 | :long => '--prerelease', 28 | :description => 'Install the pre-release Chef version' 29 | 30 | option :omnibus_url, 31 | :long => '--omnibus-url URL', 32 | :description => 'URL to download install.sh from' 33 | 34 | option :omnibus_options, 35 | :long => '--omnibus-options "OPTIONS"', 36 | :description => 'Pass options to the install.sh script' 37 | 38 | option :omnibus_version, 39 | :long => '--omnibus-version VERSION', 40 | :description => 'Deprecated. Replaced with --bootstrap-version.' 41 | 42 | option :hint, 43 | :long => '--hint HINT_NAME[=HINT_FILE]', 44 | :description => 'Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.', 45 | :proc => Proc.new { |h| 46 | Chef::Config[:knife][:hints] ||= Hash.new 47 | name, path = h.split("=") 48 | Chef::Config[:knife][:hints][name] = path ? JSON.parse(::File.read(path)) : Hash.new } 49 | 50 | def run 51 | if config[:omnibus_version] 52 | ui.warn '`--omnibus-version` is deprecated, please use `--bootstrap-version`.' 53 | Chef::Config[:knife][:bootstrap_version] = config[:omnibus_version] 54 | end 55 | 56 | validate! 57 | bootstrap.bootstrap! 58 | generate_node_config 59 | end 60 | 61 | def validate! 62 | validate_ssh_options! 63 | end 64 | 65 | def bootstrap 66 | ui.msg "Bootstrapping Chef..." 67 | KnifeSolo::Bootstraps.class_for_operating_system(operating_system).new(self) 68 | end 69 | 70 | def operating_system 71 | run_command('uname -s').stdout.strip 72 | end 73 | 74 | def chef_version 75 | if (v = Chef::Config[:knife][:bootstrap_version]) 76 | v.empty? ? nil : v 77 | else 78 | Chef::VERSION 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | require File.join(File.dirname(__FILE__), 'lib', 'knife-solo', 'info') 4 | 5 | MANIFEST_IGNORES = %w[ 6 | .travis.yml 7 | .gitignore 8 | .gitmodules 9 | Gemfile 10 | Gemfile.lock 11 | Manifest.txt 12 | README.md 13 | knife-solo.gemspec 14 | script/newb 15 | script/test 16 | ] 17 | 18 | namespace :manifest do 19 | desc 'Checks for outstanding changes to the manifest' 20 | task :verify => :update do 21 | changes = `git status --porcelain Manifest.txt` 22 | raise "Manifest has not been updated" unless changes.empty? 23 | end 24 | 25 | desc 'Updates Manifest.txt with a list of files from git' 26 | task :update do 27 | git_files = `git ls-files`.split("\n") 28 | submodule_files = `git submodule foreach -q 'for f in $(git ls-files); do echo $path/$f; done'`.split("\n") 29 | 30 | File.open('Manifest.txt', 'w') do |f| 31 | f.puts((git_files + submodule_files - MANIFEST_IGNORES).join("\n")) 32 | end 33 | end 34 | end 35 | 36 | desc 'Alias to manifest:update' 37 | task :manifest => 'manifest:update' 38 | 39 | # Returns the parsed RDoc for a single file as HTML 40 | # Somewhat gnarly, but does the job. 41 | def parsed_rdoc file 42 | options = RDoc::Options.new 43 | options.template_dir = options.template_dir_for 'darkfish' 44 | 45 | rdoc = RDoc::RDoc.current = RDoc::RDoc.new 46 | rdoc.store = RDoc::Store.new 47 | rdoc.options = options 48 | rdoc.generator = RDoc::Generator::Darkfish.new(rdoc.store, options) 49 | parsed = rdoc.parse_files([file]) 50 | parsed.first.description 51 | end 52 | 53 | desc 'Renerates gh-pages from project' 54 | task 'gh-pages' do 55 | require 'tmpdir' 56 | gem 'rdoc'; require 'rdoc/rdoc' 57 | 58 | Dir.mktmpdir do |clone| 59 | sh "git clone -b gh-pages git@github.com:matschaffer/knife-solo.git #{clone}" 60 | File.open(clone + "/index.html", 'w') do |f| 61 | f.puts '---' 62 | f.puts 'layout: default' 63 | f.puts '---' 64 | f.puts parsed_rdoc("README.rdoc") 65 | end 66 | rev = `git rev-parse HEAD`[0..7] 67 | Dir.chdir(clone) do 68 | sh "git commit --allow-empty -m 'Update index for v#{KnifeSolo.version} from README.rdoc rev #{rev}' index.html" 69 | sh "git push origin gh-pages" 70 | end 71 | end 72 | end 73 | 74 | def test_task(name, glob) 75 | Rake::TestTask.new(name) do |t| 76 | t.libs << 'test' 77 | t.warning = false 78 | t.test_files = FileList[glob] 79 | end 80 | end 81 | 82 | namespace :test do 83 | test_task(:performance, 'test/performance/*_test.rb') 84 | test_task(:integration, 'test/integration/*_test.rb') 85 | test_task(:units, 'test/*_test.rb') 86 | 87 | desc 'Run both unit and integration tests' 88 | task :all => [:units, :integration] 89 | end 90 | 91 | desc 'Alias for test:units' 92 | task :test => 'test:units' 93 | 94 | task :default => :test 95 | task :default => 'manifest:verify' 96 | 97 | task :release => :manifest 98 | task :release => 'gh-pages' unless KnifeSolo.prerelease? 99 | -------------------------------------------------------------------------------- /lib/knife-solo/node_config_command.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo 2 | module NodeConfigCommand 3 | 4 | def self.load_deps 5 | require 'fileutils' 6 | require 'pathname' 7 | end 8 | 9 | def self.included(other) 10 | other.class_eval do 11 | # Lazy load our dependencies if the including class did not call 12 | # Knife#deps yet. See KnifeSolo::SshCommand for more information. 13 | deps { KnifeSolo::NodeConfigCommand.load_deps } unless defined?(@dependency_loader) 14 | 15 | option :chef_node_name, 16 | :short => '-N NAME', 17 | :long => '--node-name NAME', 18 | :description => 'The Chef node name for your new node' 19 | 20 | option :run_list, 21 | :short => '-r RUN_LIST', 22 | :long => '--run-list RUN_LIST', 23 | :description => 'Comma separated list of roles/recipes to put to node config (if it does not exist)', 24 | :proc => lambda { |o| o.split(/[\s,]+/) }, 25 | :default => [] 26 | 27 | option :json_attributes, 28 | :short => '-j JSON_ATTRIBS', 29 | :long => '--json-attributes', 30 | :description => 'A JSON string to be added to node config (if it does not exist)', 31 | :proc => lambda { |o| JSON.parse(o) }, 32 | :default => nil 33 | 34 | option :environment, 35 | :short => '-E ENVIRONMENT', 36 | :long => '--environment ENVIRONMENT', 37 | :description => 'The Chef environment for your node' 38 | 39 | # Set default chef_repo_path for Chef >= 11.8.0 40 | Chef::Config.chef_repo_path = '.' 41 | end 42 | end 43 | 44 | def nodes_path 45 | path = Chef::Config[:node_path] 46 | if path && !path.is_a?(String) 47 | ui.error %Q{node_path is not a String: #{path.inspect}, defaulting to "nodes"} 48 | path = nil 49 | end 50 | path && File.exist?(path) ? path : 'nodes' 51 | end 52 | 53 | def node_config 54 | Pathname.new(@name_args[1] || "#{nodes_path}/#{node_name}.json") 55 | end 56 | 57 | def node_name 58 | # host method must be defined by the including class 59 | config[:chef_node_name] || host 60 | end 61 | 62 | def node_environment 63 | node = node_config.exist? ? JSON.parse(IO.read(node_config)) : {} 64 | config[:environment] || node['environment'] || '_default' 65 | end 66 | 67 | def generate_node_config 68 | if node_config.exist? 69 | Chef::Log.debug "Node config '#{node_config}' already exists" 70 | else 71 | ui.msg "Generating node config '#{node_config}'..." 72 | FileUtils.mkdir_p(node_config.dirname) 73 | File.open(node_config, 'w') do |f| 74 | attributes = config[:json_attributes] || config[:first_boot_attributes] || {} 75 | run_list = { :run_list => config[:run_list] || [] } 76 | environment = config[:environment] ? { :environment => config[:environment] } : {} 77 | automatic = host ? { :automatic => { :ipaddress => host } } : {} 78 | f.print JSON.pretty_generate(attributes.merge(run_list).merge(environment).merge(automatic)) 79 | end 80 | end 81 | end 82 | 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/support/integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'net/http' 3 | 4 | require 'support/test_case' 5 | 6 | case_pattern = $base_dir.join('integration', 'cases', '*.rb') 7 | Dir[case_pattern].each do |use_case| 8 | require use_case 9 | end 10 | 11 | # Base class for EC2 integration tests 12 | class IntegrationTest < TestCase 13 | include Loggable 14 | 15 | # Returns a name for the current test's server 16 | # that should be fairly unique. 17 | def server_name 18 | "knife-solo_#{self.class}" 19 | end 20 | 21 | # Shortcut to access the test runner 22 | def runner 23 | MiniTest::Unit.runner 24 | end 25 | 26 | # Returns the server for this test, retrieved from the test runner 27 | def server 28 | return @server if @server 29 | @server = runner.get_server(self) 30 | end 31 | 32 | # The flavor to run this test on 33 | def flavor_id 34 | "m1.small" 35 | end 36 | 37 | # Sets up a kitchen directory to work in 38 | def setup 39 | @kitchen = $base_dir.join('support', 'kitchens', self.class.to_s) 40 | FileUtils.remove_entry_secure(@kitchen, true) 41 | @kitchen.dirname.mkpath 42 | system "knife solo init #{@kitchen} --no-berkshelf --no-librarian >> #{log_file}" 43 | @start_dir = Dir.pwd 44 | # NOTE (matschaffer): On ruby 2.3 Dir.chdir won't set ENV['PWD'] which chef-config-12.8.1 uses for `working_directory` 45 | Dir.chdir(@kitchen) 46 | ENV['PWD'] = @kitchen.to_s 47 | prepare_server 48 | end 49 | 50 | # Gets back to the start dir 51 | def teardown 52 | Dir.chdir(@start_dir) 53 | end 54 | 55 | # Writes out the given node hash as a json file 56 | def write_nodefile(node) 57 | write_json_file("nodes/#{server.dns_name}.json", node) 58 | end 59 | 60 | # Writes out an object to the given file as JSON 61 | def write_json_file(file, data) 62 | FileUtils.mkpath(File.dirname(file)) 63 | File.open(file, 'w') do |f| 64 | f.print data.to_json 65 | end 66 | end 67 | 68 | # Prepares the server unless it has already been marked as such 69 | def prepare_server 70 | return if server.tags["knife_solo_prepared"] 71 | if self.class.firewall_disabled 72 | system "ssh #{connection_string} service iptables stop >> #{log_file}" 73 | end 74 | assert_subcommand prepare_command 75 | runner.tag_as(:prepared, server) 76 | end 77 | 78 | # The prepare command to use on this server 79 | def prepare_command 80 | "prepare" 81 | end 82 | 83 | # Provides the path to the runner's key file 84 | def key_file 85 | runner.key_file 86 | end 87 | 88 | # The ssh-style connection string used to connect to the current node 89 | def connection_string 90 | "-i #{key_file} #{user}@#{server.dns_name}" 91 | end 92 | 93 | # Asserts that a knife command is successful 94 | def assert_knife_command(subcommand) 95 | pid = Process.spawn("knife #{subcommand} #{connection_string} -VV", 96 | :out => log_file.to_s, 97 | :err => log_file.to_s) 98 | Process.wait(pid) 99 | 100 | assert $?.success? 101 | end 102 | 103 | # Asserts that a knife solo subcommand is successful 104 | def assert_subcommand(subcommand) 105 | assert_knife_command "solo #{subcommand}" 106 | end 107 | 108 | class << self 109 | attr_reader :firewall_disabled 110 | 111 | def disable_firewall 112 | @firewall_disabled = true 113 | end 114 | end 115 | end 116 | 117 | -------------------------------------------------------------------------------- /lib/knife-solo/cookbook_manager.rb: -------------------------------------------------------------------------------- 1 | require 'chef/mixin/convert_to_class_name' 2 | require 'knife-solo/gitignore' 3 | require 'knife-solo/tools' 4 | 5 | module KnifeSolo 6 | module CookbookManager 7 | def self.included(base) 8 | base.extend ClassMethods 9 | base.send(:include, InstanceMethods) 10 | end 11 | 12 | module ClassMethods 13 | include Chef::Mixin::ConvertToClassName 14 | 15 | # Returns an Array of libraries to load 16 | def gem_libraries 17 | raise "Must be overridden by the including class" 18 | end 19 | 20 | def gem_name 21 | gem_libraries.first 22 | end 23 | 24 | def load_gem 25 | gem_libraries.each { |lib| require lib } 26 | end 27 | 28 | # Key in Chef::Config and CLI options 29 | def config_key 30 | snake_case_basename(name).to_sym 31 | end 32 | 33 | # Returns the base name of the configuration file 34 | def conf_file_name 35 | raise "Must be overridden by the including class" 36 | end 37 | end 38 | 39 | module InstanceMethods 40 | attr_reader :config, :ui 41 | 42 | def initialize(config, ui) 43 | @config = config 44 | @ui = ui 45 | end 46 | 47 | def to_s 48 | name 49 | end 50 | 51 | def name 52 | self.class.name.split('::').last 53 | end 54 | 55 | def gem_name 56 | self.class.gem_name 57 | end 58 | 59 | def gem_installed? 60 | self.class.load_gem 61 | true 62 | rescue LoadError 63 | false 64 | end 65 | 66 | def conf_file(base = nil) 67 | base ? File.join(base, self.class.conf_file_name) : self.class.conf_file_name 68 | end 69 | 70 | def enabled_by_chef_config? 71 | KnifeSolo::Tools.config_value(config, self.class.config_key) 72 | end 73 | 74 | def conf_file_exists?(base = nil) 75 | File.exist?(conf_file(base)) 76 | end 77 | 78 | # Runs the manager and returns the path to the cookbook directory 79 | def install! 80 | raise "Must be overridden by the including class" 81 | end 82 | 83 | # Runs installer if the configuration file is found and gem installed 84 | # Returns the cookbook path or nil 85 | def install 86 | if !conf_file_exists? 87 | Chef::Log.debug "#{conf_file} not found" 88 | else 89 | begin 90 | self.class.load_gem 91 | return install! 92 | rescue LoadError => e 93 | ui.warn e.inspect 94 | ui.warn "#{name} could not be loaded" 95 | ui.warn "Please add the #{gem_name} gem to your Gemfile or install it manually with `gem install #{gem_name}`" 96 | end 97 | end 98 | nil 99 | end 100 | 101 | def bootstrap(base) 102 | ui.msg "Setting up #{name}..." 103 | unless conf_file_exists?(base) 104 | File.open(conf_file(base), 'w') { |f| f.puts(initial_config) } 105 | end 106 | if KnifeSolo::Tools.config_value(config, :git, true) && gitignores 107 | KnifeSolo::Gitignore.new(base).add(gitignores) 108 | end 109 | end 110 | 111 | # Returns content for configuration file when bootstrapping 112 | def initial_config 113 | raise "Must be overridden by the including class" 114 | end 115 | 116 | # Returns an array of strings to gitignore when bootstrapping 117 | def gitignores 118 | nil 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/bootstraps_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require 'knife-solo/bootstraps' 4 | 5 | class KnifeSolo::Bootstraps::StubOS < KnifeSolo::Bootstraps::Base 6 | end 7 | 8 | class KnifeSolo::Bootstraps::StubOS2 < KnifeSolo::Bootstraps::Base 9 | def distro 10 | {:type => 'gem'} 11 | end 12 | 13 | def gem_install 14 | # dont' actually install anything 15 | end 16 | end 17 | 18 | class BootstrapsTest < TestCase 19 | def test_bootstrap_class_exists_for 20 | assert KnifeSolo::Bootstraps.class_exists_for?('Stub OS') 21 | refute KnifeSolo::Bootstraps.class_exists_for?('Mythical OS') 22 | end 23 | 24 | def test_distro_raises_if_not_implemented 25 | assert_raises RuntimeError do 26 | bootstrap_instance.distro 27 | end 28 | end 29 | 30 | def test_bootstrap_calls_appropriate_install_method 31 | bootstrap = bootstrap_instance 32 | bootstrap.stubs(:distro).returns({:type => 'disco_gem'}) 33 | bootstrap.expects(:disco_gem_install) 34 | bootstrap.bootstrap! 35 | end 36 | 37 | def test_bootstrap_calls_pre_bootstrap_checks 38 | bootstrap = KnifeSolo::Bootstraps::StubOS2.new(mock) 39 | bootstrap.expects(:run_pre_bootstrap_checks) 40 | bootstrap.bootstrap! 41 | end 42 | 43 | def test_bootstrap_delegates_to_knife_prepare 44 | prepare = mock('chef::knife::prepare') 45 | bootstrap = KnifeSolo::Bootstraps::StubOS2.new(prepare) 46 | assert_equal prepare, bootstrap.prepare 47 | end 48 | 49 | def test_omnibus_install_methdod 50 | bootstrap = bootstrap_instance 51 | bootstrap.stubs(:distro).returns({:type => "omnibus"}) 52 | bootstrap.expects(:omnibus_install) 53 | bootstrap.bootstrap! 54 | end 55 | 56 | def test_passes_omnibus_options 57 | bootstrap = bootstrap_instance 58 | bootstrap.stubs(:distro).returns({:type => "omnibus"}) 59 | bootstrap.stubs(:http_client_get_url) 60 | bootstrap.stubs(:chef_version) 61 | 62 | options = "-v 10.16.4" 63 | matcher = regexp_matches(/\s#{Regexp.quote(options)}(\s|$)/) 64 | bootstrap.prepare.stubs(:config).returns({:omnibus_options => options}) 65 | bootstrap.prepare.expects(:stream_command).with(matcher).returns(SuccessfulResult.new) 66 | 67 | bootstrap.bootstrap! 68 | end 69 | 70 | def test_combines_omnibus_options 71 | bootstrap = bootstrap_instance 72 | bootstrap.prepare.stubs(:chef_version).returns("0.10.8-3") 73 | bootstrap.prepare.stubs(:config).returns({:omnibus_options => "-s"}) 74 | assert_equal "-s -v 0.10.8-3", bootstrap.omnibus_options 75 | end 76 | 77 | def test_passes_prerelease_omnibus_version 78 | bootstrap = bootstrap_instance 79 | bootstrap.prepare.stubs(:chef_version).returns("10.18.3") 80 | bootstrap.prepare.stubs(:config).returns({:prerelease => true}) 81 | assert_equal "-p", bootstrap.omnibus_options.strip 82 | end 83 | 84 | def test_passes_gem_version 85 | bootstrap = bootstrap_instance 86 | bootstrap.prepare.stubs(:chef_version).returns("10.16.4") 87 | assert_equal "--version 10.16.4", bootstrap.gem_options 88 | end 89 | 90 | def test_passes_prereleaes_gem_version 91 | bootstrap = bootstrap_instance 92 | bootstrap.prepare.stubs(:chef_version).returns("10.18.1") 93 | bootstrap.prepare.stubs(:config).returns({:prerelease => true}) 94 | assert_equal "--prerelease", bootstrap.gem_options 95 | end 96 | 97 | def test_wont_pass_unset_gem_version 98 | bootstrap = bootstrap_instance 99 | bootstrap.prepare.stubs(:chef_version).returns(nil) 100 | assert_equal "", bootstrap.gem_options.to_s 101 | end 102 | 103 | # *** 104 | 105 | def bootstrap_instance 106 | prepare = mock('Knife::Chef::SoloPrepare') 107 | prepare.stubs(:config).returns({}) 108 | KnifeSolo::Bootstraps::StubOS.new(prepare) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/knife-solo/bootstraps/linux.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo::Bootstraps 2 | class Linux < Base 3 | def issue 4 | commands = [ 5 | 'lsb_release -d -s', 6 | 'cat /etc/redhat-release', 7 | 'cat /etc/os-release', 8 | 'cat /etc/issue' 9 | ] 10 | result = prepare.run_with_fallbacks(commands) 11 | result.success? ? result.stdout.strip : nil 12 | end 13 | 14 | def x86? 15 | machine = run_command('uname -m').stdout.strip 16 | %w{i686 x86 x86_64}.include?(machine) 17 | end 18 | 19 | def package_list 20 | @packages.join(' ') 21 | end 22 | 23 | def gem_packages 24 | ['ruby-shadow'] 25 | end 26 | 27 | def emerge_gem_install 28 | ui.msg("Installing required packages...") 29 | run_command("sudo USE='-test' ACCEPT_KEYWORDS='~amd64' emerge -u chef") 30 | gem_install 31 | end 32 | 33 | def pacman_install 34 | ui.msg("Installing required packages...") 35 | run_command("sudo pacman -Sy ruby rsync make gcc --noconfirm") 36 | run_command("sudo gem install chef --no-user-install --no-rdoc --no-ri") 37 | end 38 | 39 | def debianoid_gem_install 40 | ui.msg "Updating apt caches..." 41 | run_command("sudo apt-get update") 42 | 43 | ui.msg "Installing required packages..." 44 | @packages = %w(ruby ruby-dev libopenssl-ruby irb 45 | build-essential wget ssl-cert rsync) 46 | run_command <<-BASH 47 | sudo DEBIAN_FRONTEND=noninteractive apt-get --yes install #{package_list} 48 | BASH 49 | 50 | gem_install 51 | end 52 | 53 | def debianoid_omnibus_install 54 | run_command("sudo apt-get update") 55 | run_command("sudo DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' --force-yes -f install rsync ca-certificates wget") 56 | omnibus_install 57 | end 58 | 59 | def zypper_omnibus_install 60 | run_command("sudo zypper --non-interactive install rsync") 61 | omnibus_install 62 | end 63 | 64 | def yum_omnibus_install 65 | run_command("sudo yum clean all") 66 | run_command("sudo yum -y install rsync") 67 | omnibus_install 68 | end 69 | 70 | def distro 71 | return @distro if @distro 72 | @distro = case issue 73 | when %r{Debian GNU/Linux [6789]} 74 | {:type => (x86? ? "debianoid_omnibus" : "debianoid_gem")} 75 | when %r{Debian} 76 | {:type => "debianoid_gem"} 77 | when %r{Raspbian} 78 | {:type => "debianoid_gem"} 79 | when %r{Linux Mint} 80 | {:type => "debianoid_gem"} 81 | when %r{Ubuntu}i 82 | {:type => (x86? ? "debianoid_omnibus" : "debianoid_gem")} 83 | when %r{Linaro} 84 | {:type => "debianoid_gem"} 85 | when %r{CentOS} 86 | {:type => "yum_omnibus"} 87 | when %r{Amazon Linux} 88 | {:type => "yum_omnibus"} 89 | when %r{Red Hat Enterprise} 90 | {:type => "yum_omnibus"} 91 | when %r{Oracle Linux Server} 92 | {:type => "yum_omnibus"} 93 | when %r{Enterprise Linux Enterprise Linux Server} 94 | {:type => "yum_omnibus"} 95 | when %r{Fedora release} 96 | {:type => "yum_omnibus"} 97 | when %r{Scientific Linux} 98 | {:type => "yum_omnibus"} 99 | when %r{CloudLinux} 100 | {:type => "yum_omnibus"} 101 | when %r{SUSE Linux Enterprise Server 1[12]} 102 | {:type => "omnibus"} 103 | when %r{openSUSE 1[23]}, %r{openSUSE Leap 42} 104 | {:type => "zypper_omnibus"} 105 | when %r{This is \\n\.\\O \(\\s \\m \\r\) \\t} 106 | {:type => "emerge_gem"} 107 | when %r{Arch Linux}, %r{Manjaro Linux} 108 | {:type => "pacman"} 109 | else 110 | raise "Distribution not recognized. Please run again with `-VV` option and file an issue: https://github.com/matschaffer/knife-solo/issues" 111 | end 112 | Chef::Log.debug("Distro detection yielded: #{@distro}") 113 | @distro 114 | end #issue 115 | 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/knife-solo/bootstraps.rb: -------------------------------------------------------------------------------- 1 | class OperatingSystemNotSupportedError < StandardError ; end 2 | 3 | module KnifeSolo 4 | module Bootstraps 5 | class OperatingSystemNotImplementedError < StandardError 6 | end 7 | 8 | def self.class_exists_for?(os_name) 9 | begin 10 | true if self.class_for_operating_system(os_name).class == Class 11 | rescue 12 | false 13 | end 14 | end 15 | 16 | def self.class_for_operating_system(os_name) 17 | begin 18 | os_class_name = os_name.gsub(/\s/,'') 19 | KnifeSolo::Bootstraps.const_get(os_class_name) 20 | rescue 21 | raise OperatingSystemNotImplementedError.new("#{os_name.inspect} not implemented. Feel free to add a bootstrap implementation in KnifeSolo::Bootstraps::#{os_class_name}") 22 | end 23 | end 24 | 25 | module Delegates 26 | def stream_command(cmd) 27 | prepare.stream_command(cmd) 28 | end 29 | 30 | def run_command(cmd) 31 | prepare.run_command(cmd) 32 | end 33 | 34 | def ui 35 | prepare.ui 36 | end 37 | 38 | def chef_version 39 | prepare.chef_version 40 | end 41 | 42 | def prepare 43 | @prepare 44 | end 45 | end #Delegates 46 | 47 | module InstallCommands 48 | 49 | def bootstrap! 50 | run_pre_bootstrap_checks 51 | send("#{distro[:type]}_install") 52 | install_ohai_hints 53 | end 54 | 55 | def distro 56 | raise "implement distro detection for #{self.class.name}" 57 | end 58 | 59 | # gems to install before chef 60 | def gem_packages 61 | [] 62 | end 63 | 64 | def http_client_get_url(url, file) 65 | stream_command <<-BASH 66 | /bin/sh -c " \ 67 | if command -v curl >/dev/null 2>&1; then \ 68 | curl -L -o '#{file}' '#{url}'; \ 69 | else \ 70 | wget -O '#{file}' '#{url}'; \ 71 | fi; \ 72 | " 73 | BASH 74 | end 75 | 76 | def omnibus_install 77 | url = prepare.config[:omnibus_url] || "https://www.opscode.com/chef/install.sh" 78 | file = File.basename(url) 79 | http_client_get_url(url, file) 80 | 81 | install_command = "sudo bash #{file} #{omnibus_options}" 82 | stream_command(install_command) 83 | end 84 | 85 | def omnibus_options 86 | options = prepare.config[:omnibus_options] || "" 87 | if prepare.config[:prerelease] 88 | options << " -p" 89 | elsif chef_version 90 | options << " -v #{chef_version}" 91 | end 92 | options 93 | end 94 | 95 | def gem_install 96 | ui.msg "Installing rubygems from source..." 97 | release = "rubygems-1.8.10" 98 | file = "#{release}.tgz" 99 | url = "http://production.cf.rubygems.org/rubygems/#{file}" 100 | http_client_get_url(url, file) 101 | run_command("tar zxf #{file}") 102 | run_command("cd #{release} && sudo ruby setup.rb --no-format-executable") 103 | run_command("sudo rm -rf #{release} #{file}") 104 | run_command("sudo gem install --no-rdoc --no-ri #{gem_packages.join(' ')}") unless gem_packages.empty? 105 | run_command("sudo gem install --no-rdoc --no-ri chef #{gem_options}") 106 | end 107 | 108 | def gem_options 109 | if prepare.config[:prerelease] 110 | "--prerelease" 111 | elsif chef_version 112 | "--version #{chef_version}" 113 | end 114 | end 115 | 116 | def install_ohai_hints 117 | hints = Chef::Config[:knife][:hints] 118 | unless hints.nil? || hints.empty? 119 | ui.msg "Setting Ohai hints..." 120 | run_command("sudo mkdir -p /etc/chef/ohai/hints") 121 | run_command("sudo rm -f /etc/chef/ohai/hints/*") 122 | hints.each do |name, hash| 123 | run_command("sudo tee -a /etc/chef/ohai/hints/#{name}.json > /dev/null < "name", 25 | "tag-value" => test.server_name, 26 | "instance-state-name" => "running").first 27 | if server 28 | logger.info "Reusing active server tagged #{test.server_name}" 29 | else 30 | logger.info "Starting server for #{test.class}..." 31 | server = compute.servers.create(:tags => { 32 | :name => test.server_name, 33 | :knife_solo_integration_user => ENV['USER'] 34 | }, 35 | :image_id => test.image_id, 36 | :flavor_id => test.flavor_id, 37 | :key_name => key_name) 38 | end 39 | server.wait_for { ready? } 40 | logger.info "#{test.class} server (#{server.dns_name}) reported ready, trying to connect to ssh..." 41 | server.wait_for do 42 | `echo | nc -w 1 #{dns_name} 22` 43 | $?.success? 44 | end 45 | 46 | unless server.tags["knife_solo_ssh_sleep_passed"] 47 | logger.info "Sleeping 10s to avoid Net::SSH locking up by connecting too early..." 48 | logger.info " (if you know a better way, please send me a note at https://github.com/matschaffer/knife-solo)" 49 | # These may have better ways: 50 | # http://rubydoc.info/gems/fog/Fog/Compute/AWS/Server:setup 51 | # http://rubydoc.info/gems/knife-ec2/Chef/Knife/Ec2ServerCreate:tcp_test_ssh 52 | sleep 10 53 | tag_as(:ssh_sleep_passed, server) 54 | end 55 | 56 | server 57 | end 58 | 59 | # Adds a knife_solo_prepared tag to the server so we can know not to re-prepare it 60 | def tag_as(state, server) 61 | compute.tags.create(resource_id: server.identity, 62 | key: "knife_solo_#{state}", 63 | value: true) 64 | end 65 | 66 | # Cleans up all the servers tagged as knife solo servers for this user. 67 | # Specify SKIP_DESTROY environment variable to skip this step and leave servers 68 | # running for inspection or reuse. 69 | def run_ec2_cleanup 70 | servers = compute.servers.all("tag-key" => "knife_solo_integration_user", 71 | "tag-value" => user, 72 | "instance-state-name" => "running") 73 | if skip_destroy? 74 | puts "\nSKIP_DESTROY specified, leaving #{servers.size} instances running" 75 | else 76 | puts <<-TXT.gsub(/^\s*/, '') 77 | === 78 | About to terminate the following instances. Please cancel (Control-C) 79 | NOW if you want to leave them running. Use SKIP_DESTROY=true to 80 | skip this step. 81 | TXT 82 | servers.each do |server| 83 | puts " * #{server.id}" 84 | end 85 | sleep 20 86 | servers.each do |server| 87 | logger.info "Destroying #{server.dns_name}..." 88 | server.destroy 89 | end 90 | end 91 | end 92 | 93 | # Attempts to create the keypair used for integration testing 94 | # unless the key file is already present locally. 95 | def create_key_pair 96 | return if key_file.exist? 97 | begin 98 | key = compute.key_pairs.create(:name => key_name) 99 | key.write(key_file) 100 | rescue Fog::Compute::AWS::Error => e 101 | raise "Unable to create KeyPair 'knife-solo', please create the keypair and save it to #{key_file}" 102 | end 103 | end 104 | 105 | def key_name 106 | config['aws']['key_name'] 107 | end 108 | 109 | def key_file 110 | $base_dir.join('support', "#{key_name}.pem") 111 | end 112 | 113 | def config_file 114 | $base_dir.join('support', 'config.yml') 115 | end 116 | 117 | def config 118 | @config ||= YAML.load_file(config_file) 119 | end 120 | 121 | # Provides a Fog compute resource associated with the 122 | # AWS account credentials provided in test/support/config.yml 123 | def compute 124 | @compute ||= Fog::Compute.new({:provider => 'AWS', 125 | :aws_access_key_id => config['aws']['access_key'], 126 | :aws_secret_access_key => config['aws']['secret']}) 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /test/node_config_command_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/kitchen_helper' 3 | 4 | require 'chef/knife' 5 | require 'knife-solo/node_config_command' 6 | 7 | class DummyNodeConfigCommand < Chef::Knife 8 | include KnifeSolo::NodeConfigCommand 9 | 10 | # This is normally declared in KnifeSolo::SshCommand 11 | def host 12 | @name_args.first 13 | end 14 | end 15 | 16 | class NodeConfigCommandTest < TestCase 17 | include KitchenHelper 18 | 19 | def setup 20 | @host = "defaulthost" 21 | end 22 | 23 | def test_node_config_defaults_to_host_name 24 | cmd = command(@host) 25 | assert_equal "nodes/#{@host}.json", cmd.node_config.to_s 26 | end 27 | 28 | def test_takes_node_config_as_second_arg 29 | cmd = command(@host, "nodes/myhost.json") 30 | assert_equal "nodes/myhost.json", cmd.node_config.to_s 31 | end 32 | 33 | def test_takes_node_config_from_option 34 | cmd = command(@host, "--node-name=mynode") 35 | assert_equal "nodes/mynode.json", cmd.node_config.to_s 36 | end 37 | 38 | def test_takes_node_config_as_second_arg_even_with_name_option 39 | cmd = command(@host, "nodes/myhost.json", "--node-name=mynode") 40 | assert_equal "nodes/myhost.json", cmd.node_config.to_s 41 | end 42 | 43 | def test_generates_a_node_config 44 | in_kitchen do 45 | cmd = command(@host) 46 | cmd.generate_node_config 47 | assert cmd.node_config.exist? 48 | 49 | assert_config_contains({"run_list" => []}, cmd) 50 | end 51 | end 52 | 53 | def test_wont_overwrite_node_config 54 | in_kitchen do 55 | cmd = command(@host, "--run-list=role[myrole]") 56 | File.open(cmd.node_config, "w") do |f| 57 | f << "testdata" 58 | end 59 | cmd.generate_node_config 60 | assert_match "testdata", cmd.node_config.read 61 | end 62 | end 63 | 64 | def test_generates_a_node_config_from_name_option 65 | in_kitchen do 66 | cmd = command(@host, "--node-name=mynode") 67 | cmd.generate_node_config 68 | assert cmd.node_config.exist? 69 | end 70 | end 71 | 72 | def test_generates_a_node_config_with_specified_run_list 73 | in_kitchen do 74 | cmd = command(@host, "--run-list=role[base],recipe[foo]") 75 | cmd.generate_node_config 76 | 77 | assert_config_contains({"run_list" => ["role[base]","recipe[foo]"]}, cmd) 78 | end 79 | end 80 | 81 | def test_generates_a_node_config_with_specified_attributes 82 | in_kitchen do 83 | foo_json = '"foo":{"bar":[1,2],"baz":"x"}' 84 | cmd = command(@host, "--json-attributes={#{foo_json}}") 85 | cmd.generate_node_config 86 | 87 | expected_hash = { 88 | "foo" => {"bar" => [1,2], "baz" => "x"}, 89 | "run_list" => [] 90 | } 91 | 92 | assert_config_contains expected_hash, cmd 93 | end 94 | end 95 | 96 | def test_generates_a_node_config_with_specified_json_attributes 97 | in_kitchen do 98 | foo_json = '"foo":99' 99 | ignored_json = '"bar":"ignored"' 100 | 101 | cmd = command(@host) 102 | cmd.config[:json_attributes] = JSON.parse("{#{foo_json}}") 103 | cmd.config[:first_boot_attributes] = JSON.parse("{#{ignored_json}}") 104 | cmd.generate_node_config 105 | 106 | expected_hash = { 107 | "foo" => 99, 108 | "run_list" => [] 109 | } 110 | 111 | assert_config_contains expected_hash, cmd 112 | end 113 | end 114 | 115 | def test_generates_a_node_config_with_specified_first_boot_attributes 116 | in_kitchen do 117 | cmd = command(@host) 118 | cmd.config[:first_boot_attributes] = {"foo"=>nil} 119 | cmd.generate_node_config 120 | 121 | expected_hash = { 122 | "foo" => nil, 123 | "run_list" => [] 124 | } 125 | 126 | assert_config_contains expected_hash, cmd 127 | end 128 | end 129 | 130 | def test_generates_a_node_config_with_specified_run_list_and_attributes 131 | in_kitchen do 132 | foo_json = '"foo":"bar"' 133 | run_list = 'recipe[baz]' 134 | cmd = command(@host, "--run-list=#{run_list}", "--json-attributes={#{foo_json}}") 135 | cmd.generate_node_config 136 | 137 | expected_hash = { 138 | "foo" => "bar", 139 | "run_list" => [run_list] 140 | } 141 | 142 | assert_config_contains expected_hash, cmd 143 | 144 | end 145 | end 146 | 147 | def test_generates_a_node_config_with_the_ip_address 148 | in_kitchen do 149 | cmd = command(@host) 150 | cmd.generate_node_config 151 | 152 | expected_hash = { 153 | "automatic" => { "ipaddress" => @host } 154 | } 155 | 156 | assert_config_contains expected_hash, cmd 157 | 158 | end 159 | end 160 | 161 | 162 | 163 | def test_creates_the_nodes_directory_if_needed 164 | outside_kitchen do 165 | cmd = command(@host, "--node-name=mynode") 166 | cmd.generate_node_config 167 | assert cmd.node_config.exist? 168 | end 169 | end 170 | 171 | private 172 | 173 | def command(*args) 174 | knife_command(DummyNodeConfigCommand, *args) 175 | end 176 | 177 | def assert_config_contains expected_hash, cmd 178 | config = JSON.parse(cmd.node_config.read) 179 | expected_hash.each do |k, v| 180 | assert_equal v, config[k] 181 | end 182 | end 183 | 184 | end 185 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | LICENSE 3 | README.rdoc 4 | Rakefile 5 | lib/chef/knife/bootstrap_solo.rb 6 | lib/chef/knife/cook.rb 7 | lib/chef/knife/kitchen.rb 8 | lib/chef/knife/prepare.rb 9 | lib/chef/knife/solo_bootstrap.rb 10 | lib/chef/knife/solo_clean.rb 11 | lib/chef/knife/solo_cook.rb 12 | lib/chef/knife/solo_init.rb 13 | lib/chef/knife/solo_prepare.rb 14 | lib/chef/knife/wash_up.rb 15 | lib/knife-solo.rb 16 | lib/knife-solo/berkshelf.rb 17 | lib/knife-solo/bootstraps.rb 18 | lib/knife-solo/bootstraps/darwin.rb 19 | lib/knife-solo/bootstraps/freebsd.rb 20 | lib/knife-solo/bootstraps/linux.rb 21 | lib/knife-solo/bootstraps/sun_os.rb 22 | lib/knife-solo/cookbook_manager.rb 23 | lib/knife-solo/cookbook_manager_selector.rb 24 | lib/knife-solo/deprecated_command.rb 25 | lib/knife-solo/gitignore.rb 26 | lib/knife-solo/info.rb 27 | lib/knife-solo/librarian.rb 28 | lib/knife-solo/node_config_command.rb 29 | lib/knife-solo/resources/knife.rb 30 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search 31 | lib/knife-solo/resources/solo.rb.erb 32 | lib/knife-solo/ssh_command.rb 33 | lib/knife-solo/ssh_connection.rb 34 | lib/knife-solo/tools.rb 35 | test/bootstraps_test.rb 36 | test/deprecated_command_test.rb 37 | test/gemfiles/Gemfile.chef-12 38 | test/gemfiles/Gemfile.chef-13 39 | test/gemfiles/Gemfile.chef-master 40 | test/gitignore_test.rb 41 | test/integration/amazon_linux_2016_03_bootstrap_test.rb 42 | test/integration/cases/apache2_bootstrap.rb 43 | test/integration/cases/apache2_cook.rb 44 | test/integration/cases/cache_path_usage.rb 45 | test/integration/cases/empty_cook.rb 46 | test/integration/cases/encrypted_data_bag.rb 47 | test/integration/cases/environment.rb 48 | test/integration/cases/knife_bootstrap.rb 49 | test/integration/cases/ohai_hints.rb 50 | test/integration/centos6_3_test.rb 51 | test/integration/centos7_test.rb 52 | test/integration/debian7_knife_bootstrap_test.rb 53 | test/integration/omnios_r151014_test.rb 54 | test/integration/scientific_linux_63_test.rb 55 | test/integration/sles_11_test.rb 56 | test/integration/ubuntu12_04_bootstrap_test.rb 57 | test/integration/ubuntu12_04_ohai_hints_test.rb 58 | test/integration/ubuntu12_04_test.rb 59 | test/integration_helper.rb 60 | test/knife_bootstrap_test.rb 61 | test/minitest/parallel.rb 62 | test/node_config_command_test.rb 63 | test/performance/ssh_performance_test.rb 64 | test/solo_bootstrap_test.rb 65 | test/solo_clean_test.rb 66 | test/solo_cook_test.rb 67 | test/solo_init_test.rb 68 | test/solo_prepare_test.rb 69 | test/ssh_command_test.rb 70 | test/support/cache_using_cookbook/metadata.rb 71 | test/support/cache_using_cookbook/recipes/default.rb 72 | test/support/config.yml.example 73 | test/support/data_bag_key 74 | test/support/ec2_runner.rb 75 | test/support/environment_cookbook/attributes/default.rb 76 | test/support/environment_cookbook/metadata.rb 77 | test/support/environment_cookbook/recipes/default.rb 78 | test/support/integration_test.rb 79 | test/support/issue_files/gentoo2011 80 | test/support/issue_files/sles11-sp1 81 | test/support/issue_files/ubuntu 82 | test/support/kitchen_helper.rb 83 | test/support/knife.rb 84 | test/support/loggable.rb 85 | test/support/secret_cookbook/metadata.rb 86 | test/support/secret_cookbook/recipes/default.rb 87 | test/support/ssh_config 88 | test/support/test_case.rb 89 | test/support/test_environment.json 90 | test/support/validation_helper.rb 91 | test/test_helper.rb 92 | test/tools_test.rb 93 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/.travis.yml 94 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/CHANGELOG 95 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/LICENSE 96 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/NOTICE 97 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/README.md 98 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/libraries/search.rb 99 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/libraries/search/overrides.rb 100 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/libraries/search/parser.rb 101 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/libraries/vendor/chef/solr_query/lucene.treetop 102 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/libraries/vendor/chef/solr_query/lucene_nodes.rb 103 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/libraries/vendor/chef/solr_query/query_transform.rb 104 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/metadata.rb 105 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/recipes/default.rb 106 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/Gemfile 107 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/data/data_bags/node/alpha.json 108 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/data/data_bags/node/beta.json 109 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/data/data_bags/node/without_json_class.json 110 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/data/data_bags/users/jerry.json 111 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/data/data_bags/users/lea.json 112 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/data/data_bags/users/mike.json 113 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/data/data_bags/users/tom.json 114 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/test_data_bags.rb 115 | lib/knife-solo/resources/patch_cookbooks/chef-solo-search/tests/test_search.rb 116 | -------------------------------------------------------------------------------- /test/solo_init_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/kitchen_helper' 3 | 4 | require 'chef/knife/solo_init' 5 | require 'fileutils' 6 | require 'knife-solo/berkshelf' 7 | require 'knife-solo/librarian' 8 | 9 | class SoloInitTest < TestCase 10 | include KitchenHelper 11 | 12 | def test_produces_folders 13 | in_kitchen do 14 | assert File.exist?("nodes") 15 | end 16 | end 17 | 18 | def test_produces_gitkeep_in_folders 19 | in_kitchen do 20 | assert File.exist?("nodes/.gitkeep") 21 | end 22 | end 23 | 24 | def test_barks_without_directory_arg 25 | cmd = command 26 | cmd.ui.expects(:fatal).with(regexp_matches(/You must specify a directory/)) 27 | $stdout.stubs(:puts) 28 | outside_kitchen do 29 | assert_exits cmd 30 | end 31 | end 32 | 33 | def test_takes_directory_as_arg 34 | outside_kitchen do 35 | command("new_kitchen").run 36 | assert File.exist?("new_kitchen/nodes") 37 | end 38 | end 39 | 40 | def test_bootstraps_berkshelf_if_berksfile_found 41 | outside_kitchen do 42 | FileUtils.touch "Berksfile" 43 | KnifeSolo::Berkshelf.any_instance.expects(:bootstrap) 44 | command(".").run 45 | end 46 | end 47 | 48 | def test_wont_bootstrap_berkshelf_if_cheffile_found 49 | outside_kitchen do 50 | FileUtils.touch "Cheffile" 51 | KnifeSolo::Berkshelf.any_instance.expects(:bootstrap).never 52 | command(".").run 53 | refute File.exist?("Berksfile") 54 | end 55 | end 56 | 57 | def test_wont_create_berksfile_by_default 58 | outside_kitchen do 59 | command("new_kitchen").run 60 | refute File.exist?("new_kitchen/Berksfile") 61 | end 62 | end 63 | 64 | def test_creates_berksfile_if_requested 65 | outside_kitchen do 66 | cmd = command("new_kitchen", "--berkshelf") 67 | KnifeSolo::Berkshelf.expects(:load_gem).never 68 | cmd.run 69 | assert File.exist?("new_kitchen/Berksfile") 70 | end 71 | end 72 | 73 | def test_wont_overwrite_berksfile 74 | outside_kitchen do 75 | File.open("Berksfile", "w") do |f| 76 | f << "testdata" 77 | end 78 | command(".", "--berkshelf").run 79 | assert_equal "testdata", IO.read("Berksfile") 80 | end 81 | end 82 | 83 | def test_wont_create_berksfile_if_denied 84 | outside_kitchen do 85 | cmd = command("new_kitchen", "--no-berkshelf") 86 | KnifeSolo::Berkshelf.expects(:load_gem).never 87 | cmd.run 88 | refute File.exist?("new_kitchen/Berksfile") 89 | end 90 | end 91 | 92 | def test_wont_create_berksfile_if_librarian_requested 93 | outside_kitchen do 94 | cmd = command("new_kitchen", "--librarian") 95 | KnifeSolo::Berkshelf.expects(:load_gem).never 96 | cmd.run 97 | refute File.exist?("new_kitchen/Berksfile") 98 | end 99 | end 100 | 101 | def test_creates_berksfile_if_gem_installed 102 | outside_kitchen do 103 | cmd = command(".") 104 | KnifeSolo::Berkshelf.expects(:load_gem).returns(true) 105 | cmd.run 106 | assert File.exist?("Berksfile") 107 | end 108 | end 109 | 110 | def test_wont_create_berksfile_if_gem_missing 111 | outside_kitchen do 112 | cmd = command(".") 113 | KnifeSolo::Berkshelf.expects(:load_gem).raises(LoadError) 114 | cmd.run 115 | refute File.exist?("Berksfile") 116 | end 117 | end 118 | 119 | def test_bootstraps_librarian_if_cheffile_found 120 | outside_kitchen do 121 | FileUtils.touch "Cheffile" 122 | KnifeSolo::Librarian.any_instance.expects(:bootstrap) 123 | command(".").run 124 | end 125 | end 126 | 127 | def test_wont_bootstrap_librarian_if_berksfile_found 128 | outside_kitchen do 129 | FileUtils.touch "Berksfile" 130 | KnifeSolo::Librarian.any_instance.expects(:bootstrap).never 131 | command(".").run 132 | refute File.exist?("Cheffile") 133 | end 134 | end 135 | 136 | def test_wont_create_cheffile_by_default 137 | outside_kitchen do 138 | command(".").run 139 | refute File.exist?("Cheffile") 140 | end 141 | end 142 | 143 | def test_creates_cheffile_if_requested 144 | outside_kitchen do 145 | cmd = command(".", "--librarian") 146 | KnifeSolo::Librarian.expects(:load_gem).never 147 | cmd.run 148 | assert File.exist?("Cheffile") 149 | end 150 | end 151 | 152 | def test_wont_overwrite_cheffile 153 | outside_kitchen do 154 | File.open("Cheffile", "w") do |f| 155 | f << "testdata" 156 | end 157 | command(".", "--librarian").run 158 | assert_equal "testdata", IO.read("Cheffile") 159 | end 160 | end 161 | 162 | def test_wont_create_cheffile_if_denied 163 | outside_kitchen do 164 | cmd = command("new_kitchen", "--no-librarian") 165 | KnifeSolo::Librarian.expects(:load_gem).never 166 | cmd.run 167 | refute File.exist?("new_kitchen/Cheffile") 168 | end 169 | end 170 | 171 | def test_wont_create_cheffile_if_berkshelf_requested 172 | outside_kitchen do 173 | cmd = command(".", "--berkshelf") 174 | KnifeSolo::Librarian.expects(:load_gem).never 175 | cmd.run 176 | refute File.exist?("Cheffile") 177 | end 178 | end 179 | 180 | def test_creates_cheffile_if_gem_installed 181 | outside_kitchen do 182 | cmd = command(".") 183 | KnifeSolo::Librarian.expects(:load_gem).returns(true) 184 | cmd.run 185 | assert File.exist?("Cheffile") 186 | end 187 | end 188 | 189 | def test_wont_create_cheffile_if_gem_missing 190 | outside_kitchen do 191 | cmd = command(".") 192 | KnifeSolo::Librarian.expects(:load_gem).raises(LoadError) 193 | cmd.run 194 | refute File.exist?("Cheffile") 195 | end 196 | end 197 | 198 | def test_gitignores_cookbooks_directory 199 | outside_kitchen do 200 | command("bar").run 201 | assert_equal "/cookbooks/", IO.read("bar/.gitignore").chomp 202 | end 203 | end 204 | 205 | def test_wont_create_gitignore_if_denied 206 | outside_kitchen do 207 | command(".", "--no-git").run 208 | refute File.exist?(".gitignore") 209 | end 210 | end 211 | 212 | def command(*args) 213 | KnifeSolo::Berkshelf.stubs(:load_gem).raises(LoadError) 214 | KnifeSolo::Librarian.stubs(:load_gem).raises(LoadError) 215 | knife_command(Chef::Knife::SoloInit, *args) 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /test/ssh_command_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require 'knife-solo/ssh_command' 4 | require 'knife-solo/tools' 5 | require 'chef/knife' 6 | require 'net/ssh' 7 | 8 | class DummySshCommand < Chef::Knife 9 | include KnifeSolo::SshCommand 10 | end 11 | 12 | class SshCommandTest < TestCase 13 | def test_separates_user_and_host 14 | assert_equal "ubuntu", command("ubuntu@10.0.0.1").user 15 | assert_equal "10.0.0.1", command("ubuntu@10.0.0.1").host 16 | end 17 | 18 | def test_defaults_to_system_user 19 | ENV['USER'] = "test" 20 | assert_equal "test", command("10.0.0.1").user 21 | end 22 | 23 | def test_takes_user_from_options 24 | cmd = command("10.0.0.1", "--ssh-user=test") 25 | cmd.validate_ssh_options! 26 | assert_equal "test", cmd.user 27 | end 28 | 29 | def test_takes_user_as_arg 30 | cmd = command("test@10.0.0.1", "--ssh-user=ignored") 31 | cmd.validate_ssh_options! 32 | assert_equal "test", cmd.user 33 | end 34 | 35 | def test_host_regex_rejects_invalid_hostnames 36 | %w[@name @@name.com name@ name@@ joe@@example.com joe@name@example.com].each do |invalid| 37 | cmd = command(invalid) 38 | refute cmd.first_cli_arg_is_a_hostname?, "#{invalid} should have been rejected" 39 | end 40 | end 41 | 42 | def test_host_regex_accpets_valid_hostnames 43 | %w[name.com name joe@example.com].each do |valid| 44 | cmd = command(valid) 45 | assert cmd.first_cli_arg_is_a_hostname?, "#{valid} should have been accepted" 46 | end 47 | end 48 | 49 | def test_prompts_for_password_if_not_provided 50 | cmd = command("10.0.0.1") 51 | cmd.ui.expects(:ask).returns("testpassword") 52 | assert_equal "testpassword", cmd.password 53 | end 54 | 55 | def test_falls_back_to_password_authentication_after_keys 56 | cmd = command("10.0.0.1", "--ssh-password=test") 57 | cmd.expects(:try_connection).raises(Net::SSH::AuthenticationFailed) 58 | cmd.detect_authentication_method 59 | assert_equal "test", cmd.connection_options[:password] 60 | end 61 | 62 | def test_try_connection_without_gateway_connects_using_ssh 63 | cmd = command("10.0.0.1") 64 | Net::SSH.expects(:start).with(cmd.host, cmd.user, cmd.connection_options) 65 | 66 | cmd.try_connection 67 | end 68 | 69 | def test_try_connection_with_gateway_connects_using_ssh_gateway 70 | cmd = command("10.0.0.1", "--ssh-gateway=user@gateway") 71 | ssh_mock = mock 'ssh_mock' 72 | Net::SSH::Gateway.expects(:new).with('gateway', 'user').returns(ssh_mock) 73 | ssh_mock.expects(:ssh).with(cmd.host, cmd.user, cmd.connection_options.select{|o| o != :gateway}) 74 | 75 | Net::SSH.expects(:start).never 76 | 77 | cmd.try_connection 78 | end 79 | 80 | def test_uses_default_keys_if_conncetion_succeeds 81 | cmd = command("10.0.0.1") 82 | assert_equal false, cmd.connection_options[:config] 83 | end 84 | 85 | def test_uses_ssh_config_if_matched 86 | ssh_config = Pathname.new(__FILE__).dirname.join('support', 'ssh_config') 87 | cmd = command("10.0.0.1", "--ssh-config-file=#{ssh_config}") 88 | 89 | assert_equal "bob", cmd.connection_options[:user] 90 | assert_equal "id_rsa_bob", cmd.connection_options[:keys].first 91 | assert_equal "bob", cmd.user 92 | assert_equal false, cmd.connection_options[:config] 93 | end 94 | 95 | def test_handles_port_specification 96 | cmd = command("10.0.0.1", "-p", "2222") 97 | assert_equal "2222", cmd.connection_options[:port] 98 | end 99 | 100 | def test_handle_startup_script 101 | cmd = command("10.0.0.1", "--startup-script=~/.bashrc") 102 | assert_equal "source ~/.bashrc && echo $TEST_PROP", cmd.processed_command("echo $TEST_PROP") 103 | end 104 | 105 | def test_handle_no_host_key_verify 106 | cmd = command("10.0.0.1", "--no-host-key-verify") 107 | assert_equal false, cmd.connection_options[:paranoid] 108 | assert_equal "/dev/null", cmd.connection_options[:user_known_hosts_file] 109 | end 110 | 111 | def test_handle_forward_agent 112 | cmd = command("10.0.0.1", "--forward-agent") 113 | assert_equal true, cmd.connection_options[:forward_agent] 114 | end 115 | 116 | 117 | def test_handle_ssh_gateway 118 | gateway = 'test@host.com' 119 | cmd = command("10.0.0.1", "--ssh-gateway", gateway) 120 | assert_equal gateway, cmd.connection_options[:gateway] 121 | end 122 | 123 | def test_handle_default_host_key_verify_is_paranoid 124 | cmd = command("10.0.0.1") 125 | assert_nil(cmd.connection_options[:paranoid]) # Net:SSH default is :paranoid => true 126 | assert_nil(cmd.connection_options[:user_known_hosts_file]) 127 | end 128 | 129 | def test_builds_cli_ssh_args 130 | DummySshCommand.any_instance.stubs(:try_connection) 131 | 132 | cmd = command("10.0.0.1") 133 | assert_equal "#{ENV['USER']}@10.0.0.1", cmd.ssh_args 134 | 135 | cmd = command("usertest@10.0.0.1", "--ssh-config-file=myconfig") 136 | cmd.validate_ssh_options! 137 | assert_equal "usertest@10.0.0.1 -F \"myconfig\"", cmd.ssh_args 138 | 139 | cmd = command("usertest@10.0.0.1", "--ssh-identity=my_rsa") 140 | cmd.validate_ssh_options! 141 | assert_equal "usertest@10.0.0.1 -i \"my_rsa\"", cmd.ssh_args 142 | 143 | cmd = command("usertest@10.0.0.1", "--identity-file=my_rsa") 144 | cmd.validate_ssh_options! 145 | assert_equal "usertest@10.0.0.1 -i \"my_rsa\"", cmd.ssh_args 146 | 147 | cmd = command("usertest@10.0.0.1", "--ssh-port=222") 148 | cmd.validate_ssh_options! 149 | assert_equal "usertest@10.0.0.1 -p 222", cmd.ssh_args 150 | 151 | cmd = command("usertest@10.0.0.1", "--no-host-key-verify") 152 | cmd.validate_ssh_options! 153 | assert_equal "usertest@10.0.0.1 -o UserKnownHostsFile=\"/dev/null\" -o StrictHostKeyChecking=no", cmd.ssh_args 154 | 155 | cmd = command("usertest@10.0.0.1", "--forward-agent") 156 | cmd.validate_ssh_options! 157 | assert_equal "usertest@10.0.0.1 -o ForwardAgent=yes", cmd.ssh_args 158 | 159 | cmd = command("usertest@10.0.0.1", "--ssh-gateway=test@host.com") 160 | cmd.validate_ssh_options! 161 | assert_equal "usertest@10.0.0.1", cmd.ssh_args 162 | end 163 | 164 | def test_barks_without_atleast_a_hostname 165 | cmd = command 166 | cmd.ui.expects(:fatal).with(regexp_matches(/hostname.*argument/)) 167 | $stdout.stubs(:puts) 168 | assert_exits { cmd.validate_ssh_options! } 169 | end 170 | 171 | def test_run_with_fallbacks_returns_first_successful_result 172 | cmds = sequence("cmds") 173 | cmd = command 174 | cmd.expects(:run_command).with("first", {}).returns(result(1, "fail")).in_sequence(cmds) 175 | cmd.expects(:run_command).with("second", {}).returns(result(0, "w00t")).in_sequence(cmds) 176 | cmd.expects(:run_command).never 177 | 178 | res = cmd.run_with_fallbacks(["first", "second", "third"]) 179 | assert_equal "w00t", res.stdout 180 | assert res.success? 181 | end 182 | 183 | def test_run_with_fallbacks_returns_error_if_all_fail 184 | cmd = command 185 | cmd.expects(:run_command).twice.returns(result(64, "fail")) 186 | 187 | res = cmd.run_with_fallbacks(["foo", "bar"]) 188 | assert_equal "", res.stdout 189 | assert_equal 1, res.exit_code 190 | end 191 | 192 | def test_handle_ssh_keepalive 193 | cmd = command("usertest@10.0.0.1", "--ssh-keepalive", '--ssh-keepalive-interval=100') 194 | cmd.validate_ssh_options! 195 | assert_equal true, cmd.connection_options[:keepalive] 196 | assert_equal 100, cmd.connection_options[:keepalive_interval] 197 | end 198 | 199 | def test_handle_no_ssh_keepalive 200 | cmd = command("usertest@10.0.0.1", "--no-ssh-keepalive") 201 | assert_equal nil, cmd.connection_options[:keepalive] 202 | end 203 | 204 | def test_handle_default_ssh_keepalive_is_true 205 | cmd = command("usertest@10.0.0.1") 206 | cmd.validate_ssh_options! 207 | assert_equal true, cmd.connection_options[:keepalive] 208 | assert_equal 300, cmd.connection_options[:keepalive_interval] 209 | end 210 | 211 | def test_handle_custom_sudo_command 212 | cmd = sudo_command("usertest@10.0.0.1", "--sudo-command=/usr/some/sudo") 213 | assert_equal "/usr/some/sudo", cmd.sudo_command 214 | end 215 | 216 | def test_replace_sudo_with_default_sudo_command 217 | cmd = sudo_command("usertest@10.0.0.1") 218 | KnifeSolo::SshConnection.any_instance.expects(:run_command).once 219 | .with("sudo -p 'knife sudo password: ' foo", nil) 220 | 221 | cmd.run_command("sudo foo") 222 | end 223 | 224 | def test_replace_sudo_with_custom_sudo_command 225 | cmd = sudo_command("usertest@10.0.0.1", "--sudo-command=/usr/some/sudo") 226 | KnifeSolo::SshConnection.any_instance.expects(:run_command).once 227 | .with("/usr/some/sudo foo", nil) 228 | 229 | cmd.run_command("sudo foo") 230 | end 231 | 232 | def test_barks_if_ssh_keepalive_is_zero 233 | cmd = command("usertest@10.0.0.1", "--ssh-keepalive-interval=0") 234 | cmd.ui.expects(:fatal).with(regexp_matches(/--ssh-keepalive-interval.*positive number/)) 235 | $stdout.stubs(:puts) 236 | assert_exits { cmd.validate_ssh_options! } 237 | end 238 | 239 | def result(code, stdout = "") 240 | res = KnifeSolo::SshConnection::ExecResult.new(code) 241 | res.stdout = stdout 242 | res 243 | end 244 | 245 | def command(*args) 246 | Net::SSH::Config.stubs(:default_files) 247 | knife_command(DummySshCommand, *args) 248 | end 249 | 250 | def sudo_command(*args) 251 | cmd = command(*args) 252 | cmd.stubs(:detect_authentication_method).returns(true) 253 | cmd.stubs(:sudo_available?).returns(true) 254 | cmd 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = knife-solo 2 | 3 | == DESCRIPTION: 4 | 5 | knife-solo adds a handful of commands that aim to make working with chef-solo as powerful as chef-server. It currently adds 5 subcommands to knife: 6 | 7 | - knife solo init is used to create a new directory structure (i.e. "kitchen") that fits with Chef's standard structure and can be used to build and store recipes. 8 | 9 | - knife solo prepare installs Chef on a given host. It's structured to auto-detect the target OS and change the installation process accordingly. 10 | 11 | - knife solo cook uploads the current kitchen (Chef repo) to the target host and runs chef-solo on that host. 12 | 13 | - knife solo bootstrap combines the two previous ones (prepare and cook). knife-solo also adds +--solo+ command line option and +knife[:solo]+ configuration parameter to knife bootstrap that can be used for triggering "knife solo bootstrap" instead of the normal template based chef-client bootstrap. 14 | 15 | - knife solo clean removes the uploaded kitchen from the target host. 16 | 17 | Preliminary Windows support for "knife solo cook" is available (see below). 18 | 19 | == USAGE: 20 | 21 | Installation is a normal gem installation. 22 | 23 | gem install knife-solo 24 | 25 | # or if using ChefDK 26 | chef gem install knife-solo 27 | 28 | If you need to install from git run: 29 | 30 | bundle && bundle exec rake install 31 | 32 | == Integration with Berkshelf & Librarian 33 | 34 | knife-solo also integrates with {Berkshelf}[http://berkshelf.com/] and {Librarian-Chef}[https://github.com/applicationsonline/librarian-chef] for managing your cookbooks out of the box. 35 | 36 | We try to do this somewhat automatically by first checking if you have either of the two gems installed. If you have both, we will default to Berkshelf. 37 | 38 | During knife solo init we'll generate the appropriate configuration file for either gem. Then during knife solo cook we'll run the installation step for whichever configuration file is in your kitchen. 39 | 40 | Both commands accept option flags to disable this feature if needed (--no-berkshelf or --no-librarian). The init command also offers enable flags to generate configuration files regardless of whether or not you have the supporting gem installed. 41 | 42 | More detailed logic for this integration is available in the {Berkshelf & Librarian-Chef integration}[https://github.com/matschaffer/knife-solo/wiki/Berkshelf-&-Librarian-Chef-integration] wiki page. 43 | 44 | === A note about the "cookbooks" directory 45 | 46 | One common "gotcha" is that you may have Berkshelf or Librarian-Chef installed without knowing it. This will generate a kitchen that is configured to use them which might not have been your intention. Once the configuration file is available, the cookbooks directory will be reserved for cookbooks that are resolved via one of those tools. Any cookbooks that you create there will be removed when you run knife solo cook. 47 | 48 | Please use site-cookbooks for custom cookbooks or (better yet) give them their own git repositories which are then included using Berkshelf or Librarian-Chef. 49 | 50 | == knife-solo commands 51 | 52 | === Init command 53 | 54 | The init command simply takes a name of the directory to store the kitchen structure. Use "." to initialize the current directory. 55 | 56 | knife solo init mychefrepo 57 | 58 | Currently the directory structure looks like this, but could change as development continues. 59 | 60 | mychefrepo/ 61 | ├── .chef 62 | │ └── knife.rb 63 | ├── cookbooks 64 | ├── data_bags 65 | ├── nodes 66 | ├── roles 67 | └── site-cookbooks 68 | 69 | === Prepare command 70 | 71 | The prepare command takes an ssh-style host argument as follows: 72 | 73 | knife solo prepare ubuntu@10.0.0.201 74 | 75 | It will look up SSH information from ~/.ssh/config or in the file specified by +-F+. You can also pass port information (+-p+), identity information (+-i+), or a password (+-P+). It will use sudo to run some of these commands and will prompt you for the password if it's not supplied on the command line. 76 | 77 | This command will make a best-effort to detect and install Chef Solo on your target operating system. We use the {Opscode Installer}[http://www.opscode.com/chef/install/] wherever possible. 78 | 79 | If you need specific behavior you can fallback to a knife bootstrap command with an empty runlist using the following: 80 | 81 | knife bootstrap --template-file bootstrap.centos.erb -u root 172.16.144.132 82 | echo '{"run_list":[]}' > nodes/172.16.144.132.json 83 | 84 | Bootstrap templates are quite simple, as shown in {this gist for bootstrap.centos.erb}[https://gist.github.com/2402433]. 85 | 86 | Or if your modifications provide some general benefit, consider sending a pull request to {this project}[https://github.com/matschaffer/knife-solo] or {the omnibus installer}[https://github.com/opscode/omnibus]. 87 | 88 | === Cook command 89 | 90 | The cook command also takes an ssh-style host argument: 91 | 92 | knife solo cook ubuntu@10.0.0.201 93 | 94 | The cook command uploads the current kitchen to the server and runs chef-solo on that server. If you only specify one argument it will look for a node config in nodes/.json. Or if you want to specify a node config you can pass the path to the file as the second argument. 95 | 96 | This uploads all of your cookbooks in addition to a patch that allows you to use data_bags in a read-only fashion from the +data_bags+ folder. 97 | 98 | This also supports encrypted data bags. To use them, set the path to your key with +encrypted_data_bag_secret+ in .chef/knife.rb. 99 | 100 | The built-in knife commands for working with data bags don't work well without a Chef server so we recommend using the {knife-solo_data_bag}[https://github.com/thbishop/knife-solo_data_bag] gem. This will provide "solo" versions of all the typical data bag commands. The default kitchen structure generated by knife solo init should be compatible with all the operations listed in the documentation for that gem. 101 | 102 | If you want to run chef-solo in legacy mode, you may use +--legacy-mode+ option or put +solo_legacy_mode true+ into .chef/knife.rb. 103 | 104 | === Bootstrap command 105 | 106 | The bootstrap command takes the same arguments and most of the options as prepare and cook: 107 | 108 | knife solo bootstrap ubuntu@10.0.0.201 109 | 110 | Under the hood it first calls +knife solo prepare+ and then +knife solo cook+ with the specified arguments and options. 111 | 112 | ==== Integration with knife bootstrap 113 | 114 | knife-solo also integrates with knife bootstrap by adding +--solo+ command line option and +knife[:solo]+ configuration parameter to it. When requested, "knife solo bootstrap" is used instead of the normal template based chef-client bootstrap. This is especially useful with other knife plugins like {knife-ec2}[https://github.com/opscode/knife-ec2] that invoke "knife bootstrap" after creating an server instance. Even if these plugins do not have the "--solo" option, you can put knife[:solo] = true in knife.rb. 115 | 116 | === Clean command 117 | 118 | The clean command takes the same arguments like prepare and cook: 119 | 120 | knife solo clean ubuntu@10.0.0.201 121 | 122 | The clean command removes an uploaded kitchen completely from the target host. This improves security because passwords etc. are not left behind on that host. 123 | 124 | === Windows support 125 | 126 | The cook command will work on Windows node if you meet the following howto: 127 | 128 | ==== Init as normally 129 | 130 | - run knife solo init 131 | 132 | ==== Prepare the node manually 133 | 134 | - install a SSH server (eg: WinSSHd) 135 | - install rsync on the node (see https://github.com/thbar/rsync-windows) 136 | - add rsync to the user PATH 137 | - install http://www.opscode.com/chef/install.msi 138 | - add nodes/hostname.json and put { "run_list": [] } in it 139 | 140 | ==== Cook 141 | 142 | - cook should work as expected automatically, if you use cygwin rsync. If you're using MinGW / Git Bash, or you have a non-standard cygdrive setting, you can set that in .chef/knife.rb: 143 | 144 | knife[:cygdrive_prefix_local] = '/cygdrive' # prefix for your local machine, set to empty string for MinGW 145 | knife[:cygdrive_prefix_remote] = '/cygdrive' # prefix on the remote windows node 146 | 147 | == DEVELOPMENT 148 | 149 | Get set up by running +./script/newb+ this will do some of the steps and guide you through the rest. If it doesn't run for you, feel free to {file an issue}[https://github.com/matschaffer/knife-solo/issues]. 150 | 151 | When running integration tests all output is sent to the log directory into a file that matches matches the test case name. The EC2Runner log is the main runner log that contains information about instance provisioning. 152 | 153 | Note that instances will remain running until your tests pass. This aids in speeding up the test cycle. Upon succesfful test completion you'll be given 10 seconds to cancel the process before the instances are cleaned up. Note that any instance tagged with knife_solo_integration_user == $USER will be cleaned up. Or if you want to leave your instances running regardless, specify SKIP_DESTROY=true as an environment variable. 154 | 155 | To make an integration test, create a file in the +test/integration+ directory and a test class that inherits from +IntegrationTest+ and includes a module from +test/integration/cases+. You can override methods as necessary, but generally you only need to override +user+ and +image_id+ to specify the user name and AMI ID. 156 | 157 | If you're interested in contributing, contact me via GitHub or have a look at the {GitHub issues page}[https://github.com/matschaffer/knife-solo/issues]. 158 | -------------------------------------------------------------------------------- /lib/knife-solo/ssh_command.rb: -------------------------------------------------------------------------------- 1 | module KnifeSolo 2 | module SshCommand 3 | 4 | def self.load_deps 5 | require 'knife-solo/ssh_connection' 6 | require 'knife-solo/tools' 7 | require 'net/ssh' 8 | require 'net/ssh/gateway' 9 | end 10 | 11 | def self.included(other) 12 | other.class_eval do 13 | # Lazy load our dependencies if the including class did not call 14 | # Knife#deps yet. Later calls to #deps override previous ones, so if 15 | # the outer class calls it, it should also call our #load_deps, i.e: 16 | # 17 | # Include KnifeSolo::SshCommand 18 | # 19 | # dep do 20 | # require 'foo' 21 | # require 'bar' 22 | # KnifeSolo::SshCommand.load_deps 23 | # end 24 | # 25 | deps { KnifeSolo::SshCommand.load_deps } unless defined?(@dependency_loader) 26 | 27 | option :ssh_config, 28 | :short => '-F CONFIG_FILE', 29 | :long => '--ssh-config-file CONFIG_FILE', 30 | :description => 'Alternate location for ssh config file' 31 | 32 | option :ssh_user, 33 | :short => '-x USERNAME', 34 | :long => '--ssh-user USERNAME', 35 | :description => 'The ssh username' 36 | 37 | option :ssh_password, 38 | :short => '-P PASSWORD', 39 | :long => '--ssh-password PASSWORD', 40 | :description => 'The ssh password' 41 | 42 | option :ssh_gateway, 43 | :long => '--ssh-gateway GATEWAY', 44 | :description => 'The ssh gateway' 45 | 46 | option :ssh_control_master, 47 | :long => '--ssh-control-master SETTING', 48 | :description => 'Control master setting to use when running rsync (use "auto" to enable)', 49 | :default => 'no' 50 | 51 | option :identity_file, 52 | :long => "--identity-file IDENTITY_FILE", 53 | :description => "The SSH identity file used for authentication. [DEPRECATED] Use --ssh-identity-file instead." 54 | 55 | option :ssh_identity_file, 56 | :short => "-i IDENTITY_FILE", 57 | :long => "--ssh-identity-file IDENTITY_FILE", 58 | :description => "The SSH identity file used for authentication" 59 | 60 | option :forward_agent, 61 | :long => '--forward-agent', 62 | :description => 'Forward SSH authentication. Adds -E to sudo, override with --sudo-command.', 63 | :boolean => true, 64 | :default => false 65 | 66 | option :ssh_port, 67 | :short => '-p PORT', 68 | :long => '--ssh-port PORT', 69 | :description => 'The ssh port' 70 | 71 | option :ssh_keepalive, 72 | :long => '--[no-]ssh-keepalive', 73 | :description => 'Use ssh keepalive', 74 | :default => true 75 | 76 | option :ssh_keepalive_interval, 77 | :long => '--ssh-keepalive-interval SECONDS', 78 | :description => 'The ssh keepalive interval', 79 | :default => 300, 80 | :proc => Proc.new { |v| v.to_i } 81 | 82 | option :startup_script, 83 | :short => '-s FILE', 84 | :long => '--startup-script FILE', 85 | :description => 'The startup script on the remote server containing variable definitions' 86 | 87 | option :sudo_command, 88 | :long => '--sudo-command SUDO_COMMAND', 89 | :description => 'The command to use instead of sudo for admin privileges' 90 | 91 | option :host_key_verify, 92 | :long => "--[no-]host-key-verify", 93 | :description => "Verify host key, enabled by default.", 94 | :boolean => true, 95 | :default => true 96 | 97 | end 98 | end 99 | 100 | def first_cli_arg_is_a_hostname? 101 | @name_args.first =~ /\A([^@]+(?>@)[^@]+|[^@]+?(?!@))\z/ 102 | end 103 | 104 | def validate_ssh_options! 105 | if config[:identity_file] 106 | ui.warn '`--identity-file` is deprecated, please use `--ssh-identity-file`.' 107 | end 108 | unless first_cli_arg_is_a_hostname? 109 | show_usage 110 | ui.fatal "You must specify [@] as the first argument" 111 | exit 1 112 | end 113 | if config[:ssh_user] 114 | host_descriptor[:user] ||= config[:ssh_user] 115 | end 116 | 117 | # NOTE: can't rely on default since it won't get called when invoked via knife bootstrap --solo 118 | if config[:ssh_keepalive_interval] && config[:ssh_keepalive_interval] <= 0 119 | ui.fatal '`--ssh-keepalive-interval` must be a positive number' 120 | exit 1 121 | end 122 | end 123 | 124 | def host_descriptor 125 | return @host_descriptor if defined?(@host_descriptor) 126 | parts = @name_args.first.split('@') 127 | @host_descriptor = { 128 | :host => parts.pop, 129 | :user => parts.pop 130 | } 131 | end 132 | 133 | def user 134 | host_descriptor[:user] || config_file_options[:user] || ENV['USER'] 135 | end 136 | 137 | def host 138 | host_descriptor[:host] 139 | end 140 | 141 | def ask_password 142 | ui.ask("Enter the password for #{user}@#{host}: ") do |q| 143 | q.echo = false 144 | q.whitespace = :chomp 145 | end 146 | end 147 | 148 | def password 149 | config[:ssh_password] ||= ask_password 150 | end 151 | 152 | def try_connection 153 | ssh_connection.session do |ssh| 154 | ssh.exec!("true") 155 | end 156 | end 157 | 158 | def config_file_options 159 | Net::SSH::Config.for(host, config_files) 160 | end 161 | 162 | def identity_file 163 | config[:ssh_identity] || config[:identity_file] || config[:ssh_identity_file] 164 | end 165 | 166 | def connection_options 167 | options = config_file_options 168 | options[:port] = config[:ssh_port] if config[:ssh_port] 169 | options[:password] = config[:ssh_password] if config[:ssh_password] 170 | options[:keys] = [identity_file] if identity_file 171 | options[:gateway] = config[:ssh_gateway] if config[:ssh_gateway] 172 | options[:forward_agent] = true if config[:forward_agent] 173 | if !config[:host_key_verify] 174 | options[:paranoid] = false 175 | options[:user_known_hosts_file] = "/dev/null" 176 | end 177 | if config[:ssh_keepalive] 178 | options[:keepalive] = config[:ssh_keepalive] 179 | options[:keepalive_interval] = config[:ssh_keepalive_interval] 180 | end 181 | # Respect users' specification of config[:ssh_config] 182 | # Prevents Net::SSH itself from applying the default ssh_config files. 183 | options[:config] = false 184 | options 185 | end 186 | 187 | def config_files 188 | Array(config[:ssh_config] || Net::SSH::Config.default_files) 189 | end 190 | 191 | def detect_authentication_method 192 | return @detected if @detected 193 | begin 194 | try_connection 195 | rescue Errno::ETIMEDOUT 196 | raise "Unable to connect to #{host}" 197 | rescue Net::SSH::AuthenticationFailed 198 | # Ensure the password is set or ask for it immediately 199 | password 200 | end 201 | @detected = true 202 | end 203 | 204 | def ssh_args 205 | args = [] 206 | 207 | args << [user, host].compact.join('@') 208 | 209 | args << "-F \"#{config[:ssh_config]}\"" if config[:ssh_config] 210 | args << "-i \"#{identity_file}\"" if identity_file 211 | args << "-o ForwardAgent=yes" if config[:forward_agent] 212 | args << "-p #{config[:ssh_port]}" if config[:ssh_port] 213 | args << "-o UserKnownHostsFile=\"#{connection_options[:user_known_hosts_file]}\"" if config[:host_key_verify] == false 214 | args << "-o StrictHostKeyChecking=no" if config[:host_key_verify] == false 215 | args << "-o ControlMaster=auto -o ControlPath=#{ssh_control_path} -o ControlPersist=3600" unless config[:ssh_control_master] == "no" 216 | 217 | args.join(' ') 218 | end 219 | 220 | def ssh_control_path 221 | dir = File.join(ENV['HOME'], '.chef', 'knife-solo-sockets') 222 | FileUtils.mkdir_p(dir) 223 | File.join(dir, '%C') 224 | end 225 | 226 | def custom_sudo_command 227 | if sudo_command=config[:sudo_command] 228 | Chef::Log.debug("Using replacement sudo command: #{sudo_command}") 229 | return sudo_command 230 | end 231 | end 232 | 233 | def standard_sudo_command 234 | return unless sudo_available? 235 | if config[:forward_agent] 236 | return 'sudo -E -p \'knife sudo password: \'' 237 | else 238 | return 'sudo -p \'knife sudo password: \'' 239 | end 240 | end 241 | 242 | def sudo_command 243 | custom_sudo_command || standard_sudo_command || '' 244 | end 245 | 246 | def startup_script 247 | config[:startup_script] 248 | end 249 | 250 | def windows_node? 251 | return @windows_node unless @windows_node.nil? 252 | @windows_node = run_command('ver', :process_sudo => false).stdout =~ /Windows/i 253 | if @windows_node 254 | Chef::Log.debug("Windows node detected") 255 | else 256 | @windows_node = false 257 | end 258 | @windows_node 259 | end 260 | 261 | def sudo_available? 262 | return @sudo_available unless @sudo_available.nil? 263 | @sudo_available = run_command('sudo -V', :process_sudo => false).success? 264 | Chef::Log.debug("`sudo` not available on #{host}") unless @sudo_available 265 | @sudo_available 266 | end 267 | 268 | def process_sudo(command) 269 | command.gsub(/sudo/, sudo_command) 270 | end 271 | 272 | def process_startup_file(command) 273 | command.insert(0, "source #{startup_script} && ") 274 | end 275 | 276 | def stream_command(command) 277 | run_command(command, :streaming => true) 278 | end 279 | 280 | def processed_command(command, options = {}) 281 | command = process_sudo(command) if options[:process_sudo] 282 | command = process_startup_file(command) if startup_script 283 | command 284 | end 285 | 286 | def run_command(command, options = {}) 287 | defaults = {:process_sudo => true} 288 | options = defaults.merge(options) 289 | 290 | detect_authentication_method 291 | 292 | Chef::Log.debug("Initial command #{command}") 293 | 294 | command = processed_command(command, options) 295 | Chef::Log.debug("Running processed command #{command}") 296 | 297 | output = ui.stdout if options[:streaming] 298 | 299 | @connection ||= ssh_connection 300 | @connection.run_command(command, output) 301 | end 302 | 303 | def ssh_connection 304 | SshConnection.new(host, user, connection_options, method(:password)) 305 | end 306 | 307 | # Runs commands from the specified array until successful. 308 | # Returns the result of the successful command or an ExecResult with 309 | # exit_code 1 if all fail. 310 | def run_with_fallbacks(commands, options = {}) 311 | commands.each do |command| 312 | result = run_command(command, options) 313 | return result if result.success? 314 | end 315 | SshConnection::ExecResult.new(1) 316 | end 317 | 318 | # TODO: 319 | # - move this to a dedicated "portability" module? 320 | # - use ruby in all cases instead? 321 | def run_portable_mkdir_p(folder, mode = nil) 322 | if windows_node? 323 | # no mkdir -p on windows - fake it 324 | run_command %Q{ruby -e "require 'fileutils'; FileUtils.mkdir_p('#{folder}', :mode => #{mode})"} 325 | else 326 | mode_option = (mode.nil? ? "" : "-m #{mode}") 327 | run_command "mkdir -p #{mode_option} #{folder}" 328 | end 329 | end 330 | 331 | end 332 | end 333 | -------------------------------------------------------------------------------- /lib/chef/knife/solo_cook.rb: -------------------------------------------------------------------------------- 1 | require 'chef/knife' 2 | 3 | require 'knife-solo/ssh_command' 4 | require 'knife-solo/node_config_command' 5 | require 'knife-solo/tools' 6 | 7 | class Chef 8 | class Knife 9 | # Approach ported from spatula (https://github.com/trotter/spatula) 10 | # Copyright 2009, Trotter Cashion 11 | class SoloCook < Knife 12 | 13 | include KnifeSolo::SshCommand 14 | include KnifeSolo::NodeConfigCommand 15 | include KnifeSolo::Tools 16 | 17 | deps do 18 | require 'chef/cookbook/chefignore' 19 | require 'knife-solo' 20 | require 'knife-solo/berkshelf' 21 | require 'knife-solo/librarian' 22 | require 'erubis' 23 | require 'pathname' 24 | KnifeSolo::SshCommand.load_deps 25 | KnifeSolo::NodeConfigCommand.load_deps 26 | end 27 | 28 | banner "knife solo cook [USER@]HOSTNAME [JSONFILE] (options)" 29 | 30 | option :chef_check, 31 | :long => '--no-chef-check', 32 | :description => 'Skip the Chef version check on the node', 33 | :default => true 34 | 35 | option :skip_chef_check, 36 | :long => '--skip-chef-check', 37 | :description => 'Deprecated. Replaced with --no-chef-check.' 38 | 39 | option :sync_only, 40 | :long => '--sync-only', 41 | :description => 'Only sync the cookbook - do not run Chef' 42 | 43 | option :sync, 44 | :long => '--no-sync', 45 | :description => 'Do not sync kitchen - only run Chef' 46 | 47 | option :berkshelf, 48 | :long => '--no-berkshelf', 49 | :description => 'Skip berks install' 50 | 51 | option :librarian, 52 | :long => '--no-librarian', 53 | :description => 'Skip librarian-chef install' 54 | 55 | option :secret_file, 56 | :long => '--secret-file SECRET_FILE', 57 | :description => 'A file containing the secret key used to encrypt data bag item values' 58 | 59 | option :why_run, 60 | :short => '-W', 61 | :long => '--why-run', 62 | :description => 'Enable whyrun mode' 63 | 64 | option :override_runlist, 65 | :short => '-o RunlistItem,RunlistItem...,', 66 | :long => '--override-runlist', 67 | :description => 'Replace current run list with specified items' 68 | 69 | option :provisioning_path, 70 | :long => '--provisioning-path path', 71 | :description => 'Where to store kitchen data on the node' 72 | 73 | option :clean_up, 74 | :long => '--clean-up', 75 | :description => 'Run the clean command after cooking' 76 | 77 | option :legacy_mode, 78 | :long => '--legacy-mode', 79 | :description => 'Run chef-solo in legacy mode' 80 | 81 | option :log_level, 82 | :short => '-l LEVEL', 83 | :long => '--log-level', 84 | :description => 'Set the log level for Chef' 85 | 86 | def run 87 | time('Run') do 88 | 89 | if config[:skip_chef_check] 90 | ui.warn '`--skip-chef-check` is deprecated, please use `--no-chef-check`.' 91 | config[:chef_check] = false 92 | end 93 | 94 | validate! 95 | 96 | ui.msg "Running Chef on #{host}..." 97 | 98 | check_chef_version if config[:chef_check] 99 | if config_value(:sync, true) 100 | generate_node_config 101 | berkshelf_install if config_value(:berkshelf, true) 102 | librarian_install if config_value(:librarian, true) 103 | patch_cookbooks_install 104 | sync_kitchen 105 | generate_solorb 106 | end 107 | cook unless config[:sync_only] 108 | 109 | clean_up if config[:clean_up] 110 | end 111 | end 112 | 113 | def validate! 114 | validate_ssh_options! 115 | 116 | if File.exist? 'solo.rb' 117 | ui.warn "solo.rb found, but since knife-solo v0.3.0 it is not used any more" 118 | ui.warn "Please read the upgrade instructions: https://github.com/matschaffer/knife-solo/wiki/Upgrading-to-0.3.0" 119 | end 120 | end 121 | 122 | def provisioning_path 123 | # TODO ~ will likely break on cmd.exe based windows sessions 124 | config_value(:provisioning_path, '~/chef-solo') 125 | end 126 | 127 | def sync_kitchen 128 | ui.msg "Uploading the kitchen..." 129 | run_portable_mkdir_p(provisioning_path, '0700') 130 | 131 | cookbook_paths.each_with_index do |path, i| 132 | upload_to_provision_path(path.to_s, "/cookbooks-#{i + 1}", 'cookbook_path') 133 | end 134 | upload_to_provision_path(node_config.to_s, 'dna.json') 135 | upload_to_provision_path(nodes_path, 'nodes') 136 | upload_to_provision_path(:role_path, 'roles') 137 | upload_to_provision_path(:data_bag_path, 'data_bags') 138 | upload_to_provision_path(config[:secret_file] || :encrypted_data_bag_secret, 'data_bag_key') 139 | upload_to_provision_path(:environment_path, 'environments') 140 | end 141 | 142 | def ssl_verify_mode 143 | Chef::Config[:ssl_verify_mode] || :verify_peer 144 | end 145 | 146 | def solo_legacy_mode 147 | Chef::Config[:solo_legacy_mode] || false 148 | end 149 | 150 | def chef_version_constraint 151 | Chef::Config[:solo_chef_version] || ">=0.10.4" 152 | end 153 | 154 | def log_level 155 | config_value(:log_level, Chef::Config[:log_level] || :warn).to_sym 156 | end 157 | 158 | def enable_reporting 159 | config_value(:enable_reporting, true) 160 | end 161 | 162 | def expand_path(path) 163 | Pathname.new(path).expand_path 164 | end 165 | 166 | def expanded_config_paths(key) 167 | Array(Chef::Config[key]).map { |path| expand_path path } 168 | end 169 | 170 | def cookbook_paths 171 | @cookbook_paths ||= expanded_config_paths(:cookbook_path) 172 | end 173 | 174 | def proxy_setting_keys 175 | [:http_proxy, :https_proxy, :http_proxy_user, :http_proxy_pass, :https_proxy_user, :https_proxy_pass, :no_proxy] 176 | end 177 | 178 | def proxy_settings 179 | proxy_setting_keys.inject(Hash.new) do |ret, key| 180 | ret[key] = Chef::Config[key] if Chef::Config[key] 181 | ret 182 | end 183 | end 184 | 185 | def add_cookbook_path(path) 186 | path = expand_path path 187 | cookbook_paths.unshift(path) unless cookbook_paths.include?(path) 188 | end 189 | 190 | def patch_cookbooks_path 191 | KnifeSolo.resource('patch_cookbooks') 192 | end 193 | 194 | def chefignore 195 | @chefignore ||= ::Chef::Cookbook::Chefignore.new("./") 196 | end 197 | 198 | # path must be adjusted to work on windows 199 | def adjust_rsync_path(path, path_prefix) 200 | path_s = path.to_s 201 | path_s.gsub(/^(\w):/) { path_prefix + "/#{$1}" } 202 | end 203 | 204 | def adjust_rsync_path_on_node(path) 205 | return path unless windows_node? 206 | adjust_rsync_path(path, config_value(:cygdrive_prefix_remote, '/cygdrive')) 207 | end 208 | 209 | def adjust_rsync_path_on_client(path) 210 | return path unless windows_client? 211 | adjust_rsync_path(path, config_value(:cygdrive_prefix_local, '/cygdrive')) 212 | end 213 | 214 | def rsync_debug 215 | '-v' if debug? 216 | end 217 | 218 | # see http://stackoverflow.com/questions/5798807/rsync-permission-denied-created-directories-have-no-permissions 219 | def rsync_permissions 220 | '--chmod=ugo=rwX' if windows_client? 221 | end 222 | 223 | def rsync_excludes 224 | (%w{revision-deploys .git .hg .svn .bzr} + chefignore.ignores).uniq 225 | end 226 | 227 | def debug? 228 | config[:verbosity] and config[:verbosity] > 0 229 | end 230 | 231 | # Time a command 232 | def time(msg) 233 | return yield unless debug? 234 | ui.msg "Starting '#{msg}'" 235 | start = Time.now 236 | yield 237 | ui.msg "#{msg} finished in #{Time.now - start} seconds" 238 | end 239 | 240 | def berkshelf_install 241 | path = KnifeSolo::Berkshelf.new(config, ui).install 242 | add_cookbook_path(path) if path 243 | end 244 | 245 | def librarian_install 246 | path = KnifeSolo::Librarian.new(config, ui).install 247 | add_cookbook_path(path) if path 248 | end 249 | 250 | def generate_solorb 251 | ui.msg "Generating solo config..." 252 | template = Erubis::Eruby.new(KnifeSolo.resource('solo.rb.erb').read) 253 | write(template.result(binding), provisioning_path + '/solo.rb') 254 | end 255 | 256 | def upload(src, dest) 257 | rsync(src, dest) 258 | end 259 | 260 | def upload_to_provision_path(src, dest, key_name = 'path') 261 | if src.is_a? Symbol 262 | key_name = src.to_s 263 | src = Chef::Config[src] 264 | end 265 | 266 | if src.nil? 267 | Chef::Log.debug "'#{key_name}' not set" 268 | elsif !src.is_a?(String) 269 | ui.error "#{key_name} is not a String: #{src.inspect}" 270 | elsif !File.exist?(src) 271 | ui.warn "Local #{key_name} '#{src}' does not exist" 272 | else 273 | upload("#{src}#{'/' if File.directory?(src)}", File.join(provisioning_path, dest)) 274 | end 275 | end 276 | 277 | # TODO probably can get Net::SSH to do this directly 278 | def write(content, dest) 279 | file = Tempfile.new(File.basename(dest)) 280 | file.write(content) 281 | file.close 282 | upload(file.path, dest) 283 | ensure 284 | file.unlink 285 | end 286 | 287 | def rsync(source_path, target_path, extra_opts = ['--delete-after', '-zt']) 288 | if config[:ssh_gateway] 289 | ssh_command = "ssh -TA #{config[:ssh_gateway]} ssh -T -o StrictHostKeyChecking=no #{ssh_args}" 290 | else 291 | ssh_command = "ssh #{ssh_args}" 292 | end 293 | 294 | cmd = ['rsync', '-rL', rsync_debug, rsync_permissions, %Q{--rsh=#{ssh_command}}] 295 | cmd += extra_opts 296 | cmd += rsync_excludes.map { |ignore| "--exclude=#{ignore}" } 297 | cmd += [ adjust_rsync_path_on_client(source_path), 298 | ':' + adjust_rsync_path_on_node(target_path) ] 299 | 300 | cmd = cmd.compact 301 | 302 | Chef::Log.debug cmd.inspect 303 | system!(*cmd) 304 | end 305 | 306 | def check_chef_version 307 | ui.msg "Checking Chef version..." 308 | unless chef_version_satisfies?(chef_version_constraint) 309 | raise "Couldn't find Chef #{chef_version_constraint} on #{host}. Please run `knife solo prepare #{ssh_args}` to ensure Chef is installed and up to date." 310 | end 311 | if node_environment != '_default' && chef_version_satisfies?('<11.6.0') 312 | ui.warn "Chef version #{chef_version} does not support environments. Environment '#{node_environment}' will be ignored." 313 | end 314 | end 315 | 316 | def chef_version_satisfies?(requirement) 317 | Gem::Requirement.new(requirement).satisfied_by? Gem::Version.new(chef_version) 318 | end 319 | 320 | # Parses "Chef: x.y.z" from the chef-solo version output 321 | def chef_version 322 | # Memoize the version to avoid multiple SSH calls 323 | @chef_version ||= lambda do 324 | cmd = %q{sudo chef-solo --version 2>/dev/null | awk '$1 == "Chef:" {print $2}'} 325 | run_command(cmd).stdout.strip 326 | end.call 327 | end 328 | 329 | def cook 330 | cmd = "sudo chef-solo -c #{provisioning_path}/solo.rb -j #{provisioning_path}/dna.json" 331 | cmd << " -l debug" if debug? 332 | cmd << " -N #{config[:chef_node_name]}" if config[:chef_node_name] 333 | cmd << " -W" if config[:why_run] 334 | cmd << " -o #{config[:override_runlist]}" if config[:override_runlist] 335 | if Gem::Version.new(::Chef::VERSION) >= Gem::Version.new("12.10.54") 336 | cmd << " --legacy-mode" if config[:legacy_mode] 337 | end 338 | 339 | ui.msg "Running Chef: #{cmd}" 340 | 341 | result = stream_command cmd 342 | raise "chef-solo failed. See output above." unless result.success? 343 | end 344 | 345 | def clean_up 346 | clean = SoloClean.new 347 | clean.ui = ui 348 | clean.name_args = @name_args 349 | clean.config.merge! config 350 | clean.run 351 | end 352 | 353 | protected 354 | 355 | def patch_cookbooks_install 356 | add_cookbook_path(patch_cookbooks_path) 357 | end 358 | end 359 | end 360 | end 361 | -------------------------------------------------------------------------------- /test/solo_cook_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'support/kitchen_helper' 3 | require 'support/validation_helper' 4 | 5 | require 'berkshelf' 6 | require 'chef/cookbook/chefignore' 7 | require 'chef/knife/solo_clean' 8 | require 'chef/knife/solo_cook' 9 | require 'fileutils' 10 | require 'knife-solo/berkshelf' 11 | require 'knife-solo/librarian' 12 | require 'librarian/action/install' 13 | 14 | class SuccessfulResult 15 | def success? 16 | true 17 | end 18 | end 19 | 20 | class SoloCookTest < TestCase 21 | include KitchenHelper 22 | include ValidationHelper::ValidationTests 23 | 24 | def test_chefignore_is_valid_object 25 | assert_instance_of Chef::Cookbook::Chefignore, command.chefignore 26 | end 27 | 28 | def test_rsync_exclude_sources_chefignore 29 | in_kitchen do 30 | file_to_ignore = "dummy.txt" 31 | File.open(file_to_ignore, 'w') {|f| f.puts "This file should be ignored"} 32 | File.open("chefignore", 'w') {|f| f.puts file_to_ignore} 33 | assert command.rsync_excludes.include?(file_to_ignore), "#{file_to_ignore} should have been excluded" 34 | end 35 | end 36 | 37 | def test_sets_ssl_verify_mode_returns_verify_peer_for_nil 38 | Chef::Config[:ssl_verify_mode] = nil 39 | assert_equal :verify_peer, command.ssl_verify_mode 40 | end 41 | 42 | def test_sets_ssl_verify_mode 43 | Chef::Config[:ssl_verify_mode] = :verify_none 44 | assert_equal :verify_none, command.ssl_verify_mode 45 | end 46 | 47 | def test_sets_solo_legacy_mode 48 | Chef::Config[:solo_legacy_mode] = true 49 | assert_equal true, command.solo_legacy_mode 50 | end 51 | 52 | def test_rsync_without_gateway_connection_options 53 | in_kitchen do 54 | 55 | cmd = knife_command(Chef::Knife::SoloCook) 56 | cmd.expects(:system!).with('rsync', 57 | '-rL', 58 | '--rsh=ssh ssh_arguments', 59 | '--exclude=revision-deploys', 60 | '--exclude=.git', 61 | '--exclude=.hg', 62 | '--exclude=.svn', 63 | '--exclude=.bzr', 64 | 'source', 65 | ':dest') 66 | 67 | cmd.stubs(:ssh_args => 'ssh_arguments') 68 | cmd.stubs(:windows_node? => false) 69 | 70 | cmd.rsync 'source', 'dest', [] 71 | end 72 | end 73 | 74 | def test_rsync_with_gateway_connection_options 75 | in_kitchen do 76 | 77 | cmd = knife_command(Chef::Knife::SoloCook) 78 | cmd.config[:ssh_gateway] = 'user@gateway' 79 | cmd.expects(:system!).with('rsync', 80 | '-rL', 81 | '--rsh=ssh -TA user@gateway ssh -T -o StrictHostKeyChecking=no ssh_arguments', 82 | '--exclude=revision-deploys', 83 | '--exclude=.git', 84 | '--exclude=.hg', 85 | '--exclude=.svn', 86 | '--exclude=.bzr', 87 | 'source', 88 | ':dest') 89 | 90 | cmd.stubs(:ssh_args => 'ssh_arguments') 91 | cmd.stubs(:windows_node? => false) 92 | 93 | cmd.rsync 'source', 'dest', [] 94 | end 95 | end 96 | 97 | def test_expanded_config_paths_returns_empty_array_for_nil 98 | Chef::Config[:foo] = nil 99 | assert_equal [], command.expanded_config_paths(:foo) 100 | end 101 | 102 | def test_expanded_config_paths_returns_pathnames 103 | Chef::Config[:foo] = ["foo"] 104 | assert_instance_of Pathname, command.expanded_config_paths(:foo).first 105 | end 106 | 107 | def test_expanded_config_paths_expands_paths 108 | Chef::Config[:foo] = ["foo", "/absolute/path"] 109 | paths = command.expanded_config_paths(:foo) 110 | assert_equal File.join(Dir.pwd, "foo"), paths[0].to_s 111 | assert_equal "/absolute/path", paths[1].to_s 112 | end 113 | 114 | def test_patch_cookbooks_paths_exists 115 | path = command.patch_cookbooks_path 116 | refute_nil path, "patch_cookbooks_path should not be nil" 117 | assert Dir.exist?(path), "patch_cookbooks_path is not a directory" 118 | end 119 | 120 | def test_cookbook_paths_expands_paths 121 | cmd = command 122 | Chef::Config.cookbook_path = ["mycookbooks", "/some/other/path"] 123 | assert_equal File.join(Dir.pwd, "mycookbooks"), cmd.cookbook_paths[0].to_s 124 | assert_equal "/some/other/path", cmd.cookbook_paths[1].to_s 125 | end 126 | 127 | def test_add_cookbook_path_prepends_the_path 128 | cmd = command 129 | Chef::Config.cookbook_path = ["mycookbooks", "/some/other/path"] 130 | cmd.add_cookbook_path "/new/path" 131 | assert_equal "/new/path", cmd.cookbook_paths[0].to_s 132 | assert_equal File.join(Dir.pwd, "mycookbooks"), cmd.cookbook_paths[1].to_s 133 | assert_equal "/some/other/path", cmd.cookbook_paths[2].to_s 134 | end 135 | 136 | # NOTE (mat): Looks like chef::config might be setting HTTP_PROXY which blocks 137 | # subsequent HTTP requests during tests (like sending coverage reports). 138 | # Commenting out until this can be re-written with appropriate stubbing. 139 | # def test_sets_proxy_settings 140 | # Chef::Config[:http_proxy] = "http://proxy:3128" 141 | # Chef::Config[:no_proxy] = nil 142 | # conf = command.proxy_settings 143 | # assert_equal({ :http_proxy => "http://proxy:3128" }, conf) 144 | # end 145 | 146 | def test_adds_patch_cookboks_with_lowest_precedence 147 | in_kitchen do 148 | cmd = command("somehost") 149 | cmd.run 150 | #note: cookbook_paths are in order of precedence (low->high) 151 | assert_equal cmd.patch_cookbooks_path, cmd.cookbook_paths[0] 152 | end 153 | end 154 | 155 | def test_does_not_run_berkshelf_if_no_berkfile 156 | in_kitchen do 157 | Berkshelf::Berksfile.any_instance.expects(:vendor).never 158 | command("somehost").run 159 | end 160 | end 161 | 162 | def test_runs_berkshelf_if_berkfile_found 163 | in_kitchen do 164 | FileUtils.touch "Berksfile" 165 | Berkshelf::Berksfile.any_instance.expects(:vendor) 166 | command("somehost").run 167 | end 168 | end 169 | 170 | def test_does_not_run_berkshelf_if_denied_by_option 171 | in_kitchen do 172 | FileUtils.touch "Berksfile" 173 | Berkshelf::Berksfile.any_instance.expects(:vendor).never 174 | command("somehost", "--no-berkshelf").run 175 | end 176 | end 177 | 178 | def test_complains_if_berkshelf_gem_missing 179 | in_kitchen do 180 | FileUtils.touch "Berksfile" 181 | cmd = command("somehost") 182 | cmd.ui.expects(:warn).with(regexp_matches(/LoadError/)) 183 | cmd.ui.expects(:warn).with(regexp_matches(/berkshelf gem/)) 184 | KnifeSolo::Berkshelf.expects(:load_gem).raises(LoadError) 185 | Berkshelf::Berksfile.any_instance.expects(:vendor).never 186 | cmd.run 187 | end 188 | end 189 | 190 | def test_wont_complain_if_berkshelf_gem_missing_but_no_berkfile 191 | in_kitchen do 192 | cmd = command("somehost") 193 | cmd.ui.expects(:fatal).never 194 | KnifeSolo::Berkshelf.expects(:load_gem).never 195 | Berkshelf::Berksfile.any_instance.expects(:vendor).never 196 | cmd.run 197 | end 198 | end 199 | 200 | def test_adds_berkshelf_path_to_cookbooks 201 | in_kitchen do 202 | FileUtils.touch "Berksfile" 203 | KnifeSolo::Berkshelf.any_instance.stubs(:berkshelf_path).returns("berkshelf/path") 204 | Berkshelf::Berksfile.any_instance.stubs(:vendor) 205 | cmd = command("somehost") 206 | cmd.run 207 | assert_equal File.join(Dir.pwd, "berkshelf/path"), cmd.cookbook_paths[1].to_s 208 | end 209 | end 210 | 211 | def test_does_not_run_librarian_if_no_cheffile 212 | in_kitchen do 213 | Librarian::Action::Install.any_instance.expects(:run).never 214 | command("somehost").run 215 | end 216 | end 217 | 218 | def test_runs_librarian_if_cheffile_found 219 | in_kitchen do 220 | FileUtils.touch "Cheffile" 221 | Librarian::Action::Install.any_instance.expects(:run) 222 | command("somehost").run 223 | end 224 | end 225 | 226 | def test_does_not_run_librarian_if_denied_by_option 227 | in_kitchen do 228 | FileUtils.touch "Cheffile" 229 | Librarian::Action::Install.any_instance.expects(:run).never 230 | command("somehost", "--no-librarian").run 231 | end 232 | end 233 | 234 | def test_complains_if_librarian_gem_missing 235 | in_kitchen do 236 | FileUtils.touch "Cheffile" 237 | cmd = command("somehost") 238 | cmd.ui.expects(:warn).with(regexp_matches(/LoadError/)) 239 | cmd.ui.expects(:warn).with(regexp_matches(/librarian-chef gem/)) 240 | KnifeSolo::Librarian.expects(:load_gem).raises(LoadError) 241 | Librarian::Action::Install.any_instance.expects(:run).never 242 | cmd.run 243 | end 244 | end 245 | 246 | def test_wont_complain_if_librarian_gem_missing_but_no_cheffile 247 | in_kitchen do 248 | cmd = command("somehost") 249 | cmd.ui.expects(:err).never 250 | KnifeSolo::Librarian.expects(:load_gem).never 251 | Librarian::Action::Install.any_instance.expects(:run).never 252 | cmd.run 253 | end 254 | end 255 | 256 | def test_adds_librarian_path_to_cookbooks 257 | ENV['LIBRARIAN_CHEF_PATH'] = "librarian/path" 258 | in_kitchen do 259 | FileUtils.touch "Cheffile" 260 | Librarian::Action::Install.any_instance.stubs(:run) 261 | cmd = command("somehost") 262 | cmd.run 263 | assert_equal File.join(Dir.pwd, "librarian/path"), cmd.cookbook_paths[1].to_s 264 | end 265 | end 266 | 267 | def test_runs_clean_after_cook_if_enabled_by_option 268 | Chef::Knife::SoloClean.any_instance.expects(:run) 269 | 270 | in_kitchen do 271 | command("somehost", "--clean-up").run 272 | end 273 | end 274 | 275 | def test_does_not_run_clean_after_cook_if_not_enabled_by_option 276 | Chef::Knife::SoloClean.any_instance.expects(:run).never 277 | 278 | in_kitchen do 279 | command("somehost").run 280 | end 281 | end 282 | 283 | def test_validates_chef_version 284 | in_kitchen do 285 | cmd = command("somehost") 286 | cmd.expects(:check_chef_version) 287 | cmd.run 288 | end 289 | end 290 | 291 | def test_does_not_validate_chef_version_if_denied_by_option 292 | in_kitchen do 293 | cmd = command("somehost", "--no-chef-check") 294 | cmd.expects(:check_chef_version).never 295 | cmd.run 296 | end 297 | end 298 | 299 | def test_accept_valid_chef_version 300 | in_kitchen do 301 | cmd = command("somehost") 302 | cmd.unstub(:check_chef_version) 303 | cmd.stubs(:chef_version).returns("11.2.0") 304 | cmd.run 305 | end 306 | end 307 | 308 | def test_barks_if_chef_not_found 309 | in_kitchen do 310 | cmd = command("somehost") 311 | cmd.unstub(:check_chef_version) 312 | cmd.stubs(:chef_version).returns("") 313 | assert_raises RuntimeError do 314 | cmd.run 315 | end 316 | end 317 | end 318 | 319 | def test_barks_if_chef_too_old 320 | in_kitchen do 321 | cmd = command("somehost") 322 | cmd.unstub(:check_chef_version) 323 | cmd.stubs(:chef_version).returns("0.8.0") 324 | assert_raises RuntimeError do 325 | cmd.run 326 | end 327 | end 328 | end 329 | 330 | def test_does_not_cook_if_sync_only_specified 331 | in_kitchen do 332 | cmd = command("somehost", "--sync-only") 333 | cmd.expects(:cook).never 334 | cmd.run 335 | end 336 | end 337 | 338 | def test_does_not_sync_if_no_sync_specified 339 | in_kitchen do 340 | cmd = command("somehost", "--no-sync") 341 | cmd.expects(:sync_kitchen).never 342 | cmd.run 343 | end 344 | end 345 | 346 | def test_passes_node_name_to_chef_solo 347 | assert_chef_solo_option "--node-name=mynode", "-N mynode" 348 | end 349 | 350 | def test_passes_whyrun_mode_to_chef_solo 351 | assert_chef_solo_option "--why-run", "-W" 352 | end 353 | 354 | def test_passes_override_runlist_to_chef_solo 355 | assert_chef_solo_option "--override-runlist=sandbox::default", "-o sandbox::default" 356 | end 357 | 358 | def test_passes_legacy_mode_to_chef_solo 359 | if Gem::Version.new(::Chef::VERSION) >= Gem::Version.new("12.10.54") 360 | assert_chef_solo_option "--legacy-mode", "--legacy-mode" 361 | else 362 | matcher = regexp_matches(/\s#{Regexp.quote("--legacy-mode")}(\s|$)/) 363 | in_kitchen do 364 | cmd = command("somehost", "--legacy-mode") 365 | cmd.expects(:stream_command).with(Not(matcher)).returns(SuccessfulResult.new) 366 | cmd.run 367 | 368 | cmd = command("somehost") 369 | cmd.expects(:stream_command).with(Not(matcher)).returns(SuccessfulResult.new) 370 | cmd.run 371 | end 372 | end 373 | end 374 | 375 | # Asserts that the chef_solo_option is passed to chef-solo iff cook_option 376 | # is specified for the cook command 377 | def assert_chef_solo_option(cook_option, chef_solo_option) 378 | matcher = regexp_matches(/\s#{Regexp.quote(chef_solo_option)}(\s|$)/) 379 | in_kitchen do 380 | cmd = command("somehost", cook_option) 381 | cmd.expects(:stream_command).with(matcher).returns(SuccessfulResult.new) 382 | cmd.run 383 | 384 | cmd = command("somehost") 385 | cmd.expects(:stream_command).with(Not(matcher)).returns(SuccessfulResult.new) 386 | cmd.run 387 | end 388 | end 389 | 390 | def command(*args) 391 | cmd = knife_command(Chef::Knife::SoloCook, *args) 392 | cmd.stubs(:check_chef_version) 393 | cmd.stubs(:run_portable_mkdir_p) 394 | cmd.stubs(:rsync) 395 | cmd.stubs(:stream_command).returns(SuccessfulResult.new) 396 | cmd 397 | end 398 | end 399 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.7.0 / 2018-11-14 2 | 3 | ## Updates 4 | 5 | * OpenSUSE Leap support (#492) 6 | * Legacy mode option (#504) 7 | * Allow net-ssh 4.x (#506) 8 | * Use sudo when running clean (#519) 9 | * Configurable log level (#522) 10 | * Debian 9 support (#524) 11 | * Removed testing for EOL chefs and rubies, ad support for new rubies (#516, #528) 12 | * Make chef version check configurable (#535) 13 | 14 | # 0.6.0 / 2016-05-23 15 | 16 | ## Changes and new features 17 | 18 | * Added support for Mac OS 10.10 ([456][]) 19 | * **BREAKING** Updated ssh key argument to `--ssh-identity-file` to match new knife conventions 20 | 21 | ## Fixes 22 | 23 | * Loosen net-ssh requirement to gain compatibility with newer ChefDK releases ([481][]) 24 | * Ensure non-interactive mode on Debian x86 boostrap ([471][]) 25 | * Include user and port in control path ([468][]) 26 | * Default control master to "no" when running from cygwin ([453][]) 27 | * Use hashed control path (requires recent openssh) ([470][]) 28 | * Pass node name down to solo.rb (forward port from 0.4.3) ([452][]) 29 | * Use `const_get` to help spot bootstrap detection errors more easily ([467][]) 30 | 31 | ## Thanks to our contributors! 32 | 33 | * [Austin Schutz][aaadschutz] 34 | * [Shawn Xu][xunnanxu] 35 | * [Derek Tamsen][derektamsen] 36 | * [Przemysław Dąbek][szemek] 37 | * [Mark Woods][thickpaddy] 38 | * [Tomohiko IIDA][iidatomohiko] 39 | * [Jeroen Jacobs][jeroenj] 40 | 41 | [470]: https://github.com/matschaffer/knife-solo/issues/470 42 | [456]: https://github.com/matschaffer/knife-solo/issues/456 43 | [481]: https://github.com/matschaffer/knife-solo/issues/481 44 | [471]: https://github.com/matschaffer/knife-solo/issues/471 45 | [468]: https://github.com/matschaffer/knife-solo/issues/468 46 | [453]: https://github.com/matschaffer/knife-solo/issues/453 47 | [452]: https://github.com/matschaffer/knife-solo/issues/452 48 | [467]: https://github.com/matschaffer/knife-solo/issues/467 49 | 50 | # 0.5.1 / 2015-08-28 51 | 52 | Re-release of 0.5.0 for test & doc fixes. 53 | 54 | # 0.5.0 / 2015-08-28 55 | 56 | ## Changes and new features 57 | 58 | * Configurable cygwin prefixes ([378][]) 59 | * Configurable sudo command ([394][]) 60 | * SSH keepalive support, required bump to Chef >=10.20 ([288][], [404][]) 61 | * Configurable secret file location ([412][]) 62 | * Allow passing options to Berkshelf (>=3 only) ([369][]) 63 | * Arch Linux support ([389][], [393][], [396][]) 64 | * Manjaro Linux support ([414][]) 65 | * Debian 8 support ([436][]) 66 | * Use control sockets to speed up rsync transfers ([440][]) 67 | * Support for ohai hints ([339][]) 68 | 69 | ## Fixes 70 | 71 | * Updates for Berkshelf 3 ([376][], [405][]) 72 | * Better warnings for berkshelf/librarian ([383][]) 73 | * Show chef command during run output ([401][]) 74 | * Make `ssl_verify_mode` `:verify_peer` by default ([413][]) 75 | * Avoid copying local http proxy info to node's knife.rb ([421][]) 76 | 77 | ## Thanks to our contributors! 78 | 79 | * [Ivan Tanev][VanTanev] 80 | * [Masato Ikeda][a2ikm] 81 | * [Graham Lyus][grahamlyus] 82 | * [Oleg Selin][OlegPS] 83 | * [Todd Willey][xtoddx] 84 | * [DQNEO][DQNEO] 85 | * [Kevin McAllister][mclazarus] 86 | * [Yoz (Jeremy) Grahame][yozlet] 87 | * [Nori Yoshioka][noric] 88 | * [Josh Yotty][jyotty] 89 | * [Markus Kern][makern] 90 | * [esio][es1o] 91 | * [Evgeny Vereshchagin][evverx] 92 | * [Tomo Masakura][masakura] 93 | * [Iavael][iavael] 94 | * [Patrick Connolly][patcon] 95 | * [Markus Kern][makern] 96 | 97 | [378]: https://github.com/matschaffer/knife-solo/issues/378 98 | [376]: https://github.com/matschaffer/knife-solo/issues/376 99 | [383]: https://github.com/matschaffer/knife-solo/issues/383 100 | [389]: https://github.com/matschaffer/knife-solo/issues/389 101 | [393]: https://github.com/matschaffer/knife-solo/issues/393 102 | [396]: https://github.com/matschaffer/knife-solo/issues/396 103 | [394]: https://github.com/matschaffer/knife-solo/issues/394 104 | [401]: https://github.com/matschaffer/knife-solo/issues/401 105 | [405]: https://github.com/matschaffer/knife-solo/issues/405 106 | [288]: https://github.com/matschaffer/knife-solo/issues/288 107 | [404]: https://github.com/matschaffer/knife-solo/issues/404 108 | [413]: https://github.com/matschaffer/knife-solo/issues/413 109 | [412]: https://github.com/matschaffer/knife-solo/issues/412 110 | [414]: https://github.com/matschaffer/knife-solo/issues/414 111 | [421]: https://github.com/matschaffer/knife-solo/issues/421 112 | [369]: https://github.com/matschaffer/knife-solo/issues/369 113 | [436]: https://github.com/matschaffer/knife-solo/issues/436 114 | [440]: https://github.com/matschaffer/knife-solo/issues/440 115 | [339]: https://github.com/matschaffer/knife-solo/issues/339 116 | 117 | # 0.4.3 / 2015-09-10 118 | 119 | ## Fixes 120 | 121 | * Pass node name down to solo.rb ([452][]) 122 | 123 | ## Thanks to our contributors! 124 | 125 | * [aaadschutz][aaadschutz] 126 | 127 | # 0.4.2 / 2014-06-05 128 | 129 | ## Changes and new features 130 | 131 | * SSH agent forwarding support ([328][], [347][], [358][]) 132 | * SSH gateway support ([8d1f4f8][]) 133 | * Support non-bash shells (csh, sh, dash, bash, and fish) ([351][]) 134 | * Add `node_name` & `ssl_verify_mode` to `solo.rb` ([359][], [363][]) 135 | * Store IP address in generated node file ([360][]) 136 | * openSUSE 13 support ([352][]) 137 | * Mavericks support ([356][]) 138 | 139 | ## Fixes 140 | 141 | * Avoid Net::SSH auto-reading default openssh configs when an ssh config file is used ([341][]) 142 | 143 | ## Thanks to our contributors! 144 | 145 | * [Yuki Sonoda][yugui] 146 | * [Todd Willey][xtoddx] 147 | * [Andy Leonard][anl] 148 | * [Robert L. Carpenter][robacarp] 149 | * [alexsiri7][alexsiri7] 150 | * [Ravil Bayramgalin][brainopia] 151 | * [Chun-wei Kuo][Domon] 152 | * [Angel Abad][angelabad] 153 | 154 | [308]: https://github.com/matschaffer/knife-solo/issues/308 155 | [328]: https://github.com/matschaffer/knife-solo/issues/328 156 | [347]: https://github.com/matschaffer/knife-solo/issues/347 157 | [351]: https://github.com/matschaffer/knife-solo/issues/351 158 | [352]: https://github.com/matschaffer/knife-solo/issues/352 159 | [358]: https://github.com/matschaffer/knife-solo/issues/358 160 | [359]: https://github.com/matschaffer/knife-solo/issues/359 161 | [360]: https://github.com/matschaffer/knife-solo/issues/360 162 | [363]: https://github.com/matschaffer/knife-solo/issues/363 163 | [356]: https://github.com/matschaffer/knife-solo/issues/356 164 | [8d1f4f8]: https://github.com/matschaffer/knife-solo/commit/8d1f4f8 165 | 166 | # 0.4.1 / 2013-12-07 167 | 168 | ## Changes and new features 169 | 170 | * Support Oracle Enterprise Linux ([311][]) 171 | * Support Arch Linux ([327][]) 172 | * Make sure wget is installed on Debianoids ([320][]) 173 | 174 | ## Fixes 175 | 176 | * Explicitly set root path for default `*_path` options in Chef 11.8.0 ([308][]) 177 | * Verify that `node_path` is a String ([308][]) 178 | * Fix compatibility with Ruby 1.8 ([0bcae4a][]) 179 | 180 | ## Thanks to our contributors! 181 | 182 | * [aromarom64][aromarom64] 183 | * [Łukasz Dubiel][bambuchaAdm] 184 | * [Takamura Soichi][piglovesyou] 185 | 186 | [308]: https://github.com/matschaffer/knife-solo/issues/308 187 | [311]: https://github.com/matschaffer/knife-solo/issues/311 188 | [320]: https://github.com/matschaffer/knife-solo/issues/320 189 | [327]: https://github.com/matschaffer/knife-solo/issues/327 190 | [0bcae4a]: https://github.com/matschaffer/knife-solo/commit/0bcae4a 191 | 192 | # 0.4.0 / 2013-10-30 193 | 194 | ## Changes and new features 195 | 196 | * Add SSH option --[no-]host-key-verify ([274]) 197 | * Add --no-sync option to skip syncing and only run Chef ([284]) 198 | * Use Omnibus installer for Debian 7 ([287]) 199 | * Support Raspbian ([295]) 200 | * Support environments (from Chef 11.6.0 on) ([285]) 201 | 202 | ## Fixes 203 | 204 | * Cache SSH connections ([265]) 205 | * Support Berkshelf 3.0 ([268]) 206 | * Follow symlinks when uploading kitchen ([279], [289]) 207 | * Quote rsync paths to avoid problems with spaces in directory names ([281], [286]) 208 | * Fix precedence of automatic cookbook_path components ([296], [298]) 209 | * Print an error message if a `*_path` configuration value is not a String ([278], [300]) 210 | * Mention knife-solo_data_bag gem in the docs ([83]) 211 | * Pin to ffi 1.9.1 due to issues with newer versions 212 | * Docs around berkshelf and librarian-chef integrations 213 | 214 | ## Thanks to our contributors! 215 | 216 | * [Andreas Josephson][teyrow] 217 | * [Markus Kern][makern] 218 | * [Michael Glass][michaelglass] 219 | * [Mathieu Allaire][allaire] 220 | 221 | [83]: https://github.com/matschaffer/knife-solo/issues/83 222 | [265]: https://github.com/matschaffer/knife-solo/issues/265 223 | [268]: https://github.com/matschaffer/knife-solo/issues/268 224 | [274]: https://github.com/matschaffer/knife-solo/issues/274 225 | [278]: https://github.com/matschaffer/knife-solo/issues/278 226 | [279]: https://github.com/matschaffer/knife-solo/issues/279 227 | [281]: https://github.com/matschaffer/knife-solo/issues/281 228 | [284]: https://github.com/matschaffer/knife-solo/issues/284 229 | [285]: https://github.com/matschaffer/knife-solo/issues/285 230 | [286]: https://github.com/matschaffer/knife-solo/issues/286 231 | [287]: https://github.com/matschaffer/knife-solo/issues/287 232 | [289]: https://github.com/matschaffer/knife-solo/issues/289 233 | [295]: https://github.com/matschaffer/knife-solo/issues/295 234 | [296]: https://github.com/matschaffer/knife-solo/issues/296 235 | [298]: https://github.com/matschaffer/knife-solo/issues/298 236 | [300]: https://github.com/matschaffer/knife-solo/issues/300 237 | 238 | # 0.3.0 / 2013-08-01 239 | 240 | **NOTE**: This release includes breaking changes. See [upgrade instructions](https://github.com/matschaffer/knife-solo/wiki/Upgrading-to-0.3.0) for more information. 241 | 242 | ## Changes and new features 243 | 244 | * BREAKING: Generate solo.rb based on knife.rb settings ([199]) 245 | - `solo.rb` is not used and a warning is issued if it is still found 246 | - You have to specify the local cookbook etc. paths in `.chef/knife.rb` 247 | * BREAKING: Set root path with `--provisioning-path` or `knife[:provisioning_path]` and use ~/chef-solo by default ([1], [86], [125], [128], [177], [197]) 248 | * BREAKING: Remove hard dependency on Librarian-Chef ([211]) 249 | - If you use Librarian integration you need to install the librarian-chef gem yourself 250 | * Add Berkshelf integration ([227]) 251 | * Automatically add librarian to cookbook paths when syncing ([226]) 252 | * Add `--solo` option and `knife[:solo]` configuration option to `knife bootstrap` ([207]) 253 | * `--prerelease` option to allow pre-release versions of chef omnibus or rubygem to be installed ([205]) 254 | * Prepare/bootstrap now installs the same version of Chef that the workstation is running ([186]) 255 | * Switch `--omnibus-version` flag to `--bootstrap-version` ([185]) 256 | * Support `--override-runlist` option ([204]) 257 | * Add proxy settings to the generated solo.rb ([254]) 258 | * Support Fedora 18 and other new EL distros ([229], [51a581]) 259 | * Support CloudLinux ([262]) 260 | * Drop support for openSUSE 11 261 | * Upgrade chef-solo-search to v0.4.0 262 | * Add `--sudo-command` option to use custom remote 'sudo' command ([256]) 263 | 264 | ## Fixes 265 | 266 | * Read protect the provision directory from the world ([1]) 267 | * FreeBSD 9.1 support 268 | * OS X (especially 10.8) support ([209], [210]) 269 | * Clear yum cache before installing rsync ([200]) 270 | * Make sure "ca-certificates" package is installed on Debian ([213]) 271 | * Ensure rsync is installed on openSUSE ([f43ba4]) 272 | * Clean up bootstrap classes ([213]) 273 | * Rsync dot files by default, exclude only VCS dirs ([d21756], [1d3485]) 274 | * Standardize messaging across commands ([215], [d37162]) 275 | * Librarian-Chef was not run by default when knife-solo was invoked from ruby ([221]) 276 | * Fix `solo init` on Ruby 1.8 ([230]) 277 | * Fix deprecated commands to include options and dependencies from the new ones ([233]) 278 | * Parse Chef version even if chef-solo command prints warnings ([235], [238]) 279 | * Upgrade CentOS in integration tests to 5.8 and 6.3 ([237]) 280 | * Default to `lsb_release` for detecting the Linux distro and add support for RHEL based systems where /etc/issue is modified ([234], [242]) 281 | * Create the directory for node_config if it does not exist ([253]) 282 | * Accept trailing whitespace in passwords ([264]) 283 | 284 | ## Thanks to our contributors! 285 | 286 | * [David Kinzer][dkinzer] 287 | * [Naoya Ito][naoya] 288 | * [David Radcliffe][dwradcliffe] 289 | * [Łukasz Dubiel][bambuchaAdm] 290 | * [kmdsbng][kmdsbng] 291 | * [Darshan Patil][dapatil] 292 | * [Shin Tokiwa][tocky] 293 | * [Sam Martin][searlm] 294 | 295 | [1]: https://github.com/matschaffer/knife-solo/issues/1 296 | [86]: https://github.com/matschaffer/knife-solo/issues/86 297 | [125]: https://github.com/matschaffer/knife-solo/issues/125 298 | [128]: https://github.com/matschaffer/knife-solo/issues/128 299 | [177]: https://github.com/matschaffer/knife-solo/issues/177 300 | [185]: https://github.com/matschaffer/knife-solo/issues/185 301 | [186]: https://github.com/matschaffer/knife-solo/issues/186 302 | [197]: https://github.com/matschaffer/knife-solo/issues/197 303 | [199]: https://github.com/matschaffer/knife-solo/issues/199 304 | [200]: https://github.com/matschaffer/knife-solo/issues/200 305 | [204]: https://github.com/matschaffer/knife-solo/issues/204 306 | [205]: https://github.com/matschaffer/knife-solo/issues/205 307 | [207]: https://github.com/matschaffer/knife-solo/issues/207 308 | [209]: https://github.com/matschaffer/knife-solo/issues/209 309 | [210]: https://github.com/matschaffer/knife-solo/issues/210 310 | [211]: https://github.com/matschaffer/knife-solo/issues/211 311 | [213]: https://github.com/matschaffer/knife-solo/issues/213 312 | [215]: https://github.com/matschaffer/knife-solo/issues/215 313 | [221]: https://github.com/matschaffer/knife-solo/issues/221 314 | [226]: https://github.com/matschaffer/knife-solo/issues/226 315 | [227]: https://github.com/matschaffer/knife-solo/issues/227 316 | [229]: https://github.com/matschaffer/knife-solo/issues/229 317 | [230]: https://github.com/matschaffer/knife-solo/issues/230 318 | [233]: https://github.com/matschaffer/knife-solo/issues/233 319 | [234]: https://github.com/matschaffer/knife-solo/issues/234 320 | [235]: https://github.com/matschaffer/knife-solo/issues/235 321 | [237]: https://github.com/matschaffer/knife-solo/issues/237 322 | [238]: https://github.com/matschaffer/knife-solo/issues/238 323 | [242]: https://github.com/matschaffer/knife-solo/issues/242 324 | [253]: https://github.com/matschaffer/knife-solo/issues/253 325 | [254]: https://github.com/matschaffer/knife-solo/issues/254 326 | [256]: https://github.com/matschaffer/knife-solo/issues/256 327 | [262]: https://github.com/matschaffer/knife-solo/issues/262 328 | [264]: https://github.com/matschaffer/knife-solo/issues/264 329 | [d21756]: https://github.com/matschaffer/knife-solo/commit/d21756 330 | [1d3485]: https://github.com/matschaffer/knife-solo/commit/1d3485 331 | [f43ba4]: https://github.com/matschaffer/knife-solo/commit/f43ba4 332 | [51a581]: https://github.com/matschaffer/knife-solo/commit/51a581 333 | [d37162]: https://github.com/matschaffer/knife-solo/commit/d37162 334 | 335 | # 0.2.0 / 2013-02-12 336 | 337 | ## Changes and new features 338 | 339 | * Post-install hook to remind people about removing old gems (#152) 340 | * Support Chef 11 (#183) 341 | * Rename Cook command's `--skip-chef-check` option to `--no-chef-check (#162) 342 | * Rename `--ssh-identity` option to `--identity-file` (#178) 343 | * Add `--ssh-user option` (#179) 344 | * Add `--no-librarian` option to bootstrap and cook commands (#180) 345 | * Generate Cheffile and .gitignore on `knife solo init --librarian` (#182) 346 | * Windows client compatibility (#156, #91) 347 | * Support Amazon Linux (#181) 348 | * Support unknown/unreleased Debian versions by using the 349 | gem installer (#172) 350 | * Drop support for Debian 5.0 Lenny (#172) 351 | * Integration tests for Debian 6 and 7 (74c6ed1 - f299a6) 352 | * Travis tests for both Chef 10 and 11 (#183) 353 | 354 | ## Fixes 355 | 356 | * Fix Debian 7.0 Wheezy support by using gem installer (#172) 357 | * Fix compatibility with Ruby 1.8.7 on work station (#170) 358 | * Fix Chef version checking if sudo promts password (#190) 359 | * Fix compatibility (net-ssh dependency) with Chef 10.20.0 and 11.2.0 (#188) 360 | * Fail CI if manifest isn't updated (#195) 361 | * Better unit tests around solo cook 362 | * Other fixes: #166, #168, #173, #194 363 | 364 | ## Thanks to our contributors! 365 | 366 | * [Russell Cardullo][russellcardullo] 367 | * [tknerr][tknerr] 368 | * [Shaun Dern][smdern] 369 | * [Mike Bain][TheAlphaTester] 370 | 371 | # 0.1.0 / 2013-01-12 372 | 373 | ## Changes and new features 374 | 375 | * Move all commands under "knife solo" namespace (#118) 376 | - Rename `knife kitchen` to `knife solo init` 377 | - Rename `knife wash_up` to `knife solo clean` 378 | * Add `knife solo bootstrap` command (#120) 379 | * OmniOS support (#144) 380 | * Detect Fedora 17 (#141) 381 | * Update chef-solo-search and add support of encrypted data bags (#127) 382 | * Support Librarian (#36) 383 | * Always install rsync from yum on RPM-based Linuxes (#157) 384 | * Debian wheezy (7) support (#165) 385 | 386 | ## Fixes 387 | 388 | * Improve help/error messages and validation (#142) 389 | * Fix exit status of "cook" if chef-solo fails (#97) 390 | * Fix option passing to the Omnibus installer (#163) 391 | * Other fixes: SuSE omnibus #146, #155, #158, #160, #164 392 | 393 | ## Documentation 394 | 395 | * Include documentation and tests in the gem (e01c23) 396 | * [Home page](http://matschaffer.github.io/knife-solo/) 397 | that reflects always the current release (#151) 398 | 399 | ## Thanks to our contributors! 400 | 401 | * [Marek Hulan][ares] 402 | * [Anton Orel][skyeagle] 403 | * [Adam Carlile][Frozenproduce] 404 | * [Chris Lundquist][ChrisLundquist] 405 | * [Hiten Parmar][hrp] 406 | * [Patrick Connolly][patcon] 407 | 408 | # 0.0.15 / 2012-11-29 409 | 410 | * Support for non-x86 omnibus (#137) 411 | * Validate hostname in wash\_up (7a9115) 412 | * Scientific Linux support (#131) 413 | * Default to SSL omnibus URL (#130) 414 | * Fixes for base debian installations (#129) 415 | * Whyrun flag support (#123) 416 | * Node-name flag support (#107) 417 | * No More Syntax Check!! (#122) 418 | 419 | * Various fixes: #138, #119, #113, d38bfd1 420 | 421 | ## Thanks to our contributors! 422 | 423 | * [David Schneider][davidsch] 424 | * [Andrew Vit][avit] 425 | * [Nick Shortway][DrGonzo65] 426 | * [Guido Serra aka Zeph][zeph] 427 | * [Patrick Connolly][patcon] 428 | * [Greg Fitzgerald][gregf] 429 | * [Bryan McLellan][btm] 430 | * [Aaron Jensen][aaronjensen] 431 | 432 | And a special thanks to [Teemu Matilainen][tmatilai] who is now on the list of direct colaborators! 433 | 434 | # 0.0.14 / 2012-09-21 435 | 436 | * Fix argument checks (#101) 437 | * Allow custom omnibus URLs (#99) 438 | * Verbose logging options (#96) 439 | 440 | ## Thanks to our contributors! 441 | 442 | * [Vaughan Rouesnel][vjpr] 443 | * [Ryan Walker][ryandub] 444 | * [Aaron Cruz][pferdefleisch] 445 | 446 | # 0.0.13 / 2012-08-16 447 | 448 | * Less agressive in-kitchen check (36a14161a1c) 449 | * New curl/wget selection during omnibus install (#84) 450 | * FreeBSD 9.0 support (#78) 451 | * Syntax-check-only switch (#74) 452 | * Validate CLI user/host args (#73) 453 | 454 | ## Thanks to our contributors! 455 | 456 | * [Deepak Kannan][deepak] 457 | * [Florian Unglaub][funglaub] 458 | 459 | # 0.0.12 / 2012-06-25 460 | 461 | * Better validation on CLI args (#68, #70) 462 | * Switch from wget to curl (#66) 463 | * Initial fedora support (not under integration) (#67) 464 | * Support new omnibus path (/opt/chef) 465 | 466 | ## Thanks to our contributors! 467 | 468 | * [Bryan Helmkamp][brynary] 469 | * [Greg Fitzgerald][gregf] 470 | * [Deepak Kannan][deepak] 471 | 472 | # 0.0.11 / 2012-06-16 473 | 474 | * Encrypted data bag support (#22) 475 | * Updated dependency version (#63, #64) 476 | * Joyent Ubuntu detection (#62) 477 | * Omnibus version selection (#61) 478 | 479 | ## Thanks to our contributors! 480 | 481 | * [Hector Castro][hectcastro] 482 | * [Sean Porter][portertech] 483 | 484 | # 0.0.10 / 2012-05-30 485 | 486 | * Include apache recipe during integration testing (#17) 487 | * Use omnibus installer on Ubuntu and RedHat (#40, #45, #58) 488 | * `knife wash_up` command for removing uploaded resources (#48) 489 | * Cleaner sudo pre-processing (#59) 490 | * Support `knife kitchen .` to init an existing kitchen (#54) 491 | 492 | ## Thanks to our contributors! 493 | 494 | * [Hector Castro][hectcastro] 495 | * [Nix-wie-weg][Nix-wie-weg] 496 | * [Justin Grevich][jgrevich] 497 | * [Ross Timson][rosstimson] 498 | 499 | # 0.0.9 / 2012-05-13 500 | 501 | * Chef 0.10.10 compatibility (b0fa50e9) 502 | * Finished support and integration testing for remaining key OSes (Issues #2 and #15) 503 | * Added support for 'chefignore' (e4bcbd1..4b578cf9) 504 | * Use `lsb_release` to detect OSes where possible (c976cc119..a31d8234b) 505 | * Ignore `tmp` and `deploy_revision` to rsync exclusion (7d252ff2b) 506 | 507 | ## Thanks to our contributors! 508 | 509 | * [Hector Castro][hectcastro] 510 | * [Amos Lanka][amoslanka] 511 | * [Roland Moriz][rmoriz] 512 | * [Tyler Rick][TylerRick] 513 | * [Motiejus Jakštys][Motiejus] 514 | 515 | # 0.0.8 / 2012-02-10 516 | 517 | * Add --startup-script which gets sourced before any command to setup env vars (e.g., ~/.bashrc) (d1489f94) 518 | * Use curl + rpm rather than rpm against direct URL for better proxy support (51ad9c51) 519 | * Integration harness improvements (1ac5cce..4be36c2) 520 | * BUG #10: Create .gitkeep's to avoid errors on sparse kitchens (074b4e0a) 521 | * Add --skip-chef-check knife option (a1a66ae) 522 | 523 | ## Thanks to our contributors! 524 | 525 | * [Cyril Ledru][patatepartie] 526 | * [Fletcher Nichol][fnichol] 527 | * [Jason Garber][jgarber] 528 | * [Greg Sterndale][gsterndale] 529 | 530 | # 0.0.7 / 2011-12-09 531 | 532 | * BUG #9: Fix intelligent sudo handling for OSes that don't have it 533 | * Move integration tests into proper test cases 534 | * CentOS 5.6 integration test 535 | 536 | # 0.0.6 / 2011-12-08 537 | 538 | * Support for Mac OS 10.5 and 10.6 (00921ebd1b93) 539 | * Parallel integration testing and SLES (167360d447..167360d447) 540 | * Dynamic sudo detection for yum-based systems (5282fc36ac3..256f27658a06cb) 541 | 542 | ## Thanks to our contributors! 543 | 544 | * [Sergio Rubio][rubiojr] 545 | * [Nat Lownes][natlownes] 546 | 547 | # 0.0.5 / 2011-10-31 548 | 549 | * Started on integration testing via EC2 550 | * Add openSuSE support. Installation via zypper. (64ff2edf42) 551 | * Upgraded Rubygems to 1.8.10 (8ac1f4d43a) 552 | 553 | # 0.0.4 / 2011-10-07 554 | 555 | * Chef 0.10.4 based databag and search method (a800880e6d) 556 | * Proper path for roles (b143ae290a) 557 | * Test fixes for CI compatibility (ccf4247125..62b8bd498d) 558 | 559 | ## Thanks to our contributors! 560 | 561 | * [John Dewey][retr0h] 562 | 563 | # 0.0.3 / 2011-07-31 564 | 565 | * Kitchen directory generation 566 | * Prepare tested on ubuntu 567 | * Generate node config on prepare 568 | * Cook via rsync 569 | 570 | [ChrisLundquist]: https://github.com/ChrisLundquist 571 | [DQNEO]: https://github.com/DQNEO 572 | [Domon]: https://github.com/Domon 573 | [DrGonzo65]: https://github.com/DrGonzo65 574 | [Frozenproduce]: https://github.com/Frozenproduce 575 | [Motiejus]: https://github.com/Motiejus 576 | [Nix-wie-weg]: https://github.com/Nix-wie-weg 577 | [OlegPS]: https://github.com/OlegPS 578 | [TheAlphaTester]: https://github.com/TheAlphaTester 579 | [TylerRick]: https://github.com/TylerRick 580 | [VanTanev]: https://github.com/VanTanev 581 | [a2ikm]: https://github.com/a2ikm 582 | [aaronjensen]: https://github.com/aaronjensen 583 | [alexsiri7]: https://github.com/alexsiri7 584 | [allaire]: https://github.com/allaire 585 | [amoslanka]: https://github.com/amoslanka 586 | [angelabad]: https://github.com/angelabad 587 | [anl]: https://github.com/anl 588 | [ares]: https://github.com/ares 589 | [aromarom64]: https://github.com/aromarom64 590 | [avit]: https://github.com/avit 591 | [bambuchaAdm]: https://github.com/bambuchaAdm 592 | [brainopia]: https://github.com/brainopia 593 | [brynary]: https://github.com/brynary 594 | [btm]: https://github.com/btm 595 | [dapatil]: https://github.com/dapatil 596 | [davidsch]: https://github.com/davidsch 597 | [deepak]: https://github.com/deepak 598 | [dkinzer]: https://github.com/dkinzer 599 | [dwradcliffe]: https://github.com/dwradcliffe 600 | [es1o]: https://github.com/es1o 601 | [evverx]: https://github.com/evverx 602 | [fnichol]: https://github.com/fnichol 603 | [funglaub]: https://github.com/funglaub 604 | [grahamlyus]: https://github.com/grahamlyus 605 | [gregf]: https://github.com/gregf 606 | [gsterndale]: https://github.com/gsterndale 607 | [hectcastro]: https://github.com/hectcastro 608 | [hrp]: https://github.com/hrp 609 | [iavael]: https://github.com/iavael 610 | [jgarber]: https://github.com/jgarber 611 | [jgrevich]: https://github.com/jgrevich 612 | [jyotty]: https://github.com/jyotty 613 | [kmdsbng]: https://github.com/kmdsbng 614 | [makern]: https://github.com/makern 615 | [masakura]: https://github.com/masakura 616 | [mclazarus]: https://github.com/mclazarus 617 | [michaelglass]: https://github.com/michaelglass 618 | [naoya]: https://github.com/naoya 619 | [natlownes]: https://github.com/natlownes 620 | [noric]: https://github.com/noric 621 | [patatepartie]: https://github.com/patatepartie 622 | [patcon]: https://github.com/patcon 623 | [pferdefleisch]: https://github.com/pferdefleisch 624 | [piglovesyou]: https://github.com/piglovesyou 625 | [portertech]: https://github.com/portertech 626 | [retr0h]: https://github.com/retr0h 627 | [rmoriz]: https://github.com/rmoriz 628 | [robacarp]: https://github.com/robacarp 629 | [rosstimson]: https://github.com/rosstimson 630 | [rubiojr]: https://github.com/rubiojr 631 | [russellcardullo]: https://github.com/russellcardullo 632 | [ryandub]: https://github.com/ryandub 633 | [searlm]: https://github.com/searlm 634 | [skyeagle]: https://github.com/skyeagle 635 | [smdern]: https://github.com/smdern 636 | [teyrow]: https://github.com/teyrow 637 | [tknerr]: https://github.com/tknerr 638 | [tmatilai]: https://github.com/tmatilai 639 | [tocky]: https://github.com/tocky 640 | [vjpr]: https://github.com/vjpr 641 | [xtoddx]: https://github.com/xtoddx 642 | [yozlet]: https://github.com/yozlet 643 | [yugui]: https://github.com/yugui 644 | [zeph]: https://github.com/zeph 645 | [aaadschutz]: https://github.com/aaadschutz 646 | [xunnanxu]: https://github.com/xunnanxu 647 | [derektamsen]: https://github.com/derektamsen 648 | [szemek]: https://github.com/szemek 649 | [thickpaddy]: https://github.com/thickpaddy 650 | [iidatomohiko]: https://github.com/iidatomohiko 651 | [jeroenj]: https://github.com/jeroenj 652 | --------------------------------------------------------------------------------