summaryrefslogtreecommitdiff
path: root/lib/chef/knife/core
diff options
context:
space:
mode:
authorSeth Chisamore <schisamo@opscode.com>2012-10-30 10:39:35 -0400
committerSeth Chisamore <schisamo@opscode.com>2012-10-30 10:39:35 -0400
commit24dc69a9a97e82a6e4207de68d6dcc664178249b (patch)
tree19bb289c9f88b4bbab066bc56b95d6d222fd5c35 /lib/chef/knife/core
parent9348c1c9c80ee757354d624b7dc1b78ebc7605c4 (diff)
downloadchef-24dc69a9a97e82a6e4207de68d6dcc664178249b.tar.gz
[OC-3564] move core Chef to the repo root \o/ \m/
The opscode/chef repository now only contains the core Chef library code used by chef-client, knife and chef-solo!
Diffstat (limited to 'lib/chef/knife/core')
-rw-r--r--lib/chef/knife/core/bootstrap_context.rb106
-rw-r--r--lib/chef/knife/core/cookbook_scm_repo.rb160
-rw-r--r--lib/chef/knife/core/generic_presenter.rb204
-rw-r--r--lib/chef/knife/core/node_editor.rb130
-rw-r--r--lib/chef/knife/core/node_presenter.rb137
-rw-r--r--lib/chef/knife/core/object_loader.rb112
-rw-r--r--lib/chef/knife/core/subcommand_loader.rb112
-rw-r--r--lib/chef/knife/core/text_formatter.rb86
-rw-r--r--lib/chef/knife/core/ui.rb219
9 files changed, 1266 insertions, 0 deletions
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