summaryrefslogtreecommitdiff
path: root/lib/chef/knife
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/knife')
-rw-r--r--lib/chef/knife/bootstrap.rb99
-rw-r--r--lib/chef/knife/bootstrap/chef_vault_handler.rb165
-rw-r--r--lib/chef/knife/bootstrap/client_builder.rb190
-rw-r--r--lib/chef/knife/bootstrap/templates/README.md (renamed from lib/chef/knife/bootstrap/README.md)0
-rw-r--r--lib/chef/knife/bootstrap/templates/archlinux-gems.erb (renamed from lib/chef/knife/bootstrap/archlinux-gems.erb)9
-rw-r--r--lib/chef/knife/bootstrap/templates/chef-aix.erb (renamed from lib/chef/knife/bootstrap/chef-aix.erb)9
-rw-r--r--lib/chef/knife/bootstrap/templates/chef-full.erb (renamed from lib/chef/knife/bootstrap/chef-full.erb)9
-rw-r--r--lib/chef/knife/core/bootstrap_context.rb8
8 files changed, 473 insertions, 16 deletions
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb
index 19a329199d..f23c15fa70 100644
--- a/lib/chef/knife/bootstrap.rb
+++ b/lib/chef/knife/bootstrap.rb
@@ -19,12 +19,17 @@
require 'chef/knife'
require 'chef/knife/data_bag_secret_options'
require 'erubis'
+require 'chef/knife/bootstrap/chef_vault_handler'
+require 'chef/knife/bootstrap/client_builder'
class Chef
class Knife
class Bootstrap < Knife
include DataBagSecretOptions
+ attr_accessor :client_builder
+ attr_accessor :chef_vault_handler
+
deps do
require 'chef/knife/core/bootstrap_context'
require 'chef/json_compat'
@@ -194,10 +199,55 @@ class Chef
:description => "Verify the SSL cert for HTTPS requests to the Chef server API.",
:boolean => true
+ option :bootstrap_vault_file,
+ :long => '--bootstrap-vault-file VAULT_FILE',
+ :description => 'A JSON file with a list of vault(s) and item(s) to be updated'
+
+ option :bootstrap_vault_json,
+ :long => '--bootstrap-vault-json VAULT_JSON',
+ :description => 'A JSON string with the vault(s) and item(s) to be updated'
+
+ option :bootstrap_vault_item,
+ :long => '--bootstrap-vault-item VAULT_ITEM',
+ :description => 'A single vault and item to update as "vault:item"',
+ :proc => Proc.new { |i|
+ (vault, item) = i.split(/:/)
+ Chef::Config[:knife][:bootstrap_vault_item] ||= {}
+ Chef::Config[:knife][:bootstrap_vault_item][vault] ||= []
+ Chef::Config[:knife][:bootstrap_vault_item][vault].push(item)
+ Chef::Config[:knife][:bootstrap_vault_item]
+ }
+
+ def initialize(argv=[])
+ super
+ @client_builder = Chef::Knife::Bootstrap::ClientBuilder.new(
+ chef_config: Chef::Config,
+ knife_config: config,
+ ui: ui,
+ )
+ @chef_vault_handler = Chef::Knife::Bootstrap::ChefVaultHandler.new(
+ knife_config: config,
+ ui: ui
+ )
+ end
+
+ # The default bootstrap template to use to bootstrap a server This is a public API hook
+ # which knife plugins use or inherit and override.
+ #
+ # @return [String] Default bootstrap template
def default_bootstrap_template
"chef-full"
end
+ # The server_name is the DNS or IP we are going to connect to, it is not necessarily
+ # the node name, the fqdn, or the hostname of the server. This is a public API hook
+ # which knife plugins use or inherit and override.
+ #
+ # @return [String] The DNS or IP that bootstrap will connect to
+ def server_name
+ Array(@name_args).first
+ end
+
def bootstrap_template
# The order here is important. We want to check if we have the new Chef 12 option is set first.
# Knife cloud plugins unfortunately all set a default option for the :distro so it should be at
@@ -216,7 +266,7 @@ class Chef
# Otherwise search the template directories until we find the right one
bootstrap_files = []
- bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap', "#{template}.erb")
+ bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap/templates', "#{template}.erb")
bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{template}.erb") if Chef::Knife.chef_config_dir
bootstrap_files << File.join(ENV['HOME'], '.chef', 'bootstrap', "#{template}.erb") if ENV['HOME']
bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{template}.erb"))
@@ -237,20 +287,45 @@ class Chef
template_file
end
+ def secret
+ @secret ||= encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil
+ end
+
+ def bootstrap_context
+ @bootstrap_context ||= Knife::Core::BootstrapContext.new(
+ config,
+ config[:run_list],
+ Chef::Config,
+ secret
+ )
+ end
+
def render_template
template_file = find_template
template = IO.read(template_file).chomp
- secret = encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil
- context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config, secret)
- Erubis::Eruby.new(template).evaluate(context)
+ Erubis::Eruby.new(template).evaluate(bootstrap_context)
end
def run
validate_name_args!
- @node_name = Array(@name_args).first
$stdout.sync = true
- ui.info("Connecting to #{ui.color(@node_name, :bold)}")
+
+ # chef-vault integration must use the new client-side hawtness, otherwise to use the
+ # new client-side hawtness, just delete your validation key.
+ if chef_vault_handler.doing_chef_vault? || !File.exist?(File.expand_path(Chef::Config[:validation_key]))
+ client_builder.run
+
+ chef_vault_handler.run(node_name: config[:chef_node_name])
+
+ bootstrap_context.client_pem = client_builder.client_path
+ else
+ ui.info("Doing old-style registration with the validation key at #{Chef::Config[:validation_key]}...")
+ ui.info("Delete your validation key in order to use your user credentials instead")
+ ui.info("")
+ end
+
+ ui.info("Connecting to #{ui.color(server_name, :bold)}")
begin
knife_ssh.run
@@ -265,24 +340,19 @@ class Chef
end
def validate_name_args!
- if Array(@name_args).first.nil?
+ if server_name.nil?
ui.error("Must pass an FQDN or ip to bootstrap")
exit 1
- elsif Array(@name_args).first == "windows"
+ elsif server_name == "windows"
+ # catches "knife bootstrap windows" when that command is not installed
ui.warn("Hostname containing 'windows' specified. Please install 'knife-windows' if you are attempting to bootstrap a Windows node via WinRM.")
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 ]
-
- # command line arguments and config file values are now merged into config in Chef::Knife#merge_configs
ssh.config[:ssh_user] = config[:ssh_user]
ssh.config[:ssh_password] = config[:ssh_password]
ssh.config[:ssh_port] = config[:ssh_port]
@@ -311,7 +381,6 @@ class Chef
command
end
-
end
end
end
diff --git a/lib/chef/knife/bootstrap/chef_vault_handler.rb b/lib/chef/knife/bootstrap/chef_vault_handler.rb
new file mode 100644
index 0000000000..749f61e6da
--- /dev/null
+++ b/lib/chef/knife/bootstrap/chef_vault_handler.rb
@@ -0,0 +1,165 @@
+#
+# Author:: Lamont Granquist (<lamont@chef.io>)
+# Copyright:: Copyright (c) 2015 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 Bootstrap < Knife
+ class ChefVaultHandler
+
+ # @return [Hash] knife merged config, typically @config
+ attr_accessor :knife_config
+
+ # @return [Chef::Knife::UI] ui object for output
+ attr_accessor :ui
+
+ # @return [String] name of the node (technically name of the client)
+ attr_reader :node_name
+
+ # @param knife_config [Hash] knife merged config, typically @config
+ # @param ui [Chef::Knife::UI] ui object for output
+ def initialize(knife_config: {}, ui: nil)
+ @knife_config = knife_config
+ @ui = ui
+ end
+
+ # Updates the chef vault items for the newly created node.
+ #
+ # @param node_name [String] name of the node (technically name of the client)
+ # @todo: node_name should be mandatory (ruby 2.0 compat)
+ def run(node_name: nil)
+ return unless doing_chef_vault?
+
+ sanity_check
+
+ @node_name = node_name
+
+ ui.info("Updating Chef Vault, waiting for client to be searchable..") while wait_for_client
+
+ update_bootstrap_vault_json!
+ end
+
+ # Iterate through all the vault items to update. Items may be either a String
+ # or an Array of Strings:
+ #
+ # {
+ # "vault1": "item",
+ # "vault2": [ "item1", "item2", "item2" ]
+ # }
+ #
+ def update_bootstrap_vault_json!
+ vault_json.each do |vault, items|
+ [ items ].flatten.each do |item|
+ update_vault(vault, item)
+ end
+ end
+ end
+
+ # @return [Boolean] if we've got chef vault options to act on or not
+ def doing_chef_vault?
+ !!(bootstrap_vault_json || bootstrap_vault_file || bootstrap_vault_item)
+ end
+
+ private
+
+ # warn if the user has given mutual conflicting options
+ def sanity_check
+ if bootstrap_vault_item && (bootstrap_vault_json || bootstrap_vault_file)
+ ui.warn "--vault-item given with --vault-list or --vault-file, ignoring the latter"
+ end
+
+ if bootstrap_vault_json && bootstrap_vault_file
+ ui.warn "--vault-list given with --vault-file, ignoring the latter"
+ end
+ end
+
+ # @return [String] string with serialized JSON representing the chef vault items
+ def bootstrap_vault_json
+ knife_config[:bootstrap_vault_json]
+ end
+
+ # @return [String] JSON text in a file representing the chef vault items
+ def bootstrap_vault_file
+ knife_config[:bootstrap_vault_file]
+ end
+
+ # @return [Hash] Ruby object representing the chef vault items to create
+ def bootstrap_vault_item
+ knife_config[:bootstrap_vault_item]
+ end
+
+ # Helper to return a ruby object represeting all the data bags and items
+ # to update via chef-vault.
+ #
+ # @return [Hash] deserialized ruby hash with all the vault items
+ def vault_json
+ @vault_json ||=
+ begin
+ if bootstrap_vault_item
+ bootstrap_vault_item
+ else
+ json = bootstrap_vault_json ? bootstrap_vault_json : File.read(bootstrap_vault_file)
+ Chef::JSONCompat.from_json(json)
+ end
+ end
+ end
+
+ # Update an individual vault item and save it
+ #
+ # @param vault [String] name of the chef-vault encrypted data bag
+ # @param item [String] name of the chef-vault encrypted item
+ def update_vault(vault, item)
+ require_chef_vault!
+ bootstrap_vault_item = load_chef_bootstrap_vault_item(vault, item)
+ bootstrap_vault_item.clients("name:#{node_name}")
+ bootstrap_vault_item.save
+ end
+
+ # Hook to stub out ChefVault
+ #
+ # @param vault [String] name of the chef-vault encrypted data bag
+ # @param item [String] name of the chef-vault encrypted item
+ # @returns [ChefVault::Item] ChefVault::Item object
+ def load_chef_bootstrap_vault_item(vault, item)
+ ChefVault::Item.load(vault, item)
+ end
+
+ public :load_chef_bootstrap_vault_item # for stubbing
+
+ # Helper used to spin waiting for the client to appear in search.
+ #
+ # @return [Boolean] true if the client is searchable
+ def wait_for_client
+ sleep 1
+ !Chef::Search::Query.new.search(:client, "name:#{node_name}")[0]
+ end
+
+ # Helper to very lazily require the chef-vault gem
+ def require_chef_vault!
+ @require_chef_vault ||=
+ begin
+ require 'chef-vault'
+ true
+ rescue LoadError
+ raise "Knife bootstrap cannot configure chef vault items when the chef-vault gem is not installed"
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/bootstrap/client_builder.rb b/lib/chef/knife/bootstrap/client_builder.rb
new file mode 100644
index 0000000000..b9c1d98bec
--- /dev/null
+++ b/lib/chef/knife/bootstrap/client_builder.rb
@@ -0,0 +1,190 @@
+#
+# Author:: Lamont Granquist (<lamont@chef.io>)
+# Copyright:: Copyright (c) 2015 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/node'
+require 'chef/rest'
+require 'chef/api_client/registration'
+require 'chef/api_client'
+require 'tmpdir'
+
+class Chef
+ class Knife
+ class Bootstrap < Knife
+ class ClientBuilder
+
+ # @return [Hash] knife merged config, typically @config
+ attr_accessor :knife_config
+ # @return [Hash] chef config object
+ attr_accessor :chef_config
+ # @return [Chef::Knife::UI] ui object for output
+ attr_accessor :ui
+
+ # @param knife_config [Hash] Hash of knife config settings
+ # @param chef_config [Hash] Hash of chef config settings
+ # @param ui [Chef::Knife::UI] UI object for output
+ def initialize(knife_config: {}, chef_config: {}, ui: nil)
+ @knife_config = knife_config
+ @chef_config = chef_config
+ @ui = ui
+ end
+
+ # Main entry. Prompt the user to clean up any old client or node objects. Then create
+ # the new client, then create the new node.
+ def run
+ sanity_check
+
+ ui.info("Creating new client for #{node_name}")
+
+ create_client!
+
+ ui.info("Creating new node for #{node_name}")
+
+ create_node!
+ end
+
+ # Tempfile to use to write newly created client credentials to.
+ #
+ # This method is public so that the knife bootstrapper can read then and pass the value into
+ # the handler for chef vault which needs the client cert we create here.
+ #
+ # We hang onto the tmpdir as an ivar as well so that it will not get GC'd and removed
+ #
+ # @return [String] path to the generated client.pem
+ def client_path
+ @client_path ||=
+ begin
+ @tmpdir = Dir.mktmpdir
+ File.join(@tmpdir, "#{node_name}.pem")
+ end
+ end
+
+ private
+
+ # @return [String] node name from the knife_config
+ def node_name
+ knife_config[:chef_node_name]
+ end
+
+ # @return [String] enviroment from the knife_config
+ def environment
+ knife_config[:environment]
+ end
+
+ # @return [String] run_list from the knife_config
+ def run_list
+ knife_config[:run_list]
+ end
+
+ # @return [Hash,Array] Object representation of json first-boot attributes from the knife_config
+ def first_boot_attributes
+ knife_config[:first_boot_attributes]
+ end
+
+ # @return [String] chef server url from the Chef::Config
+ def chef_server_url
+ chef_config[:chef_server_url]
+ end
+
+ # Accesses the run_list and coerces it into an Array, changing nils into
+ # the empty Array, and splitting strings representations of run_lists into
+ # Arrays.
+ #
+ # @return [Array] run_list coerced into an array
+ def normalized_run_list
+ case run_list
+ when nil
+ []
+ when String
+ run_list.split(/\s*,\s*/)
+ when Array
+ run_list
+ end
+ end
+
+ # Create the client object and save it to the Chef API
+ def create_client!
+ Chef::ApiClient::Registration.new(node_name, client_path, http_api: rest).run
+ end
+
+ # Create the node object (via the lazy accessor) and save it to the Chef API
+ def create_node!
+ node.save
+ end
+
+ # Create a new Chef::Node. Supports creating the node with its name, run_list, attributes
+ # and environment. This injects a rest object into the Chef::Node which uses the client key
+ # for authentication so that the client creates the node and therefore we get the acls setup
+ # correctly.
+ #
+ # @return [Chef::Node] new chef node to create
+ def node
+ @node ||=
+ begin
+ node = Chef::Node.new(chef_server_rest: client_rest)
+ node.name(node_name)
+ node.run_list(normalized_run_list)
+ node.normal_attrs = first_boot_attributes if first_boot_attributes
+ node.environment(environment) if environment
+ node
+ end
+ end
+
+ # Check for the existence of a node and/or client already on the server. If the node
+ # already exists, we must delete it in order to proceed so that we can create a new node
+ # object with the permissions of the new client. There is a use case for creating a new
+ # client and wiring it up to a precreated node object, but we do currently support that.
+ #
+ # We prompt the user about what to do and will fail hard if we do not get confirmation to
+ # delete any prior node/client objects.
+ def sanity_check
+ if resource_exists?("nodes/#{node_name}")
+ ui.confirm("Node #{node_name} exists, overwrite it")
+ rest.delete("nodes/#{node_name}")
+ end
+ if resource_exists?("clients/#{node_name}")
+ ui.confirm("Client #{node_name} exists, overwrite it")
+ rest.delete("clients/#{node_name}")
+ end
+ end
+
+ # Check if an relative path exists on the chef server
+ #
+ # @param relative_path [String] URI path relative to the chef organization
+ # @return [Boolean] if the relative path exists or returns a 404
+ def resource_exists?(relative_path)
+ rest.get_rest(relative_path)
+ true
+ rescue Net::HTTPServerException => e
+ raise unless e.response.code == "404"
+ false
+ end
+
+ # @return [Chef::REST] REST client using the client credentials
+ def client_rest
+ @client_rest ||= Chef::REST.new(chef_server_url, node_name, client_path)
+ end
+
+ # @return [Chef::REST] REST client using the cli user's knife credentials
+ # this uses the users's credentials
+ def rest
+ @rest ||= Chef::REST.new(chef_server_url)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/bootstrap/README.md b/lib/chef/knife/bootstrap/templates/README.md
index 13a0fe7ada..13a0fe7ada 100644
--- a/lib/chef/knife/bootstrap/README.md
+++ b/lib/chef/knife/bootstrap/templates/README.md
diff --git a/lib/chef/knife/bootstrap/archlinux-gems.erb b/lib/chef/knife/bootstrap/templates/archlinux-gems.erb
index 581293daa3..55d2c0cc12 100644
--- a/lib/chef/knife/bootstrap/archlinux-gems.erb
+++ b/lib/chef/knife/bootstrap/templates/archlinux-gems.erb
@@ -11,10 +11,12 @@ fi
mkdir -p /etc/chef
+<% if validation_key -%>
cat > /etc/chef/validation.pem <<'EOP'
<%= validation_key %>
EOP
chmod 0600 /etc/chef/validation.pem
+<% end -%>
<% if encrypted_data_bag_secret -%>
cat > /etc/chef/encrypted_data_bag_secret <<'EOP'
@@ -39,6 +41,13 @@ EOP
<% end -%>
<% end -%>
+<% if client_pem -%>
+cat > /etc/chef/client.pem <<'EOP'
+<%= ::File.read(::File.expand_path(client_pem)) %>
+EOP
+chmod 0600 /etc/chef/client.pem
+<% end -%>
+
cat > /etc/chef/client.rb <<'EOP'
log_level :info
log_location STDOUT
diff --git a/lib/chef/knife/bootstrap/chef-aix.erb b/lib/chef/knife/bootstrap/templates/chef-aix.erb
index 013ad1decb..45fbba7b48 100644
--- a/lib/chef/knife/bootstrap/chef-aix.erb
+++ b/lib/chef/knife/bootstrap/templates/chef-aix.erb
@@ -24,10 +24,19 @@ fi
mkdir -p /etc/chef
+<% if client_pem -%>
+cat > /etc/chef/client.pem <<'EOP'
+<%= ::File.read(::File.expand_path(client_pem)) %>
+EOP
+chmod 0600 /etc/chef/client.pem
+<% end -%>
+
+<% if validation_key -%>
cat > /etc/chef/validation.pem <<'EOP'
<%= validation_key %>
EOP
chmod 0600 /etc/chef/validation.pem
+<% end -%>
<% if encrypted_data_bag_secret -%>
cat > /etc/chef/encrypted_data_bag_secret <<'EOP'
diff --git a/lib/chef/knife/bootstrap/chef-full.erb b/lib/chef/knife/bootstrap/templates/chef-full.erb
index f49fafa98b..17d7a9e3b5 100644
--- a/lib/chef/knife/bootstrap/chef-full.erb
+++ b/lib/chef/knife/bootstrap/templates/chef-full.erb
@@ -38,10 +38,19 @@ exists() {
mkdir -p /etc/chef
+<% if client_pem -%>
+cat > /etc/chef/client.pem <<'EOP'
+<%= ::File.read(::File.expand_path(client_pem)) %>
+EOP
+chmod 0600 /etc/chef/client.pem
+<% end -%>
+
+<% if validation_key -%>
cat > /etc/chef/validation.pem <<'EOP'
<%= validation_key %>
EOP
chmod 0600 /etc/chef/validation.pem
+<% end -%>
<% if encrypted_data_bag_secret -%>
cat > /etc/chef/encrypted_data_bag_secret <<'EOP'
diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb
index 60db34c8d0..7197653489 100644
--- a/lib/chef/knife/core/bootstrap_context.rb
+++ b/lib/chef/knife/core/bootstrap_context.rb
@@ -30,6 +30,8 @@ class Chef
#
class BootstrapContext
+ attr_accessor :client_pem
+
def initialize(config, run_list, chef_config, secret = nil)
@config = config
@run_list = run_list
@@ -42,7 +44,11 @@ class Chef
end
def validation_key
- IO.read(File.expand_path(@chef_config[:validation_key]))
+ if File.exist?(File.expand_path(@chef_config[:validation_key]))
+ IO.read(File.expand_path(@chef_config[:validation_key]))
+ else
+ false
+ end
end
def encrypted_data_bag_secret