diff options
Diffstat (limited to 'lib/chef/knife')
90 files changed, 7870 insertions, 0 deletions
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb new file mode 100644 index 0000000000..a8e9201c26 --- /dev/null +++ b/lib/chef/knife/bootstrap.rb @@ -0,0 +1,234 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' +require 'erubis' + +class Chef + class Knife + class Bootstrap < Knife + + deps do + require 'chef/knife/core/bootstrap_context' + require 'chef/json_compat' + require 'tempfile' + require 'highline' + require 'net/ssh' + require 'net/ssh/multi' + require 'chef/knife/ssh' + Chef::Knife::Ssh.load_deps + end + + banner "knife bootstrap FQDN (options)" + + option :ssh_user, + :short => "-x USERNAME", + :long => "--ssh-user USERNAME", + :description => "The ssh username", + :default => "root" + + option :ssh_password, + :short => "-P PASSWORD", + :long => "--ssh-password PASSWORD", + :description => "The ssh password" + + option :ssh_port, + :short => "-p PORT", + :long => "--ssh-port PORT", + :description => "The ssh port", + :default => "22", + :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key } + + option :ssh_gateway, + :short => "-G GATEWAY", + :long => "--ssh-gateway GATEWAY", + :description => "The ssh gateway", + :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key } + + option :identity_file, + :short => "-i IDENTITY_FILE", + :long => "--identity-file IDENTITY_FILE", + :description => "The SSH identity file used for authentication" + + option :chef_node_name, + :short => "-N NAME", + :long => "--node-name NAME", + :description => "The Chef node name for your new node" + + option :prerelease, + :long => "--prerelease", + :description => "Install the pre-release chef gems" + + option :bootstrap_version, + :long => "--bootstrap-version VERSION", + :description => "The version of Chef to install", + :proc => lambda { |v| Chef::Config[:knife][:bootstrap_version] = v } + + option :bootstrap_proxy, + :long => "--bootstrap-proxy PROXY_URL", + :description => "The proxy server for the node being bootstrapped", + :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p } + + option :distro, + :short => "-d DISTRO", + :long => "--distro DISTRO", + :description => "Bootstrap a distro using a template", + :default => "chef-full" + + option :use_sudo, + :long => "--sudo", + :description => "Execute the bootstrap via sudo", + :boolean => true + + option :template_file, + :long => "--template-file TEMPLATE", + :description => "Full path to location of template to use", + :default => false + + option :run_list, + :short => "-r RUN_LIST", + :long => "--run-list RUN_LIST", + :description => "Comma separated list of roles/recipes to apply", + :proc => lambda { |o| o.split(/[\s,]+/) }, + :default => [] + + option :first_boot_attributes, + :short => "-j JSON_ATTRIBS", + :long => "--json-attributes", + :description => "A JSON string to be added to the first run of chef-client", + :proc => lambda { |o| JSON.parse(o) }, + :default => {} + + option :host_key_verify, + :long => "--[no-]host-key-verify", + :description => "Verify host key, enabled by default.", + :boolean => true, + :default => true + + option :hint, + :long => "--hint HINT_NAME[=HINT_FILE]", + :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.", + :proc => Proc.new { |h| + Chef::Config[:knife][:hints] ||= Hash.new + name, path = h.split("=") + Chef::Config[:knife][:hints][name] = path ? JSON.parse(::File.read(path)) : Hash.new } + + def find_template(template=nil) + # Are we bootstrapping using an already shipped template? + if config[:template_file] + bootstrap_files = config[:template_file] + else + bootstrap_files = [] + bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap', "#{config[:distro]}.erb") + bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{config[:distro]}.erb") if Knife.chef_config_dir + bootstrap_files << File.join(ENV['HOME'], '.chef', 'bootstrap', "#{config[:distro]}.erb") if ENV['HOME'] + bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{config[:distro]}.erb")) + bootstrap_files.flatten! + end + + template = Array(bootstrap_files).find do |bootstrap_template| + Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}") + File.exists?(bootstrap_template) + end + + unless template + ui.info("Can not find bootstrap definition for #{config[:distro]}") + raise Errno::ENOENT + end + + Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}") + + template + end + + def render_template(template=nil) + context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config) + Erubis::Eruby.new(template).evaluate(context) + end + + def read_template + IO.read(@template_file).chomp + end + + def run + validate_name_args! + @template_file = find_template(config[:bootstrap_template]) + @node_name = Array(@name_args).first + # back compat--templates may use this setting: + config[:server_name] = @node_name + + $stdout.sync = true + + ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}") + + begin + knife_ssh.run + rescue Net::SSH::AuthenticationFailed + unless config[:ssh_password] + ui.info("Failed to authenticate #{config[:ssh_user]} - trying password auth") + knife_ssh_with_password_auth.run + end + end + end + + def validate_name_args! + if Array(@name_args).first.nil? + ui.error("Must pass an FQDN or ip to bootstrap") + exit 1 + end + end + + def server_name + Array(@name_args).first + end + + def knife_ssh + ssh = Chef::Knife::Ssh.new + ssh.ui = ui + ssh.name_args = [ server_name, ssh_command ] + ssh.config[:ssh_user] = config[:ssh_user] + ssh.config[:ssh_password] = config[:ssh_password] + ssh.config[:ssh_port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port] + ssh.config[:ssh_gateway] = Chef::Config[:knife][:ssh_gateway] || config[:ssh_gateway] + ssh.config[:identity_file] = config[:identity_file] + ssh.config[:manual] = true + ssh.config[:host_key_verify] = config[:host_key_verify] + ssh.config[:on_error] = :raise + ssh + end + + def knife_ssh_with_password_auth + ssh = knife_ssh + ssh.config[:identity_file] = nil + ssh.config[:ssh_password] = ssh.get_password + ssh + end + + def ssh_command + command = render_template(read_template) + + if config[:use_sudo] + command = "sudo #{command}" + end + + command + end + + end + end +end diff --git a/lib/chef/knife/bootstrap/archlinux-gems.erb b/lib/chef/knife/bootstrap/archlinux-gems.erb new file mode 100644 index 0000000000..85d6236197 --- /dev/null +++ b/lib/chef/knife/bootstrap/archlinux-gems.erb @@ -0,0 +1,75 @@ +bash -c ' +<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> + +if [ ! -f /usr/bin/chef-client ]; then + pacman -Syy + pacman -S --noconfirm ruby ntp base-devel + ntpdate -u pool.ntp.org + gem install ohai --no-rdoc --no-ri --verbose + gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %> +fi + +mkdir -p /etc/chef +( +cat <<'EOP' +<%= validation_key %> +EOP +) > /tmp/validation.pem +awk NF /tmp/validation.pem > /etc/chef/validation.pem +rm /tmp/validation.pem +chmod 0600 /etc/chef/validation.pem + +<% if @chef_config[:encrypted_data_bag_secret] -%> +( +cat <<'EOP' +<%= encrypted_data_bag_secret %> +EOP +) > /tmp/encrypted_data_bag_secret +awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret +rm /tmp/encrypted_data_bag_secret +chmod 0600 /etc/chef/encrypted_data_bag_secret +<% end -%> + +<%# Generate Ohai Hints -%> +<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> +mkdir -p /etc/chef/ohai/hints + +<% @chef_config[:knife][:hints].each do |name, hash| -%> +( +cat <<'EOP' +<%= hash.to_json %> +EOP +) > /etc/chef/ohai/hints/<%= name %>.json +<% end -%> +<% end -%> + +( +cat <<'EOP' +log_level :info +log_location STDOUT +chef_server_url "<%= @chef_config[:chef_server_url] %>" +validation_client_name "<%= @chef_config[:validation_client_name] %>" +<% if @config[:chef_node_name] -%> +node_name "<%= @config[:chef_node_name] %>" +<% else -%> +# Using default node name (fqdn) +<% end -%> +# ArchLinux follows the Filesystem Hierarchy Standard +file_cache_path "/var/cache/chef" +file_backup_path "/var/lib/chef/backup" +pid_file "/var/run/chef/client.pid" +cache_options({ :path => "/var/cache/chef/checksums", :skip_expires => true}) +<% if knife_config[:bootstrap_proxy] %> +http_proxy "<%= knife_config[:bootstrap_proxy] %>" +https_proxy "<%= knife_config[:bootstrap_proxy] %>" +<% end -%> +EOP +) > /etc/chef/client.rb + +( +cat <<'EOP' +<%= first_boot.to_json %> +EOP +) > /etc/chef/first-boot.json + +<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/centos5-gems.erb b/lib/chef/knife/bootstrap/centos5-gems.erb new file mode 100644 index 0000000000..f9626c3c2b --- /dev/null +++ b/lib/chef/knife/bootstrap/centos5-gems.erb @@ -0,0 +1,71 @@ +bash -c ' +<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> + +if [ ! -f /usr/bin/chef-client ]; then + wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm + rpm -Uvh epel-release-5-4.noarch.rpm + wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://rpm.aegisco.com/aegisco/rhel/aegisco-rhel.rpm + rpm -Uvh aegisco-rhel.rpm + + yum install -y ruby ruby-devel gcc gcc-c++ automake autoconf make + + cd /tmp + wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://production.cf.rubygems.org/rubygems/rubygems-1.6.2.tgz + tar zxf rubygems-1.6.2.tgz + cd rubygems-1.6.2 + ruby setup.rb --no-format-executable +fi + +gem update --system +gem update +gem install ohai --no-rdoc --no-ri --verbose +gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %> + +mkdir -p /etc/chef + +( +cat <<'EOP' +<%= validation_key %> +EOP +) > /tmp/validation.pem +awk NF /tmp/validation.pem > /etc/chef/validation.pem +rm /tmp/validation.pem +chmod 0600 /etc/chef/validation.pem + +<% if @chef_config[:encrypted_data_bag_secret] -%> +( +cat <<'EOP' +<%= encrypted_data_bag_secret %> +EOP +) > /tmp/encrypted_data_bag_secret +awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret +rm /tmp/encrypted_data_bag_secret +chmod 0600 /etc/chef/encrypted_data_bag_secret +<% end -%> + +<%# Generate Ohai Hints -%> +<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> +mkdir -p /etc/chef/ohai/hints + +<% @chef_config[:knife][:hints].each do |name, hash| -%> +( +cat <<'EOP' +<%= hash.to_json %> +EOP +) > /etc/chef/ohai/hints/<%= name %>.json +<% end -%> +<% end -%> + +( +cat <<'EOP' +<%= config_content %> +EOP +) > /etc/chef/client.rb + +( +cat <<'EOP' +<%= first_boot.to_json %> +EOP +) > /etc/chef/first-boot.json + +<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/chef-full.erb b/lib/chef/knife/bootstrap/chef-full.erb new file mode 100644 index 0000000000..771ef85884 --- /dev/null +++ b/lib/chef/knife/bootstrap/chef-full.erb @@ -0,0 +1,73 @@ +bash -c ' +<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> + +exists() { + if command -v $1 &>/dev/null + then + return 0 + else + return 1 + fi +} + +install_sh="http://opscode.com/chef/install.sh" +version_string="-v <%= chef_version %>" + +if ! exists /usr/bin/chef-client; then + if exists wget; then + bash <(wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> ${install_sh} -O -) ${version_string} + else + if exists curl; then + bash <(curl -L <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> ${install_sh}) ${version_string} + fi + fi +fi + +mkdir -p /etc/chef + +( +cat <<'EOP' +<%= validation_key %> +EOP +) > /tmp/validation.pem +awk NF /tmp/validation.pem > /etc/chef/validation.pem +rm /tmp/validation.pem +chmod 0600 /etc/chef/validation.pem + +<% if @chef_config[:encrypted_data_bag_secret] -%> +( +cat <<'EOP' +<%= encrypted_data_bag_secret %> +EOP +) > /tmp/encrypted_data_bag_secret +awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret +rm /tmp/encrypted_data_bag_secret +chmod 0600 /etc/chef/encrypted_data_bag_secret +<% end -%> + +<%# Generate Ohai Hints -%> +<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> +mkdir -p /etc/chef/ohai/hints + +<% @chef_config[:knife][:hints].each do |name, hash| -%> +( +cat <<'EOP' +<%= hash.to_json %> +EOP +) > /etc/chef/ohai/hints/<%= name %>.json +<% end -%> +<% end -%> + +( +cat <<'EOP' +<%= config_content %> +EOP +) > /etc/chef/client.rb + +( +cat <<'EOP' +<%= first_boot.to_json %> +EOP +) > /etc/chef/first-boot.json + +<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/fedora13-gems.erb b/lib/chef/knife/bootstrap/fedora13-gems.erb new file mode 100644 index 0000000000..a8448342df --- /dev/null +++ b/lib/chef/knife/bootstrap/fedora13-gems.erb @@ -0,0 +1,58 @@ +bash -c ' +<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> + +yum install -y ruby ruby-devel gcc gcc-c++ automake autoconf rubygems make + +gem update --system +gem update +gem install ohai --no-rdoc --no-ri --verbose +gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %> + +mkdir -p /etc/chef + +( +cat <<'EOP' +<%= validation_key %> +EOP +) > /tmp/validation.pem +awk NF /tmp/validation.pem > /etc/chef/validation.pem +rm /tmp/validation.pem +chmod 0600 /etc/chef/validation.pem + +<% if @chef_config[:encrypted_data_bag_secret] -%> +( +cat <<'EOP' +<%= encrypted_data_bag_secret %> +EOP +) > /tmp/encrypted_data_bag_secret +awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret +rm /tmp/encrypted_data_bag_secret +chmod 0600 /etc/chef/encrypted_data_bag_secret +<% end -%> + +<%# Generate Ohai Hints -%> +<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> +mkdir -p /etc/chef/ohai/hints + +<% @chef_config[:knife][:hints].each do |name, hash| -%> +( +cat <<'EOP' +<%= hash.to_json %> +EOP +) > /etc/chef/ohai/hints/<%= name %>.json +<% end -%> +<% end -%> + +( +cat <<'EOP' +<%= config_content %> +EOP +) > /etc/chef/client.rb + +( +cat <<'EOP' +<%= first_boot.to_json %> +EOP +) > /etc/chef/first-boot.json + +<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb b/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb new file mode 100644 index 0000000000..0e44361d82 --- /dev/null +++ b/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb @@ -0,0 +1,65 @@ +bash -c ' +<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> + +if [ ! -f /usr/bin/chef-client ]; then + apt-get install -y wget + echo "chef chef/chef_server_url string <%= @chef_config[:chef_server_url] %>" | debconf-set-selections + [ -f /etc/apt/sources.list.d/opscode.list ] || echo "deb http://apt.opscode.com <%= chef_version.to_f == 0.10 ? "lucid-0.10" : "lucid" %> main" > /etc/apt/sources.list.d/opscode.list + wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>-O- http://apt.opscode.com/packages@opscode.com.gpg.key | apt-key add - +fi +apt-get update +apt-get install -y chef + +( +cat <<'EOP' +<%= validation_key %> +EOP +) > /tmp/validation.pem +awk NF /tmp/validation.pem > /etc/chef/validation.pem +rm /tmp/validation.pem +chmod 0600 /etc/chef/validation.pem + +<% if @chef_config[:encrypted_data_bag_secret] -%> +( +cat <<'EOP' +<%= encrypted_data_bag_secret %> +EOP +) > /tmp/encrypted_data_bag_secret +awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret +rm /tmp/encrypted_data_bag_secret +chmod 0600 /etc/chef/encrypted_data_bag_secret +<% end -%> + +<%# Generate Ohai Hints -%> +<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> +mkdir -p /etc/chef/ohai/hints + +<% @chef_config[:knife][:hints].each do |name, hash| -%> +( +cat <<'EOP' +<%= hash.to_json %> +EOP +) > /etc/chef/ohai/hints/<%= name %>.json +<% end -%> +<% end -%> + +<% unless @chef_config[:validation_client_name] == "chef-validator" -%> +[ `grep -qx "validation_client_name \"<%= @chef_config[:validation_client_name] %>\"" /etc/chef/client.rb` ] || echo "validation_client_name \"<%= @chef_config[:validation_client_name] %>\"" >> /etc/chef/client.rb +<% end -%> + +<% if @config[:chef_node_name] %> +[ `grep -qx "node_name \"<%= @config[:chef_node_name] %>\"" /etc/chef/client.rb` ] || echo "node_name \"<%= @config[:chef_node_name] %>\"" >> /etc/chef/client.rb +<% end -%> + +<% if knife_config[:bootstrap_proxy] %> +echo 'http_proxy "knife_config[:bootstrap_proxy]"' >> /etc/chef/client.rb +echo 'https_proxy "knife_config[:bootstrap_proxy]"' >> /etc/chef/client.rb +<% end -%> + +( +cat <<'EOP' +<%= first_boot.to_json %> +EOP +) > /etc/chef/first-boot.json + +<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb b/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb new file mode 100644 index 0000000000..63448fc4d3 --- /dev/null +++ b/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb @@ -0,0 +1,65 @@ +bash -c ' +<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> + +if [ ! -f /usr/bin/chef-client ]; then + apt-get update + apt-get install -y ruby ruby1.8-dev build-essential wget libruby-extras libruby1.8-extras + cd /tmp + wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://production.cf.rubygems.org/rubygems/rubygems-1.6.2.tgz + tar zxf rubygems-1.6.2.tgz + cd rubygems-1.6.2 + ruby setup.rb --no-format-executable +fi + +gem update --no-rdoc --no-ri +gem install ohai --no-rdoc --no-ri --verbose +gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %> + +mkdir -p /etc/chef + +( +cat <<'EOP' +<%= validation_key %> +EOP +) > /tmp/validation.pem +awk NF /tmp/validation.pem > /etc/chef/validation.pem +rm /tmp/validation.pem +chmod 0600 /etc/chef/validation.pem + +<% if @chef_config[:encrypted_data_bag_secret] -%> +( +cat <<'EOP' +<%= encrypted_data_bag_secret %> +EOP +) > /tmp/encrypted_data_bag_secret +awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret +rm /tmp/encrypted_data_bag_secret +chmod 0600 /etc/chef/encrypted_data_bag_secret +<% end -%> + +<%# Generate Ohai Hints -%> +<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> +mkdir -p /etc/chef/ohai/hints + +<% @chef_config[:knife][:hints].each do |name, hash| -%> +( +cat <<'EOP' +<%= hash.to_json %> +EOP +) > /etc/chef/ohai/hints/<%= name %>.json +<% end -%> +<% end -%> + +( +cat <<'EOP' +<%= config_content %> +EOP +) > /etc/chef/client.rb + +( +cat <<'EOP' +<%= first_boot.to_json %> +EOP +) > /etc/chef/first-boot.json + +<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/ubuntu12.04-gems.erb b/lib/chef/knife/bootstrap/ubuntu12.04-gems.erb new file mode 100644 index 0000000000..e7da7db39b --- /dev/null +++ b/lib/chef/knife/bootstrap/ubuntu12.04-gems.erb @@ -0,0 +1,60 @@ +bash -c ' +<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> + +if [ ! -f /usr/bin/chef-client ]; then + aptitude update + aptitude install -y ruby ruby1.8-dev build-essential wget libruby1.8 rubygems +fi + +gem update --no-rdoc --no-ri +gem install ohai --no-rdoc --no-ri --verbose +gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %> + +mkdir -p /etc/chef + +( +cat <<'EOP' +<%= validation_key %> +EOP +) > /tmp/validation.pem +awk NF /tmp/validation.pem > /etc/chef/validation.pem +rm /tmp/validation.pem +chmod 0600 /etc/chef/validation.pem + +<% if @chef_config[:encrypted_data_bag_secret] -%> +( +cat <<'EOP' +<%= encrypted_data_bag_secret %> +EOP +) > /tmp/encrypted_data_bag_secret +awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret +rm /tmp/encrypted_data_bag_secret +chmod 0600 /etc/chef/encrypted_data_bag_secret +<% end -%> + +<%# Generate Ohai Hints -%> +<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> +mkdir -p /etc/chef/ohai/hints + +<% @chef_config[:knife][:hints].each do |name, hash| -%> +( +cat <<'EOP' +<%= hash.to_json %> +EOP +) > /etc/chef/ohai/hints/<%= name %>.json +<% end -%> +<% end -%> + +( +cat <<'EOP' +<%= config_content %> +EOP +) > /etc/chef/client.rb + +( +cat <<'EOP' +<%= first_boot.to_json %> +EOP +) > /etc/chef/first-boot.json + +<%= start_chef %>' diff --git a/lib/chef/knife/client_bulk_delete.rb b/lib/chef/knife/client_bulk_delete.rb new file mode 100644 index 0000000000..8bf2c2f116 --- /dev/null +++ b/lib/chef/knife/client_bulk_delete.rb @@ -0,0 +1,65 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class ClientBulkDelete < Knife + + deps do + require 'chef/api_client' + require 'chef/json_compat' + end + + banner "knife client bulk delete REGEX (options)" + + def run + if name_args.length < 1 + ui.fatal("You must supply a regular expression to match the results against") + exit 42 + end + all_clients = Chef::ApiClient.list(true) + + matcher = /#{name_args[0]}/ + clients_to_delete = {} + all_clients.each do |name, client| + next unless name =~ matcher + clients_to_delete[client.name] = client + end + + if clients_to_delete.empty? + ui.info "No clients match the expression /#{name_args[0]}/" + exit 0 + end + + ui.msg("The following clients will be deleted:") + ui.msg("") + ui.msg(ui.list(clients_to_delete.keys.sort, :columns_down)) + ui.msg("") + ui.confirm("Are you sure you want to delete these clients") + + clients_to_delete.sort.each do |name, client| + client.destroy + ui.msg("Deleted client #{name}") + end + end + end + end +end + diff --git a/lib/chef/knife/client_create.rb b/lib/chef/knife/client_create.rb new file mode 100644 index 0000000000..5b5078b574 --- /dev/null +++ b/lib/chef/knife/client_create.rb @@ -0,0 +1,80 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class ClientCreate < Knife + + deps do + require 'chef/api_client' + require 'chef/json_compat' + end + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the key to a file" + + option :admin, + :short => "-a", + :long => "--admin", + :description => "Create the client as an admin", + :boolean => true + + banner "knife client create CLIENT (options)" + + def run + @client_name = @name_args[0] + + if @client_name.nil? + show_usage + ui.fatal("You must specify a client name") + exit 1 + end + + client = Chef::ApiClient.new + client.name(@client_name) + client.admin(config[:admin]) + + output = edit_data(client) + + # Chef::ApiClient.save will try to create a client and if it exists will update it instead silently + client = output.save + + # We only get a private_key on client creation, not on client update. + if client['private_key'] + ui.info("Created #{output}") + + if config[:file] + File.open(config[:file], "w") do |f| + f.print(client['private_key']) + end + else + puts client['private_key'] + end + else + ui.error "Client '#{client['name']}' already exists" + exit 1 + end + end + end + end +end + diff --git a/lib/chef/knife/client_delete.rb b/lib/chef/knife/client_delete.rb new file mode 100644 index 0000000000..6a6fae7ea0 --- /dev/null +++ b/lib/chef/knife/client_delete.rb @@ -0,0 +1,46 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class ClientDelete < Knife + + deps do + require 'chef/api_client' + require 'chef/json_compat' + end + + banner "knife client delete CLIENT (options)" + + def run + @client_name = @name_args[0] + + if @client_name.nil? + show_usage + ui.fatal("You must specify a client name") + exit 1 + end + + delete_object(Chef::ApiClient, @client_name) + end + + end + end +end diff --git a/lib/chef/knife/client_edit.rb b/lib/chef/knife/client_edit.rb new file mode 100644 index 0000000000..c81bce902a --- /dev/null +++ b/lib/chef/knife/client_edit.rb @@ -0,0 +1,45 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class ClientEdit < Knife + + deps do + require 'chef/api_client' + require 'chef/json_compat' + end + + banner "knife client edit CLIENT (options)" + + def run + @client_name = @name_args[0] + + if @client_name.nil? + show_usage + ui.fatal("You must specify a client name") + exit 1 + end + + edit_object(Chef::ApiClient, @client_name) + end + end + end +end diff --git a/lib/chef/knife/client_list.rb b/lib/chef/knife/client_list.rb new file mode 100644 index 0000000000..da0bf12dc3 --- /dev/null +++ b/lib/chef/knife/client_list.rb @@ -0,0 +1,42 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class ClientList < Knife + + deps do + require 'chef/api_client' + require 'chef/json_compat' + end + + banner "knife client list (options)" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + output(format_list_for_display(Chef::ApiClient.list)) + end + end + end +end diff --git a/lib/chef/knife/client_reregister.rb b/lib/chef/knife/client_reregister.rb new file mode 100644 index 0000000000..73a93ec31d --- /dev/null +++ b/lib/chef/knife/client_reregister.rb @@ -0,0 +1,58 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class ClientReregister < Knife + + deps do + require 'chef/api_client' + require 'chef/json_compat' + end + + banner "knife client reregister CLIENT (options)" + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the key to a file" + + def run + @client_name = @name_args[0] + + if @client_name.nil? + show_usage + ui.fatal("You must specify a client name") + exit 1 + end + + client = Chef::ApiClient.load(@client_name) + key = client.save(new_key=true) + if config[:file] + File.open(config[:file], "w") do |f| + f.print(key['private_key']) + end + else + ui.msg key['private_key'] + end + end + end + end +end diff --git a/lib/chef/knife/client_show.rb b/lib/chef/knife/client_show.rb new file mode 100644 index 0000000000..5c2ffb4183 --- /dev/null +++ b/lib/chef/knife/client_show.rb @@ -0,0 +1,52 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class ClientShow < Knife + + deps do + require 'chef/api_client' + require 'chef/json_compat' + end + + banner "knife client show CLIENT (options)" + + option :attribute, + :short => "-a ATTR", + :long => "--attribute ATTR", + :description => "Show only one attribute" + + def run + @client_name = @name_args[0] + + if @client_name.nil? + show_usage + ui.fatal("You must specify a client name") + exit 1 + end + + client = Chef::ApiClient.load(@client_name) + output(format_for_display(client)) + end + + end + end +end diff --git a/lib/chef/knife/configure.rb b/lib/chef/knife/configure.rb new file mode 100644 index 0000000000..0be7093e29 --- /dev/null +++ b/lib/chef/knife/configure.rb @@ -0,0 +1,168 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class Configure < Knife + attr_reader :chef_server, :new_client_name, :admin_client_name, :admin_client_key + attr_reader :chef_repo, :new_client_key, :validation_client_name, :validation_key + + deps do + require 'ohai' + Chef::Knife::ClientCreate.load_deps + end + + banner "knife configure (options)" + + option :repository, + :short => "-r REPO", + :long => "--repository REPO", + :description => "The path to your chef-repo" + + option :initial, + :short => "-i", + :long => "--initial", + :boolean => true, + :description => "Create an initial API Client" + + option :admin_client_name, + :long => "--admin-client-name NAME", + :description => "The existing admin clientname (usually chef-webui)" + + option :admin_client_key, + :long => "--admin-client-key PATH", + :description => "The path to the admin client's private key (usually a file named webui.pem)" + + option :validation_client_name, + :long => "--validation-client-name NAME", + :description => "The validation clientname (usually chef-validator)" + + option :validation_key, + :long => "--validation-key PATH", + :description => "The location of the location of the validation key (usually a file named validation.pem)" + + def configure_chef + # We are just faking out the system so that you can do this without a key specified + Chef::Config[:node_name] = 'woot' + super + Chef::Config[:node_name] = nil + end + + def run + ask_user_for_config_path + + FileUtils.mkdir_p(chef_config_path) + + ask_user_for_config + + ::File.open(config[:config_file], "w") do |f| + f.puts <<-EOH +log_level :info +log_location STDOUT +node_name '#{new_client_name}' +client_key '#{new_client_key}' +validation_client_name '#{validation_client_name}' +validation_key '#{validation_key}' +chef_server_url '#{chef_server}' +cache_type 'BasicFile' +cache_options( :path => '#{File.join(chef_config_path, "checksums")}' ) +EOH + unless chef_repo.empty? + f.puts "cookbook_path [ '#{chef_repo}/cookbooks' ]" + end + end + + if config[:initial] + ui.msg("Creating initial API user...") + Chef::Config[:chef_server_url] = chef_server + Chef::Config[:node_name] = admin_client_name + Chef::Config[:client_key] = admin_client_key + client_create = Chef::Knife::ClientCreate.new + client_create.name_args = [ new_client_name ] + client_create.config[:admin] = true + client_create.config[:file] = new_client_key + client_create.config[:yes] = true + client_create.config[:disable_editing] = true + client_create.run + else + ui.msg("*****") + ui.msg("") + ui.msg("You must place your client key in:") + ui.msg(" #{new_client_key}") + ui.msg("Before running commands with Knife!") + ui.msg("") + ui.msg("*****") + ui.msg("") + ui.msg("You must place your validation key in:") + ui.msg(" #{validation_key}") + ui.msg("Before generating instance data with Knife!") + ui.msg("") + ui.msg("*****") + end + + ui.msg("Configuration file written to #{config[:config_file]}") + end + + def ask_user_for_config_path + config[:config_file] ||= ask_question("Where should I put the config file? ", :default => "#{Chef::Config[:user_home]}/.chef/knife.rb") + # have to use expand path to expand the tilde character to the user's home + config[:config_file] = File.expand_path(config[:config_file]) + if File.exists?(config[:config_file]) + confirm("Overwrite #{config[:config_file]}") + end + end + + def ask_user_for_config + server_name = guess_servername + @chef_server = config[:chef_server_url] || ask_question("Please enter the chef server URL: ", :default => "http://#{server_name}:4000") + if config[:initial] + @new_client_name = config[:node_name] || ask_question("Please enter a clientname for the new client: ", :default => Etc.getlogin) + @admin_client_name = config[:admin_client_name] || ask_question("Please enter the existing admin clientname: ", :default => 'chef-webui') + @admin_client_key = config[:admin_client_key] || ask_question("Please enter the location of the existing admin client's private key: ", :default => '/etc/chef/webui.pem') + @admin_client_key = File.expand_path(@admin_client_key) + else + @new_client_name = config[:node_name] || ask_question("Please enter an existing username or clientname for the API: ", :default => Etc.getlogin) + end + @validation_client_name = config[:validation_client_name] || ask_question("Please enter the validation clientname: ", :default => 'chef-validator') + @validation_key = config[:validation_key] || ask_question("Please enter the location of the validation key: ", :default => '/etc/chef/validation.pem') + @validation_key = File.expand_path(@validation_key) + @chef_repo = config[:repository] || ask_question("Please enter the path to a chef repository (or leave blank): ") + + @new_client_key = config[:client_key] || File.join(chef_config_path, "#{@new_client_name}.pem") + @new_client_key = File.expand_path(@new_client_key) + end + + def guess_servername + o = Ohai::System.new + o.require_plugin 'os' + o.require_plugin 'hostname' + o[:fqdn] || 'localhost' + end + + def config_file + config[:config_file] + end + + def chef_config_path + File.dirname(config_file) + end + end + end +end diff --git a/lib/chef/knife/configure_client.rb b/lib/chef/knife/configure_client.rb new file mode 100644 index 0000000000..838d9a1f58 --- /dev/null +++ b/lib/chef/knife/configure_client.rb @@ -0,0 +1,50 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class ConfigureClient < Knife + banner "knife configure client DIRECTORY" + + def run + unless @config_dir = @name_args[0] + ui.fatal "You must provide the directory to put the files in" + show_usage + exit(1) + end + + ui.info("Creating client configuration") + FileUtils.mkdir_p(@config_dir) + ui.info("Writing client.rb") + File.open(File.join(@config_dir, "client.rb"), "w") do |file| + file.puts('log_level :info') + file.puts('log_location STDOUT') + file.puts("chef_server_url '#{Chef::Config[:chef_server_url]}'") + file.puts("validation_client_name '#{Chef::Config[:validation_client_name]}'") + end + ui.info("Writing validation.pem") + File.open(File.join(@config_dir, 'validation.pem'), "w") do |validation| + validation.puts(IO.read(Chef::Config[:validation_key])) + end + end + + end + end +end diff --git a/lib/chef/knife/cookbook_bulk_delete.rb b/lib/chef/knife/cookbook_bulk_delete.rb new file mode 100644 index 0000000000..f8ad74d856 --- /dev/null +++ b/lib/chef/knife/cookbook_bulk_delete.rb @@ -0,0 +1,72 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookBulkDelete < Knife + + deps do + require 'chef/knife/cookbook_delete' + require 'chef/cookbook_version' + end + + option :purge, :short => '-p', :long => '--purge', :boolean => true, :description => 'Permanently remove files from backing data store' + + banner "knife cookbook bulk delete REGEX (options)" + + def run + unless regex_str = @name_args.first + ui.fatal("You must supply a regular expression to match the results against") + exit 42 + end + + regex = Regexp.new(regex_str) + + all_cookbooks = Chef::CookbookVersion.list + cookbooks_names = all_cookbooks.keys.grep(regex) + cookbooks_to_delete = cookbooks_names.inject({}) { |hash, name| hash[name] = all_cookbooks[name];hash } + ui.msg "All versions of the following cookbooks will be deleted:" + ui.msg "" + ui.msg ui.list(cookbooks_to_delete.keys.sort, :columns_down) + ui.msg "" + + unless config[:yes] + ui.confirm("Do you really want to delete these cookbooks? (Y/N) ", false) + + if config[:purge] + ui.msg("Files that are common to multiple cookbooks are shared, so purging the files may break other cookbooks.") + ui.confirm("Are you sure you want to purge files instead of just deleting the cookbooks") + end + ui.msg "" + end + + + cookbooks_names.each do |cookbook_name| + versions = rest.get_rest("cookbooks/#{cookbook_name}")[cookbook_name]["versions"].map {|v| v["version"]}.flatten + versions.each do |version| + object = rest.delete_rest("cookbooks/#{cookbook_name}/#{version}#{config[:purge] ? "?purge=true" : ""}") + ui.info("Deleted cookbook #{cookbook_name.ljust(25)} [#{version}]") + end + end + end + end + end +end diff --git a/lib/chef/knife/cookbook_create.rb b/lib/chef/knife/cookbook_create.rb new file mode 100644 index 0000000000..c2e92e6b42 --- /dev/null +++ b/lib/chef/knife/cookbook_create.rb @@ -0,0 +1,297 @@ +# +# Author:: Nuo Yan (<nuo@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookCreate < Knife + + deps do + require 'chef/json_compat' + require 'uri' + require 'fileutils' + end + + banner "knife cookbook create COOKBOOK (options)" + + option :cookbook_path, + :short => "-o PATH", + :long => "--cookbook-path PATH", + :description => "The directory where the cookbook will be created" + + option :readme_format, + :short => "-r FORMAT", + :long => "--readme-format FORMAT", + :description => "Format of the README file, supported formats are 'md' (markdown) and 'rdoc' (rdoc)" + + option :cookbook_license, + :short => "-I LICENSE", + :long => "--license LICENSE", + :description => "License for cookbook, apachev2, gplv2, gplv3, mit or none" + + option :cookbook_copyright, + :short => "-C COPYRIGHT", + :long => "--copyright COPYRIGHT", + :description => "Name of Copyright holder" + + option :cookbook_email, + :short => "-m EMAIL", + :long => "--email EMAIL", + :description => "Email address of cookbook maintainer" + + def run + self.config = Chef::Config.merge!(config) + if @name_args.length < 1 + show_usage + ui.fatal("You must specify a cookbook name") + exit 1 + end + + if default_cookbook_path_empty? && parameter_empty?(config[:cookbook_path]) + raise ArgumentError, "Default cookbook_path is not specified in the knife.rb config file, and a value to -o is not provided. Nowhere to write the new cookbook to." + end + + cookbook_path = File.expand_path(Array(config[:cookbook_path]).first) + cookbook_name = @name_args.first + copyright = config[:cookbook_copyright] || "YOUR_COMPANY_NAME" + email = config[:cookbook_email] || "YOUR_EMAIL" + license = ((config[:cookbook_license] != "false") && config[:cookbook_license]) || "none" + readme_format = ((config[:readme_format] != "false") && config[:readme_format]) || "md" + create_cookbook(cookbook_path,cookbook_name, copyright, license) + create_readme(cookbook_path,cookbook_name,readme_format) + create_changelog(cookbook_path,cookbook_name) + create_metadata(cookbook_path,cookbook_name, copyright, email, license,readme_format) + end + + def create_cookbook(dir, cookbook_name, copyright, license) + msg("** Creating cookbook #{cookbook_name}") + FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "attributes")}" + FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "recipes")}" + FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "definitions")}" + FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "libraries")}" + FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "resources")}" + FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "providers")}" + FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "files", "default")}" + FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "templates", "default")}" + unless File.exists?(File.join(dir, cookbook_name, "recipes", "default.rb")) + open(File.join(dir, cookbook_name, "recipes", "default.rb"), "w") do |file| + file.puts <<-EOH +# +# Cookbook Name:: #{cookbook_name} +# Recipe:: default +# +# Copyright #{Time.now.year}, #{copyright} +# +EOH + case license + when "apachev2" + file.puts <<-EOH +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +EOH + when "gplv2" + file.puts <<-EOH +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +EOH + when "gplv3" + file.puts <<-EOH +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +EOH + when "mit" + file.puts <<-EOH +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +EOH + when "none" + file.puts <<-EOH +# All rights reserved - Do Not Redistribute +# +EOH + end + end + end + end + + def create_changelog(dir, cookbook_name) + msg("** Creating CHANGELOG for cookbook: #{cookbook_name}") + unless File.exists?(File.join(dir,cookbook_name,'CHANGELOG.md')) + open(File.join(dir, cookbook_name, 'CHANGELOG.md'),'w') do |file| + file.puts <<-EOH +# CHANGELOG for #{cookbook_name} + +This file is used to list changes made in each version of #{cookbook_name}. + +## 0.1.0: + +* Initial release of #{cookbook_name} + +- - - +Check the [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) for help with Markdown. + +The [Github Flavored Markdown page](http://github.github.com/github-flavored-markdown/) describes the differences between markdown on github and standard markdown. +EOH + end + end + end + + def create_readme(dir, cookbook_name,readme_format) + msg("** Creating README for cookbook: #{cookbook_name}") + unless File.exists?(File.join(dir, cookbook_name, "README.#{readme_format}")) + open(File.join(dir, cookbook_name, "README.#{readme_format}"), "w") do |file| + case readme_format + when "rdoc" + file.puts <<-EOH += DESCRIPTION: + += REQUIREMENTS: + += ATTRIBUTES: + += USAGE: + +EOH + when "md","mkd","txt" + file.puts <<-EOH +Description +=========== + +Requirements +============ + +Attributes +========== + +Usage +===== + +EOH + else + file.puts <<-EOH +Description + +Requirements + +Attributes + +Usage + +EOH + end + end + end + end + + def create_metadata(dir, cookbook_name, copyright, email, license,readme_format) + msg("** Creating metadata for cookbook: #{cookbook_name}") + + license_name = case license + when "apachev2" + "Apache 2.0" + when "gplv2" + "GNU Public License 2.0" + when "gplv3" + "GNU Public License 3.0" + when "mit" + "MIT" + when "none" + "All rights reserved" + end + + unless File.exists?(File.join(dir, cookbook_name, "metadata.rb")) + open(File.join(dir, cookbook_name, "metadata.rb"), "w") do |file| + if File.exists?(File.join(dir, cookbook_name, "README.#{readme_format}")) + long_description = "long_description IO.read(File.join(File.dirname(__FILE__), 'README.#{readme_format}'))" + end + file.puts <<-EOH +maintainer "#{copyright}" +maintainer_email "#{email}" +license "#{license_name}" +description "Installs/Configures #{cookbook_name}" +#{long_description} +version "0.1.0" +EOH + end + end + end + + private + + def default_cookbook_path_empty? + Chef::Config[:cookbook_path].nil? || Chef::Config[:cookbook_path].empty? + end + + def parameter_empty?(parameter) + parameter.nil? || parameter.empty? + end + + end + end +end diff --git a/lib/chef/knife/cookbook_delete.rb b/lib/chef/knife/cookbook_delete.rb new file mode 100644 index 0000000000..f436d270bd --- /dev/null +++ b/lib/chef/knife/cookbook_delete.rb @@ -0,0 +1,151 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookDelete < Knife + + attr_accessor :cookbook_name, :version + + deps do + require 'chef/cookbook_version' + end + + option :all, :short => '-a', :long => '--all', :boolean => true, :description => 'delete all versions' + + option :purge, :short => '-p', :long => '--purge', :boolean => true, :description => 'Permanently remove files from backing data store' + + banner "knife cookbook delete COOKBOOK VERSION (options)" + + def run + confirm("Files that are common to multiple cookbooks are shared, so purging the files may disable other cookbooks. Are you sure you want to purge files instead of just deleting the cookbook") if config[:purge] + @cookbook_name, @version = name_args + if @cookbook_name && @version + delete_explicit_version + elsif @cookbook_name && config[:all] + delete_all_versions + elsif @cookbook_name && @version.nil? + delete_without_explicit_version + elsif @cookbook_name.nil? + show_usage + ui.fatal("You must provide the name of the cookbook to delete") + exit(1) + end + end + + def delete_explicit_version + delete_object(Chef::CookbookVersion, "#{@cookbook_name} version #{@version}", "cookbook") do + delete_request("cookbooks/#{@cookbook_name}/#{@version}") + end + end + + def delete_all_versions + confirm("Do you really want to delete all versions of #{@cookbook_name}") + delete_all_without_confirmation + end + + def delete_all_without_confirmation + # look up the available versions again just in case the user + # got to the list of versions to delete and selected 'all' + # and also a specific version + @available_versions = nil + Array(available_versions).each do |version| + delete_version_without_confirmation(version) + end + end + + def delete_without_explicit_version + if available_versions.nil? + # we already logged an error or 2 about it, so just bail + exit(1) + elsif available_versions.size == 1 + @version = available_versions.first + delete_explicit_version + else + versions_to_delete = ask_which_versions_to_delete + delete_versions_without_confirmation(versions_to_delete) + end + end + + def available_versions + @available_versions ||= rest.get_rest("cookbooks/#{@cookbook_name}").map do |name, url_and_version| + url_and_version["versions"].map {|url_by_version| url_by_version["version"]} + end.flatten + rescue Net::HTTPServerException => e + if e.to_s =~ /^404/ + ui.error("Cannot find a cookbook named #{@cookbook_name} to delete") + nil + else + raise + end + end + + def ask_which_versions_to_delete + question = "Which version(s) do you want to delete?\n" + valid_responses = {} + available_versions.each_with_index do |version, index| + valid_responses[(index + 1).to_s] = version + question << "#{index + 1}. #{@cookbook_name} #{version}\n" + end + valid_responses[(available_versions.size + 1).to_s] = :all + question << "#{available_versions.size + 1}. All versions\n\n" + responses = ask_question(question).split(',').map { |response| response.strip } + + if responses.empty? + ui.error("No versions specified, exiting") + exit(1) + end + versions = responses.map do |response| + if version = valid_responses[response] + version + else + ui.error("#{response} is not a valid choice, skipping it") + end + end + versions.compact + end + + def delete_version_without_confirmation(version) + object = delete_request("cookbooks/#{@cookbook_name}/#{version}") + output(format_for_display(object)) if config[:print_after] + ui.info("Deleted cookbook[#{@cookbook_name}][#{version}]") + end + + def delete_versions_without_confirmation(versions) + versions.each do |version| + if version == :all + delete_all_without_confirmation + break + else + delete_version_without_confirmation(version) + end + end + end + + private + + def delete_request(path) + path += "?purge=true" if config[:purge] + rest.delete_rest(path) + end + + end + end +end diff --git a/lib/chef/knife/cookbook_download.rb b/lib/chef/knife/cookbook_download.rb new file mode 100644 index 0000000000..1da1121b22 --- /dev/null +++ b/lib/chef/knife/cookbook_download.rb @@ -0,0 +1,137 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookDownload < Knife + + attr_reader :version + attr_accessor :cookbook_name + + deps do + require 'chef/cookbook_version' + end + + banner "knife cookbook download COOKBOOK [VERSION] (options)" + + option :latest, + :short => "-N", + :long => "--latest", + :description => "The version of the cookbook to download", + :boolean => true + + option :download_directory, + :short => "-d DOWNLOAD_DIRECTORY", + :long => "--dir DOWNLOAD_DIRECTORY", + :description => "The directory to download the cookbook into", + :default => Dir.pwd + + option :force, + :short => "-f", + :long => "--force", + :description => "Force download over the download directory if it exists" + + # TODO: tim/cw: 5-23-2010: need to implement knife-side + # specificity for downloads - need to implement --platform and + # --fqdn here + def run + @cookbook_name, @version = @name_args + + if @cookbook_name.nil? + show_usage + ui.fatal("You must specify a cookbook name") + exit 1 + elsif @version.nil? + @version = determine_version + end + + ui.info("Downloading #{@cookbook_name} cookbook version #{@version}") + + cookbook = rest.get_rest("cookbooks/#{@cookbook_name}/#{@version}") + manifest = cookbook.manifest + + basedir = File.join(config[:download_directory], "#{@cookbook_name}-#{cookbook.version}") + if File.exists?(basedir) + if config[:force] + Chef::Log.debug("Deleting #{basedir}") + FileUtils.rm_rf(basedir) + else + ui.fatal("Directory #{basedir} exists, use --force to overwrite") + exit + end + end + + Chef::CookbookVersion::COOKBOOK_SEGMENTS.each do |segment| + next unless manifest.has_key?(segment) + ui.info("Downloading #{segment}") + manifest[segment].each do |segment_file| + dest = File.join(basedir, segment_file['path'].gsub('/', File::SEPARATOR)) + Chef::Log.debug("Downloading #{segment_file['path']} to #{dest}") + FileUtils.mkdir_p(File.dirname(dest)) + rest.sign_on_redirect = false + tempfile = rest.get_rest(segment_file['url'], true) + FileUtils.mv(tempfile.path, dest) + end + end + ui.info("Cookbook downloaded to #{basedir}") + end + + def determine_version + if available_versions.size == 1 + @version = available_versions.first + elsif config[:latest] + @version = available_versions.map { |v| Chef::Version.new(v) }.sort.last + else + ask_which_version + end + end + + def available_versions + @available_versions ||= begin + versions = Chef::CookbookVersion.available_versions(@cookbook_name).map do |version| + Chef::Version.new(version) + end + versions.sort! + versions + end + @available_versions + end + + def ask_which_version + question = "Which version do you want to download?\n" + valid_responses = {} + available_versions.each_with_index do |version, index| + valid_responses[(index + 1).to_s] = version + question << "#{index + 1}. #{@cookbook_name} #{version}\n" + end + question += "\n" + response = ask_question(question).strip + + unless @version = valid_responses[response] + ui.error("'#{response}' is not a valid value.") + exit(1) + end + @version + end + + end + end +end diff --git a/lib/chef/knife/cookbook_list.rb b/lib/chef/knife/cookbook_list.rb new file mode 100644 index 0000000000..75f18a154b --- /dev/null +++ b/lib/chef/knife/cookbook_list.rb @@ -0,0 +1,47 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Nuo Yan (<nuo@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010, 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookList < Knife + + banner "knife cookbook list (options)" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + option :all_versions, + :short => "-a", + :long => "--all", + :description => "Show all available versions." + + def run + env = config[:environment] + num_versions = config[:all_versions] ? "num_versions=all" : "num_versions=1" + api_endpoint = env ? "/environments/#{env}/cookbooks?#{num_versions}" : "/cookbooks?#{num_versions}" + cookbook_versions = rest.get_rest(api_endpoint) + ui.output(format_cookbook_list_for_display(cookbook_versions)) + end + end + end +end diff --git a/lib/chef/knife/cookbook_metadata.rb b/lib/chef/knife/cookbook_metadata.rb new file mode 100644 index 0000000000..dfa69ae39f --- /dev/null +++ b/lib/chef/knife/cookbook_metadata.rb @@ -0,0 +1,108 @@ +# +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookMetadata < Knife + + deps do + require 'chef/cookbook_loader' + require 'chef/cookbook/metadata' + end + + banner "knife cookbook metadata COOKBOOK (options)" + + option :cookbook_path, + :short => "-o PATH:PATH", + :long => "--cookbook-path PATH:PATH", + :description => "A colon-separated path to look for cookbooks in", + :proc => lambda { |o| o.split(":") } + + option :all, + :short => "-a", + :long => "--all", + :description => "Generate metadata for all cookbooks, rather than just a single cookbook" + + def run + config[:cookbook_path] ||= Chef::Config[:cookbook_path] + + if config[:all] + cl = Chef::CookbookLoader.new(config[:cookbook_path]) + cl.load_cookbooks + cl.each do |cname, cookbook| + generate_metadata(cname.to_s) + end + else + cookbook_name = @name_args[0] + if cookbook_name.nil? || cookbook_name.empty? + ui.error "You must specify the cookbook to generate metadata for, or use the --all option." + exit 1 + end + generate_metadata(cookbook_name) + end + end + + def generate_metadata(cookbook) + Array(config[:cookbook_path]).reverse.each do |path| + file = File.expand_path(File.join(path, cookbook, 'metadata.rb')) + if File.exists?(file) + generate_metadata_from_file(cookbook, file) + else + validate_metadata_json(path, cookbook) + end + end + end + + def generate_metadata_from_file(cookbook, file) + ui.info("Generating metadata for #{cookbook} from #{file}") + md = Chef::Cookbook::Metadata.new + md.name(cookbook) + md.from_file(file) + json_file = File.join(File.dirname(file), 'metadata.json') + File.open(json_file, "w") do |f| + f.write(Chef::JSONCompat.to_json_pretty(md)) + end + generated = true + Chef::Log.debug("Generated #{json_file}") + rescue Exceptions::ObsoleteDependencySyntax, Exceptions::InvalidVersionConstraint => e + ui.stderr.puts "ERROR: The cookbook '#{cookbook}' contains invalid or obsolete metadata syntax." + ui.stderr.puts "in #{file}:" + ui.stderr.puts + ui.stderr.puts e.message + exit 1 + end + + def validate_metadata_json(path, cookbook) + json_file = File.join(path, cookbook, 'metadata.json') + if File.exist?(json_file) + Chef::Cookbook::Metadata.validate_json(IO.read(json_file)) + end + rescue Exceptions::ObsoleteDependencySyntax, Exceptions::InvalidVersionConstraint => e + ui.stderr.puts "ERROR: The cookbook '#{cookbook}' contains invalid or obsolete metadata syntax." + ui.stderr.puts "in #{json_file}:" + ui.stderr.puts + ui.stderr.puts e.message + exit 1 + end + + end + end +end diff --git a/lib/chef/knife/cookbook_metadata_from_file.rb b/lib/chef/knife/cookbook_metadata_from_file.rb new file mode 100644 index 0000000000..eb1afa8b11 --- /dev/null +++ b/lib/chef/knife/cookbook_metadata_from_file.rb @@ -0,0 +1,44 @@ +# +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Matthew Kent (<mkent@magoazul.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# Copyright:: Copyright (c) 2010 Matthew Kent +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookMetadataFromFile < Knife + + deps do + require 'chef/cookbook/metadata' + end + + banner "knife cookbook metadata from FILE (options)" + + def run + file = @name_args[0] + cookbook = File.basename(File.dirname(file)) + + @metadata = Chef::Knife::CookbookMetadata.new + @metadata.generate_metadata_from_file(cookbook, file) + end + + end + end +end diff --git a/lib/chef/knife/cookbook_show.rb b/lib/chef/knife/cookbook_show.rb new file mode 100644 index 0000000000..3545d20817 --- /dev/null +++ b/lib/chef/knife/cookbook_show.rb @@ -0,0 +1,102 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookShow < Knife + + deps do + require 'chef/json_compat' + require 'uri' + require 'chef/cookbook_version' + end + + banner "knife cookbook show COOKBOOK [VERSION] [PART] [FILENAME] (options)" + + option :fqdn, + :short => "-f FQDN", + :long => "--fqdn FQDN", + :description => "The FQDN of the host to see the file for" + + option :platform, + :short => "-p PLATFORM", + :long => "--platform PLATFORM", + :description => "The platform to see the file for" + + option :platform_version, + :short => "-V VERSION", + :long => "--platform-version VERSION", + :description => "The platform version to see the file for" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + case @name_args.length + when 4 # We are showing a specific file + node = Hash.new + node[:fqdn] = config[:fqdn] if config.has_key?(:fqdn) + node[:platform] = config[:platform] if config.has_key?(:platform) + node[:platform_version] = config[:platform_version] if config.has_key?(:platform_version) + + class << node + def attribute?(name) + has_key?(name) + end + end + + cookbook_name, segment, filename = @name_args[0], @name_args[2], @name_args[3] + cookbook_version = @name_args[1] == 'latest' ? '_latest' : @name_args[1] + + cookbook = rest.get_rest("cookbooks/#{cookbook_name}/#{cookbook_version}") + manifest_entry = cookbook.preferred_manifest_record(node, segment, filename) + temp_file = rest.get_rest(manifest_entry[:url], true) + + # the temp file is cleaned up elsewhere + temp_file.open if temp_file.closed? + pretty_print(temp_file.read) + + when 3 # We are showing a specific part of the cookbook + cookbook_version = @name_args[1] == 'latest' ? '_latest' : @name_args[1] + result = rest.get_rest("cookbooks/#{@name_args[0]}/#{cookbook_version}") + output(result.manifest[@name_args[2]]) + when 2 # We are showing the whole cookbook data + cookbook_version = @name_args[1] == 'latest' ? '_latest' : @name_args[1] + output(rest.get_rest("cookbooks/#{@name_args[0]}/#{cookbook_version}")) + when 1 # We are showing the cookbook versions (all of them) + cookbook_name = @name_args[0] + env = config[:environment] + api_endpoint = env ? "environments/#{env}/cookbooks/#{cookbook_name}" : "cookbooks/#{cookbook_name}" + output(format_cookbook_list_for_display(rest.get_rest(api_endpoint))) + when 0 + show_usage + ui.fatal("You must specify a cookbook name") + exit 1 + end + end + end + end +end + + + + diff --git a/lib/chef/knife/cookbook_site_download.rb b/lib/chef/knife/cookbook_site_download.rb new file mode 100644 index 0000000000..645b1728e6 --- /dev/null +++ b/lib/chef/knife/cookbook_site_download.rb @@ -0,0 +1,109 @@ +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookSiteDownload < Knife + + deps do + require 'fileutils' + end + + banner "knife cookbook site download COOKBOOK [VERSION] (options)" + category "cookbook site" + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "The filename to write to" + + option :force, + :long => "--force", + :description => "Force download deprecated version" + + def run + if current_cookbook_deprecated? + message = 'DEPRECATION: This cookbook has been deprecated. ' + message << "It has been replaced by #{replacement_cookbook}." + ui.warn message + + unless config[:force] + ui.warn 'Use --force to force download deprecated cookbook.' + return + end + end + + download_cookbook + end + + def version + @version = desired_cookbook_data['version'] + end + + private + def cookbooks_api_url + 'http://cookbooks.opscode.com/api/v1/cookbooks' + end + + def current_cookbook_data + @current_cookbook_data ||= begin + noauth_rest.get_rest "#{cookbooks_api_url}/#{@name_args[0]}" + end + end + + def current_cookbook_deprecated? + current_cookbook_data['deprecated'] == true + end + + def desired_cookbook_data + @desired_cookbook_data ||= begin + uri = if @name_args.length == 1 + current_cookbook_data['latest_version'] + else + specific_cookbook_version_url + end + + noauth_rest.get_rest uri + end + end + + def download_cookbook + ui.info "Downloading #{@name_args[0]} from the cookbooks site at version #{version} to #{download_location}" + noauth_rest.sign_on_redirect = false + tf = noauth_rest.get_rest desired_cookbook_data["file"], true + + ::FileUtils.cp tf.path, download_location + ui.info "Cookbook saved: #{download_location}" + end + + def download_location + config[:file] ||= File.join Dir.pwd, "#{@name_args[0]}-#{version}.tar.gz" + config[:file] + end + + def replacement_cookbook + replacement = File.basename(current_cookbook_data['replacement']) + end + + def specific_cookbook_version_url + "#{cookbooks_api_url}/#{@name_args[0]}/versions/#{@name_args[1].gsub('.', '_')}" + end + end + end +end diff --git a/lib/chef/knife/cookbook_site_install.rb b/lib/chef/knife/cookbook_site_install.rb new file mode 100644 index 0000000000..b2e0d84751 --- /dev/null +++ b/lib/chef/knife/cookbook_site_install.rb @@ -0,0 +1,155 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' +require 'shellwords' + +class Chef + class Knife + + class CookbookSiteInstall < Knife + + deps do + require 'chef/mixin/shell_out' + require 'chef/knife/core/cookbook_scm_repo' + require 'chef/cookbook/metadata' + end + + banner "knife cookbook site install COOKBOOK [VERSION] (options)" + category "cookbook site" + + option :no_deps, + :short => "-D", + :long => "--skip-dependencies", + :boolean => true, + :default => false, + :description => "Skips automatic dependency installation." + + option :cookbook_path, + :short => "-o PATH:PATH", + :long => "--cookbook-path PATH:PATH", + :description => "A colon-separated path to look for cookbooks in", + :proc => lambda { |o| o.split(":") } + + option :default_branch, + :short => "-B BRANCH", + :long => "--branch BRANCH", + :description => "Default branch to work with", + :default => "master" + + option :use_current_branch, + :short => "-b", + :long => "--use-current-branch", + :description => "Use the current branch", + :boolean => true, + :default => false + + attr_reader :cookbook_name + attr_reader :vendor_path + + def run + extend Chef::Mixin::ShellOut + + if config[:cookbook_path] + Chef::Config[:cookbook_path] = config[:cookbook_path] + else + config[:cookbook_path] = Chef::Config[:cookbook_path] + end + + @cookbook_name = parse_name_args! + # Check to ensure we have a valid source of cookbooks before continuing + # + @install_path = File.expand_path(config[:cookbook_path].first) + ui.info "Installing #@cookbook_name to #{@install_path}" + + @repo = CookbookSCMRepo.new(@install_path, ui, config) + #cookbook_path = File.join(vendor_path, name_args[0]) + upstream_file = File.join(@install_path, "#{@cookbook_name}.tar.gz") + + @repo.sanity_check + unless config[:use_current_branch] + @repo.reset_to_default_state + @repo.prepare_to_import(@cookbook_name) + end + + downloader = download_cookbook_to(upstream_file) + clear_existing_files(File.join(@install_path, @cookbook_name)) + extract_cookbook(upstream_file, downloader.version) + + # TODO: it'd be better to store these outside the cookbook repo and + # keep them around, e.g., in ~/Library/Caches on OS X. + ui.info("removing downloaded tarball") + File.unlink(upstream_file) + + if @repo.finalize_updates_to(@cookbook_name, downloader.version) + unless config[:use_current_branch] + @repo.reset_to_default_state + end + @repo.merge_updates_from(@cookbook_name, downloader.version) + else + unless config[:use_current_branch] + @repo.reset_to_default_state + end + end + + unless config[:no_deps] + md = Chef::Cookbook::Metadata.new + md.from_file(File.join(@install_path, @cookbook_name, "metadata.rb")) + md.dependencies.each do |cookbook, version_list| + # Doesn't do versions.. yet + nv = self.class.new + nv.config = config + nv.name_args = [ cookbook ] + nv.run + end + end + end + + def parse_name_args! + if name_args.empty? + ui.error("Please specify a cookbook to download and install.") + exit 1 + elsif name_args.size >= 2 + unless name_args.last.match(/^(\d+)(\.\d+){1,2}$/) and name_args.size == 2 + ui.error("Installing multiple cookbooks at once is not supported.") + exit 1 + end + end + name_args.first + end + + def download_cookbook_to(download_path) + downloader = Chef::Knife::CookbookSiteDownload.new + downloader.config[:file] = download_path + downloader.name_args = name_args + downloader.run + downloader + end + + def extract_cookbook(upstream_file, version) + ui.info("Uncompressing #{@cookbook_name} version #{version}.") + shell_out!("tar zxvf #{Shellwords.escape upstream_file}", :cwd => @install_path) + end + + def clear_existing_files(cookbook_path) + ui.info("Removing pre-existing version.") + FileUtils.rmtree(cookbook_path) if File.directory?(cookbook_path) + end + end + end +end diff --git a/lib/chef/knife/cookbook_site_list.rb b/lib/chef/knife/cookbook_site_list.rb new file mode 100644 index 0000000000..96c4ef0eed --- /dev/null +++ b/lib/chef/knife/cookbook_site_list.rb @@ -0,0 +1,62 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookSiteList < Knife + + banner "knife cookbook site list (options)" + category "cookbook site" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + if config[:with_uri] + cookbooks = Hash.new + get_cookbook_list.each{ |k,v| cookbooks[k] = v['cookbook'] } + ui.output(format_for_display(cookbooks)) + else + ui.msg(ui.list(get_cookbook_list.keys.sort, :columns_down)) + end + end + + def get_cookbook_list(items=10, start=0, cookbook_collection={}) + cookbooks_url = "http://cookbooks.opscode.com/api/v1/cookbooks?items=#{items}&start=#{start}" + cr = noauth_rest.get_rest(cookbooks_url) + cr["items"].each do |cookbook| + cookbook_collection[cookbook["cookbook_name"]] = cookbook + end + new_start = start + cr["items"].length + if new_start < cr["total"] + get_cookbook_list(items, new_start, cookbook_collection) + else + cookbook_collection + end + end + end + end +end + + + + diff --git a/lib/chef/knife/cookbook_site_search.rb b/lib/chef/knife/cookbook_site_search.rb new file mode 100644 index 0000000000..5df7d67327 --- /dev/null +++ b/lib/chef/knife/cookbook_site_search.rb @@ -0,0 +1,51 @@ +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookSiteSearch < Knife + + banner "knife cookbook site search QUERY (options)" + category "cookbook site" + + def run + output(search_cookbook(name_args[0])) + end + + def search_cookbook(query, items=10, start=0, cookbook_collection={}) + cookbooks_url = "http://cookbooks.opscode.com/api/v1/search?q=#{query}&items=#{items}&start=#{start}" + cr = noauth_rest.get_rest(cookbooks_url) + cr["items"].each do |cookbook| + cookbook_collection[cookbook["cookbook_name"]] = cookbook + end + new_start = start + cr["items"].length + if new_start < cr["total"] + search_cookbook(query, items, new_start, cookbook_collection) + else + cookbook_collection + end + end + end + end +end + + + + + diff --git a/lib/chef/knife/cookbook_site_share.rb b/lib/chef/knife/cookbook_site_share.rb new file mode 100644 index 0000000000..77c4895dcc --- /dev/null +++ b/lib/chef/knife/cookbook_site_share.rb @@ -0,0 +1,114 @@ +# Author:: Nuo Yan (<nuo@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookSiteShare < Knife + + deps do + require 'chef/cookbook_uploader' + require 'chef/cookbook_site_streaming_uploader' + end + + banner "knife cookbook site share COOKBOOK CATEGORY (options)" + category "cookbook site" + + option :cookbook_path, + :short => "-o PATH:PATH", + :long => "--cookbook-path PATH:PATH", + :description => "A colon-separated path to look for cookbooks in", + :proc => lambda { |o| Chef::Config.cookbook_path = o.split(":") } + + def run + if @name_args.length < 2 + show_usage + ui.fatal("You must specify the cookbook name and the category you want to share this cookbook to.") + exit 1 + end + + config[:cookbook_path] ||= Chef::Config[:cookbook_path] + + cookbook_name = @name_args[0] + category = @name_args[1] + cl = Chef::CookbookLoader.new(config[:cookbook_path]) + if cl.cookbook_exists?(cookbook_name) + cookbook = cl[cookbook_name] + Chef::CookbookUploader.new(cookbook,config[:cookbook_path]).validate_cookbooks + tmp_cookbook_dir = Chef::CookbookSiteStreamingUploader.create_build_dir(cookbook) + begin + Chef::Log.debug("Temp cookbook directory is #{tmp_cookbook_dir.inspect}") + ui.info("Making tarball #{cookbook_name}.tgz") + Chef::Mixin::Command.run_command(:command => "tar -czf #{cookbook_name}.tgz #{cookbook_name}", :cwd => tmp_cookbook_dir) + rescue => e + ui.error("Error making tarball #{cookbook_name}.tgz: #{e.message}. Set log level to debug (-l debug) for more information.") + Chef::Log.debug("\n#{e.backtrace.join("\n")}") + exit(1) + end + + begin + do_upload("#{tmp_cookbook_dir}/#{cookbook_name}.tgz", category, Chef::Config[:node_name], Chef::Config[:client_key]) + ui.info("Upload complete!") + Chef::Log.debug("Removing local staging directory at #{tmp_cookbook_dir}") + FileUtils.rm_rf tmp_cookbook_dir + rescue => e + ui.error("Error uploading cookbook #{cookbook_name} to the Opscode Cookbook Site: #{e.message}. Set log level to debug (-l debug) for more information.") + Chef::Log.debug("\n#{e.backtrace.join("\n")}") + exit(1) + end + + else + ui.error("Could not find cookbook #{cookbook_name} in your cookbook path.") + exit(1) + end + + end + + def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename) + uri = "http://cookbooks.opscode.com/api/v1/cookbooks" + + category_string = { 'category'=>cookbook_category }.to_json + + http_resp = Chef::CookbookSiteStreamingUploader.post(uri, user_id, user_secret_filename, { + :tarball => File.open(cookbook_filename), + :cookbook => category_string + }) + + res = Chef::JSONCompat.from_json(http_resp.body) + if http_resp.code.to_i != 201 + if res['error_messages'] + if res['error_messages'][0] =~ /Version already exists/ + ui.error "The same version of this cookbook already exists on the Opscode Cookbook Site." + exit(1) + else + ui.error "#{res['error_messages'][0]}" + exit(1) + end + else + ui.error "Unknown error while sharing cookbook" + ui.error "Server response: #{http_resp.body}" + exit(1) + end + end + res + end + end + + end +end diff --git a/lib/chef/knife/cookbook_site_show.rb b/lib/chef/knife/cookbook_site_show.rb new file mode 100644 index 0000000000..a02dd61fc8 --- /dev/null +++ b/lib/chef/knife/cookbook_site_show.rb @@ -0,0 +1,60 @@ +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookSiteShow < Knife + + banner "knife cookbook site show COOKBOOK [VERSION] (options)" + category "cookbook site" + + def run + output(format_for_display(get_cookbook_data)) + end + + def get_cookbook_data + case @name_args.length + when 1 + noauth_rest.get_rest("http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}") + when 2 + noauth_rest.get_rest("http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}/versions/#{name_args[1].gsub('.', '_')}") + end + end + + def get_cookbook_list(items=10, start=0, cookbook_collection={}) + cookbooks_url = "http://cookbooks.opscode.com/api/v1/cookbooks?items=#{items}&start=#{start}" + cr = noauth_rest.get_rest(cookbooks_url) + cr["items"].each do |cookbook| + cookbook_collection[cookbook["cookbook_name"]] = cookbook + end + new_start = start + cr["items"].length + if new_start < cr["total"] + get_cookbook_list(items, new_start, cookbook_collection) + else + cookbook_collection + end + end + end + end +end + + + + + diff --git a/lib/chef/knife/cookbook_site_unshare.rb b/lib/chef/knife/cookbook_site_unshare.rb new file mode 100644 index 0000000000..a2828549a0 --- /dev/null +++ b/lib/chef/knife/cookbook_site_unshare.rb @@ -0,0 +1,56 @@ +# +# Author:: Stephen Delano (<stephen@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookSiteUnshare < Knife + + deps do + require 'chef/json_compat' + end + + banner "knife cookbook site unshare COOKBOOK" + category "cookbook site" + + def run + @cookbook_name = @name_args[0] + if @cookbook_name.nil? + show_usage + ui.fatal "You must provide the name of the cookbook to unshare" + exit 1 + end + + confirm "Do you really want to unshare the cookbook #{@cookbook_name}" + + begin + rest.delete_rest "http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}" + rescue Net::HTTPServerException => e + raise e unless e.message =~ /Forbidden/ + ui.error "Forbidden: You must be the maintainer of #{@cookbook_name} to unshare it." + exit 1 + end + + ui.info "Unshared cookbook #{@cookbook_name}" + end + + end + end +end diff --git a/lib/chef/knife/cookbook_site_vendor.rb b/lib/chef/knife/cookbook_site_vendor.rb new file mode 100644 index 0000000000..82575958bd --- /dev/null +++ b/lib/chef/knife/cookbook_site_vendor.rb @@ -0,0 +1,46 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' +require 'chef/knife/cookbook_site_install' + +class Chef::Knife::CookbookSiteVendor < Chef::Knife::CookbookSiteInstall + + def self.load_deps + superclass.load_deps + end + + def self.options=(new_opts) + superclass.options = new_opts + end + + def self.options + superclass.options + end + + banner(<<-B) +************************************************* +DEPRECATED: please use knife cookbook site install +************************************************* + +#{superclass.banner} +B + + category 'deprecated' + +end diff --git a/lib/chef/knife/cookbook_test.rb b/lib/chef/knife/cookbook_test.rb new file mode 100644 index 0000000000..dc8f12d135 --- /dev/null +++ b/lib/chef/knife/cookbook_test.rb @@ -0,0 +1,95 @@ +# +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Matthew Kent (<mkent@magoazul.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# Copyright:: Copyright (c) 2010 Matthew Kent +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require 'chef/knife' + +class Chef + class Knife + class CookbookTest < Knife + + deps do + require 'chef/checksum_cache' + require 'chef/cookbook/syntax_check' + end + + banner "knife cookbook test [COOKBOOKS...] (options)" + + option :cookbook_path, + :short => "-o PATH:PATH", + :long => "--cookbook-path PATH:PATH", + :description => "A colon-separated path to look for cookbooks in", + :proc => lambda { |o| o.split(":") } + + option :all, + :short => "-a", + :long => "--all", + :description => "Test all cookbooks, rather than just a single cookbook" + + def run + config[:cookbook_path] ||= Chef::Config[:cookbook_path] + + checked_a_cookbook = false + if config[:all] + cl = cookbook_loader + cl.load_cookbooks + cl.each do |key, cookbook| + checked_a_cookbook = true + test_cookbook(key) + end + else + @name_args.each do |cb| + ui.info "checking #{cb}" + next unless cookbook_loader.cookbook_exists?(cb) + checked_a_cookbook = true + test_cookbook(cb) + end + end + unless checked_a_cookbook + ui.warn("No cookbooks to test in #{Array(config[:cookbook_path]).join(',')} - is your cookbook path misconfigured?") + end + end + + def test_cookbook(cookbook) + ui.info("Running syntax check on #{cookbook}") + Array(config[:cookbook_path]).reverse.each do |path| + syntax_checker = Chef::Cookbook::SyntaxCheck.for_cookbook(cookbook, path) + test_ruby(syntax_checker) + test_templates(syntax_checker) + end + end + + + def test_ruby(syntax_checker) + ui.info("Validating ruby files") + exit(1) unless syntax_checker.validate_ruby_files + end + + def test_templates(syntax_checker) + ui.info("Validating templates") + exit(1) unless syntax_checker.validate_templates + end + + def cookbook_loader + @cookbook_loader ||= Chef::CookbookLoader.new(config[:cookbook_path]) + end + + end + end +end diff --git a/lib/chef/knife/cookbook_upload.rb b/lib/chef/knife/cookbook_upload.rb new file mode 100644 index 0000000000..b9ecc02a54 --- /dev/null +++ b/lib/chef/knife/cookbook_upload.rb @@ -0,0 +1,295 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Author:: Nuo Yan (<yan.nuo@gmail.com>) +# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class CookbookUpload < Knife + + CHECKSUM = "checksum" + MATCH_CHECKSUM = /[0-9a-f]{32,}/ + + deps do + require 'chef/exceptions' + require 'chef/cookbook_loader' + require 'chef/cookbook_uploader' + end + + banner "knife cookbook upload [COOKBOOKS...] (options)" + + option :cookbook_path, + :short => "-o PATH:PATH", + :long => "--cookbook-path PATH:PATH", + :description => "A colon-separated path to look for cookbooks in", + :proc => lambda { |o| o.split(":") } + + option :freeze, + :long => '--freeze', + :description => 'Freeze this version of the cookbook so that it cannot be overwritten', + :boolean => true + + option :all, + :short => "-a", + :long => "--all", + :description => "Upload all cookbooks, rather than just a single cookbook" + + option :force, + :long => '--force', + :boolean => true, + :description => "Update cookbook versions even if they have been frozen" + + option :environment, + :short => '-E', + :long => '--environment ENVIRONMENT', + :description => "Set ENVIRONMENT's version dependency match the version you're uploading.", + :default => nil + + option :depends, + :short => "-d", + :long => "--include-dependencies", + :description => "Also upload cookbook dependencies" + + def run + # Sanity check before we load anything from the server + unless config[:all] + if @name_args.empty? + show_usage + ui.fatal("You must specify the --all flag or at least one cookbook name") + exit 1 + end + end + + config[:cookbook_path] ||= Chef::Config[:cookbook_path] + + if @name_args.empty? and ! config[:all] + show_usage + ui.fatal("You must specify the --all flag or at least one cookbook name") + exit 1 + end + + assert_environment_valid! + version_constraints_to_update = {} + upload_failures = 0 + upload_ok = 0 + + # Get a list of cookbooks and their versions from the server + # to check for the existence of a cookbook's dependencies. + @server_side_cookbooks = Chef::CookbookVersion.list_all_versions + justify_width = @server_side_cookbooks.map {|name| name.size}.max.to_i + 2 + if config[:all] + + cbs = [] + cookbook_repo.each do |cookbook_name, cookbook| + cbs << cookbook + cookbook.freeze_version if config[:freeze] + version_constraints_to_update[cookbook_name] = cookbook.version + end + begin + upload(cbs, justify_width) + rescue Exceptions::CookbookFrozen + ui.warn("Not updating version constraints for some cookbooks in the environment as the cookbook is frozen.") + end + ui.info("Uploaded all cookbooks.") + else + if @name_args.empty? + show_usage + ui.error("You must specify the --all flag or at least one cookbook name") + exit 1 + end + + cookbooks_to_upload.each do |cookbook_name, cookbook| + cookbook.freeze_version if config[:freeze] + begin + upload([cookbook], justify_width) + upload_ok += 1 + version_constraints_to_update[cookbook_name] = cookbook.version + rescue Exceptions::CookbookNotFoundInRepo => e + upload_failures += 1 + ui.error("Could not find cookbook #{cookbook_name} in your cookbook path, skipping it") + Log.debug(e) + upload_failures += 1 + rescue Exceptions::CookbookFrozen + ui.warn("Not updating version constraints for #{cookbook_name} in the environment as the cookbook is frozen.") + upload_failures += 1 + end + end + + upload_failures += @name_args.length - @cookbooks_to_upload.length + + if upload_failures == 0 + ui.info "Uploaded #{upload_ok} cookbook#{upload_ok > 1 ? "s" : ""}." + elsif upload_failures > 0 && upload_ok > 0 + ui.warn "Uploaded #{upload_ok} cookbook#{upload_ok > 1 ? "s" : ""} ok but #{upload_failures} " + + "cookbook#{upload_failures > 1 ? "s" : ""} upload failed." + elsif upload_failures > 0 && upload_ok == 0 + ui.error "Failed to upload #{upload_failures} cookbook#{upload_failures > 1 ? "s" : ""}." + exit 1 + end + end + + unless version_constraints_to_update.empty? + update_version_constraints(version_constraints_to_update) if config[:environment] + end + end + + def cookbooks_to_upload + @cookbooks_to_upload ||= + if config[:all] + cookbook_repo.load_cookbooks + else + upload_set = {} + @name_args.each do |cookbook_name| + begin + if ! upload_set.has_key?(cookbook_name) + upload_set[cookbook_name] = cookbook_repo[cookbook_name] + if config[:depends] + upload_set[cookbook_name].metadata.dependencies.each { |dep, ver| @name_args << dep } + end + end + rescue Exceptions::CookbookNotFoundInRepo => e + ui.error("Could not find cookbook #{cookbook_name} in your cookbook path, skipping it") + Log.debug(e) + end + end + upload_set + end + end + + def cookbook_repo + @cookbook_loader ||= begin + Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, config[:cookbook_path]) } + Chef::CookbookLoader.new(config[:cookbook_path]) + end + end + + def update_version_constraints(new_version_constraints) + new_version_constraints.each do |cookbook_name, version| + environment.cookbook_versions[cookbook_name] = "= #{version}" + end + environment.save + end + + def environment + @environment ||= config[:environment] ? Environment.load(config[:environment]) : nil + end + + def warn_about_cookbook_shadowing + unless cookbook_repo.merged_cookbooks.empty? + ui.warn "* " * 40 + ui.warn(<<-WARNING) +The cookbooks: #{cookbook_repo.merged_cookbooks.join(', ')} exist in multiple places in your cookbook_path. +A composite version of these cookbooks has been compiled for uploading. + +#{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this behavior will be removed and you will no longer +be able to have the same version of a cookbook in multiple places in your cookbook_path. +WARNING + ui.warn "The affected cookbooks are located:" + ui.output ui.format_for_display(cookbook_repo.merged_cookbook_paths) + ui.warn "* " * 40 + end + end + + private + + def assert_environment_valid! + environment + rescue Net::HTTPServerException => e + if e.response.code.to_s == "404" + ui.error "The environment #{config[:environment]} does not exist on the server, aborting." + Log.debug(e) + exit 1 + else + raise + end + end + + def upload(cookbooks, justify_width) + cookbooks.each do |cb| + ui.info("Uploading #{cb.name.to_s.ljust(justify_width + 10)} [#{cb.version}]") + check_for_broken_links!(cb) + check_for_dependencies!(cb) + end + Chef::CookbookUploader.new(cookbooks, config[:cookbook_path], :force => config[:force]).upload_cookbooks + rescue Net::HTTPServerException => e + case e.response.code + when "409" + ui.error "Version #{cookbook.version} of cookbook #{cookbook.name} is frozen. Use --force to override." + Log.debug(e) + raise Exceptions::CookbookFrozen + else + raise + end + end + + def check_for_broken_links!(cookbook) + # MUST!! dup the cookbook version object--it memoizes its + # manifest object, but the manifest becomes invalid when you + # regenerate the metadata + broken_files = cookbook.dup.manifest_records_by_path.select do |path, info| + info[CHECKSUM].nil? || info[CHECKSUM] !~ MATCH_CHECKSUM + end + unless broken_files.empty? + broken_filenames = Array(broken_files).map {|path, info| path} + ui.error "The cookbook #{cookbook.name} has one or more broken files" + ui.error "This is probably caused by broken symlinks in the cookbook directory" + ui.error "The broken file(s) are: #{broken_filenames.join(' ')}" + exit 1 + end + end + + def check_for_dependencies!(cookbook) + # for each dependency, check if the version is on the server, or + # the version is in the cookbooks being uploaded. If not, exit and warn the user. + cookbook.metadata.dependencies.each do |cookbook_name, version| + unless check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version) + ui.error "Cookbook #{cookbook.name} depends on cookbook #{cookbook_name} version #{version}," + ui.error "which is not currently being uploaded and cannot be found on the server." + exit 1 + end + end + end + + def check_server_side_cookbooks(cookbook_name, version) + if @server_side_cookbooks[cookbook_name].nil? + false + else + versions = @server_side_cookbooks[cookbook_name]['versions'].collect {|versions| versions["version"]} + Log.debug "Versions of cookbook '#{cookbook_name}' returned by the server: #{versions.join(", ")}" + @server_side_cookbooks[cookbook_name]["versions"].each do |versions_hash| + if Chef::VersionConstraint.new(version).include?(versions_hash["version"]) + Log.debug "Matched cookbook '#{cookbook_name}' with constraint '#{version}' to cookbook version '#{versions_hash['version']}' on the server" + return true + end + end + false + end + end + + def check_uploading_cookbooks(cookbook_name, version) + if (! cookbooks_to_upload[cookbook_name].nil?) && Chef::VersionConstraint.new(version).include?(cookbooks_to_upload[cookbook_name].version) + Log.debug "Matched cookbook '#{cookbook_name}' with constraint '#{version}' to a local cookbook." + return true + end + false + end + end + end +end diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb new file mode 100644 index 0000000000..71dc008d39 --- /dev/null +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -0,0 +1,106 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/run_list' +class Chef + class Knife + module Core + # Instances of BootstrapContext are the context objects (i.e., +self+) for + # bootstrap templates. For backwards compatability, they +must+ set the + # following instance variables: + # * @config - a hash of knife's config values + # * @run_list - the run list for the node to boostrap + # + class BootstrapContext + + def initialize(config, run_list, chef_config) + @config = config + @run_list = run_list + @chef_config = chef_config + end + + def bootstrap_version_string + if @config[:prerelease] + "--prerelease" + else + "--version #{chef_version}" + end + end + + def bootstrap_environment + @chef_config[:environment] || '_default' + end + + def validation_key + IO.read(@chef_config[:validation_key]) + end + + def encrypted_data_bag_secret + IO.read(@chef_config[:encrypted_data_bag_secret]) + end + + def config_content + client_rb = <<-CONFIG +log_level :info +log_location STDOUT +chef_server_url "#{@chef_config[:chef_server_url]}" +validation_client_name "#{@chef_config[:validation_client_name]}" +CONFIG + if @config[:chef_node_name] + client_rb << %Q{node_name "#{@config[:chef_node_name]}"\n} + else + client_rb << "# Using default node name (fqdn)\n" + end + + if knife_config[:bootstrap_proxy] + client_rb << %Q{http_proxy "#{knife_config[:bootstrap_proxy]}"\n} + client_rb << %Q{https_proxy "#{knife_config[:bootstrap_proxy]}"\n} + end + + if @chef_config[:encrypted_data_bag_secret] + client_rb << %Q{encrypted_data_bag_secret "/etc/chef/encrypted_data_bag_secret"\n} + end + + client_rb + end + + def start_chef + # If the user doesn't have a client path configure, let bash use the PATH for what it was designed for + client_path = @chef_config[:chef_client_path] || 'chef-client' + s = "#{client_path} -j /etc/chef/first-boot.json" + s << " -E #{bootstrap_environment}" if chef_version.to_f != 0.9 # only use the -E option on Chef 0.10+ + s + end + + def knife_config + @chef_config.key?(:knife) ? @chef_config[:knife] : {} + end + + def chef_version + knife_config[:bootstrap_version] || Chef::VERSION + end + + def first_boot + (@config[:first_boot_attributes] || {}).merge(:run_list => @run_list) + end + + end + end + end +end + diff --git a/lib/chef/knife/core/cookbook_scm_repo.rb b/lib/chef/knife/core/cookbook_scm_repo.rb new file mode 100644 index 0000000000..727cff3153 --- /dev/null +++ b/lib/chef/knife/core/cookbook_scm_repo.rb @@ -0,0 +1,160 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/shell_out' + +class Chef + class Knife + class CookbookSCMRepo + + DIRTY_REPO = /^[\s]+M/ + + include Chef::Mixin::ShellOut + + attr_reader :repo_path + attr_reader :default_branch + attr_reader :use_current_branch + attr_reader :ui + + def initialize(repo_path, ui, opts={}) + @repo_path = repo_path + @ui = ui + @default_branch = 'master' + @use_current_branch = false + apply_opts(opts) + end + + def sanity_check + unless ::File.directory?(repo_path) + ui.error("The cookbook repo path #{repo_path} does not exist or is not a directory") + exit 1 + end + unless git_repo?(repo_path) + ui.error "The cookbook repo #{repo_path} is not a git repository." + ui.info("Use `git init` to initialize a git repo") + exit 1 + end + if use_current_branch + @default_branch = get_current_branch() + end + unless branch_exists?(default_branch) + ui.error "The default branch '#{default_branch}' does not exist" + ui.info "If this is a new git repo, make sure you have at least one commit before installing cookbooks" + exit 1 + end + cmd = git('status --porcelain') + if cmd.stdout =~ DIRTY_REPO + ui.error "You have uncommitted changes to your cookbook repo (#{repo_path}):" + ui.msg cmd.stdout + ui.info "Commit or stash your changes before importing cookbooks" + exit 1 + end + # TODO: any untracked files in the cookbook directory will get nuked later + # make this an error condition also. + true + end + + def reset_to_default_state + ui.info("Checking out the #{default_branch} branch.") + git("checkout #{default_branch}") + end + + def prepare_to_import(cookbook_name) + branch = "chef-vendor-#{cookbook_name}" + if branch_exists?(branch) + ui.info("Pristine copy branch (#{branch}) exists, switching to it.") + git("checkout #{branch}") + else + ui.info("Creating pristine copy branch #{branch}") + git("checkout -b #{branch}") + end + end + + def finalize_updates_to(cookbook_name, version) + if update_count = updated?(cookbook_name) + ui.info "#{update_count} files updated, committing changes" + git("add #{cookbook_name}") + git("commit -m \"Import #{cookbook_name} version #{version}\" -- #{cookbook_name}") + ui.info("Creating tag cookbook-site-imported-#{cookbook_name}-#{version}") + git("tag -f cookbook-site-imported-#{cookbook_name}-#{version}") + true + else + ui.info("No changes made to #{cookbook_name}") + false + end + end + + def merge_updates_from(cookbook_name, version) + branch = "chef-vendor-#{cookbook_name}" + Dir.chdir(repo_path) do + if system("git merge #{branch}") + ui.info("Cookbook #{cookbook_name} version #{version} successfully installed") + else + ui.error("You have merge conflicts - please resolve manually") + ui.info("Merge status (cd #{repo_path}; git status):") + system("git status") + exit 3 + end + end + end + + def updated?(cookbook_name) + update_count = git("status --porcelain -- #{cookbook_name}").stdout.strip.lines.count + update_count == 0 ? nil : update_count + end + + def branch_exists?(branch_name) + git("branch --no-color").stdout.lines.any? {|l| l =~ /\s#{Regexp.escape(branch_name)}(?:\s|$)/ } + end + + def get_current_branch() + ref = git("symbolic-ref HEAD").stdout + ref.chomp.split('/')[2] + end + + private + + def git_repo?(directory) + if File.directory?(File.join(directory, '.git')) + return true + elsif File.dirname(directory) == directory + return false + else + git_repo?(File.dirname(directory)) + end + end + + def apply_opts(opts) + opts.each do |option, value| + case option.to_s + when 'default_branch' + @default_branch = value + when 'use_current_branch' + @use_current_branch = value + end + end + end + + def git(command) + shell_out!("git #{command}", :cwd => repo_path) + end + + end + end +end + diff --git a/lib/chef/knife/core/generic_presenter.rb b/lib/chef/knife/core/generic_presenter.rb new file mode 100644 index 0000000000..0866f10147 --- /dev/null +++ b/lib/chef/knife/core/generic_presenter.rb @@ -0,0 +1,204 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife/core/text_formatter' + +class Chef + class Knife + module Core + + #==Chef::Knife::Core::GenericPresenter + # The base presenter class for displaying structured data in knife commands. + # This is not an abstract base class, and it is suitable for displaying + # most kinds of objects that knife needs to display. + class GenericPresenter + + attr_reader :ui + attr_reader :config + + # Instaniates a new GenericPresenter. This is generally handled by the + # Chef::Knife::UI object, though you need to match the signature of this + # method if you intend to use your own presenter instead. + def initialize(ui, config) + @ui, @config = ui, config + end + + # Is the selected output format a data interchange format? + # Returns true if the selected output format is json or yaml, false + # otherwise. Knife search uses this to adjust its data output so as not + # to produce invalid JSON output. + def interchange? + case parse_format_option + when :json, :yaml + true + else + false + end + end + + # Returns a String representation of +data+ that is suitable for output + # to a terminal or perhaps for data interchange with another program. + # The representation of the +data+ depends on the value of the + # `config[:format]` setting. + def format(data) + case parse_format_option + when :summary + summarize(data) + when :text + text_format(data) + when :json + Chef::JSONCompat.to_json_pretty(data) + when :yaml + require 'yaml' + YAML::dump(data) + when :pp + require 'stringio' + # If you were looking for some attribute and there is only one match + # just dump the attribute value + if config[:attribute] and data.length == 1 + data.values[0] + else + out = StringIO.new + PP.pp(data, out) + out.string + end + end + end + + # Converts the user-supplied value of `config[:format]` to a Symbol + # representing the desired output format. + # ===Returns + # returns one of :summary, :text, :json, :yaml, or :pp + # ===Raises + # Raises an ArgumentError if the desired output format could not be + # determined from the value of `config[:format]` + def parse_format_option + case config[:format] + when "summary", /^s/, nil + :summary + when "text", /^t/ + :text + when "json", /^j/ + :json + when "yaml", /^y/ + :yaml + when "pp", /^p/ + :pp + else + raise ArgumentError, "Unknown output format #{config[:format]}" + end + end + + # Summarize the data. Defaults to text format output, + # which may not be very summary-like + def summarize(data) + text_format(data) + end + + # Converts the +data+ to a String in the text format. Uses + # Chef::Knife::Core::TextFormatter + def text_format(data) + TextFormatter.new(data, ui).formatted_data + end + + def format_list_for_display(list) + config[:with_uri] ? list : list.keys.sort { |a,b| a <=> b } + end + + def format_for_display(data) + if formatting_subset_of_data? + format_data_subset_for_display(data) + elsif config[:id_only] + name_or_id_for(data) + elsif config[:environment] && data.respond_to?(:chef_environment) + {"chef_environment" => data.chef_environment} + else + data + end + end + + def format_data_subset_for_display(data) + subset = if config[:attribute] + result = {} + Array(config[:attribute]).each do |nested_value_spec| + nested_value = extract_nested_value(data, nested_value_spec) + result[nested_value_spec] = nested_value + end + result + elsif config[:run_list] + run_list = data.run_list.run_list + { "run_list" => run_list } + else + raise ArgumentError, "format_data_subset_for_display requires attribute, run_list, or id_only config option to be set" + end + {name_or_id_for(data) => subset } + end + + def name_or_id_for(data) + data.respond_to?(:name) ? data.name : data["id"] + end + + def formatting_subset_of_data? + config[:attribute] || config[:run_list] + end + + + def extract_nested_value(data, nested_value_spec) + nested_value_spec.split(".").each do |attr| + if data.nil? + nil # don't get no method error on nil + elsif data.respond_to?(attr.to_sym) + data = data.send(attr.to_sym) + elsif data.respond_to?(:[]) + data = data[attr] + else + data = begin + data.send(attr.to_sym) + rescue NoMethodError + nil + end + end + end + ( !data.kind_of?(Array) && data.respond_to?(:to_hash) ) ? data.to_hash : data + end + + def format_cookbook_list_for_display(item) + if config[:with_uri] + item.inject({}) do |collected, (cookbook, versions)| + collected[cookbook] = Hash.new + versions['versions'].each do |ver| + collected[cookbook][ver['version']] = ver['url'] + end + collected + end + else + versions_by_cookbook = item.inject({}) do |collected, ( cookbook, versions )| + collected[cookbook] = versions["versions"].map {|v| v['version']} + collected + end + key_length = versions_by_cookbook.empty? ? 0 : versions_by_cookbook.keys.map {|name| name.size }.max + 2 + versions_by_cookbook.sort.map do |cookbook, versions| + "#{cookbook.ljust(key_length)} #{versions.join(' ')}" + end + end + end + + end + end + end +end diff --git a/lib/chef/knife/core/node_editor.rb b/lib/chef/knife/core/node_editor.rb new file mode 100644 index 0000000000..22ba3eaa25 --- /dev/null +++ b/lib/chef/knife/core/node_editor.rb @@ -0,0 +1,130 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/json_compat' +require 'chef/node' + +class Chef + class Knife + class NodeEditor + + attr_reader :node + attr_reader :ui + attr_reader :config + + def initialize(node, ui, config) + @node, @ui, @config = node, ui, config + end + + def edit_node + abort "You specified the --disable_editing option, nothing to edit" if config[:disable_editing] + assert_editor_set! + + updated_node_data = edit_data(view) + apply_updates(updated_node_data) + @updated_node + end + + def view + result = {} + result["name"] = node.name + result["chef_environment"] = node.chef_environment + result["normal"] = node.normal_attrs + result["run_list"] = node.run_list + + if config[:all_attributes] + result["default"] = node.default_attrs + result["override"] = node.override_attrs + result["automatic"] = node.automatic_attrs + end + Chef::JSONCompat.to_json_pretty(result) + end + + def edit_data(text) + edited_data = tempfile_for(text) {|filename| system("#{config[:editor]} #{filename}")} + Chef::JSONCompat.from_json(edited_data) + end + + def apply_updates(updated_data) + if node.name and node.name != updated_data["name"] + ui.warn "Changing the name of a node results in a new node being created, #{node.name} will not be modified or removed." + confirm = ui.confirm "Proceed with creation of new node" + end + + @updated_node = Node.new.tap do |n| + n.name( updated_data["name"] ) + n.chef_environment( updated_data["chef_environment"] ) + n.run_list( updated_data["run_list"]) + n.normal_attrs = updated_data["normal"] + + if config[:all_attributes] + n.default_attrs = updated_data["default"] + n.override_attrs = updated_data["override"] + n.automatic_attrs = updated_data["automatic"] + else + n.default_attrs = node.default_attrs + n.override_attrs = node.override_attrs + n.automatic_attrs = node.automatic_attrs + end + end + end + + def updated? + pristine_copy = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(node), :create_additions => false) + updated_copy = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(@updated_node), :create_additions => false) + unless pristine_copy == updated_copy + updated_properties = %w{name normal chef_environment run_list default override automatic}.reject do |key| + pristine_copy[key] == updated_copy[key] + end + end + ( pristine_copy != updated_copy ) && updated_properties + end + + private + + def abort(message) + ui.error(message) + exit 1 + end + + def assert_editor_set! + unless config[:editor] + abort "You must set your EDITOR environment variable or configure your editor via knife.rb" + end + end + + def tempfile_for(data) + # TODO: include useful info like the node name in the temp file + # name + basename = "knife-edit-" << rand(1_000_000_000_000_000).to_s.rjust(15, '0') << '.js' + filename = File.join(Dir.tmpdir, basename) + File.open(filename, "w+") do |f| + f.sync = true + f.puts data + end + + yield filename + + IO.read(filename) + ensure + File.unlink(filename) + end + end + end +end + diff --git a/lib/chef/knife/core/node_presenter.rb b/lib/chef/knife/core/node_presenter.rb new file mode 100644 index 0000000000..a35baf2264 --- /dev/null +++ b/lib/chef/knife/core/node_presenter.rb @@ -0,0 +1,137 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife/core/text_formatter' +require 'chef/knife/core/generic_presenter' + +class Chef + class Knife + module Core + + # This module may be included into a knife subcommand class to automatically + # add configuration options used by the NodePresenter + module NodeFormattingOptions + # :nodoc: + # Would prefer to do this in a rational way, but can't be done b/c of + # Mixlib::CLI's design :( + def self.included(includer) + includer.class_eval do + option :medium_output, + :short => '-m', + :long => '--medium', + :boolean => true, + :default => false, + :description => 'Include normal attributes in the output' + + option :long_output, + :short => '-l', + :long => '--long', + :boolean => true, + :default => false, + :description => 'Include all attributes in the output' + end + end + end + + #==Chef::Knife::Core::NodePresenter + # A customized presenter for Chef::Node objects. Supports variable-length + # output formats for displaying node data + class NodePresenter < GenericPresenter + + def format(data) + if parse_format_option == :json + summarize_json(data) + else + super + end + end + + def summarize_json(data) + if data.kind_of?(Chef::Node) + node = data + result = {} + + result["name"] = node.name + result["chef_environment"] = node.chef_environment + result["run_list"] = node.run_list + result["normal"] = node.normal_attrs + + if config[:long_output] + result["default"] = node.default_attrs + result["override"] = node.override_attrs + result["automatic"] = node.automatic_attrs + end + + Chef::JSONCompat.to_json_pretty(result) + else + Chef::JSONCompat.to_json_pretty(data) + end + end + + # Converts a Chef::Node object to a string suitable for output to a + # terminal. If config[:medium_output] or config[:long_output] are set + # the volume of output is adjusted accordingly. Uses colors if enabled + # in the the ui object. + def summarize(data) + if data.kind_of?(Chef::Node) + node = data + # special case ec2 with their split horizon whatsis. + ip = (node[:ec2] && node[:ec2][:public_ipv4]) || node[:ipaddress] + + summarized=<<-SUMMARY +#{ui.color('Node Name:', :bold)} #{ui.color(node.name, :bold)} +#{key('Environment:')} #{node.chef_environment} +#{key('FQDN:')} #{node[:fqdn]} +#{key('IP:')} #{ip} +#{key('Run List:')} #{node.run_list} +#{key('Roles:')} #{Array(node[:roles]).join(', ')} +#{key('Recipes:')} #{Array(node[:recipes]).join(', ')} +#{key('Platform:')} #{node[:platform]} #{node[:platform_version]} +#{key('Tags:')} #{Array(node[:tags]).join(', ')} +SUMMARY + if config[:medium_output] || config[:long_output] + summarized +=<<-MORE +#{key('Attributes:')} +#{text_format(node.normal_attrs)} +MORE + end + if config[:long_output] + summarized +=<<-MOST +#{key('Default Attributes:')} +#{text_format(node.default_attrs)} +#{key('Override Attributes:')} +#{text_format(node.override_attrs)} +#{key('Automatic Attributes (Ohai Data):')} +#{text_format(node.automatic_attrs)} +MOST + end + summarized + else + super + end + end + + def key(key_text) + ui.color(key_text, :cyan) + end + + end + end + end +end + diff --git a/lib/chef/knife/core/object_loader.rb b/lib/chef/knife/core/object_loader.rb new file mode 100644 index 0000000000..1d207c10d1 --- /dev/null +++ b/lib/chef/knife/core/object_loader.rb @@ -0,0 +1,112 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Knife + module Core + class ObjectLoader + + attr_reader :ui + attr_reader :klass + + class ObjectType + FILE = 1 + FOLDER = 2 + end + + def initialize(klass, ui) + @klass = klass + @ui = ui + end + + def load_from(repo_location, *components) + unless object_file = find_file(repo_location, *components) + ui.error "Could not find or open file '#{components.last}' in current directory or in '#{repo_location}/#{components.join('/')}'" + exit 1 + end + object_from_file(object_file) + end + + # When someone makes this awesome, please update the above error message. + def find_file(repo_location, *components) + if file_exists_and_is_readable?(File.expand_path( components.last )) + File.expand_path( components.last ) + else + relative_path = File.join(Dir.pwd, repo_location, *components) + if file_exists_and_is_readable?(relative_path) + relative_path + else + nil + end + end + end + + # Find all objects in the given location + # If the object type is File it will look for all *.{json,rb} + # files, otherwise it will lookup for folders only (useful for + # data_bags) + # + # @param [String] path - base look up location + # + # @return [Array<String>] basenames of the found objects + # + # @api public + def find_all_objects(path) + path = File.join(path, '*') + path << '.{json,rb}' + objects = Dir.glob(File.expand_path(path)) + objects.map { |o| File.basename(o) } + end + + def find_all_object_dirs(path) + path = File.join(path, '*') + objects = Dir.glob(File.expand_path(path)) + objects.delete_if { |o| !File.directory?(o) } + objects.map { |o| File.basename(o) } + end + + def object_from_file(filename) + case filename + when /\.(js|json)$/ + r = Yajl::Parser.parse(IO.read(filename)) + + # Chef::DataBagItem doesn't work well with the json_create method + if @klass == Chef::DataBagItem + r + else + @klass.json_create(r) + end + when /\.rb$/ + r = klass.new + r.from_file(filename) + r + else + ui.fatal("File must end in .js, .json, or .rb") + exit 30 + end + end + + def file_exists_and_is_readable?(file) + File.exists?(file) && File.readable?(file) + end + + end + end + end +end + diff --git a/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb new file mode 100644 index 0000000000..314f54bc0b --- /dev/null +++ b/lib/chef/knife/core/subcommand_loader.rb @@ -0,0 +1,112 @@ +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2009, 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/version' +class Chef + class Knife + class SubcommandLoader + + CHEF_FILE_IN_GEM = /chef-[\d]+\.[\d]+\.[\d]+/ + CURRENT_CHEF_GEM = /chef-#{Regexp.escape(Chef::VERSION)}/ + + attr_reader :chef_config_dir + attr_reader :env + + def initialize(chef_config_dir, env=ENV) + @chef_config_dir, @env = chef_config_dir, env + @forced_activate = {} + end + + # Load all the sub-commands + def load_commands + subcommand_files.each { |subcommand| Kernel.load subcommand } + true + end + + # Returns an Array of paths to knife commands located in chef_config_dir/plugins/knife/ + # and ~/.chef/plugins/knife/ + def site_subcommands + user_specific_files = [] + + if chef_config_dir + user_specific_files.concat Dir.glob(File.expand_path("plugins/knife/*.rb", chef_config_dir)) + end + + # finally search ~/.chef/plugins/knife/*.rb + user_specific_files.concat Dir.glob(File.join(env['HOME'], '.chef', 'plugins', 'knife', '*.rb')) if env['HOME'] + + user_specific_files + end + + # Returns a Hash of paths to knife commands built-in to chef, or installed via gem. + # If rubygems is not installed, falls back to globbing the knife directory. + # The Hash is of the form {"relative/path" => "/absolute/path"} + #-- + # Note: the "right" way to load the plugins is to require the relative path, i.e., + # require 'chef/knife/command' + # but we're getting frustrated by bugs at every turn, and it's slow besides. So + # subcommand loader has been modified to load the plugins by using Kernel.load + # with the absolute path. + def gem_and_builtin_subcommands + # search all gems for chef/knife/*.rb + require 'rubygems' + find_subcommands_via_rubygems + rescue LoadError + find_subcommands_via_dirglob + end + + def subcommand_files + @subcommand_files ||= (gem_and_builtin_subcommands.values + site_subcommands).flatten.uniq + end + + def find_subcommands_via_dirglob + # The "require paths" of the core knife subcommands bundled with chef + files = Dir[File.expand_path('../../../knife/*.rb', __FILE__)] + subcommand_files = {} + files.each do |knife_file| + rel_path = knife_file[/#{CHEF_ROOT}#{Regexp.escape(File::SEPARATOR)}(.*)\.rb/,1] + subcommand_files[rel_path] = knife_file + end + subcommand_files + end + + def find_subcommands_via_rubygems + files = Gem.find_files 'chef/knife/*.rb' + files.reject! {|f| from_old_gem?(f) } + subcommand_files = {} + files.each do |file| + rel_path = file[/(#{Regexp.escape File.join('chef', 'knife', '')}.*)\.rb/, 1] + subcommand_files[rel_path] = file + end + + subcommand_files.merge(find_subcommands_via_dirglob) + end + + private + + # wow, this is a sad hack :( + # Gem.find_files finds files in all versions of a gem, which + # means that if chef 0.10 and 0.9.x are installed, we'll try to + # require, e.g., chef/knife/ec2_server_create, which will cause + # a gem activation error. So remove files from older chef gems. + def from_old_gem?(path) + path =~ CHEF_FILE_IN_GEM && path !~ CURRENT_CHEF_GEM + end + end + end +end diff --git a/lib/chef/knife/core/text_formatter.rb b/lib/chef/knife/core/text_formatter.rb new file mode 100644 index 0000000000..60328488b2 --- /dev/null +++ b/lib/chef/knife/core/text_formatter.rb @@ -0,0 +1,86 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Knife + module Core + class TextFormatter + + attr_reader :data + attr_reader :ui + + def initialize(data, ui) + @ui = ui + @data = if data.respond_to?(:display_hash) + data.display_hash + elsif data.kind_of?(Array) + data + elsif data.respond_to?(:to_hash) + data.to_hash + else + data + end + end + + def formatted_data + @formatted_data ||= text_format(data) + end + + def text_format(data) + buffer = '' + + if data.respond_to?(:keys) + justify_width = data.keys.map {|k| k.to_s.size }.max.to_i + 1 + data.sort.each do |key, value| + # key: ['value'] should be printed as key: value + if value.kind_of?(Array) && value.size == 1 && is_singleton(value[0]) + value = value[0] + end + if is_singleton(value) + # Strings are printed as key: value. + justified_key = ui.color("#{key}:".ljust(justify_width), :cyan) + buffer << "#{justified_key} #{value}\n" + else + # Arrays and hashes get indented on their own lines. + buffer << ui.color("#{key}:\n", :cyan) + lines = text_format(value).split("\n") + lines.each { |line| buffer << " #{line}\n" } + end + end + elsif data.kind_of?(Array) + data.each_index do |index| + item = data[index] + buffer << text_format(data[index]) + # Separate items with newlines if it's an array of hashes or an + # array of arrays + buffer << "\n" if !is_singleton(data[index]) && index != data.size-1 + end + else + buffer << "#{data}\n" + end + buffer + end + + def is_singleton(value) + !(value.kind_of?(Array) || value.respond_to?(:keys)) + end + end + end + end +end + diff --git a/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb new file mode 100644 index 0000000000..85e9612315 --- /dev/null +++ b/lib/chef/knife/core/ui.rb @@ -0,0 +1,219 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2009, 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'forwardable' +require 'chef/platform' +require 'chef/knife/core/generic_presenter' + +class Chef + class Knife + + #==Chef::Knife::UI + # The User Interaction class used by knife. + class UI + + extend Forwardable + + attr_reader :stdout + attr_reader :stderr + attr_reader :stdin + attr_reader :config + + attr_reader :presenter + + def_delegator :@presenter, :format_list_for_display + def_delegator :@presenter, :format_for_display + def_delegator :@presenter, :format_cookbook_list_for_display + + def initialize(stdout, stderr, stdin, config) + @stdout, @stderr, @stdin, @config = stdout, stderr, stdin, config + @presenter = Chef::Knife::Core::GenericPresenter.new(self, config) + end + + # Creates a new +presenter_class+ object and uses it to format structured + # data for display. By default, a Chef::Knife::Core::GenericPresenter + # object is used. + def use_presenter(presenter_class) + @presenter = presenter_class.new(self, config) + end + + def highline + @highline ||= begin + require 'highline' + HighLine.new + end + end + + # Prints a message to stdout. Aliased as +info+ for compatibility with + # the logger API. + def msg(message) + stdout.puts message + end + + alias :info :msg + + # Prints a msg to stderr. Used for warn, error, and fatal. + def err(message) + stderr.puts message + end + + # Print a warning message + def warn(message) + err("#{color('WARNING:', :yellow, :bold)} #{message}") + end + + # Print an error message + def error(message) + err("#{color('ERROR:', :red, :bold)} #{message}") + end + + # Print a message describing a fatal error. + def fatal(message) + err("#{color('FATAL:', :red, :bold)} #{message}") + end + + def color(string, *colors) + if color? + highline.color(string, *colors) + else + string + end + end + + # Should colored output be used? For output to a terminal, this is + # determined by the value of `config[:color]`. When output is not to a + # terminal, colored output is never used + def color? + Chef::Config[:color] && stdout.tty? && !Chef::Platform.windows? + end + + def ask(*args, &block) + highline.ask(*args, &block) + end + + def list(*args) + highline.list(*args) + end + + # Formats +data+ using the configured presenter and outputs the result + # via +msg+. Formatting can be customized by configuring a different + # presenter. See +use_presenter+ + def output(data) + msg @presenter.format(data) + end + + # Determines if the output format is a data interchange format, i.e., + # JSON or YAML + def interchange? + @presenter.interchange? + end + + def ask_question(question, opts={}) + question = question + "[#{opts[:default]}] " if opts[:default] + + if opts[:default] and config[:defaults] + opts[:default] + else + stdout.print question + a = stdin.readline.strip + + if opts[:default] + a.empty? ? opts[:default] : a + else + a + end + end + end + + def pretty_print(data) + stdout.puts data + end + + def edit_data(data, parse_output=true) + output = Chef::JSONCompat.to_json_pretty(data) + + if (!config[:disable_editing]) + filename = "knife-edit-" + 0.upto(20) { filename += rand(9).to_s } + filename << ".js" + filename = File.join(Dir.tmpdir, filename) + tf = File.open(filename, "w") + tf.sync = true + tf.puts output + tf.close + raise "Please set EDITOR environment variable" unless system("#{config[:editor]} #{tf.path}") + tf = File.open(filename, "r") + output = tf.gets(nil) + tf.close + File.unlink(filename) + end + + parse_output ? Chef::JSONCompat.from_json(output) : output + end + + def edit_object(klass, name) + object = klass.load(name) + + output = edit_data(object) + + # Only make the save if the user changed the object. + # + # Output JSON for the original (object) and edited (output), then parse + # them without reconstituting the objects into real classes + # (create_additions=false). Then, compare the resulting simple objects, + # which will be Array/Hash/String/etc. + # + # We wouldn't have to do these shenanigans if all the editable objects + # implemented to_hash, or if to_json against a hash returned a string + # with stable key order. + object_parsed_again = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(object), :create_additions => false) + output_parsed_again = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(output), :create_additions => false) + if object_parsed_again != output_parsed_again + output.save + self.msg("Saved #{output}") + else + self.msg("Object unchanged, not saving") + end + output(format_for_display(object)) if config[:print_after] + end + + def confirm(question, append_instructions=true) + return true if config[:yes] + + stdout.print question + stdout.print "? (Y/N) " if append_instructions + answer = stdin.readline + answer.chomp! + case answer + when "Y", "y" + true + when "N", "n" + self.msg("You said no, so I'm done here.") + exit 3 + else + self.msg("I have no idea what to do with #{answer}") + self.msg("Just say Y or N, please.") + confirm(question) + end + end + + end + end +end diff --git a/lib/chef/knife/data_bag_create.rb b/lib/chef/knife/data_bag_create.rb new file mode 100644 index 0000000000..e644ab78d3 --- /dev/null +++ b/lib/chef/knife/data_bag_create.rb @@ -0,0 +1,93 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright (c) 2009-2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class DataBagCreate < Knife + + deps do + require 'chef/data_bag' + require 'chef/encrypted_data_bag_item' + end + + banner "knife data bag create BAG [ITEM] (options)" + category "data bag" + + option :secret, + :short => "-s SECRET", + :long => "--secret ", + :description => "The secret key to use to encrypt data bag item values" + + option :secret_file, + :long => "--secret-file SECRET_FILE", + :description => "A file containing the secret key to use to encrypt data bag item values" + + def read_secret + if config[:secret] + config[:secret] + else + Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) + end + end + + def use_encryption + if config[:secret] && config[:secret_file] + ui.fatal("please specify only one of --secret, --secret-file") + exit(1) + end + config[:secret] || config[:secret_file] + end + + def run + @data_bag_name, @data_bag_item_name = @name_args + + if @data_bag_name.nil? + show_usage + ui.fatal("You must specify a data bag name") + exit 1 + end + + # create the data bag + begin + rest.post_rest("data", { "name" => @data_bag_name }) + ui.info("Created data_bag[#{@data_bag_name}]") + rescue Net::HTTPServerException => e + raise unless e.to_s =~ /^409/ + ui.info("Data bag #{@data_bag_name} already exists") + end + + # if an item is specified, create it, as well + if @data_bag_item_name + create_object({ "id" => @data_bag_item_name }, "data_bag_item[#{@data_bag_item_name}]") do |output| + item = Chef::DataBagItem.from_hash( + if use_encryption + Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret) + else + output + end) + item.data_bag(@data_bag_name) + rest.post_rest("data/#{@data_bag_name}", item) + end + end + end + end + end +end diff --git a/lib/chef/knife/data_bag_delete.rb b/lib/chef/knife/data_bag_delete.rb new file mode 100644 index 0000000000..f8e52018a6 --- /dev/null +++ b/lib/chef/knife/data_bag_delete.rb @@ -0,0 +1,51 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class DataBagDelete < Knife + + deps do + require 'chef/data_bag' + end + + banner "knife data bag delete BAG [ITEM] (options)" + category "data bag" + + def run + if @name_args.length == 2 + delete_object(Chef::DataBagItem, @name_args[1], "data_bag_item") do + rest.delete_rest("data/#{@name_args[0]}/#{@name_args[1]}") + end + elsif @name_args.length == 1 + delete_object(Chef::DataBag, @name_args[0], "data_bag") do + rest.delete_rest("data/#{@name_args[0]}") + end + else + show_usage + ui.fatal("You must specify at least a data bag name") + exit 1 + end + end + end + end +end + + diff --git a/lib/chef/knife/data_bag_edit.rb b/lib/chef/knife/data_bag_edit.rb new file mode 100644 index 0000000000..234c77177d --- /dev/null +++ b/lib/chef/knife/data_bag_edit.rb @@ -0,0 +1,94 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright (c) 2009-2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class DataBagEdit < Knife + + deps do + require 'chef/data_bag_item' + require 'chef/encrypted_data_bag_item' + end + + banner "knife data bag edit BAG ITEM (options)" + category "data bag" + + option :secret, + :short => "-s SECRET", + :long => "--secret ", + :description => "The secret key to use to encrypt data bag item values" + + option :secret_file, + :long => "--secret-file SECRET_FILE", + :description => "A file containing the secret key to use to encrypt data bag item values" + + def read_secret + if config[:secret] + config[:secret] + else + Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) + end + end + + def use_encryption + if config[:secret] && config[:secret_file] + stdout.puts "please specify only one of --secret, --secret-file" + exit(1) + end + config[:secret] || config[:secret_file] + end + + def load_item(bag, item_name) + item = Chef::DataBagItem.load(bag, item_name) + if use_encryption + Chef::EncryptedDataBagItem.new(item, read_secret).to_hash + else + item + end + end + + def edit_item(item) + output = edit_data(item) + if use_encryption + Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret) + else + output + end + end + + def run + if @name_args.length != 2 + stdout.puts "You must supply the data bag and an item to edit!" + stdout.puts opt_parser + exit 1 + end + item = load_item(@name_args[0], @name_args[1]) + output = edit_item(item) + rest.put_rest("data/#{@name_args[0]}/#{@name_args[1]}", output) + stdout.puts("Saved data_bag_item[#{@name_args[1]}]") + ui.output(output) if config[:print_after] + end + end + end +end + + + diff --git a/lib/chef/knife/data_bag_from_file.rb b/lib/chef/knife/data_bag_from_file.rb new file mode 100644 index 0000000000..275cbeac52 --- /dev/null +++ b/lib/chef/knife/data_bag_from_file.rb @@ -0,0 +1,136 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class DataBagFromFile < Knife + + deps do + require 'chef/data_bag' + require 'chef/data_bag_item' + require 'chef/knife/core/object_loader' + require 'chef/json_compat' + require 'chef/encrypted_data_bag_item' + end + + banner "knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)" + category "data bag" + + option :secret, + :short => "-s SECRET", + :long => "--secret ", + :description => "The secret key to use to encrypt data bag item values" + + option :secret_file, + :long => "--secret-file SECRET_FILE", + :description => "A file containing the secret key to use to encrypt data bag item values" + + option :all, + :short => "-a", + :long => "--all", + :description => "Upload all data bags or all items for specified data bags" + + def read_secret + if config[:secret] + config[:secret] + else + Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) + end + end + + def use_encryption + if config[:secret] && config[:secret_file] + ui.fatal("please specify only one of --secret, --secret-file") + exit(1) + end + config[:secret] || config[:secret_file] + end + + def loader + @loader ||= Knife::Core::ObjectLoader.new(DataBagItem, ui) + end + + def run + if config[:all] == true + load_all_data_bags(@name_args) + else + if @name_args.size < 2 + ui.msg(opt_parser) + exit(1) + end + @data_bag = @name_args.shift + load_data_bag_items(@data_bag, @name_args) + end + end + + private + def data_bags_path + @data_bag_path ||= "data_bags" + end + + def find_all_data_bags + loader.find_all_object_dirs("./#{data_bags_path}") + end + + def find_all_data_bag_items(data_bag) + loader.find_all_objects("./#{data_bags_path}/#{data_bag}") + end + + def load_all_data_bags(args) + data_bags = args.empty? ? find_all_data_bags : [args.shift] + data_bags.each do |data_bag| + load_data_bag_items(data_bag) + end + end + + def load_data_bag_items(data_bag, items = nil) + items ||= find_all_data_bag_items(data_bag) + item_paths = normalize_item_paths(items) + item_paths.each do |item_path| + item = loader.load_from("#{data_bags_path}", data_bag, item_path) + item = if use_encryption + secret = read_secret + Chef::EncryptedDataBagItem.encrypt_data_bag_item(item, secret) + else + item + end + dbag = Chef::DataBagItem.new + dbag.data_bag(data_bag) + dbag.raw_data = item + dbag.save + ui.info("Updated data_bag_item[#{dbag.data_bag}::#{dbag.id}]") + end + end + + def normalize_item_paths(args) + paths = Array.new + args.each do |path| + if File.directory?(path) + paths.concat(Dir.glob(File.join(path, "*.json"))) + else + paths << path + end + end + paths + end + end + end +end diff --git a/lib/chef/knife/data_bag_list.rb b/lib/chef/knife/data_bag_list.rb new file mode 100644 index 0000000000..31dcf984f6 --- /dev/null +++ b/lib/chef/knife/data_bag_list.rb @@ -0,0 +1,46 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class DataBagList < Knife + + deps do + require 'chef/data_bag' + end + + banner "knife data bag list (options)" + category "data bag" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + output(format_list_for_display(Chef::DataBag.list)) + end + end + end +end + + + + diff --git a/lib/chef/knife/data_bag_show.rb b/lib/chef/knife/data_bag_show.rb new file mode 100644 index 0000000000..81b1425f78 --- /dev/null +++ b/lib/chef/knife/data_bag_show.rb @@ -0,0 +1,81 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Seth Falcon (<seth@opscode.com>) +# Copyright:: Copyright (c) 2009-2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class DataBagShow < Knife + + deps do + require 'chef/data_bag' + require 'chef/encrypted_data_bag_item' + end + + banner "knife data bag show BAG [ITEM] (options)" + category "data bag" + + option :secret, + :short => "-s SECRET", + :long => "--secret ", + :description => "The secret key to use to decrypt data bag item values" + + option :secret_file, + :long => "--secret-file SECRET_FILE", + :description => "A file containing the secret key to use to decrypt data bag item values" + + def read_secret + if config[:secret] + config[:secret] + else + Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) + end + end + + def use_encryption + if config[:secret] && config[:secret_file] + stdout.puts "please specify only one of --secret, --secret-file" + exit(1) + end + config[:secret] || config[:secret_file] + end + + def run + display = case @name_args.length + when 2 + if use_encryption + raw = Chef::EncryptedDataBagItem.load(@name_args[0], + @name_args[1], + read_secret) + format_for_display(raw.to_hash) + else + format_for_display(Chef::DataBagItem.load(@name_args[0], @name_args[1]).raw_data) + end + when 1 + format_list_for_display(Chef::DataBag.load(@name_args[0])) + else + stdout.puts opt_parser + exit(1) + end + output(display) + end + end + end +end + diff --git a/lib/chef/knife/delete.rb b/lib/chef/knife/delete.rb new file mode 100644 index 0000000000..d7482b3085 --- /dev/null +++ b/lib/chef/knife/delete.rb @@ -0,0 +1,33 @@ +require 'chef/chef_fs/knife' +require 'chef/chef_fs/file_system' + +class Chef + class Knife + class Delete < Chef::ChefFS::Knife + banner "knife delete [PATTERN1 ... PATTERNn]" + + common_options + + option :recurse, + :long => '--[no-]recurse', + :boolean => true, + :default => false, + :description => "Delete directories recursively." + + def run + # Get the matches (recursively) + pattern_args.each do |pattern| + Chef::ChefFS::FileSystem.list(chef_fs, pattern) do |result| + begin + result.delete(config[:recurse]) + puts "Deleted #{result.path_for_printing}" + rescue Chef::ChefFS::FileSystem::NotFoundError + STDERR.puts "#{result.path_for_printing}: No such file or directory" + end + end + end + end + end + end +end + diff --git a/lib/chef/knife/diff.rb b/lib/chef/knife/diff.rb new file mode 100644 index 0000000000..57d3bf3f0c --- /dev/null +++ b/lib/chef/knife/diff.rb @@ -0,0 +1,46 @@ +require 'chef/chef_fs/knife' +require 'chef/chef_fs/command_line' + +class Chef + class Knife + class Diff < Chef::ChefFS::Knife + banner "knife diff PATTERNS" + + common_options + + option :recurse, + :long => '--[no-]recurse', + :boolean => true, + :default => true, + :description => "List directories recursively." + + option :name_only, + :long => '--name-only', + :boolean => true, + :description => "Only show names of modified files." + + option :name_status, + :long => '--name-status', + :boolean => true, + :description => "Only show names and statuses of modified files: Added, Deleted, Modified, and Type Changed." + + def run + if config[:name_only] + output_mode = :name_only + end + if config[:name_status] + output_mode = :name_status + end + patterns = pattern_args_from(name_args.length > 0 ? name_args : [ "" ]) + + # Get the matches (recursively) + patterns.each do |pattern| + Chef::ChefFS::CommandLine.diff(pattern, chef_fs, local_fs, config[:recurse] ? nil : 1, output_mode) do |diff| + puts diff + end + end + end + end + end +end + diff --git a/lib/chef/knife/download.rb b/lib/chef/knife/download.rb new file mode 100644 index 0000000000..f891e55530 --- /dev/null +++ b/lib/chef/knife/download.rb @@ -0,0 +1,47 @@ +require 'chef/chef_fs/knife' +require 'chef/chef_fs/command_line' + +class Chef + class Knife + class Download < Chef::ChefFS::Knife + banner "knife download PATTERNS" + + common_options + + option :recurse, + :long => '--[no-]recurse', + :boolean => true, + :default => true, + :description => "List directories recursively." + + option :purge, + :long => '--[no-]purge', + :boolean => true, + :default => false, + :description => "Delete matching local files and directories that do not exist remotely." + + option :force, + :long => '--[no-]force', + :boolean => true, + :default => false, + :description => "Force upload of files even if they match (quicker and harmless, but doesn't print out what it changed)" + + option :dry_run, + :long => '--dry-run', + :short => '-n', + :boolean => true, + :default => false, + :description => "Don't take action, only print what would happen" + + def run + patterns = pattern_args_from(name_args.length > 0 ? name_args : [ "" ]) + + # Get the matches (recursively) + patterns.each do |pattern| + Chef::ChefFS::FileSystem.copy_to(pattern, chef_fs, local_fs, config[:recurse] ? nil : 1, config) + end + end + end + end +end + diff --git a/lib/chef/knife/environment_create.rb b/lib/chef/knife/environment_create.rb new file mode 100644 index 0000000000..6bc00d52b9 --- /dev/null +++ b/lib/chef/knife/environment_create.rb @@ -0,0 +1,53 @@ +# +# Author:: Stephen Delano (<stephen@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class EnvironmentCreate < Knife + + deps do + require 'chef/environment' + require 'chef/json_compat' + end + + banner "knife environment create ENVIRONMENT (options)" + + option :description, + :short => "-d DESCRIPTION", + :long => "--description DESCRIPTION", + :description => "The environment description" + + def run + env_name = @name_args[0] + + if env_name.nil? + show_usage + ui.fatal("You must specify an environment name") + exit 1 + end + + env = Chef::Environment.new + env.name(env_name) + env.description(config[:description]) if config[:description] + create_object(env) + end + end + end +end diff --git a/lib/chef/knife/environment_delete.rb b/lib/chef/knife/environment_delete.rb new file mode 100644 index 0000000000..e17841f805 --- /dev/null +++ b/lib/chef/knife/environment_delete.rb @@ -0,0 +1,45 @@ +# +# Author:: Stephen Delano (<stephen@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class EnvironmentDelete < Knife + + deps do + require 'chef/environment' + require 'chef/json_compat' + end + + banner "knife environment delete ENVIRONMENT (options)" + + def run + env_name = @name_args[0] + + if env_name.nil? + show_usage + ui.fatal("You must specify an environment name") + exit 1 + end + + delete_object(Chef::Environment, env_name) + end + end + end +end diff --git a/lib/chef/knife/environment_edit.rb b/lib/chef/knife/environment_edit.rb new file mode 100644 index 0000000000..54962ac20d --- /dev/null +++ b/lib/chef/knife/environment_edit.rb @@ -0,0 +1,45 @@ +# +# Author:: Stephen Delano (<stephen@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class EnvironmentEdit < Knife + + deps do + require 'chef/environment' + require 'chef/json_compat' + end + + banner "knife environment edit ENVIRONMENT (options)" + + def run + env_name = @name_args[0] + + if env_name.nil? + show_usage + ui.fatal("You must specify an environment name") + exit 1 + end + + edit_object(Chef::Environment, env_name) + end + end + end +end diff --git a/lib/chef/knife/environment_from_file.rb b/lib/chef/knife/environment_from_file.rb new file mode 100644 index 0000000000..af72f84622 --- /dev/null +++ b/lib/chef/knife/environment_from_file.rb @@ -0,0 +1,83 @@ +# +# Author:: Stephen Delano (<stephen@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Knife + class EnvironmentFromFile < Knife + + deps do + require 'chef/environment' + require 'chef/knife/core/object_loader' + end + + banner "knife environment from file FILE [FILE..] (options)" + + option :all, + :short => "-a", + :long => "--all", + :description => "Upload all environments" + + def loader + @loader ||= Knife::Core::ObjectLoader.new(Chef::Environment, ui) + end + + def environments_path + @environments_path ||= "environments" + end + + def find_all_environments + loader.find_all_objects("./#{environments_path}/") + end + + def load_all_environments + environments = find_all_environments + if environments.empty? + ui.fatal("Unable to find any environment files in '#{environments_path}'") + exit(1) + end + environments.each do |env| + load_environment(env) + end + end + + def load_environment(env) + updated = loader.load_from("environments", env) + updated.save + output(format_for_display(updated)) if config[:print_after] + ui.info("Updated Environment #{updated.name}") + end + + + def run + if config[:all] == true + load_all_environments + else + if @name_args[0].nil? + show_usage + ui.fatal("You must specify a file to load") + exit 1 + end + + @name_args.each do |arg| + load_environment(arg) + end + end + end + end + end +end diff --git a/lib/chef/knife/environment_list.rb b/lib/chef/knife/environment_list.rb new file mode 100644 index 0000000000..4e70818093 --- /dev/null +++ b/lib/chef/knife/environment_list.rb @@ -0,0 +1,42 @@ +# +# Author:: Stephen Delano (<stephen@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class EnvironmentList < Knife + + deps do + require 'chef/environment' + require 'chef/json_compat' + end + + banner "knife environment list (options)" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + output(format_list_for_display(Chef::Environment.list)) + end + end + end +end diff --git a/lib/chef/knife/environment_show.rb b/lib/chef/knife/environment_show.rb new file mode 100644 index 0000000000..a2b1636f47 --- /dev/null +++ b/lib/chef/knife/environment_show.rb @@ -0,0 +1,53 @@ +# +# Author:: Stephen Delano (<stephen@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class EnvironmentShow < Knife + + deps do + require 'chef/environment' + require 'chef/json_compat' + end + + @attrs_to_show = [] + option :attribute, + :short => "-a [ATTR]", + :long => "--attribute [ATTR]", + :proc => lambda {|val| @attrs_to_show << val}, + :description => "Show one or more attributes" + + banner "knife environment show ENVIRONMENT (options)" + + def run + env_name = @name_args[0] + + if env_name.nil? + show_usage + ui.fatal("You must specify an environment name") + exit 1 + end + + env = Chef::Environment.load(env_name) + output(format_for_display(env)) + end + end + end +end diff --git a/lib/chef/knife/exec.rb b/lib/chef/knife/exec.rb new file mode 100644 index 0000000000..3e8196c616 --- /dev/null +++ b/lib/chef/knife/exec.rb @@ -0,0 +1,86 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef::Knife::Exec < Chef::Knife + + banner "knife exec [SCRIPT] (options)" + + option :exec, + :short => "-E CODE", + :long => "--exec CODE", + :description => "a string of Chef code to execute" + + option :script_path, + :short => "-p PATH:PATH", + :long => "--script-path PATH:PATH", + :description => "A colon-separated path to look for scripts in", + :proc => lambda { |o| o.split(":") } + + deps do + require 'chef/shell/ext' + end + + def run + config[:script_path] ||= Array(Chef::Config[:script_path]) + + # Default script paths are chef-repo/.chef/scripts and ~/.chef/scripts + config[:script_path] << File.join(Chef::Knife.chef_config_dir, 'scripts') if Chef::Knife.chef_config_dir + config[:script_path] << File.join(ENV['HOME'], '.chef', 'scripts') if ENV['HOME'] + + scripts = Array(name_args) + context = Object.new + Shell::Extensions.extend_context_object(context) + if config[:exec] + context.instance_eval(config[:exec], "-E Argument", 0) + elsif !scripts.empty? + scripts.each do |script| + file = find_script(script) + context.instance_eval(IO.read(file), file, 0) + end + else + script = STDIN.read + context.instance_eval(script, "STDIN", 0) + end + end + + def find_script(x) + # Try to find a script. First try expanding the path given. + script = File.expand_path(x) + return script if File.exists?(script) + + # Failing that, try searching the script path. If we can't find + # anything, fail gracefully. + Chef::Log.debug("Searching script_path: #{config[:script_path].inspect}") + + config[:script_path].each do |path| + path = File.expand_path(path) + test = File.join(path, x) + Chef::Log.debug("Testing: #{test}") + if File.exists?(test) + script = test + Chef::Log.debug("Found: #{test}") + return script + end + end + ui.error("\"#{x}\" not found in current directory or script_path, giving up.") + exit(1) + end + +end diff --git a/lib/chef/knife/help.rb b/lib/chef/knife/help.rb new file mode 100644 index 0000000000..13fe674704 --- /dev/null +++ b/lib/chef/knife/help.rb @@ -0,0 +1,103 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Knife + class Help < Chef::Knife + + banner "knife help [list|TOPIC]" + + def run + if name_args.empty? + ui.info "Usage: knife SUBCOMMAND (options)" + ui.msg "" + # This command is atypical, the user is likely not interested in usage of + # this command, but knife in general. So hack the banner. + opt_parser.banner = "General Knife Options:" + ui.msg opt_parser.to_s + ui.msg "" + ui.info "For further help:" + ui.info(<<-MOAR_HELP) + knife help list list help topics + knife help knife show general knife help + knife help TOPIC display the manual for TOPIC + knife SUBCOMMAND --help show the options for a command +MOAR_HELP + exit 1 + else + @query = name_args.join('-') + end + + + + case @query + when 'topics', 'list' + print_help_topics + exit 1 + when 'intro', 'knife' + @topic = 'knife' + else + @topic = find_manpages_for_query(@query) + end + + manpage_path = find_manpage_path(@topic) + exec "man #{manpage_path}" + end + + def help_topics + # The list of help topics is generated by a rake task from the available man pages + # This constant is provided in help_topics.rb which is automatically required/loaded by the knife subcommand loader. + HELP_TOPICS + end + + def print_help_topics + ui.info "Available help topics are: " + help_topics.collect {|t| t.gsub(/knife-/, '') }.sort.each do |topic| + ui.msg " #{topic}" + end + end + + def find_manpages_for_query(query) + possibilities = help_topics.select do |manpage| + ::File.fnmatch("knife-#{query}*", manpage) || ::File.fnmatch("#{query}*", manpage) + end + if possibilities.empty? + ui.error "No help found for '#{query}'" + ui.msg "" + print_help_topics + exit 1 + elsif possibilities.size == 1 + possibilities.first + else + ui.info "Multiple help topics match your query. Pick one:" + ui.highline.choose(*possibilities) + end + end + + def find_manpage_path(topic) + if ::File.exists?(::File.expand_path("../distro/common/man/man1/#{topic}.1", CHEF_ROOT)) + # If we've provided the man page in the gem, give that + return ::File.expand_path("../distro/common/man/man1/#{topic}.1", CHEF_ROOT) + else + # Otherwise, we'll just be using MANPATH + topic + end + end + end + end +end diff --git a/lib/chef/knife/help_topics.rb b/lib/chef/knife/help_topics.rb new file mode 100644 index 0000000000..8427204fd6 --- /dev/null +++ b/lib/chef/knife/help_topics.rb @@ -0,0 +1,4 @@ +# Do not edit this file by hand +# This file is autogenerated by the docs:list rake task from the available manpages + +HELP_TOPICS = ["chef-shell", "knife-bootstrap", "knife-client", "knife-configure", "knife-cookbook-site", "knife-cookbook", "knife-data-bag", "knife-environment", "knife-exec", "knife-index", "knife-node", "knife-role", "knife-search", "knife-ssh", "knife-status", "knife-tag", "knife", "shef"] diff --git a/lib/chef/knife/index_rebuild.rb b/lib/chef/knife/index_rebuild.rb new file mode 100644 index 0000000000..b35da77619 --- /dev/null +++ b/lib/chef/knife/index_rebuild.rb @@ -0,0 +1,50 @@ +# +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 Daniel DeLeo +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class IndexRebuild < Knife + + banner "knife index rebuild (options)" + option :yes, + :short => "-y", + :long => "--yes", + :boolean => true, + :description => "don't bother to ask if I'm sure" + + def run + nag + output rest.post_rest("/search/reindex", {}) + end + + def nag + unless config[:yes] + yea_or_nay = ask_question("This operation is destructive. Rebuilding the index may take some time. You sure? (yes/no): ") + unless yea_or_nay =~ /^y/i + puts "aborting" + exit 7 + end + end + end + + + end + end +end diff --git a/lib/chef/knife/list.rb b/lib/chef/knife/list.rb new file mode 100644 index 0000000000..30fcb5fa35 --- /dev/null +++ b/lib/chef/knife/list.rb @@ -0,0 +1,109 @@ +require 'chef/chef_fs/knife' +require 'chef/chef_fs/file_system' + +class Chef + class Knife + class List < Chef::ChefFS::Knife + banner "knife list [-dR] [PATTERN1 ... PATTERNn]" + + common_options + + option :recursive, + :short => '-R', + :boolean => true, + :description => "List directories recursively." + option :bare_directories, + :short => '-d', + :boolean => true, + :description => "When directories match the pattern, do not show the directories' children." + + def run + patterns = name_args.length == 0 ? [""] : name_args + + # Get the matches (recursively) + results = [] + dir_results = [] + pattern_args_from(patterns).each do |pattern| + Chef::ChefFS::FileSystem.list(chef_fs, pattern) do |result| + if result.dir? && !config[:bare_directories] + dir_results += add_dir_result(result) + elsif result.exists? + results << result + elsif pattern.exact_path + STDERR.puts "#{format_path(result.path)}: No such file or directory" + end + end + end + + results = results.sort_by { |result| result.path } + dir_results = dir_results.sort_by { |result| result[0].path } + + if results.length == 0 && dir_results.length == 1 + results = dir_results[0][1] + dir_results = [] + end + + print_result_paths results + dir_results.each do |result, children| + puts "" + puts "#{format_path(result.path)}:" + print_results(children.map { |result| result.name }.sort, "") + end + end + + def add_dir_result(result) + begin + children = result.children.sort_by { |child| child.name } + rescue Chef::ChefFS::FileSystem::NotFoundError + STDERR.puts "#{format_path(result.path)}: No such file or directory" + return [] + end + + result = [ [ result, children ] ] + if config[:recursive] + children.each do |child| + if child.dir? + result += add_dir_result(child) + end + end + end + result + end + + def list_dirs_recursive(children) + results = children.select { |child| child.dir? }.to_a + results.each do |child| + results += list_dirs_recursive(child.children) + end + results + end + + def print_result_paths(results, indent = "") + print_results(results.map { |result| format_path(result.path) }, indent) + end + + def print_results(results, indent) + return if results.length == 0 + + print_space = results.map { |result| result.length }.max + 2 + # TODO: tput cols is not cross platform + columns = $stdout.isatty ? Integer(`tput cols`) : 0 + current_column = 0 + results.each do |result| + if current_column != 0 && current_column + print_space > columns + puts "" + current_column = 0 + end + if current_column == 0 + print indent + current_column += indent.length + end + print result + (' ' * (print_space - result.length)) + current_column += print_space + end + puts "" + end + end + end +end + diff --git a/lib/chef/knife/node_bulk_delete.rb b/lib/chef/knife/node_bulk_delete.rb new file mode 100644 index 0000000000..89b2abe6ae --- /dev/null +++ b/lib/chef/knife/node_bulk_delete.rb @@ -0,0 +1,80 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class NodeBulkDelete < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + end + + banner "knife node bulk delete REGEX (options)" + + def run + if name_args.length < 1 + ui.fatal("You must supply a regular expression to match the results against") + exit 42 + end + + + nodes_to_delete = {} + matcher = /#{name_args[0]}/ + + all_nodes.each do |name, node| + next unless name =~ matcher + nodes_to_delete[name] = node + end + + if nodes_to_delete.empty? + ui.msg "No nodes match the expression /#{name_args[0]}/" + exit 0 + end + + ui.msg("The following nodes will be deleted:") + ui.msg("") + ui.msg(ui.list(nodes_to_delete.keys.sort, :columns_down)) + ui.msg("") + ui.confirm("Are you sure you want to delete these nodes") + + + nodes_to_delete.sort.each do |name, node| + node.destroy + ui.msg("Deleted node #{name}") + end + end + + def all_nodes + node_uris_by_name = Chef::Node.list + + node_uris_by_name.keys.inject({}) do |nodes_by_name, name| + nodes_by_name[name] = Chef::Node.new.tap {|n| n.name(name)} + nodes_by_name + end + end + + end + end +end + + + + diff --git a/lib/chef/knife/node_create.rb b/lib/chef/knife/node_create.rb new file mode 100644 index 0000000000..7f50a30c80 --- /dev/null +++ b/lib/chef/knife/node_create.rb @@ -0,0 +1,50 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class NodeCreate < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + end + + banner "knife node create NODE (options)" + + def run + @node_name = @name_args[0] + + if @node_name.nil? + show_usage + ui.fatal("You must specify a node name") + exit 1 + end + + node = Chef::Node.new + node.name(@node_name) + create_object(node) + end + end + end +end + + + diff --git a/lib/chef/knife/node_delete.rb b/lib/chef/knife/node_delete.rb new file mode 100644 index 0000000000..a645d32035 --- /dev/null +++ b/lib/chef/knife/node_delete.rb @@ -0,0 +1,47 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class NodeDelete < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + end + + banner "knife node delete NODE (options)" + + def run + @node_name = @name_args[0] + + if @node_name.nil? + show_usage + ui.fatal("You must specify a node name") + exit 1 + end + + delete_object(Chef::Node, @node_name) + end + + end + end +end + diff --git a/lib/chef/knife/node_edit.rb b/lib/chef/knife/node_edit.rb new file mode 100644 index 0000000000..0d6b8fcf6c --- /dev/null +++ b/lib/chef/knife/node_edit.rb @@ -0,0 +1,72 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + + class NodeEdit < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + require 'chef/knife/core/node_editor' + end + + banner "knife node edit NODE (options)" + + option :all_attributes, + :short => "-a", + :long => "--all", + :boolean => true, + :description => "Display all attributes when editing" + + def run + if node_name.nil? + show_usage + ui.fatal("You must specify a node name") + exit 1 + end + + updated_node = node_editor.edit_node + if updated_values = node_editor.updated? + ui.info "Saving updated #{updated_values.join(', ')} on node #{node.name}" + updated_node.save + else + ui.info "Node not updated, skipping node save" + end + end + + def node_name + @node_name ||= @name_args[0] + end + + def node_editor + @node_editor ||= Knife::NodeEditor.new(node, ui, config) + end + + def node + @node ||= Chef::Node.load(node_name) + end + + end + end +end + + diff --git a/lib/chef/knife/node_from_file.rb b/lib/chef/knife/node_from_file.rb new file mode 100644 index 0000000000..d69392a8db --- /dev/null +++ b/lib/chef/knife/node_from_file.rb @@ -0,0 +1,50 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class NodeFromFile < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + require 'chef/knife/core/object_loader' + end + + banner "knife node from file FILE (options)" + + def loader + @loader ||= Knife::Core::ObjectLoader.new(Chef::Node, ui) + end + + def run + updated = loader.load_from('nodes', @name_args[0]) + + updated.save + + output(format_for_display(updated)) if config[:print_after] + + ui.info("Updated Node #{updated.name}!") + end + + end + end +end + diff --git a/lib/chef/knife/node_list.rb b/lib/chef/knife/node_list.rb new file mode 100644 index 0000000000..3926d101cf --- /dev/null +++ b/lib/chef/knife/node_list.rb @@ -0,0 +1,46 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class NodeList < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + end + + banner "knife node list (options)" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + env = Chef::Config[:environment] + output(format_list_for_display( env ? Chef::Node.list_by_environment(env) : Chef::Node.list )) + end + + end + end +end + + diff --git a/lib/chef/knife/node_run_list_add.rb b/lib/chef/knife/node_run_list_add.rb new file mode 100644 index 0000000000..dcd41ae997 --- /dev/null +++ b/lib/chef/knife/node_run_list_add.rb @@ -0,0 +1,75 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class NodeRunListAdd < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + end + + banner "knife node run_list add [NODE] [ENTRY[,ENTRY]] (options)" + + option :after, + :short => "-a ITEM", + :long => "--after ITEM", + :description => "Place the ENTRY in the run list after ITEM" + + def run + node = Chef::Node.load(@name_args[0]) + if @name_args.size > 2 + # Check for nested lists and create a single plain one + entries = @name_args[1..-1].map do |entry| + entry.split(',').map { |e| e.strip } + end.flatten + else + # Convert to array and remove the extra spaces + entries = @name_args[1].split(',').map { |e| e.strip } + end + + add_to_run_list(node, entries, config[:after]) + + node.save + + config[:run_list] = true + + output(format_for_display(node)) + end + + def add_to_run_list(node, entries, after=nil) + if after + nlist = [] + node.run_list.each do |entry| + nlist << entry + if entry == after + entries.each { |e| nlist << e } + end + end + node.run_list.reset!(nlist) + else + entries.each { |e| node.run_list << e } + end + end + + end + end +end diff --git a/lib/chef/knife/node_run_list_remove.rb b/lib/chef/knife/node_run_list_remove.rb new file mode 100644 index 0000000000..8519fd590a --- /dev/null +++ b/lib/chef/knife/node_run_list_remove.rb @@ -0,0 +1,48 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class NodeRunListRemove < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + end + + banner "knife node run_list remove [NODE] [ENTRIES] (options)" + + def run + node = Chef::Node.load(@name_args[0]) + entries = @name_args[1].split(',') + + entries.each { |e| node.run_list.remove(e) } + + node.save + + config[:run_list] = true + + output(format_for_display(node)) + end + + end + end +end + diff --git a/lib/chef/knife/node_show.rb b/lib/chef/knife/node_show.rb new file mode 100644 index 0000000000..4b0be18890 --- /dev/null +++ b/lib/chef/knife/node_show.rb @@ -0,0 +1,73 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' +require 'chef/knife/core/node_presenter' + +class Chef + class Knife + class NodeShow < Knife + + include Knife::Core::NodeFormattingOptions + + deps do + require 'chef/node' + require 'chef/json_compat' + end + + banner "knife node show NODE (options)" + + @attrs_to_show = [] + option :attribute, + :short => "-a [ATTR]", + :long => "--attribute [ATTR]", + :proc => lambda {|val| @attrs_to_show << val}, + :description => "Show one or more attributes" + + option :run_list, + :short => "-r", + :long => "--run-list", + :description => "Show only the run list" + + option :environment, + :short => "-E", + :long => "--environment", + :description => "Show only the Chef environment" + + def run + ui.use_presenter Knife::Core::NodePresenter + @node_name = @name_args[0] + + if @node_name.nil? + show_usage + ui.fatal("You must specify a node name") + exit 1 + end + + node = Chef::Node.load(@node_name) + output(format_for_display(node)) + self.class.attrs_to_show = [] + end + + def self.attrs_to_show=(attrs) + @attrs_to_show = attrs + end + end + end +end + diff --git a/lib/chef/knife/raw.rb b/lib/chef/knife/raw.rb new file mode 100644 index 0000000000..ad5d5f33ef --- /dev/null +++ b/lib/chef/knife/raw.rb @@ -0,0 +1,108 @@ +require 'json' + +class Chef + class Knife + class Raw < Chef::Knife + banner "knife raw REQUEST_PATH" + + option :method, + :long => '--method METHOD', + :short => '-m METHOD', + :default => "GET", + :description => "Request method (GET, POST, PUT or DELETE)" + + option :pretty, + :long => '--[no-]pretty', + :boolean => true, + :default => true, + :description => "Pretty-print JSON output" + + option :input, + :long => '--input FILE', + :short => '-i FILE', + :description => "Name of file to use for PUT or POST" + + def run + if name_args.length == 0 + show_usage + ui.fatal("You must provide the path you want to hit on the server") + exit(1) + elsif name_args.length > 1 + show_usage + ui.fatal("Only one path accepted for knife raw") + exit(1) + end + + path = name_args[0] + data = false + if config[:input] + data = IO.read(config[:input]) + end + chef_rest = Chef::REST.new(Chef::Config[:chef_server_url]) + puts api_request(chef_rest, config[:method].to_sym, chef_rest.create_url(name_args[0]), {}, data) + end + + ACCEPT_ENCODING = "Accept-Encoding".freeze + ENCODING_GZIP_DEFLATE = "gzip;q=1.0,deflate;q=0.6,identity;q=0.3".freeze + + def redirected_to(response) + return nil unless response.kind_of?(Net::HTTPRedirection) + # Net::HTTPNotModified is undesired subclass of Net::HTTPRedirection so test for this + return nil if response.kind_of?(Net::HTTPNotModified) + response['location'] + end + + def api_request(chef_rest, method, url, headers={}, data=false) + json_body = data +# json_body = data ? Chef::JSONCompat.to_json(data) : nil + # Force encoding to binary to fix SSL related EOFErrors + # cf. http://tickets.opscode.com/browse/CHEF-2363 + # http://redmine.ruby-lang.org/issues/5233 +# json_body.force_encoding(Encoding::BINARY) if json_body.respond_to?(:force_encoding) + headers = build_headers(chef_rest, method, url, headers, json_body) + + chef_rest.retriable_rest_request(method, url, json_body, headers) do |rest_request| + response = rest_request.call {|r| r.read_body} + + response_body = chef_rest.decompress_body(response) + + if response.kind_of?(Net::HTTPSuccess) + if config[:pretty] && response['content-type'] =~ /json/ + JSON.pretty_generate(JSON.parse(response_body, :create_additions => false)) + else + response_body + end + elsif redirect_location = redirected_to(response) + raise "Redirected to #{create_url(redirect_location)}" + follow_redirect {api_request(:GET, create_url(redirect_location))} + else + # have to decompress the body before making an exception for it. But the body could be nil. + response.body.replace(chef_rest.decompress_body(response)) if response.body.respond_to?(:replace) + + if response['content-type'] =~ /json/ + exception = response_body + msg = "HTTP Request Returned #{response.code} #{response.message}: " + msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s) + Chef::Log.info(msg) + end + puts response.body + response.error! + end + end + end + + def build_headers(chef_rest, method, url, headers={}, json_body=false, raw=false) +# headers = @default_headers.merge(headers) + #headers['Accept'] = "application/json" unless raw + headers['Accept'] = "application/json" unless raw + headers["Content-Type"] = 'application/json' if json_body + headers['Content-Length'] = json_body.bytesize.to_s if json_body + headers[Chef::REST::RESTRequest::ACCEPT_ENCODING] = Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE + headers.merge!(chef_rest.authentication_headers(method, url, json_body)) if chef_rest.sign_requests? + headers.merge!(Chef::Config[:custom_http_headers]) if Chef::Config[:custom_http_headers] + headers + end + end + end +end + diff --git a/lib/chef/knife/recipe_list.rb b/lib/chef/knife/recipe_list.rb new file mode 100644 index 0000000000..ed7d2a9509 --- /dev/null +++ b/lib/chef/knife/recipe_list.rb @@ -0,0 +1,32 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' +class Chef::Knife::RecipeList < Chef::Knife + + banner "knife recipe list [PATTERN]" + + def run + recipes = rest.get_rest('cookbooks/_recipes') + if pattern = @name_args.first + recipes = recipes.grep(Regexp.new(pattern)) + end + output(recipes) + end + +end diff --git a/lib/chef/knife/role_bulk_delete.rb b/lib/chef/knife/role_bulk_delete.rb new file mode 100644 index 0000000000..8b24f55846 --- /dev/null +++ b/lib/chef/knife/role_bulk_delete.rb @@ -0,0 +1,70 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class RoleBulkDelete < Knife + + deps do + require 'chef/role' + require 'chef/json_compat' + end + + banner "knife role bulk delete REGEX (options)" + + def run + if @name_args.length < 1 + ui.error("You must supply a regular expression to match the results against") + exit 1 + end + + all_roles = Chef::Role.list(true) + + matcher = /#{@name_args[0]}/ + roles_to_delete = {} + all_roles.each do |name, role| + next unless name =~ matcher + roles_to_delete[role.name] = role + end + + if roles_to_delete.empty? + ui.info "No roles match the expression /#{@name_args[0]}/" + exit 0 + end + + ui.msg("The following roles will be deleted:") + ui.msg("") + ui.msg(ui.list(roles_to_delete.keys.sort, :columns_down)) + ui.msg("") + ui.confirm("Are you sure you want to delete these roles") + + roles_to_delete.sort.each do |name, role| + role.destroy + ui.msg("Deleted role #{name}") + end + end + end + end +end + + + + + diff --git a/lib/chef/knife/role_create.rb b/lib/chef/knife/role_create.rb new file mode 100644 index 0000000000..e9e363e870 --- /dev/null +++ b/lib/chef/knife/role_create.rb @@ -0,0 +1,55 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class RoleCreate < Knife + + deps do + require 'chef/role' + require 'chef/json_compat' + end + + banner "knife role create ROLE (options)" + + option :description, + :short => "-d DESC", + :long => "--description DESC", + :description => "The role description" + + def run + @role_name = @name_args[0] + + if @role_name.nil? + show_usage + ui.fatal("You must specify a role name") + exit 1 + end + + role = Chef::Role.new + role.name(@role_name) + role.description(config[:description]) if config[:description] + create_object(role) + end + end + end +end + + diff --git a/lib/chef/knife/role_delete.rb b/lib/chef/knife/role_delete.rb new file mode 100644 index 0000000000..b823f37359 --- /dev/null +++ b/lib/chef/knife/role_delete.rb @@ -0,0 +1,47 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class RoleDelete < Knife + + deps do + require 'chef/role' + require 'chef/json_compat' + end + + banner "knife role delete ROLE (options)" + + def run + @role_name = @name_args[0] + + if @role_name.nil? + show_usage + ui.fatal("You must specify a role name") + exit 1 + end + + delete_object(Chef::Role, @role_name) + end + + end + end +end + diff --git a/lib/chef/knife/role_edit.rb b/lib/chef/knife/role_edit.rb new file mode 100644 index 0000000000..b0580988a0 --- /dev/null +++ b/lib/chef/knife/role_edit.rb @@ -0,0 +1,48 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class RoleEdit < Knife + + deps do + require 'chef/role' + require 'chef/json_compat' + end + + banner "knife role edit ROLE (options)" + + def run + @role_name = @name_args[0] + + if @role_name.nil? + show_usage + ui.fatal("You must specify a role name") + exit 1 + end + + ui.edit_object(Chef::Role, @role_name) + end + end + end +end + + + diff --git a/lib/chef/knife/role_from_file.rb b/lib/chef/knife/role_from_file.rb new file mode 100644 index 0000000000..c80218b753 --- /dev/null +++ b/lib/chef/knife/role_from_file.rb @@ -0,0 +1,56 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class RoleFromFile < Knife + + deps do + require 'chef/role' + require 'chef/knife/core/object_loader' + require 'chef/json_compat' + end + + banner "knife role from file FILE [FILE..] (options)" + + def loader + @loader ||= Knife::Core::ObjectLoader.new(Chef::Role, ui) + end + + def run + @name_args.each do |arg| + updated = loader.load_from("roles", arg) + + updated.save + + output(format_for_display(updated)) if config[:print_after] + + ui.info("Updated Role #{updated.name}!") + end + end + + end + end +end + + + + + diff --git a/lib/chef/knife/role_list.rb b/lib/chef/knife/role_list.rb new file mode 100644 index 0000000000..0f105b2188 --- /dev/null +++ b/lib/chef/knife/role_list.rb @@ -0,0 +1,43 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class RoleList < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + end + + banner "knife role list (options)" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + output(format_list_for_display(Chef::Role.list)) + end + end + end +end + diff --git a/lib/chef/knife/role_show.rb b/lib/chef/knife/role_show.rb new file mode 100644 index 0000000000..2f09794cbb --- /dev/null +++ b/lib/chef/knife/role_show.rb @@ -0,0 +1,54 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class RoleShow < Knife + + deps do + require 'chef/node' + require 'chef/json_compat' + end + + banner "knife role show ROLE (options)" + + option :attribute, + :short => "-a ATTR", + :long => "--attribute ATTR", + :description => "Show only one attribute" + + def run + @role_name = @name_args[0] + + if @role_name.nil? + show_usage + ui.fatal("You must specify a role name") + exit 1 + end + + role = Chef::Role.load(@role_name) + output(format_for_display(config[:environment] ? role.environment(config[:environment]) : role)) + end + + end + end +end + + diff --git a/lib/chef/knife/search.rb b/lib/chef/knife/search.rb new file mode 100644 index 0000000000..da739c4e62 --- /dev/null +++ b/lib/chef/knife/search.rb @@ -0,0 +1,141 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' +require 'chef/knife/core/node_presenter' + +class Chef + class Knife + class Search < Knife + + deps do + require 'chef/node' + require 'chef/environment' + require 'chef/api_client' + require 'chef/search/query' + end + + include Knife::Core::NodeFormattingOptions + + banner "knife search INDEX QUERY (options)" + + option :sort, + :short => "-o SORT", + :long => "--sort SORT", + :description => "The order to sort the results in", + :default => nil + + option :start, + :short => "-b ROW", + :long => "--start ROW", + :description => "The row to start returning results at", + :default => 0, + :proc => lambda { |i| i.to_i } + + option :rows, + :short => "-R INT", + :long => "--rows INT", + :description => "The number of rows to return", + :default => 1000, + :proc => lambda { |i| i.to_i } + + option :attribute, + :short => "-a ATTR", + :long => "--attribute ATTR", + :description => "Show only one attribute" + + option :run_list, + :short => "-r", + :long => "--run-list", + :description => "Show only the run list" + + option :id_only, + :short => "-i", + :long => "--id-only", + :description => "Show only the ID of matching objects" + + option :query, + :short => "-q QUERY", + :long => "--query QUERY", + :description => "The search query; useful to protect queries starting with -" + + def run + if config[:query] && @name_args[1] + ui.error "please specify query as an argument or an option via -q, not both" + ui.msg opt_parser + exit 1 + end + raw_query = config[:query] || @name_args[1] + if !raw_query || raw_query.empty? + ui.error "no query specified" + ui.msg opt_parser + exit 1 + end + + if name_args[0].nil? + ui.error "you must specify an item type to search for" + exit 1 + end + + if name_args[0] == 'node' + ui.use_presenter Knife::Core::NodePresenter + end + + + q = Chef::Search::Query.new + query = URI.escape(raw_query, + Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) + + result_items = [] + result_count = 0 + + rows = config[:rows] + start = config[:start] + begin + q.search(@name_args[0], query, config[:sort], start, rows) do |item| + formatted_item = format_for_display(item) + # if formatted_item.respond_to?(:has_key?) && !formatted_item.has_key?('id') + # formatted_item['id'] = item.has_key?('id') ? item['id'] : item.name + # end + result_items << formatted_item + result_count += 1 + end + rescue Net::HTTPServerException => e + msg = Chef::JSONCompat.from_json(e.response.body)["error"].first + ui.error("knife search failed: #{msg}") + exit 1 + end + + if ui.interchange? + output({:results => result_count, :rows => result_items}) + else + ui.msg "#{result_count} items found" + ui.msg("\n") + result_items.each do |item| + output(item) + ui.msg("\n") + end + end + end + end + end +end + + + + diff --git a/lib/chef/knife/show.rb b/lib/chef/knife/show.rb new file mode 100644 index 0000000000..7075315b08 --- /dev/null +++ b/lib/chef/knife/show.rb @@ -0,0 +1,32 @@ +require 'chef/chef_fs/knife' +require 'chef/chef_fs/file_system' + +class Chef + class Knife + class Show < Chef::ChefFS::Knife + banner "knife show [PATTERN1 ... PATTERNn]" + + common_options + + def run + # Get the matches (recursively) + pattern_args.each do |pattern| + Chef::ChefFS::FileSystem.list(chef_fs, pattern) do |result| + if result.dir? + STDERR.puts "#{result.path_for_printing}: is a directory" if pattern.exact_path + else + begin + value = result.read + puts "#{result.path_for_printing}:" + output(format_for_display(value)) + rescue Chef::ChefFS::FileSystem::NotFoundError + STDERR.puts "#{result.path_for_printing}: No such file or directory" + end + end + end + end + end + end + end +end + diff --git a/lib/chef/knife/ssh.rb b/lib/chef/knife/ssh.rb new file mode 100644 index 0000000000..a1b37723a6 --- /dev/null +++ b/lib/chef/knife/ssh.rb @@ -0,0 +1,444 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class Ssh < Knife + + deps do + require 'net/ssh' + require 'net/ssh/multi' + require 'readline' + require 'chef/exceptions' + require 'chef/search/query' + require 'chef/mixin/shell_out' + require 'mixlib/shellout' + end + + include Chef::Mixin::ShellOut + + attr_writer :password + + banner "knife ssh QUERY COMMAND (options)" + + option :concurrency, + :short => "-C NUM", + :long => "--concurrency NUM", + :description => "The number of concurrent connections", + :default => nil, + :proc => lambda { |o| o.to_i } + + option :attribute, + :short => "-a ATTR", + :long => "--attribute ATTR", + :description => "The attribute to use for opening the connection - default depends on the context", + :proc => Proc.new { |key| Chef::Config[:knife][:ssh_attribute] = key.strip } + + option :manual, + :short => "-m", + :long => "--manual-list", + :boolean => true, + :description => "QUERY is a space separated list of servers", + :default => false + + option :ssh_user, + :short => "-x USERNAME", + :long => "--ssh-user USERNAME", + :description => "The ssh username" + + option :ssh_password, + :short => "-P PASSWORD", + :long => "--ssh-password PASSWORD", + :description => "The ssh password" + + option :ssh_port, + :short => "-p PORT", + :long => "--ssh-port PORT", + :description => "The ssh port", + :default => "22", + :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key } + + option :ssh_gateway, + :short => "-G GATEWAY", + :long => "--ssh-gateway GATEWAY", + :description => "The ssh gateway", + :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key } + + option :identity_file, + :short => "-i IDENTITY_FILE", + :long => "--identity-file IDENTITY_FILE", + :description => "The SSH identity file used for authentication" + + option :host_key_verify, + :long => "--[no-]host-key-verify", + :description => "Verify host key, enabled by default.", + :boolean => true, + :default => true + + def session + config[:on_error] ||= :skip + ssh_error_handler = Proc.new do |server| + if config[:manual] + node_name = server.host + else + @action_nodes.each do |n| + node_name = n if format_for_display(n)[config[:attribute]] == server.host + end + end + case config[:on_error] + when :skip + ui.warn "Failed to connect to #{node_name} -- #{$!.class.name}: #{$!.message}" + $!.backtrace.each { |l| Chef::Log.debug(l) } + when :raise + #Net::SSH::Multi magic to force exception to be re-raised. + throw :go, :raise + end + end + + @session ||= Net::SSH::Multi.start(:concurrent_connections => config[:concurrency], :on_error => ssh_error_handler) + end + + def configure_session + list = case config[:manual] + when true + @name_args[0].split(" ") + when false + r = Array.new + q = Chef::Search::Query.new + @action_nodes = q.search(:node, @name_args[0])[0] + @action_nodes.each do |item| + # we should skip the loop to next iteration if the item returned by the search is nil + next if item.nil? + # if a command line attribute was not passed, and we have a cloud public_hostname, use that. + # see #configure_attribute for the source of config[:attribute] and config[:override_attribute] + if !config[:override_attribute] && item[:cloud] and item[:cloud][:public_hostname] + i = item[:cloud][:public_hostname] + elsif config[:override_attribute] + i = extract_nested_value(item, config[:override_attribute]) + else + i = extract_nested_value(item, config[:attribute]) + end + # next if we couldn't find the specified attribute in the returned node object + next if i.nil? + r.push(i) + end + r + end + if list.length == 0 + if @action_nodes.length == 0 + ui.fatal("No nodes returned from search!") + else + ui.fatal("#{@action_nodes.length} #{@action_nodes.length > 1 ? "nodes":"node"} found, " + + "but do not have the required attribute to stablish the connection. " + + "Try setting another attribute to open the connection using --attribute.") + end + exit 10 + end + session_from_list(list) + end + + def session_from_list(list) + config[:ssh_gateway] ||= Chef::Config[:knife][:ssh_gateway] + if config[:ssh_gateway] + gw_host, gw_user = config[:ssh_gateway].split('@').reverse + gw_host, gw_port = gw_host.split(':') + gw_opts = gw_port ? { :port => gw_port } : {} + + session.via(gw_host, gw_user || config[:ssh_user], gw_opts) + end + + list.each do |item| + Chef::Log.debug("Adding #{item}") + + hostspec = config[:ssh_user] ? "#{config[:ssh_user]}@#{item}" : item + session_opts = {} + session_opts[:keys] = File.expand_path(config[:identity_file]) if config[:identity_file] + session_opts[:keys_only] = true if config[:identity_file] + session_opts[:password] = config[:ssh_password] if config[:ssh_password] + session_opts[:port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port] + session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug + + if !config[:host_key_verify] + session_opts[:paranoid] = false + session_opts[:user_known_hosts_file] = "/dev/null" + end + + session.use(hostspec, session_opts) + + @longest = item.length if item.length > @longest + end + + session + end + + def fixup_sudo(command) + command.sub(/^sudo/, 'sudo -p \'knife sudo password: \'') + end + + def print_data(host, data) + if data =~ /\n/ + data.split(/\n/).each { |d| print_data(host, d) } + else + padding = @longest - host.length + str = ui.color(host, :cyan) + (" " * (padding + 1)) + data + ui.msg(str) + end + end + + def ssh_command(command, subsession=nil) + exit_status = 0 + subsession ||= session + command = fixup_sudo(command) + command.force_encoding('binary') if command.respond_to?(:force_encoding) + subsession.open_channel do |ch| + ch.request_pty + ch.exec command do |ch, success| + raise ArgumentError, "Cannot execute #{command}" unless success + ch.on_data do |ichannel, data| + print_data(ichannel[:host], data) + if data =~ /^knife sudo password: / + ichannel.send_data("#{get_password}\n") + end + end + ch.on_request "exit-status" do |ichannel, data| + exit_status = data.read_long + end + end + end + session.loop + exit_status + end + + def get_password + @password ||= ui.ask("Enter your password: ") { |q| q.echo = false } + end + + # Present the prompt and read a single line from the console. It also + # detects ^D and returns "exit" in that case. Adds the input to the + # history, unless the input is empty. Loops repeatedly until a non-empty + # line is input. + def read_line + loop do + command = reader.readline("#{ui.color('knife-ssh>', :bold)} ", true) + + if command.nil? + command = "exit" + puts(command) + else + command.strip! + end + + unless command.empty? + return command + end + end + end + + def reader + Readline + end + + def interactive + puts "Connected to #{ui.list(session.servers_for.collect { |s| ui.color(s.host, :cyan) }, :inline, " and ")}" + puts + puts "To run a command on a list of servers, do:" + puts " on SERVER1 SERVER2 SERVER3; COMMAND" + puts " Example: on latte foamy; echo foobar" + puts + puts "To exit interactive mode, use 'quit!'" + puts + while 1 + command = read_line + case command + when 'quit!' + puts 'Bye!' + break + when /^on (.+?); (.+)$/ + raw_list = $1.split(" ") + server_list = Array.new + session.servers.each do |session_server| + server_list << session_server if raw_list.include?(session_server.host) + end + command = $2 + ssh_command(command, session.on(*server_list)) + else + ssh_command(command) + end + end + end + + def screen + tf = Tempfile.new("knife-ssh-screen") + if File.exist? "#{ENV["HOME"]}/.screenrc" + tf.puts("source #{ENV["HOME"]}/.screenrc") + end + tf.puts("caption always '%-Lw%{= BW}%50>%n%f* %t%{-}%+Lw%<'") + tf.puts("hardstatus alwayslastline 'knife ssh #{@name_args[0]}'") + window = 0 + session.servers_for.each do |server| + tf.print("screen -t \"#{server.host}\" #{window} ssh ") + tf.print("-i #{config[:identity_file]} ") if config[:identity_file] + server.user ? tf.puts("#{server.user}@#{server.host}") : tf.puts(server.host) + window += 1 + end + tf.close + exec("screen -c #{tf.path}") + end + + def tmux + ssh_dest = lambda do |server| + identity = "-i #{config[:identity_file]} " if config[:identity_file] + prefix = server.user ? "#{server.user}@" : "" + "'ssh #{identity}#{prefix}#{server.host}'" + end + + new_window_cmds = lambda do + if session.servers_for.size > 1 + [""] + session.servers_for[1..-1].map do |server| + "new-window -a -n '#{server.host}' #{ssh_dest.call(server)}" + end + else + [] + end.join(" \\; ") + end + + tmux_name = "'knife ssh #{@name_args[0].gsub(/:/,'=')}'" + begin + server = session.servers_for.first + cmd = ["tmux new-session -d -s #{tmux_name}", + "-n '#{server.host}'", ssh_dest.call(server), + new_window_cmds.call].join(" ") + shell_out!(cmd) + exec("tmux attach-session -t #{tmux_name}") + rescue Chef::Exceptions::Exec + end + end + + def macterm + begin + require 'appscript' + rescue LoadError + STDERR.puts "you need the rb-appscript gem to use knife ssh macterm. `(sudo) gem install rb-appscript` to install" + raise + end + + Appscript.app("/Applications/Utilities/Terminal.app").windows.first.activate + Appscript.app("System Events").application_processes["Terminal.app"].keystroke("n", :using=>:command_down) + term = Appscript.app('Terminal') + window = term.windows.first.get + + (session.servers_for.size - 1).times do |i| + window.activate + Appscript.app("System Events").application_processes["Terminal.app"].keystroke("t", :using=>:command_down) + end + + session.servers_for.each_with_index do |server, tab_number| + cmd = "unset PROMPT_COMMAND; echo -e \"\\033]0;#{server.host}\\007\"; ssh #{server.user ? "#{server.user}@#{server.host}" : server.host}" + Appscript.app('Terminal').do_script(cmd, :in => window.tabs[tab_number + 1].get) + end + end + + def configure_attribute + # Setting 'knife[:ssh_attribute] = "foo"' in knife.rb => Chef::Config[:knife][:ssh_attribute] == 'foo' + # Running 'knife ssh -a foo' => both Chef::Config[:knife][:ssh_attribute] && config[:attribute] == foo + # Thus we can differentiate between a config file value and a command line override at this point by checking config[:attribute] + # We can tell here if fqdn was passed from the command line, rather than being the default, by checking config[:attribute] + # However, after here, we cannot tell these things, so we must preserve config[:attribute] + config[:override_attribute] = config[:attribute] || Chef::Config[:knife][:ssh_attribute] + config[:attribute] = (Chef::Config[:knife][:ssh_attribute] || + config[:attribute] || + "fqdn").strip + end + + def cssh + cssh_cmd = nil + %w[csshX cssh].each do |cmd| + begin + # Unix and Mac only + cssh_cmd = shell_out!("which #{cmd}").stdout.strip + break + rescue Mixlib::ShellOut::ShellCommandFailed + end + end + raise Chef::Exceptions::Exec, "no command found for cssh" unless cssh_cmd + + session.servers_for.each do |server| + cssh_cmd << " #{server.user ? "#{server.user}@#{server.host}" : server.host}" + end + Chef::Log.debug("starting cssh session with command: #{cssh_cmd}") + exec(cssh_cmd) + end + + def get_stripped_unfrozen_value(value) + return nil if value.nil? + value.strip + end + + def configure_user + config[:ssh_user] = get_stripped_unfrozen_value(config[:ssh_user] || + Chef::Config[:knife][:ssh_user]) + end + + def configure_identity_file + config[:identity_file] = get_stripped_unfrozen_value(config[:identity_file] || + Chef::Config[:knife][:ssh_identity_file]) + end + + def extract_nested_value(data_structure, path_spec) + ui.presenter.extract_nested_value(data_structure, path_spec) + end + + def run + extend Chef::Mixin::Command + + @longest = 0 + + configure_attribute + configure_user + configure_identity_file + configure_session + + exit_status = + case @name_args[1] + when "interactive" + interactive + when "screen" + screen + when "tmux" + tmux + when "macterm" + macterm + when "cssh" + cssh + when "csshx" + Chef::Log.warn("knife ssh csshx will be deprecated in a future release") + Chef::Log.warn("please use knife ssh cssh instead") + cssh + else + ssh_command(@name_args[1..-1].join(" ")) + end + + session.close + exit_status + end + + end + end +end diff --git a/lib/chef/knife/status.rb b/lib/chef/knife/status.rb new file mode 100644 index 0000000000..ceb394ce3a --- /dev/null +++ b/lib/chef/knife/status.rb @@ -0,0 +1,119 @@ +# +# Author:: Ian Meyer (<ianmmeyer@gmail.com>) +# Copyright:: Copyright (c) 2010 Ian Meyer +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class Status < Knife + + deps do + require 'highline' + require 'chef/search/query' + end + + banner "knife status QUERY (options)" + + option :run_list, + :short => "-r", + :long => "--run-list", + :description => "Show the run list" + + option :sort_reverse, + :short => "-s", + :long => "--sort-reverse", + :description => "Sort the status list by last run time descending" + + option :hide_healthy, + :short => "-H", + :long => "--hide-healthy", + :description => "Hide nodes that have run chef in the last hour" + + def highline + @h ||= HighLine.new + end + + def run + all_nodes = [] + q = Chef::Search::Query.new + query = @name_args[0] || "*:*" + q.search(:node, query) do |node| + all_nodes << node + end + all_nodes.sort { |n1, n2| + if (config[:sort_reverse] || Chef::Config[:knife][:sort_status_reverse]) + (n2["ohai_time"] or 0) <=> (n1["ohai_time"] or 0) + else + (n1["ohai_time"] or 0) <=> (n2["ohai_time"] or 0) + end + }.each do |node| + if node.has_key?("ec2") + fqdn = node['ec2']['public_hostname'] + ipaddress = node['ec2']['public_ipv4'] + else + fqdn = node['fqdn'] + ipaddress = node['ipaddress'] + end + hours, minutes, seconds = time_difference_in_hms(node["ohai_time"]) + hours_text = "#{hours} hour#{hours == 1 ? ' ' : 's'}" + minutes_text = "#{minutes} minute#{minutes == 1 ? ' ' : 's'}" + run_list = ", #{node.run_list}." if config[:run_list] + if hours > 24 + color = :red + text = hours_text + elsif hours >= 1 + color = :yellow + text = hours_text + else + color = :green + text = minutes_text + end + + line_parts = Array.new + line_parts << @ui.color(text, color) + " ago" << node.name + line_parts << fqdn if fqdn + line_parts << ipaddress if ipaddress + line_parts << run_list if run_list + + if node['platform'] + platform = node['platform'] + if node['platform_version'] + platform << " #{node['platform_version']}" + end + line_parts << platform + end + highline.say(line_parts.join(', ') + '.') unless (config[:hide_healthy] && hours < 1) + end + + end + + # :nodoc: + # TODO: this is duplicated from StatusHelper in the Webui. dedup. + def time_difference_in_hms(unix_time) + now = Time.now.to_i + difference = now - unix_time.to_i + hours = (difference / 3600).to_i + difference = difference % 3600 + minutes = (difference / 60).to_i + seconds = (difference % 60) + return [hours, minutes, seconds] + end + + end + end +end diff --git a/lib/chef/knife/tag_create.rb b/lib/chef/knife/tag_create.rb new file mode 100644 index 0000000000..d3ca95242d --- /dev/null +++ b/lib/chef/knife/tag_create.rb @@ -0,0 +1,52 @@ +# +# Author:: Ryan Davis (<ryand-ruby@zenspider.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: Nuo Yan (<nuo@opscode.com>) +# Copyright:: Copyright (c) 2011 Ryan Davis and Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class TagCreate < Knife + + deps do + require 'chef/node' + end + + banner "knife tag create NODE TAG ..." + + def run + name = @name_args[0] + tags = @name_args[1..-1] + + if name.nil? || tags.nil? || tags.empty? + show_usage + ui.fatal("You must specify a node name and at least one tag.") + exit 1 + end + + node = Chef::Node.load name + tags.each do |tag| + (node.tags << tag).uniq! + end + node.save + ui.info("Created tags #{tags.join(", ")} for node #{name}.") + end + end + end +end diff --git a/lib/chef/knife/tag_delete.rb b/lib/chef/knife/tag_delete.rb new file mode 100644 index 0000000000..10751db216 --- /dev/null +++ b/lib/chef/knife/tag_delete.rb @@ -0,0 +1,60 @@ +# +# Author:: Ryan Davis (<ryand-ruby@zenspider.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: Nuo Yan (<nuo@opscode.com>) +# Copyright:: Copyright (c) 2011 Ryan Davis and Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class TagDelete < Knife + + deps do + require 'chef/node' + end + + banner "knife tag delete NODE TAG ..." + + def run + name = @name_args[0] + tags = @name_args[1..-1] + + if name.nil? || tags.nil? || tags.empty? + show_usage + ui.fatal("You must specify a node name and at least one tag.") + exit 1 + end + + node = Chef::Node.load name + deleted_tags = Array.new + tags.each do |tag| + unless node.tags.delete(tag).nil? + deleted_tags << tag + end + end + node.save + message = if deleted_tags.empty? + "Nothing has changed. The tags requested to be deleted do not exist." + else + "Deleted tags #{deleted_tags.join(", ")} for node #{name}." + end + ui.info(message) + end + end + end +end diff --git a/lib/chef/knife/tag_list.rb b/lib/chef/knife/tag_list.rb new file mode 100644 index 0000000000..499eb8578c --- /dev/null +++ b/lib/chef/knife/tag_list.rb @@ -0,0 +1,47 @@ +# +# Author:: Ryan Davis (<ryand-ruby@zenspider.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Author:: Nuo Yan (<nuo@opscode.com>) +# Copyright:: Copyright (c) 2011 Ryan Davis and Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' + +class Chef + class Knife + class TagList < Knife + + deps do + require 'chef/node' + end + + banner "knife tag list NODE" + + def run + name = @name_args[0] + + if name.nil? + show_usage + ui.fatal("You must specify a node name.") + exit 1 + end + + node = Chef::Node.load(name) + output(node.tags) + end + end + end +end diff --git a/lib/chef/knife/upload.rb b/lib/chef/knife/upload.rb new file mode 100644 index 0000000000..ff8616543e --- /dev/null +++ b/lib/chef/knife/upload.rb @@ -0,0 +1,47 @@ +require 'chef/chef_fs/knife' +require 'chef/chef_fs/command_line' + +class Chef + class Knife + class Upload < Chef::ChefFS::Knife + banner "knife upload PATTERNS" + + common_options + + option :recurse, + :long => '--[no-]recurse', + :boolean => true, + :default => true, + :description => "List directories recursively." + + option :purge, + :long => '--[no-]purge', + :boolean => true, + :default => false, + :description => "Delete matching local files and directories that do not exist remotely." + + option :force, + :long => '--[no-]force', + :boolean => true, + :default => false, + :description => "Force upload of files even if they match (quicker and harmless, but doesn't print out what it changed)" + + option :dry_run, + :long => '--dry-run', + :short => '-n', + :boolean => true, + :default => false, + :description => "Don't take action, only print what would happen" + + def run + patterns = pattern_args_from(name_args.length > 0 ? name_args : [ "" ]) + + # Get the matches (recursively) + patterns.each do |pattern| + Chef::ChefFS::FileSystem.copy_to(pattern, local_fs, chef_fs, config[:recurse] ? nil : 1, config) + end + end + end + end +end + |