summaryrefslogtreecommitdiff
path: root/knife
diff options
context:
space:
mode:
Diffstat (limited to 'knife')
-rw-r--r--knife/Gemfile29
-rw-r--r--knife/LICENSE201
-rw-r--r--knife/Rakefile38
-rwxr-xr-xknife/bin/knife24
-rw-r--r--knife/knife.gemspec65
-rw-r--r--knife/lib/chef/application/knife.rb234
-rw-r--r--knife/lib/chef/chef_fs/knife.rb160
-rw-r--r--knife/lib/chef/knife.rb675
-rw-r--r--knife/lib/chef/knife/acl_add.rb57
-rw-r--r--knife/lib/chef/knife/acl_base.rb183
-rw-r--r--knife/lib/chef/knife/acl_bulk_add.rb78
-rw-r--r--knife/lib/chef/knife/acl_bulk_remove.rb83
-rw-r--r--knife/lib/chef/knife/acl_remove.rb62
-rw-r--r--knife/lib/chef/knife/acl_show.rb56
-rw-r--r--knife/lib/chef/knife/bootstrap.rb1191
-rw-r--r--knife/lib/chef/knife/bootstrap/chef_vault_handler.rb162
-rw-r--r--knife/lib/chef/knife/bootstrap/client_builder.rb212
-rw-r--r--knife/lib/chef/knife/bootstrap/templates/README.md11
-rw-r--r--knife/lib/chef/knife/bootstrap/templates/chef-full.erb242
-rw-r--r--knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb278
-rw-r--r--knife/lib/chef/knife/bootstrap/train_connector.rb336
-rw-r--r--knife/lib/chef/knife/client_bulk_delete.rb104
-rw-r--r--knife/lib/chef/knife/client_create.rb101
-rw-r--r--knife/lib/chef/knife/client_delete.rb62
-rw-r--r--knife/lib/chef/knife/client_edit.rb52
-rw-r--r--knife/lib/chef/knife/client_key_create.rb73
-rw-r--r--knife/lib/chef/knife/client_key_delete.rb80
-rw-r--r--knife/lib/chef/knife/client_key_edit.rb83
-rw-r--r--knife/lib/chef/knife/client_key_list.rb73
-rw-r--r--knife/lib/chef/knife/client_key_show.rb80
-rw-r--r--knife/lib/chef/knife/client_list.rb41
-rw-r--r--knife/lib/chef/knife/client_reregister.rb58
-rw-r--r--knife/lib/chef/knife/client_show.rb48
-rw-r--r--knife/lib/chef/knife/config_get.rb39
-rw-r--r--knife/lib/chef/knife/config_get_profile.rb37
-rw-r--r--knife/lib/chef/knife/config_list.rb139
-rw-r--r--knife/lib/chef/knife/config_list_profiles.rb37
-rw-r--r--knife/lib/chef/knife/config_show.rb127
-rw-r--r--knife/lib/chef/knife/config_use.rb61
-rw-r--r--knife/lib/chef/knife/config_use_profile.rb47
-rw-r--r--knife/lib/chef/knife/configure.rb150
-rw-r--r--knife/lib/chef/knife/configure_client.rb48
-rw-r--r--knife/lib/chef/knife/cookbook_bulk_delete.rb71
-rw-r--r--knife/lib/chef/knife/cookbook_delete.rb151
-rw-r--r--knife/lib/chef/knife/cookbook_download.rb142
-rw-r--r--knife/lib/chef/knife/cookbook_list.rb47
-rw-r--r--knife/lib/chef/knife/cookbook_metadata.rb106
-rw-r--r--knife/lib/chef/knife/cookbook_metadata_from_file.rb49
-rw-r--r--knife/lib/chef/knife/cookbook_show.rb98
-rw-r--r--knife/lib/chef/knife/cookbook_upload.rb292
-rw-r--r--knife/lib/chef/knife/core/bootstrap_context.rb264
-rw-r--r--knife/lib/chef/knife/core/cookbook_scm_repo.rb159
-rw-r--r--knife/lib/chef/knife/core/cookbook_site_streaming_uploader.rb249
-rw-r--r--knife/lib/chef/knife/core/formatting_options.rb49
-rw-r--r--knife/lib/chef/knife/core/gem_glob_loader.rb134
-rw-r--r--knife/lib/chef/knife/core/generic_presenter.rb232
-rw-r--r--knife/lib/chef/knife/core/hashed_command_loader.rb100
-rw-r--r--knife/lib/chef/knife/core/node_editor.rb130
-rw-r--r--knife/lib/chef/knife/core/node_presenter.rb133
-rw-r--r--knife/lib/chef/knife/core/object_loader.rb116
-rw-r--r--knife/lib/chef/knife/core/status_presenter.rb147
-rw-r--r--knife/lib/chef/knife/core/subcommand_loader.rb208
-rw-r--r--knife/lib/chef/knife/core/text_formatter.rb85
-rw-r--r--knife/lib/chef/knife/core/ui.rb338
-rw-r--r--knife/lib/chef/knife/core/windows_bootstrap_context.rb406
-rw-r--r--knife/lib/chef/knife/data_bag_create.rb81
-rw-r--r--knife/lib/chef/knife/data_bag_delete.rb49
-rw-r--r--knife/lib/chef/knife/data_bag_edit.rb74
-rw-r--r--knife/lib/chef/knife/data_bag_from_file.rb113
-rw-r--r--knife/lib/chef/knife/data_bag_list.rb42
-rw-r--r--knife/lib/chef/knife/data_bag_secret_options.rb122
-rw-r--r--knife/lib/chef/knife/data_bag_show.rb69
-rw-r--r--knife/lib/chef/knife/delete.rb125
-rw-r--r--knife/lib/chef/knife/deps.rb156
-rw-r--r--knife/lib/chef/knife/diff.rb83
-rw-r--r--knife/lib/chef/knife/download.rb84
-rw-r--r--knife/lib/chef/knife/edit.rb88
-rw-r--r--knife/lib/chef/knife/environment_compare.rb128
-rw-r--r--knife/lib/chef/knife/environment_create.rb52
-rw-r--r--knife/lib/chef/knife/environment_delete.rb44
-rw-r--r--knife/lib/chef/knife/environment_edit.rb44
-rw-r--r--knife/lib/chef/knife/environment_from_file.rb84
-rw-r--r--knife/lib/chef/knife/environment_list.rb41
-rw-r--r--knife/lib/chef/knife/environment_show.rb47
-rw-r--r--knife/lib/chef/knife/exec.rb99
-rw-r--r--knife/lib/chef/knife/group_add.rb55
-rw-r--r--knife/lib/chef/knife/group_create.rb49
-rw-r--r--knife/lib/chef/knife/group_destroy.rb53
-rw-r--r--knife/lib/chef/knife/group_list.rb43
-rw-r--r--knife/lib/chef/knife/group_remove.rb56
-rw-r--r--knife/lib/chef/knife/group_show.rb49
-rw-r--r--knife/lib/chef/knife/key_create.rb112
-rw-r--r--knife/lib/chef/knife/key_create_base.rb50
-rw-r--r--knife/lib/chef/knife/key_delete.rb55
-rw-r--r--knife/lib/chef/knife/key_edit.rb118
-rw-r--r--knife/lib/chef/knife/key_edit_base.rb55
-rw-r--r--knife/lib/chef/knife/key_list.rb90
-rw-r--r--knife/lib/chef/knife/key_list_base.rb45
-rw-r--r--knife/lib/chef/knife/key_show.rb53
-rw-r--r--knife/lib/chef/knife/list.rb177
-rw-r--r--knife/lib/chef/knife/node_bulk_delete.rb75
-rw-r--r--knife/lib/chef/knife/node_create.rb47
-rw-r--r--knife/lib/chef/knife/node_delete.rb46
-rw-r--r--knife/lib/chef/knife/node_edit.rb70
-rw-r--r--knife/lib/chef/knife/node_environment_set.rb53
-rw-r--r--knife/lib/chef/knife/node_from_file.rb51
-rw-r--r--knife/lib/chef/knife/node_list.rb44
-rw-r--r--knife/lib/chef/knife/node_policy_set.rb79
-rw-r--r--knife/lib/chef/knife/node_run_list_add.rb104
-rw-r--r--knife/lib/chef/knife/node_run_list_remove.rb67
-rw-r--r--knife/lib/chef/knife/node_run_list_set.rb66
-rw-r--r--knife/lib/chef/knife/node_show.rb63
-rw-r--r--knife/lib/chef/knife/null.rb12
-rw-r--r--knife/lib/chef/knife/org_create.rb70
-rw-r--r--knife/lib/chef/knife/org_delete.rb32
-rw-r--r--knife/lib/chef/knife/org_edit.rb48
-rw-r--r--knife/lib/chef/knife/org_list.rb44
-rw-r--r--knife/lib/chef/knife/org_show.rb31
-rw-r--r--knife/lib/chef/knife/org_user_add.rb62
-rw-r--r--knife/lib/chef/knife/org_user_remove.rb103
-rw-r--r--knife/lib/chef/knife/raw.rb123
-rw-r--r--knife/lib/chef/knife/recipe_list.rb32
-rw-r--r--knife/lib/chef/knife/rehash.rb50
-rw-r--r--knife/lib/chef/knife/role_bulk_delete.rb66
-rw-r--r--knife/lib/chef/knife/role_create.rb53
-rw-r--r--knife/lib/chef/knife/role_delete.rb46
-rw-r--r--knife/lib/chef/knife/role_edit.rb45
-rw-r--r--knife/lib/chef/knife/role_env_run_list_add.rb87
-rw-r--r--knife/lib/chef/knife/role_env_run_list_clear.rb55
-rw-r--r--knife/lib/chef/knife/role_env_run_list_remove.rb57
-rw-r--r--knife/lib/chef/knife/role_env_run_list_replace.rb60
-rw-r--r--knife/lib/chef/knife/role_env_run_list_set.rb70
-rw-r--r--knife/lib/chef/knife/role_from_file.rb51
-rw-r--r--knife/lib/chef/knife/role_list.rb42
-rw-r--r--knife/lib/chef/knife/role_run_list_add.rb87
-rw-r--r--knife/lib/chef/knife/role_run_list_clear.rb55
-rw-r--r--knife/lib/chef/knife/role_run_list_remove.rb56
-rw-r--r--knife/lib/chef/knife/role_run_list_replace.rb60
-rw-r--r--knife/lib/chef/knife/role_run_list_set.rb69
-rw-r--r--knife/lib/chef/knife/role_show.rb48
-rw-r--r--knife/lib/chef/knife/search.rb194
-rw-r--r--knife/lib/chef/knife/serve.rb65
-rw-r--r--knife/lib/chef/knife/show.rb72
-rw-r--r--knife/lib/chef/knife/ssh.rb645
-rw-r--r--knife/lib/chef/knife/ssl_check.rb284
-rw-r--r--knife/lib/chef/knife/ssl_fetch.rb162
-rw-r--r--knife/lib/chef/knife/status.rb95
-rw-r--r--knife/lib/chef/knife/supermarket_download.rb121
-rw-r--r--knife/lib/chef/knife/supermarket_install.rb192
-rw-r--r--knife/lib/chef/knife/supermarket_list.rb76
-rw-r--r--knife/lib/chef/knife/supermarket_search.rb53
-rw-r--r--knife/lib/chef/knife/supermarket_share.rb166
-rw-r--r--knife/lib/chef/knife/supermarket_show.rb66
-rw-r--r--knife/lib/chef/knife/supermarket_unshare.rb61
-rw-r--r--knife/lib/chef/knife/tag_create.rb52
-rw-r--r--knife/lib/chef/knife/tag_delete.rb60
-rw-r--r--knife/lib/chef/knife/tag_list.rb47
-rw-r--r--knife/lib/chef/knife/upload.rb86
-rw-r--r--knife/lib/chef/knife/user_create.rb143
-rw-r--r--knife/lib/chef/knife/user_delete.rb151
-rw-r--r--knife/lib/chef/knife/user_dissociate.rb42
-rw-r--r--knife/lib/chef/knife/user_edit.rb94
-rw-r--r--knife/lib/chef/knife/user_invite_add.rb43
-rw-r--r--knife/lib/chef/knife/user_invite_list.rb34
-rw-r--r--knife/lib/chef/knife/user_invite_rescind.rb63
-rw-r--r--knife/lib/chef/knife/user_key_create.rb73
-rw-r--r--knife/lib/chef/knife/user_key_delete.rb80
-rw-r--r--knife/lib/chef/knife/user_key_edit.rb83
-rw-r--r--knife/lib/chef/knife/user_key_list.rb73
-rw-r--r--knife/lib/chef/knife/user_key_show.rb80
-rw-r--r--knife/lib/chef/knife/user_list.rb43
-rw-r--r--knife/lib/chef/knife/user_password.rb70
-rw-r--r--knife/lib/chef/knife/user_reregister.rb59
-rw-r--r--knife/lib/chef/knife/user_show.rb52
-rw-r--r--knife/lib/chef/knife/version.rb24
-rw-r--r--knife/lib/chef/knife/xargs.rb282
-rw-r--r--knife/lib/chef/knife/yaml_convert.rb91
177 files changed, 18764 insertions, 0 deletions
diff --git a/knife/Gemfile b/knife/Gemfile
new file mode 100644
index 0000000000..57c6a52f93
--- /dev/null
+++ b/knife/Gemfile
@@ -0,0 +1,29 @@
+source "https://rubygems.org"
+
+group(:development, :test) do
+ gem "cheffish", ">= 14" # testing only , but why didn't this need to explicit in chef?
+ gem "webmock" # testing only
+ gem "rake"
+ gem "rspec"
+ gem "chef-bin", path: "../chef-bin"
+end
+
+group(:ruby_prof) do
+ # ruby-prof 1.3.0 does not compile on our centos6 builders/kitchen testers
+ gem "ruby-prof", "< 1.3.0"
+end
+
+group(:omnibus_package, :pry) do
+ gem "pry"
+ gem "pry-byebug"
+ gem "pry-stack_explorer"
+end
+
+group(:chefstyle) do
+ gem "chefstyle", git: "https://github.com/chef/chefstyle.git", branch: "master"
+end
+
+gem "ohai", git: "https://github.com/chef/ohai.git", branch: "master"
+gem "chef", path: ".."
+gem "chef-utils", path: File.expand_path("../chef-utils", __dir__) if File.exist?(File.expand_path("../chef-utils", __dir__))
+gem "chef-config", path: File.expand_path("../chef-config", __dir__) if File.exist?(File.expand_path("../chef-config", __dir__))
diff --git a/knife/LICENSE b/knife/LICENSE
new file mode 100644
index 0000000000..11069edd79
--- /dev/null
+++ b/knife/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+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.
diff --git a/knife/Rakefile b/knife/Rakefile
new file mode 100644
index 0000000000..ae65175839
--- /dev/null
+++ b/knife/Rakefile
@@ -0,0 +1,38 @@
+# Copyright:: Copyright (c) Chef Software 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 "rubygems"
+require "bundler/gem_tasks"
+Bundler::GemHelper.install_tasks
+
+begin
+ require "rspec/core/rake_task"
+
+ desc "Run all knife specs"
+ RSpec::Core::RakeTask.new(:spec) do |t|
+ t.verbose = false
+ t.rspec_opts = %w{--profile}
+ t.pattern = FileList["../spec/unit/knife/**/*_spec.rb"] +
+ FileList["../spec/unit/knife_spec.rb"] +
+ FileList["../spec/unit/application/knife_spec.rb"] +
+ FileList["../spec/integration/knife/*_spec.rb"] +
+ FileList["../spec/functional/knife/*_spec.rb"]
+ t.ruby_opts = "-I ../spec"
+
+ end
+rescue LoadError
+ puts "rspec not available. bundle install first to make sure all dependencies are installed."
+end
diff --git a/knife/bin/knife b/knife/bin/knife
new file mode 100755
index 0000000000..85ac3b91e9
--- /dev/null
+++ b/knife/bin/knife
@@ -0,0 +1,24 @@
+#!/usr/bin/env ruby
+#
+# ./knife - Chef CLI interface
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright 2009-2018, Chef Software 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.
+
+$:.unshift(File.expand_path(File.join(__dir__, "..", "lib")))
+require "chef/application/knife"
+
+Chef::Application::Knife.new.run
diff --git a/knife/knife.gemspec b/knife/knife.gemspec
new file mode 100644
index 0000000000..a9f3e2926d
--- /dev/null
+++ b/knife/knife.gemspec
@@ -0,0 +1,65 @@
+$:.unshift(File.dirname(__FILE__) + "/lib")
+require_relative "lib/chef/knife/version"
+
+Gem::Specification.new do |s|
+ s.name = "knife"
+ s.version = Chef::Knife::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.extra_rdoc_files = ["README.md", "LICENSE" ]
+ s.summary = "Let's find a good description."
+ s.description = s.summary
+ s.license = "Apache-2.0"
+ s.author = "Adam Jacob"
+ s.email = "adam@chef.io" # These seem a bit out of date, and this address probably doesn't go anywhere anymore?
+ s.homepage = "https://www.chef.io"
+
+ s.required_ruby_version = ">= 2.6.0"
+
+ s.add_dependency "chef-config", "= #{Chef::Knife::VERSION}"
+ s.add_dependency "chef-utils", "= #{Chef::Knife::VERSION}"
+ s.add_dependency "chef", "= #{Chef::Knife::VERSION}"
+ s.add_dependency "train-core", "~> 3.2", ">= 3.2.28" # 3.2.28 fixes sudo prompts. See https://github.com/chef/chef/pull/9635
+ s.add_dependency "train-winrm", ">= 0.2.5"
+ s.add_dependency "license-acceptance", ">= 1.0.5", "< 3"
+ s.add_dependency "mixlib-cli", ">= 2.1.1", "< 3.0"
+ s.add_dependency "mixlib-archive", ">= 0.4", "< 2.0"
+ s.add_dependency "ohai", "~> 17.0"
+ s.add_dependency "ffi", ">= 1.9.25", "< 1.14.0" # 1.14 breaks i386 windows. It should be fixed in 1.14.3
+ s.add_dependency "ffi-yajl", "~> 2.2"
+ s.add_dependency "net-ssh", ">= 5.1", "< 7"
+ s.add_dependency "net-ssh-multi", "~> 1.2", ">= 1.2.1"
+ s.add_dependency "ed25519", "~> 1.2" # ed25519 ssh key support
+ s.add_dependency "bcrypt_pbkdf", "~> 1.1" # ed25519 ssh key support
+ s.add_dependency "highline", ">= 1.6.9", "< 3" # Used in UI to present a list, no other usage.
+
+ s.add_dependency "tty-prompt", "~> 0.21" # knife ui.ask prompt
+ s.add_dependency "tty-screen", "~> 0.6" # knife list
+ s.add_dependency "tty-table", "~> 0.11" # knife render table output.
+ s.add_dependency "pastel" # knife ui.color
+ s.add_dependency "erubis", "~> 2.7"
+ s.add_dependency "chef-vault" # knife vault
+
+ s.add_development_dependency "chefstyle"
+
+ s.bindir = "bin"
+ s.executables = %w{ knife }
+
+ s.require_paths = %w{ lib }
+ s.files = %w{Gemfile Rakefile LICENSE README.md knife.gemspec} +
+ Dir.glob("lib/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) } +
+ Dir.glob("../spec/**/*", File::FNM_DOTMATCH).reject do |f|
+ File.directory?(f) || (
+ !File.path(f).match(/knife*/) &&
+ !File.path(f).match(/spec.data*/)
+ )
+ end
+
+ s.metadata = {
+ "bug_tracker_uri" => "https://github.com/chef/chef/issues",
+ "changelog_uri" => "https://github.com/chef/chef/blob/master/CHANGELOG.md",
+ "documentation_uri" => "https://docs.chef.io/",
+ "homepage_uri" => "https://www.chef.io",
+ "mailing_list_uri" => "https://discourse.chef.io/",
+ "source_code_uri" => "https://github.com/chef/chef/",
+ }
+end
diff --git a/knife/lib/chef/application/knife.rb b/knife/lib/chef/application/knife.rb
new file mode 100644
index 0000000000..9893effbe2
--- /dev/null
+++ b/knife/lib/chef/application/knife.rb
@@ -0,0 +1,234 @@
+#
+# Author:: Adam Jacob (<adam@chef.io)
+# Copyright:: Copyright (c) Chef Software 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/application"
+require_relative "../knife"
+require "mixlib/log"
+require "ohai/config"
+module Net
+ autoload :HTTP, "net/http"
+end
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef::Application::Knife < Chef::Application
+
+ NO_COMMAND_GIVEN = "You need to pass a sub-command (e.g., knife SUB-COMMAND)\n".freeze
+
+ banner "Usage: knife sub-command (options)"
+
+ option :config_file,
+ short: "-c CONFIG",
+ long: "--config CONFIG",
+ description: "The configuration file to use.",
+ proc: lambda { |path| File.expand_path(path, Dir.pwd) }
+
+ option :config_option,
+ long: "--config-option OPTION=VALUE",
+ description: "Override a single configuration option.",
+ proc: lambda { |option, existing|
+ (existing ||= []) << option
+ existing
+ }
+
+ verbosity_level = 0
+ option :verbosity,
+ short: "-V",
+ long: "--verbose",
+ description: "More verbose output. Use twice (-VV) for additional verbosity and three times (-VVV) for maximum verbosity.",
+ proc: Proc.new { verbosity_level += 1 },
+ default: 0
+
+ option :color,
+ long: "--[no-]color",
+ boolean: true,
+ default: true,
+ description: "Use colored output, defaults to enabled."
+
+ option :environment,
+ short: "-E ENVIRONMENT",
+ long: "--environment ENVIRONMENT",
+ description: "Set the #{ChefUtils::Dist::Infra::PRODUCT} environment (except for in searches, where this will be flagrantly ignored)."
+
+ option :editor,
+ short: "-e EDITOR",
+ long: "--editor EDITOR",
+ description: "Set the editor to use for interactive commands.",
+ default: ENV["EDITOR"]
+
+ option :disable_editing,
+ short: "-d",
+ long: "--disable-editing",
+ description: "Do not open EDITOR, just accept the data as is.",
+ boolean: true,
+ default: false
+
+ option :help,
+ short: "-h",
+ long: "--help",
+ description: "Show this help message.",
+ on: :tail,
+ boolean: true
+
+ option :node_name,
+ short: "-u USER",
+ long: "--user USER",
+ description: "#{ChefUtils::Dist::Server::PRODUCT} API client username."
+
+ option :client_key,
+ short: "-k KEY",
+ long: "--key KEY",
+ description: "#{ChefUtils::Dist::Server::PRODUCT} API client key.",
+ proc: lambda { |path| File.expand_path(path, Dir.pwd) }
+
+ option :chef_server_url,
+ short: "-s URL",
+ long: "--server-url URL",
+ description: "#{ChefUtils::Dist::Server::PRODUCT} URL."
+
+ option :yes,
+ short: "-y",
+ long: "--yes",
+ description: "Say yes to all prompts for confirmation."
+
+ option :defaults,
+ long: "--defaults",
+ description: "Accept default values for all questions."
+
+ option :print_after,
+ long: "--print-after",
+ description: "Show the data after a destructive operation."
+
+ option :format,
+ short: "-F FORMAT",
+ long: "--format FORMAT",
+ description: "Which format to use for output.",
+ in: %w{summary text json yaml pp},
+ default: "summary"
+
+ option :local_mode,
+ short: "-z",
+ long: "--local-mode",
+ description: "Point knife commands at local repository instead of #{ChefUtils::Dist::Server::PRODUCT}.",
+ boolean: true
+
+ option :chef_zero_host,
+ long: "--chef-zero-host HOST",
+ description: "Host to start #{ChefUtils::Dist::Zero::PRODUCT} on."
+
+ option :chef_zero_port,
+ long: "--chef-zero-port PORT",
+ description: "Port (or port range) to start #{ChefUtils::Dist::Zero::PRODUCT} on. Port ranges like 1000,1010 or 8889-9999 will try all given ports until one works."
+
+ option :listen,
+ long: "--[no-]listen",
+ description: "Whether a local mode (-z) server binds to a port.",
+ boolean: false
+
+ option :version,
+ short: "-v",
+ long: "--version",
+ description: "Show #{ChefUtils::Dist::Infra::PRODUCT} version.",
+ boolean: true,
+ proc: lambda { |v| puts "#{ChefUtils::Dist::Infra::PRODUCT}: #{::Chef::VERSION}" },
+ exit: 0
+
+ option :fips,
+ long: "--[no-]fips",
+ description: "Enable FIPS mode.",
+ boolean: true,
+ default: nil
+
+ option :profile,
+ long: "--profile PROFILE",
+ description: "The credentials profile to select."
+
+ # Run knife
+ def run
+ ChefConfig::PathHelper.per_tool_home_environment = "KNIFE_HOME"
+ Mixlib::Log::Formatter.show_time = false
+ validate_and_parse_options
+ quiet_traps
+ Chef::Knife.run(ARGV, options)
+ exit 0
+ end
+
+ private
+
+ def quiet_traps
+ trap("TERM") do
+ exit 1
+ end
+
+ trap("INT") do
+ exit 2
+ end
+ end
+
+ def validate_and_parse_options
+ # Checking ARGV validity *before* parse_options because parse_options
+ # mangles ARGV in some situations
+ if no_command_given?
+ print_help_and_exit(1, NO_COMMAND_GIVEN)
+ elsif no_subcommand_given?
+ if want_help? || want_version?
+ print_help_and_exit(0)
+ else
+ print_help_and_exit(2, NO_COMMAND_GIVEN)
+ end
+ end
+ end
+
+ def no_subcommand_given?
+ ARGV[0] =~ /^-/
+ end
+
+ def no_command_given?
+ ARGV.empty?
+ end
+
+ def want_help?
+ ARGV[0] =~ /^(--help|-h)$/
+ end
+
+ def want_version?
+ ARGV[0] =~ /^(--version|-v)$/
+ end
+
+ def print_help_and_exit(exitcode = 1, fatal_message = nil)
+ Chef::Log.error(fatal_message) if fatal_message
+
+ begin
+ parse_options
+ rescue OptionParser::InvalidOption => e
+ puts "#{e}\n"
+ end
+
+ if want_help?
+ puts "#{ChefUtils::Dist::Infra::PRODUCT}: #{Chef::VERSION}"
+ puts
+ puts "Docs: #{ChefUtils::Dist::Org::KNIFE_DOCS}"
+ puts "Patents: #{ChefUtils::Dist::Org::PATENTS}"
+ puts
+ end
+
+ puts opt_parser
+ puts
+ Chef::Knife.list_commands
+ exit exitcode
+ end
+
+end
diff --git a/knife/lib/chef/chef_fs/knife.rb b/knife/lib/chef/chef_fs/knife.rb
new file mode 100644
index 0000000000..9e165ab7ea
--- /dev/null
+++ b/knife/lib/chef/chef_fs/knife.rb
@@ -0,0 +1,160 @@
+#
+# Author:: John Keiser (<jkeiser@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require "pathname" unless defined?(Pathname)
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ module ChefFS
+ class Knife < Chef::Knife
+ # Workaround for CHEF-3932
+ def self.deps
+ super do
+ require "chef/config" unless defined?(Chef::Config)
+ require "chef/chef_fs/parallelizer" unless defined?(Chef::ChefFS::Parallelizer)
+ require "chef/chef_fs/config" unless defined?(Chef::ChefFS::Config)
+ require "chef/chef_fs/file_pattern" unless defined?(Chef::ChefFS::FilePattern)
+ require "chef/chef_fs/path_utils" unless defined?(Chef::ChefFS::PathUtils)
+ yield
+ end
+ end
+
+ def self.inherited(c)
+ super
+
+ # Ensure we always get to do our includes, whether subclass calls deps or not
+ c.deps do
+ end
+ end
+
+ option :repo_mode,
+ long: "--repo-mode MODE",
+ description: "Specifies the local repository layout. Values: static, everything, hosted_everything. Default: everything/hosted_everything"
+
+ option :chef_repo_path,
+ long: "--chef-repo-path PATH",
+ description: "Overrides the location of #{ChefUtils::Dist::Infra::PRODUCT} repo. Default is specified by chef_repo_path in the config"
+
+ option :concurrency,
+ long: "--concurrency THREADS",
+ description: "Maximum number of simultaneous requests to send (default: 10)"
+
+ def configure_chef
+ super
+ Chef::Config[:repo_mode] = config[:repo_mode] if config[:repo_mode]
+ Chef::Config[:concurrency] = config[:concurrency].to_i if config[:concurrency]
+
+ # --chef-repo-path forcibly overrides all other paths
+ if config[:chef_repo_path]
+ Chef::Config[:chef_repo_path] = config[:chef_repo_path]
+ Chef::ChefFS::Config::INFLECTIONS.each_value do |variable_name|
+ Chef::Config.delete("#{variable_name}_path".to_sym)
+ end
+ end
+
+ @chef_fs_config = Chef::ChefFS::Config.new(Chef::Config, Dir.pwd, config, ui)
+
+ Chef::ChefFS::Parallelizer.threads = (Chef::Config[:concurrency] || 10) - 1
+ end
+
+ def chef_fs
+ @chef_fs_config.chef_fs
+ end
+
+ def create_chef_fs
+ @chef_fs_config.create_chef_fs
+ end
+
+ def local_fs
+ @chef_fs_config.local_fs
+ end
+
+ def create_local_fs
+ @chef_fs_config.create_local_fs
+ end
+
+ def pattern_args
+ @pattern_args ||= pattern_args_from(name_args)
+ end
+
+ def pattern_args_from(args)
+ args.map { |arg| pattern_arg_from(arg) }
+ end
+
+ def pattern_arg_from(arg)
+ inferred_path = nil
+ if Chef::ChefFS::PathUtils.is_absolute?(arg)
+ # We should be able to use this as-is - but the user might have incorrectly provided
+ # us with a path that is based off of the OS root path instead of the Chef-FS root.
+ # Do a quick and dirty sanity check.
+ if possible_server_path = @chef_fs_config.server_path(arg)
+ ui.warn("The absolute path provided is suspicious: #{arg}")
+ ui.warn("If you wish to refer to a file location, please provide a path that is rooted at the chef-repo.")
+ ui.warn("Consider writing '#{possible_server_path}' instead of '#{arg}'")
+ end
+ # Use the original path because we can't be sure.
+ inferred_path = arg
+ elsif arg[0, 1] == "~"
+ # Let's be nice and fix it if possible - but warn the user.
+ ui.warn("A path relative to a user home directory has been provided: #{arg}")
+ ui.warn("Paths provided need to be rooted at the chef-repo being considered or be relative paths.")
+ inferred_path = @chef_fs_config.server_path(arg)
+ ui.warn("Using '#{inferred_path}' as the path instead of '#{arg}'.")
+ elsif Pathname.new(arg).absolute?
+ # It is definitely a system absolute path (such as C:\ or \\foo\bar) but it cannot be
+ # interpreted as a Chef-FS absolute path. Again attempt to be nice but warn the user.
+ ui.warn("An absolute file system path that isn't a server path was provided: #{arg}")
+ ui.warn("Paths provided need to be rooted at the chef-repo being considered or be relative paths.")
+ inferred_path = @chef_fs_config.server_path(arg)
+ ui.warn("Using '#{inferred_path}' as the path instead of '#{arg}'.")
+ elsif @chef_fs_config.base_path.nil?
+ # These are all relative paths. We can't resolve and root paths unless we are in the
+ # chef repo.
+ ui.error("Attempt to use relative path '#{arg}' when current directory is outside the repository path.")
+ ui.error("Current working directory is '#{@chef_fs_config.cwd}'.")
+ exit(1)
+ else
+ inferred_path = Chef::ChefFS::PathUtils.join(@chef_fs_config.base_path, arg)
+ end
+ Chef::ChefFS::FilePattern.new(inferred_path)
+ end
+
+ def format_path(entry)
+ @chef_fs_config.format_path(entry)
+ end
+
+ def parallelize(inputs, options = {}, &block)
+ Chef::ChefFS::Parallelizer.parallelize(inputs, options, &block)
+ end
+
+ def discover_repo_dir(dir)
+ %w{.chef cookbooks data_bags environments roles}.each do |subdir|
+ return dir if File.directory?(File.join(dir, subdir))
+ end
+ # If this isn't it, check the parent
+ parent = File.dirname(dir)
+ if parent && parent != dir
+ discover_repo_dir(parent)
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife.rb b/knife/lib/chef/knife.rb
new file mode 100644
index 0000000000..67f5f7d54d
--- /dev/null
+++ b/knife/lib/chef/knife.rb
@@ -0,0 +1,675 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Christopher Brown (<cb@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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" unless defined?(Forwardable)
+require_relative "knife/version"
+require "mixlib/cli" unless defined?(Mixlib::CLI)
+require "chef-utils/dsl/default_paths" unless defined?(ChefUtils::DSL::DefaultPaths)
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+require "chef/workstation_config_loader" unless defined?(Chef::WorkstationConfigLoader)
+require "chef/mixin/convert_to_class_name" unless defined?(Chef::ConvertToClassName)
+require "chef/mixin/default_paths" unless defined?(Chef::Mixin::DefaultPaths)
+require_relative "knife/core/subcommand_loader"
+require_relative "knife/core/ui"
+require "chef/local_mode" unless defined?(Chef::LocalMode)
+require "chef/server_api" unless defined?(Chef::ServerAPI)
+require "http/authenticator" unless defined?(Chef::HTTP::Authenticator)
+require "http/http_request" unless defined?(Chef::HTTP::HTTPRequest)
+require "http" unless defined?(Chef::HTTP)
+# End
+
+require "pp" unless defined?(PP)
+
+require_relative "application/knife"
+
+class Chef
+ class Knife
+
+ Chef::HTTP::HTTPRequest.user_agent = "#{ChefUtils::Dist::Infra::PRODUCT} Knife#{Chef::HTTP::HTTPRequest::UA_COMMON}"
+
+ include Mixlib::CLI
+ include ChefUtils::DSL::DefaultPaths
+ extend Chef::Mixin::ConvertToClassName
+ extend Forwardable
+
+ # @note Backwards Compat:
+ # Ideally, we should not vomit all of these methods into this base class;
+ # instead, they should be accessed by hitting the ui object directly.
+ def_delegator :@ui, :stdout
+ def_delegator :@ui, :stderr
+ def_delegator :@ui, :stdin
+ def_delegator :@ui, :msg
+ def_delegator :@ui, :ask_question
+ def_delegator :@ui, :pretty_print
+ def_delegator :@ui, :output
+ def_delegator :@ui, :format_list_for_display
+ def_delegator :@ui, :format_for_display
+ def_delegator :@ui, :format_cookbook_list_for_display
+ def_delegator :@ui, :edit_data
+ def_delegator :@ui, :edit_hash
+ def_delegator :@ui, :edit_object
+ def_delegator :@ui, :confirm
+
+ attr_accessor :name_args
+ attr_accessor :ui
+
+ # knife acl subcommands are grouped in this category using this constant to verify.
+ OPSCODE_HOSTED_CHEF_ACCESS_CONTROL = %w{acl group user}.freeze
+
+ # knife opc subcommands are grouped in this category using this constant to verify.
+ CHEF_ORGANIZATION_MANAGEMENT = %w{opc}.freeze
+
+ # Configure mixlib-cli to always separate defaults from user-supplied CLI options
+ def self.use_separate_defaults?
+ true
+ end
+
+ def self.ui
+ @ui ||= Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
+ end
+
+ def self.msg(msg = "")
+ ui.msg(msg)
+ end
+
+ def self.reset_config_loader!
+ @@chef_config_dir = nil
+ @config_loader = nil
+ end
+
+ def self.reset_subcommands!
+ @@subcommands = {}
+ @subcommands_by_category = nil
+ end
+
+ def self.inherited(subclass)
+ super
+ unless subclass.unnamed?
+ subcommands[subclass.snake_case_name] = subclass
+ subcommand_files[subclass.snake_case_name] +=
+ if subclass.superclass.to_s == "Chef::ChefFS::Knife"
+ # ChefFS-based commands have a superclass that defines an
+ # inherited method which calls super. This means that the
+ # top of the call stack is not the class definition for
+ # our subcommand. Try the second entry in the call stack.
+ [path_from_caller(caller[1])]
+ else
+ [path_from_caller(caller[0])]
+ end
+ end
+ end
+
+ # Explicitly set the category for the current command to +new_category+
+ # The category is normally determined from the first word of the command
+ # name, but some commands make more sense using two or more words
+ # @param new_category [String] value to set the category to (see examples)
+ #
+ # @example Data bag commands would be in the 'data' category by default. To
+ # put them in the 'data bag' category:
+ # category('data bag')
+ def self.category(new_category)
+ @category = new_category
+ end
+
+ def self.subcommand_category
+ @category || snake_case_name.split("_").first unless unnamed?
+ end
+
+ def self.snake_case_name
+ convert_to_snake_case(name.split("::").last) unless unnamed?
+ end
+
+ def self.common_name
+ snake_case_name.split("_").join(" ")
+ end
+
+ # Does this class have a name? (Classes created via Class.new don't)
+ def self.unnamed?
+ name.nil? || name.empty?
+ end
+
+ def self.subcommand_loader
+ @subcommand_loader ||= Chef::Knife::SubcommandLoader.for_config(chef_config_dir)
+ end
+
+ def self.load_commands
+ @commands_loaded ||= subcommand_loader.load_commands
+ end
+
+ def self.guess_category(args)
+ subcommand_loader.guess_category(args)
+ end
+
+ def self.subcommand_class_from(args)
+ if args.size == 1 && args[0].strip.casecmp("rehash") == 0
+ # To prevent issues with the rehash file not pointing to the correct plugins,
+ # we always use the glob loader when regenerating the rehash file
+ @subcommand_loader = Chef::Knife::SubcommandLoader.gem_glob_loader(chef_config_dir)
+ end
+ subcommand_loader.command_class_from(args) || subcommand_not_found!(args)
+ end
+
+ def self.subcommands
+ @@subcommands ||= {}
+ end
+
+ def self.subcommand_files
+ @@subcommand_files ||= Hash.new([])
+ end
+
+ def self.subcommands_by_category
+ unless @subcommands_by_category
+ @subcommands_by_category = Hash.new { |hash, key| hash[key] = [] }
+ subcommands.each do |snake_cased, klass|
+ @subcommands_by_category[klass.subcommand_category] << snake_cased
+ end
+ end
+ @subcommands_by_category
+ end
+
+ # Shared with subclasses
+ @@chef_config_dir = nil
+
+ def self.config_loader
+ @config_loader ||= WorkstationConfigLoader.new(nil, Chef::Log)
+ end
+
+ def self.load_config(explicit_config_file, profile)
+ config_loader.explicit_config_file = explicit_config_file
+ config_loader.profile = profile
+ config_loader.load
+
+ ui.warn("No knife configuration file found. See https://docs.chef.io/config_rb/ for details.") if config_loader.no_config_found?
+
+ config_loader
+ rescue Exceptions::ConfigurationError => e
+ ui.error(ui.color("CONFIGURATION ERROR:", :red) + e.message)
+ exit 1
+ end
+
+ def self.chef_config_dir
+ @@chef_config_dir ||= config_loader.chef_config_dir
+ end
+
+ # Run knife for the given +args+ (ARGV), adding +options+ to the list of
+ # CLI options that the subcommand knows how to handle.
+ #
+ # @param args [Array] The arguments. Usually ARGV
+ # @param options [Mixlib::CLI option parser hash] These +options+ are how
+ # subcommands know about global knife CLI options
+ #
+ def self.run(args, options = {})
+ # Fallback debug logging. Normally the logger isn't configured until we
+ # read the config, but this means any logging that happens before the
+ # config file is read may be lost. If the KNIFE_DEBUG variable is set, we
+ # setup the logger for debug logging to stderr immediately to catch info
+ # from early in the setup process.
+ if ENV["KNIFE_DEBUG"]
+ Chef::Log.init($stderr)
+ Chef::Log.level(:debug)
+ end
+
+ subcommand_class = subcommand_class_from(args)
+ subcommand_class.options = options.merge!(subcommand_class.options)
+ subcommand_class.load_deps
+ instance = subcommand_class.new(args)
+ instance.configure_chef
+ instance.run_with_pretty_exceptions
+ end
+
+ def self.dependency_loaders
+ @dependency_loaders ||= []
+ end
+
+ def self.deps(&block)
+ dependency_loaders << block
+ end
+
+ def self.load_deps
+ dependency_loaders.each(&:call)
+ end
+
+ OFFICIAL_PLUGINS = %w{lpar openstack push rackspace vcenter}.freeze
+
+ class << self
+ def list_commands(preferred_category = nil)
+ category_desc = preferred_category ? preferred_category + " " : ""
+ msg "Available #{category_desc}subcommands: (for details, knife SUB-COMMAND --help)\n\n"
+ subcommand_loader.list_commands(preferred_category).sort.each do |category, commands|
+ next if /deprecated/i.match?(category)
+
+ msg "** #{category.upcase} COMMANDS **"
+ commands.sort.each do |command|
+ subcommand_loader.load_command(command)
+ msg subcommands[command].banner if subcommands[command]
+ end
+ msg
+ end
+ end
+
+ private
+
+ # @api private
+ def path_from_caller(caller_line)
+ caller_line.split(/:\d+/).first
+ end
+
+ # Error out and print usage. probably because the arguments given by the
+ # user could not be resolved to a subcommand.
+ # @api private
+ def subcommand_not_found!(args)
+ ui.fatal("Cannot find subcommand for: '#{args.join(" ")}'")
+
+ # Mention rehash when the subcommands cache(plugin_manifest.json) is used
+ if subcommand_loader.is_a?(Chef::Knife::SubcommandLoader::HashedCommandLoader)
+ ui.info("If this is a recently installed plugin, please run 'knife rehash' to update the subcommands cache.")
+ end
+
+ if CHEF_ORGANIZATION_MANAGEMENT.include?(args[0])
+ list_commands("CHEF ORGANIZATION MANAGEMENT")
+ elsif category_commands = guess_category(args)
+ list_commands(category_commands)
+ elsif OFFICIAL_PLUGINS.include?(args[0]) # command was an uninstalled official chef knife plugin
+ ui.info("Use `#{ChefUtils::Dist::Infra::EXEC} gem install knife-#{args[0]}` to install the plugin into Chef Workstation")
+ else
+ list_commands
+ end
+
+ exit 10
+ end
+
+ # @api private
+ def reset_config_path!
+ @@chef_config_dir = nil
+ end
+
+ end
+
+ reset_config_path!
+
+ # Create a new instance of the current class configured for the given
+ # arguments and options
+ def initialize(argv = [])
+ super() # having to call super in initialize is the most annoying anti-pattern :(
+ @ui = Chef::Knife::UI.new(STDOUT, STDERR, STDIN, config)
+
+ command_name_words = self.class.snake_case_name.split("_")
+
+ # Mixlib::CLI ignores the embedded name_args
+ @name_args = parse_options(argv)
+ @name_args.delete(command_name_words.join("-"))
+ @name_args.reject! { |name_arg| command_name_words.delete(name_arg) }
+
+ # knife node run_list add requires that we have extra logic to handle
+ # the case that command name words could be joined by an underscore :/
+ command_name_joined = command_name_words.join("_")
+ @name_args.reject! { |name_arg| command_name_joined == name_arg }
+
+ # Similar handling for hyphens.
+ command_name_joined = command_name_words.join("-")
+ @name_args.reject! { |name_arg| command_name_joined == name_arg }
+
+ if config[:help]
+ msg opt_parser
+ exit 1
+ end
+
+ # Grab a copy before config merge occurs, so that we can later identify
+ # where a given config value is sourced from.
+ @original_config = config.dup
+
+ # copy Mixlib::CLI over so that it can be configured in config.rb/knife.rb
+ # config file
+ Chef::Config[:verbosity] = config[:verbosity] if config[:verbosity]
+ end
+
+ def parse_options(args)
+ super
+ rescue OptionParser::InvalidOption => e
+ puts "Error: " + e.to_s
+ show_usage
+ exit(1)
+ end
+
+ # This is all set and default mixlib-config values. We only need the default
+ # values here (the set values are explicitly mixed in again later), but there is
+ # no mixlib-config API to get a Hash back with only the default values.
+ #
+ # Assumption: since config_file_defaults is the lowest precedence it doesn't matter
+ # that we include the set values here, but this is a hack and makes the name of the
+ # method a lie. FIXME: make the name not a lie by adding an API to mixlib-config.
+ #
+ # @api private
+ #
+ def config_file_defaults
+ Chef::Config[:knife].save(true) # this is like "dup" to a (real) Hash, and includes default values (and user set values)
+ end
+
+ # This is only the user-set mixlib-config values. We do not include the defaults
+ # here so that the config defaults do not override the cli defaults.
+ #
+ # @api private
+ #
+ def config_file_settings
+ Chef::Config[:knife].save(false) # this is like "dup" to a (real) Hash, and does not include default values (just user set values)
+ end
+
+ # config is merged in this order (inverse of precedence)
+ # config_file_defaults - Chef::Config[:knife] defaults from chef-config (XXX: this also includes the settings, but they get overwritten)
+ # default_config - mixlib-cli defaults (accessor from mixlib-cli)
+ # config_file_settings - Chef::Config[:knife] user settings from the client.rb file
+ # config - mixlib-cli settings (accessor from mixlib-cli)
+ #
+ def merge_configs
+ # Update our original_config - if someone has created a knife command
+ # instance directly, they are likely ot have set cmd.config values directly
+ # as well, at which point our saved original config is no longer up to date.
+ @original_config = config.dup
+ # other code may have a handle to the config object, so use Hash#replace to deliberately
+ # update-in-place.
+ config.replace(config_file_defaults.merge(default_config).merge(config_file_settings).merge(config))
+ end
+
+ #
+ # Determine the source of a given configuration key
+ #
+ # @argument key [Symbol] a configuration key
+ # @return [Symbol,NilClass] return the source of the config key,
+ # one of:
+ # - :cli - this was explicitly provided on the CLI
+ # - :config - this came from Chef::Config[:knife] explicitly being set
+ # - :cli_default - came from a declared CLI `option`'s `default` value.
+ # - :config_default - this came from Chef::Config[:knife]'s defaults
+ # - nil - if the key could not be found in any source.
+ # This can happen when it is invalid, or has been
+ # set directly into #config without then calling #merge_config
+ def config_source(key)
+ return :cli if @original_config.include? key
+ return :config if config_file_settings.key? key
+ return :cli_default if default_config.include? key
+ return :config_default if config_file_defaults.key? key # must come after :config check
+
+ nil
+ end
+
+ # Catch-all method that does any massaging needed for various config
+ # components, such as expanding file paths and converting verbosity level
+ # into log level.
+ def apply_computed_config
+ Chef::Config[:color] = config[:color]
+
+ case Chef::Config[:verbosity]
+ when 0, nil
+ Chef::Config[:log_level] = :warn
+ when 1
+ Chef::Config[:log_level] = :info
+ when 2
+ Chef::Config[:log_level] = :debug
+ else
+ Chef::Config[:log_level] = :trace
+ end
+
+ Chef::Config[:log_level] = :trace if ENV["KNIFE_DEBUG"]
+
+ Chef::Config[:node_name] = config[:node_name] if config[:node_name]
+ Chef::Config[:client_key] = config[:client_key] if config[:client_key]
+ Chef::Config[:chef_server_url] = config[:chef_server_url] if config[:chef_server_url]
+ Chef::Config[:environment] = config[:environment] if config[:environment]
+
+ Chef::Config.local_mode = config[:local_mode] if config.key?(:local_mode)
+
+ Chef::Config.listen = config[:listen] if config.key?(:listen)
+
+ if Chef::Config.local_mode && !Chef::Config.key?(:cookbook_path) && !Chef::Config.key?(:chef_repo_path)
+ Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd)
+ end
+ Chef::Config.chef_zero.host = config[:chef_zero_host] if config[:chef_zero_host]
+ Chef::Config.chef_zero.port = config[:chef_zero_port] if config[:chef_zero_port]
+
+ # Expand a relative path from the config directory. Config from command
+ # line should already be expanded, and absolute paths will be unchanged.
+ if Chef::Config[:client_key] && config[:config_file]
+ Chef::Config[:client_key] = File.expand_path(Chef::Config[:client_key], File.dirname(config[:config_file]))
+ end
+
+ Mixlib::Log::Formatter.show_time = false
+ Chef::Log.init(Chef::Config[:log_location])
+ Chef::Log.level(Chef::Config[:log_level] || :error)
+ end
+
+ def configure_chef
+ # knife needs to send logger output to STDERR by default
+ Chef::Config[:log_location] = STDERR
+ config_loader = self.class.load_config(config[:config_file], config[:profile])
+ config[:config_file] = config_loader.config_location
+
+ # For CLI options like `--config-option key=value`. These have to get
+ # parsed and applied separately.
+ extra_config_options = config.delete(:config_option)
+
+ merge_configs
+ apply_computed_config
+
+ # This has to be after apply_computed_config so that Mixlib::Log is configured
+ Chef::Log.info("Using configuration from #{config[:config_file]}") if config[:config_file]
+
+ begin
+ Chef::Config.apply_extra_config_options(extra_config_options)
+ rescue ChefConfig::UnparsableConfigOption => e
+ ui.error e.message
+ show_usage
+ exit(1)
+ end
+
+ Chef::Config.export_proxies
+ end
+
+ def show_usage
+ stdout.puts("USAGE: " + opt_parser.to_s)
+ end
+
+ def run_with_pretty_exceptions(raise_exception = false)
+ unless respond_to?(:run)
+ ui.error "You need to add a #run method to your knife command before you can use it"
+ end
+ ENV["PATH"] = default_paths if Chef::Config[:enforce_default_paths] || Chef::Config[:enforce_path_sanity]
+ maybe_setup_fips
+ Chef::LocalMode.with_server_connectivity do
+ run
+ end
+ rescue Exception => e
+ raise if raise_exception || ( Chef::Config[:verbosity] && Chef::Config[:verbosity] >= 2 )
+
+ humanize_exception(e)
+ exit 100
+ end
+
+ def humanize_exception(e)
+ case e
+ when SystemExit
+ raise # make sure exit passes through.
+ when Net::HTTPClientException, Net::HTTPFatalError
+ humanize_http_exception(e)
+ when OpenSSL::SSL::SSLError
+ ui.error "Could not establish a secure connection to the server."
+ ui.info "Use `knife ssl check` to troubleshoot your SSL configuration."
+ ui.info "If your server uses a self-signed certificate, you can use"
+ ui.info "`knife ssl fetch` to make knife trust the server's certificates."
+ ui.info ""
+ ui.info "Original Exception: #{e.class.name}: #{e.message}"
+ when Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError
+ ui.error "Network Error: #{e.message}"
+ ui.info "Check your knife configuration and network settings"
+ when NameError, NoMethodError
+ ui.error "knife encountered an unexpected error"
+ ui.info "This may be a bug in the '#{self.class.common_name}' knife command or plugin"
+ ui.info "Please collect the output of this command with the `-VVV` option before filing a bug report."
+ ui.info "Exception: #{e.class.name}: #{e.message}"
+ when Chef::Exceptions::PrivateKeyMissing
+ ui.error "Your private key could not be loaded from #{api_key}"
+ ui.info "Check your configuration file and ensure that your private key is readable"
+ when Chef::Exceptions::InvalidRedirect
+ ui.error "Invalid Redirect: #{e.message}"
+ ui.info "Change your server location in config.rb/knife.rb to the server's FQDN to avoid unwanted redirections."
+ else
+ ui.error "#{e.class.name}: #{e.message}"
+ end
+ end
+
+ def humanize_http_exception(e)
+ response = e.response
+ case response
+ when Net::HTTPUnauthorized
+ ui.error "Failed to authenticate to #{server_url} as #{username} with key #{api_key}"
+ ui.info "Response: #{format_rest_error(response)}"
+ when Net::HTTPForbidden
+ ui.error "You authenticated successfully to #{server_url} as #{username} but you are not authorized for this action."
+ proxy_env_vars = ENV.to_hash.keys.map(&:downcase) & %w{http_proxy https_proxy ftp_proxy socks_proxy no_proxy}
+ unless proxy_env_vars.empty?
+ ui.error "There are proxy servers configured, your server url may need to be added to NO_PROXY."
+ end
+ ui.info "Response: #{format_rest_error(response)}"
+ when Net::HTTPBadRequest
+ ui.error "The data in your request was invalid"
+ ui.info "Response: #{format_rest_error(response)}"
+ when Net::HTTPNotFound
+ ui.error "The object you are looking for could not be found"
+ ui.info "Response: #{format_rest_error(response)}"
+ when Net::HTTPInternalServerError
+ ui.error "internal server error"
+ ui.info "Response: #{format_rest_error(response)}"
+ when Net::HTTPBadGateway
+ ui.error "bad gateway"
+ ui.info "Response: #{format_rest_error(response)}"
+ when Net::HTTPServiceUnavailable
+ ui.error "Service temporarily unavailable"
+ ui.info "Response: #{format_rest_error(response)}"
+ when Net::HTTPNotAcceptable
+ version_header = Chef::JSONCompat.from_json(response["x-ops-server-api-version"])
+ client_api_version = version_header["request_version"]
+ min_server_version = version_header["min_version"]
+ max_server_version = version_header["max_version"]
+ ui.error "The API version that Knife is using is not supported by the server you sent this request to."
+ ui.info "The request that Knife sent was using API version #{client_api_version}."
+ ui.info "The server you sent the request to supports a min API version of #{min_server_version} and a max API version of #{max_server_version}."
+ ui.info "Please either update your #{ChefUtils::Dist::Infra::PRODUCT} or the server to be a compatible set."
+ else
+ ui.error response.message
+ ui.info "Response: #{format_rest_error(response)}"
+ end
+ end
+
+ def username
+ Chef::Config[:node_name]
+ end
+
+ def api_key
+ Chef::Config[:client_key]
+ end
+
+ # Parses JSON from the error response sent by Chef Server and returns the
+ # error message
+ #--
+ # TODO: this code belongs in Chef::REST
+ def format_rest_error(response)
+ Array(Chef::JSONCompat.from_json(response.body)["error"]).join("; ")
+ rescue Exception
+ response.body
+ end
+
+ # FIXME: yard with @yield
+ def create_object(object, pretty_name = nil, object_class: nil)
+ output = if object_class
+ edit_data(object, object_class: object_class)
+ else
+ edit_hash(object)
+ end
+
+ if Kernel.block_given?
+ output = yield(output)
+ else
+ output.save
+ end
+
+ pretty_name ||= output
+
+ msg("Created #{pretty_name}")
+
+ output(output) if config[:print_after]
+ end
+
+ # FIXME: yard with @yield
+ def delete_object(klass, name, delete_name = nil)
+ confirm("Do you really want to delete #{name}")
+
+ if Kernel.block_given?
+ object = yield
+ else
+ object = klass.load(name)
+ object.destroy
+ end
+
+ output(format_for_display(object)) if config[:print_after]
+
+ obj_name = delete_name ? "#{delete_name}[#{name}]" : object
+ msg("Deleted #{obj_name}")
+ end
+
+ # helper method for testing if a field exists
+ # and returning the usage and proper error if not
+ def test_mandatory_field(field, fieldname)
+ if field.nil?
+ show_usage
+ ui.fatal("You must specify a #{fieldname}")
+ exit 1
+ end
+ end
+
+ def rest
+ @rest ||= begin
+ Chef::ServerAPI.new(Chef::Config[:chef_server_url])
+ end
+ end
+
+ def noauth_rest
+ @rest ||= begin
+ require "chef/http/simple_json" unless defined?(Chef::HTTP::SimpleJSON)
+ Chef::HTTP::SimpleJSON.new(Chef::Config[:chef_server_url])
+ end
+ end
+
+ def server_url
+ Chef::Config[:chef_server_url]
+ end
+
+ def maybe_setup_fips
+ unless config[:fips].nil?
+ Chef::Config[:fips] = config[:fips]
+ end
+ Chef::Config.init_openssl
+ end
+
+ def root_rest
+ @root_rest ||= begin
+ require "chef/server_api" unless defined? Chef::ServerAPI
+ Chef::ServerAPI.new(Chef::Config[:chef_server_root])
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/acl_add.rb b/knife/lib/chef/knife/acl_add.rb
new file mode 100644
index 0000000000..144a18fcb1
--- /dev/null
+++ b/knife/lib/chef/knife/acl_add.rb
@@ -0,0 +1,57 @@
+#
+# Author:: Steven Danna (steve@chef.io)
+# Author:: Jeremiah Snapp (jeremiah@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class AclAdd < Chef::Knife
+ category "acl"
+ banner "knife acl add MEMBER_TYPE MEMBER_NAME OBJECT_TYPE OBJECT_NAME PERMS"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ member_type, member_name, object_type, object_name, perms = name_args
+
+ if name_args.length != 5
+ show_usage
+ ui.fatal "You must specify the member type [client|group], member name, object type, object name and perms"
+ exit 1
+ end
+
+ unless %w{client group}.include?(member_type)
+ ui.fatal "ERROR: To enforce best practice, knife-acl can only add a client or a group to an ACL."
+ ui.fatal " See the knife-acl README for more information."
+ exit 1
+ end
+ validate_perm_type!(perms)
+ validate_member_name!(member_name)
+ validate_object_name!(object_name)
+ validate_object_type!(object_type)
+ validate_member_exists!(member_type, member_name)
+
+ add_to_acl!(member_type, member_name, object_type, object_name, perms)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/acl_base.rb b/knife/lib/chef/knife/acl_base.rb
new file mode 100644
index 0000000000..0835d1ac05
--- /dev/null
+++ b/knife/lib/chef/knife/acl_base.rb
@@ -0,0 +1,183 @@
+#
+# Author:: Steven Danna (steve@chef.io)
+# Author:: Jeremiah Snapp (<jeremiah@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ module AclBase
+
+ PERM_TYPES = %w{create read update delete grant}.freeze unless defined? PERM_TYPES
+ MEMBER_TYPES = %w{client group user}.freeze unless defined? MEMBER_TYPES
+ OBJECT_TYPES = %w{clients containers cookbooks data environments groups nodes roles policies policy_groups}.freeze unless defined? OBJECT_TYPES
+ OBJECT_NAME_SPEC = /^[\-[:alnum:]_\.]+$/.freeze unless defined? OBJECT_NAME_SPEC
+
+ def validate_object_type!(type)
+ unless OBJECT_TYPES.include?(type)
+ ui.fatal "Unknown object type \"#{type}\". The following types are permitted: #{OBJECT_TYPES.join(", ")}"
+ exit 1
+ end
+ end
+
+ def validate_object_name!(name)
+ unless OBJECT_NAME_SPEC.match(name)
+ ui.fatal "Invalid name: #{name}"
+ exit 1
+ end
+ end
+
+ def validate_member_type!(type)
+ unless MEMBER_TYPES.include?(type)
+ ui.fatal "Unknown member type \"#{type}\". The following types are permitted: #{MEMBER_TYPES.join(", ")}"
+ exit 1
+ end
+ end
+
+ def validate_member_name!(name)
+ # Same rules apply to objects and members
+ validate_object_name!(name)
+ end
+
+ def validate_perm_type!(perms)
+ perms.split(",").each do |perm|
+ unless PERM_TYPES.include?(perm)
+ ui.fatal "Invalid permission \"#{perm}\". The following permissions are permitted: #{PERM_TYPES.join(",")}"
+ exit 1
+ end
+ end
+ end
+
+ def validate_member_exists!(member_type, member_name)
+ true if rest.get_rest("#{member_type}s/#{member_name}")
+ rescue NameError
+ # ignore "NameError: uninitialized constant Chef::ApiClient" when finding a client
+ true
+ rescue
+ ui.fatal "#{member_type} '#{member_name}' does not exist"
+ exit 1
+ end
+
+ def is_usag?(gname)
+ gname.length == 32 && gname =~ /^[0-9a-f]+$/
+ end
+
+ def get_acl(object_type, object_name)
+ rest.get_rest("#{object_type}/#{object_name}/_acl?detail=granular")
+ end
+
+ def get_ace(object_type, object_name, perm)
+ get_acl(object_type, object_name)[perm]
+ end
+
+ def add_to_acl!(member_type, member_name, object_type, object_name, perms)
+ acl = get_acl(object_type, object_name)
+ perms.split(",").each do |perm|
+ ui.msg "Adding '#{member_name}' to '#{perm}' ACE of '#{object_name}'"
+ ace = acl[perm]
+
+ case member_type
+ when "client", "user"
+ # Our PUT body depends on the type of reply we get from _acl?detail=granular
+ # When the server replies with json attributes 'users' and 'clients',
+ # we'll want to modify entries under the same keys they arrived.- their presence
+ # in the body tells us that CS will accept them in a PUT.
+ # Older version of chef-server will continue to use 'actors' for a combined list
+ # and expect the same in the body.
+ key = "#{member_type}s"
+ key = "actors" unless ace.key? key
+ next if ace[key].include?(member_name)
+
+ ace[key] << member_name
+ when "group"
+ next if ace["groups"].include?(member_name)
+
+ ace["groups"] << member_name
+ end
+
+ update_ace!(object_type, object_name, perm, ace)
+ end
+ end
+
+ def remove_from_acl!(member_type, member_name, object_type, object_name, perms)
+ acl = get_acl(object_type, object_name)
+ perms.split(",").each do |perm|
+ ui.msg "Removing '#{member_name}' from '#{perm}' ACE of '#{object_name}'"
+ ace = acl[perm]
+
+ case member_type
+ when "client", "user"
+ key = "#{member_type}s"
+ key = "actors" unless ace.key? key
+ next unless ace[key].include?(member_name)
+
+ ace[key].delete(member_name)
+ when "group"
+ next unless ace["groups"].include?(member_name)
+
+ ace["groups"].delete(member_name)
+ end
+
+ update_ace!(object_type, object_name, perm, ace)
+ end
+ end
+
+ def update_ace!(object_type, object_name, ace_type, ace)
+ rest.put_rest("#{object_type}/#{object_name}/_acl/#{ace_type}", ace_type => ace)
+ end
+
+ def add_to_group!(member_type, member_name, group_name)
+ validate_member_exists!(member_type, member_name)
+ existing_group = rest.get_rest("groups/#{group_name}")
+ ui.msg "Adding '#{member_name}' to '#{group_name}' group"
+ unless existing_group["#{member_type}s"].include?(member_name)
+ existing_group["#{member_type}s"] << member_name
+ new_group = {
+ "groupname" => existing_group["groupname"],
+ "orgname" => existing_group["orgname"],
+ "actors" => {
+ "users" => existing_group["users"],
+ "clients" => existing_group["clients"],
+ "groups" => existing_group["groups"],
+ },
+ }
+ rest.put_rest("groups/#{group_name}", new_group)
+ end
+ end
+
+ def remove_from_group!(member_type, member_name, group_name)
+ validate_member_exists!(member_type, member_name)
+ existing_group = rest.get_rest("groups/#{group_name}")
+ ui.msg "Removing '#{member_name}' from '#{group_name}' group"
+ if existing_group["#{member_type}s"].include?(member_name)
+ existing_group["#{member_type}s"].delete(member_name)
+ new_group = {
+ "groupname" => existing_group["groupname"],
+ "orgname" => existing_group["orgname"],
+ "actors" => {
+ "users" => existing_group["users"],
+ "clients" => existing_group["clients"],
+ "groups" => existing_group["groups"],
+ },
+ }
+ rest.put_rest("groups/#{group_name}", new_group)
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/acl_bulk_add.rb b/knife/lib/chef/knife/acl_bulk_add.rb
new file mode 100644
index 0000000000..4992fe2afa
--- /dev/null
+++ b/knife/lib/chef/knife/acl_bulk_add.rb
@@ -0,0 +1,78 @@
+#
+# Author:: Jeremiah Snapp (jeremiah@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class AclBulkAdd < Chef::Knife
+ category "acl"
+ banner "knife acl bulk add MEMBER_TYPE MEMBER_NAME OBJECT_TYPE REGEX PERMS"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ member_type, member_name, object_type, regex, perms = name_args
+ object_name_matcher = /#{regex}/
+
+ if name_args.length != 5
+ show_usage
+ ui.fatal "You must specify the member type [client|group], member name, object type, object name REGEX and perms"
+ exit 1
+ end
+
+ unless %w{client group}.include?(member_type)
+ ui.fatal "ERROR: To enforce best practice, knife-acl can only add a client or a group to an ACL."
+ ui.fatal " See the knife-acl README for more information."
+ exit 1
+ end
+ validate_perm_type!(perms)
+ validate_member_name!(member_name)
+ validate_object_type!(object_type)
+ validate_member_exists!(member_type, member_name)
+
+ if %w{containers groups}.include?(object_type)
+ ui.fatal "bulk modifying the ACL of #{object_type} is not permitted"
+ exit 1
+ end
+
+ objects_to_modify = []
+ all_objects = rest.get_rest(object_type)
+ objects_to_modify = all_objects.keys.select { |object_name| object_name =~ object_name_matcher }
+
+ if objects_to_modify.empty?
+ ui.info "No #{object_type} match the expression /#{regex}/"
+ exit 0
+ end
+
+ ui.msg("The ACL of the following #{object_type} will be modified:")
+ ui.msg("")
+ ui.msg(ui.list(objects_to_modify.sort, :columns_down))
+ ui.msg("")
+ ui.confirm("Are you sure you want to modify the ACL of these #{object_type}?")
+
+ objects_to_modify.each do |object_name|
+ add_to_acl!(member_type, member_name, object_type, object_name, perms)
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/acl_bulk_remove.rb b/knife/lib/chef/knife/acl_bulk_remove.rb
new file mode 100644
index 0000000000..0f35f1e2fb
--- /dev/null
+++ b/knife/lib/chef/knife/acl_bulk_remove.rb
@@ -0,0 +1,83 @@
+#
+# Author:: Jeremiah Snapp (jeremiah@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class AclBulkRemove < Chef::Knife
+ category "acl"
+ banner "knife acl bulk remove MEMBER_TYPE MEMBER_NAME OBJECT_TYPE REGEX PERMS"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ member_type, member_name, object_type, regex, perms = name_args
+ object_name_matcher = /#{regex}/
+
+ if name_args.length != 5
+ show_usage
+ ui.fatal "You must specify the member type [client|group|user], member name, object type, object name REGEX and perms"
+ exit 1
+ end
+
+ if member_name == "pivotal" && %w{client user}.include?(member_type)
+ ui.fatal "ERROR: 'pivotal' is a system user so knife-acl will not remove it from an ACL."
+ exit 1
+ end
+ if member_name == "admins" && member_type == "group" && perms.to_s.split(",").include?("grant")
+ ui.fatal "ERROR: knife-acl will not remove the 'admins' group from the 'grant' ACE."
+ ui.fatal " Removal could prevent future attempts to modify permissions."
+ exit 1
+ end
+ validate_perm_type!(perms)
+ validate_member_type!(member_type)
+ validate_member_name!(member_name)
+ validate_object_type!(object_type)
+ validate_member_exists!(member_type, member_name)
+
+ if %w{containers groups}.include?(object_type)
+ ui.fatal "bulk modifying the ACL of #{object_type} is not permitted"
+ exit 1
+ end
+
+ objects_to_modify = []
+ all_objects = rest.get_rest(object_type)
+ objects_to_modify = all_objects.keys.select { |object_name| object_name =~ object_name_matcher }
+
+ if objects_to_modify.empty?
+ ui.info "No #{object_type} match the expression /#{regex}/"
+ exit 0
+ end
+
+ ui.msg("The ACL of the following #{object_type} will be modified:")
+ ui.msg("")
+ ui.msg(ui.list(objects_to_modify.sort, :columns_down))
+ ui.msg("")
+ ui.confirm("Are you sure you want to modify the ACL of these #{object_type}?")
+
+ objects_to_modify.each do |object_name|
+ remove_from_acl!(member_type, member_name, object_type, object_name, perms)
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/acl_remove.rb b/knife/lib/chef/knife/acl_remove.rb
new file mode 100644
index 0000000000..13f089ff3e
--- /dev/null
+++ b/knife/lib/chef/knife/acl_remove.rb
@@ -0,0 +1,62 @@
+#
+# Author:: Steven Danna (steve@chef.io)
+# Author:: Jeremiah Snapp (jeremiah@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class AclRemove < Chef::Knife
+ category "acl"
+ banner "knife acl remove MEMBER_TYPE MEMBER_NAME OBJECT_TYPE OBJECT_NAME PERMS"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ member_type, member_name, object_type, object_name, perms = name_args
+
+ if name_args.length != 5
+ show_usage
+ ui.fatal "You must specify the member type [client|group|user], member name, object type, object name and perms"
+ exit 1
+ end
+
+ if member_name == "pivotal" && %w{client user}.include?(member_type)
+ ui.fatal "ERROR: 'pivotal' is a system user so knife-acl will not remove it from an ACL."
+ exit 1
+ end
+ if member_name == "admins" && member_type == "group" && perms.to_s.split(",").include?("grant")
+ ui.fatal "ERROR: knife-acl will not remove the 'admins' group from the 'grant' ACE."
+ ui.fatal " Removal could prevent future attempts to modify permissions."
+ exit 1
+ end
+ validate_perm_type!(perms)
+ validate_member_type!(member_type)
+ validate_member_name!(member_name)
+ validate_object_name!(object_name)
+ validate_object_type!(object_type)
+ validate_member_exists!(member_type, member_name)
+
+ remove_from_acl!(member_type, member_name, object_type, object_name, perms)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/acl_show.rb b/knife/lib/chef/knife/acl_show.rb
new file mode 100644
index 0000000000..d3a5002b30
--- /dev/null
+++ b/knife/lib/chef/knife/acl_show.rb
@@ -0,0 +1,56 @@
+#
+# Author:: Steven Danna (steve@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class AclShow < Chef::Knife
+ category "acl"
+ banner "knife acl show OBJECT_TYPE OBJECT_NAME"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ object_type, object_name = name_args
+
+ if name_args.length != 2
+ show_usage
+ ui.fatal "You must specify an object type and object name"
+ exit 1
+ end
+
+ validate_object_type!(object_type)
+ validate_object_name!(object_name)
+ acl = get_acl(object_type, object_name)
+ PERM_TYPES.each do |perm|
+ # Filter out the actors field if we have
+ # users and clients. Note that if one is present,
+ # both will be - but we're checking both for completeness.
+ if acl[perm].key?("users") && acl[perm].key?("clients")
+ acl[perm].delete "actors"
+ end
+ end
+ ui.output acl
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/bootstrap.rb b/knife/lib/chef/knife/bootstrap.rb
new file mode 100644
index 0000000000..d57614cb3d
--- /dev/null
+++ b/knife/lib/chef/knife/bootstrap.rb
@@ -0,0 +1,1191 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "data_bag_secret_options"
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+require "license_acceptance/cli_flags/mixlib_cli"
+module LicenseAcceptance
+ autoload :Acceptor, "license_acceptance/acceptor"
+end
+
+class Chef
+ class Knife
+ class Bootstrap < Knife
+ include DataBagSecretOptions
+ include LicenseAcceptance::CLIFlags::MixlibCLI
+
+ SUPPORTED_CONNECTION_PROTOCOLS ||= %w{ssh winrm}.freeze
+ WINRM_AUTH_PROTOCOL_LIST ||= %w{plaintext kerberos ssl negotiate}.freeze
+
+ # Common connectivity options
+ option :connection_user,
+ short: "-U USERNAME",
+ long: "--connection-user USERNAME",
+ description: "Authenticate to the target host with this user account."
+
+ option :connection_password,
+ short: "-P PASSWORD",
+ long: "--connection-password PASSWORD",
+ description: "Authenticate to the target host with this password."
+
+ option :connection_port,
+ short: "-p PORT",
+ long: "--connection-port PORT",
+ description: "The port on the target node to connect to."
+
+ option :connection_protocol,
+ short: "-o PROTOCOL",
+ long: "--connection-protocol PROTOCOL",
+ description: "The protocol to use to connect to the target node.",
+ in: SUPPORTED_CONNECTION_PROTOCOLS
+
+ option :max_wait,
+ short: "-W SECONDS",
+ long: "--max-wait SECONDS",
+ description: "The maximum time to wait for the initial connection to be established."
+
+ option :session_timeout,
+ long: "--session-timeout SECONDS",
+ description: "The number of seconds to wait for each connection operation to be acknowledged while running bootstrap.",
+ default: 60
+
+ # WinRM Authentication
+ option :winrm_ssl_peer_fingerprint,
+ long: "--winrm-ssl-peer-fingerprint FINGERPRINT",
+ description: "SSL certificate fingerprint expected from the target."
+
+ option :ca_trust_file,
+ short: "-f CA_TRUST_PATH",
+ long: "--ca-trust-file CA_TRUST_PATH",
+ description: "The Certificate Authority (CA) trust file used for SSL transport."
+
+ option :winrm_no_verify_cert,
+ long: "--winrm-no-verify-cert",
+ description: "Do not verify the SSL certificate of the target node for WinRM.",
+ boolean: true
+
+ option :winrm_ssl,
+ long: "--winrm-ssl",
+ description: "Use SSL in the WinRM connection."
+
+ option :winrm_auth_method,
+ short: "-w AUTH-METHOD",
+ long: "--winrm-auth-method AUTH-METHOD",
+ description: "The WinRM authentication method to use.",
+ in: WINRM_AUTH_PROTOCOL_LIST
+
+ option :winrm_basic_auth_only,
+ long: "--winrm-basic-auth-only",
+ description: "For WinRM basic authentication when using the 'ssl' auth method.",
+ boolean: true
+
+ # This option was provided in knife bootstrap windows winrm,
+ # but it is ignored in knife-windows/WinrmSession, and so remains unimplemented here.
+ # option :kerberos_keytab_file,
+ # :short => "-T KEYTAB_FILE",
+ # :long => "--keytab-file KEYTAB_FILE",
+ # :description => "The Kerberos keytab file used for authentication"
+
+ option :kerberos_realm,
+ short: "-R KERBEROS_REALM",
+ long: "--kerberos-realm KERBEROS_REALM",
+ description: "The Kerberos realm used for authentication."
+
+ option :kerberos_service,
+ short: "-S KERBEROS_SERVICE",
+ long: "--kerberos-service KERBEROS_SERVICE",
+ description: "The Kerberos service used for authentication."
+
+ ## SSH Authentication
+ option :ssh_gateway,
+ short: "-G GATEWAY",
+ long: "--ssh-gateway GATEWAY",
+ description: "The SSH gateway."
+
+ option :ssh_gateway_identity,
+ long: "--ssh-gateway-identity SSH_GATEWAY_IDENTITY",
+ description: "The SSH identity file used for gateway authentication."
+
+ option :ssh_forward_agent,
+ short: "-A",
+ long: "--ssh-forward-agent",
+ description: "Enable SSH agent forwarding.",
+ boolean: true
+
+ option :ssh_identity_file,
+ short: "-i IDENTITY_FILE",
+ long: "--ssh-identity-file IDENTITY_FILE",
+ description: "The SSH identity file used for authentication."
+
+ option :ssh_verify_host_key,
+ long: "--ssh-verify-host-key VALUE",
+ description: "Verify host key. Default is 'always'.",
+ in: %w{always accept_new accept_new_or_local_tunnel never},
+ default: "always"
+
+ #
+ # bootstrap options
+ #
+
+ # client.rb content via chef-full/bootstrap_context
+ option :bootstrap_version,
+ long: "--bootstrap-version VERSION",
+ description: "The version of #{ChefUtils::Dist::Infra::PRODUCT} to install."
+
+ option :channel,
+ long: "--channel CHANNEL",
+ description: "Install from the given channel. Default is 'stable'.",
+ default: "stable",
+ in: %w{stable current unstable}
+
+ # client.rb content via chef-full/bootstrap_context
+ option :bootstrap_proxy,
+ long: "--bootstrap-proxy PROXY_URL",
+ description: "The proxy server for the node being bootstrapped."
+
+ # client.rb content via bootstrap_context
+ option :bootstrap_proxy_user,
+ long: "--bootstrap-proxy-user PROXY_USER",
+ description: "The proxy authentication username for the node being bootstrapped."
+
+ # client.rb content via bootstrap_context
+ option :bootstrap_proxy_pass,
+ long: "--bootstrap-proxy-pass PROXY_PASS",
+ description: "The proxy authentication password for the node being bootstrapped."
+
+ # client.rb content via bootstrap_context
+ option :bootstrap_no_proxy,
+ long: "--bootstrap-no-proxy [NO_PROXY_URL|NO_PROXY_IP]",
+ description: "Do not proxy locations for the node being bootstrapped"
+
+ # client.rb content via bootstrap_context
+ option :bootstrap_template,
+ short: "-t TEMPLATE",
+ long: "--bootstrap-template TEMPLATE",
+ description: "Bootstrap #{ChefUtils::Dist::Infra::PRODUCT} using a built-in or custom template. Set to the full path of an erb template or use one of the built-in templates."
+
+ # client.rb content via bootstrap_context
+ option :node_ssl_verify_mode,
+ long: "--node-ssl-verify-mode [peer|none]",
+ description: "Whether or not to verify the SSL cert for all HTTPS requests.",
+ proc: Proc.new { |v|
+ valid_values = %w{none peer}
+ unless valid_values.include?(v)
+ raise "Invalid value '#{v}' for --node-ssl-verify-mode. Valid values are: #{valid_values.join(", ")}"
+ end
+
+ v
+ }
+
+ # bootstrap_context - client.rb
+ option :node_verify_api_cert,
+ long: "--[no-]node-verify-api-cert",
+ description: "Verify the SSL cert for HTTPS requests to the #{ChefUtils::Dist::Server::PRODUCT} API.",
+ boolean: true
+
+ # runtime - sudo settings (train handles sudo)
+ option :use_sudo,
+ long: "--sudo",
+ description: "Execute the bootstrap via sudo.",
+ boolean: true
+
+ # runtime - sudo settings (train handles sudo)
+ option :preserve_home,
+ long: "--sudo-preserve-home",
+ description: "Preserve non-root user HOME environment variable with sudo.",
+ boolean: true
+
+ # runtime - sudo settings (train handles sudo)
+ option :use_sudo_password,
+ long: "--use-sudo-password",
+ description: "Execute the bootstrap via sudo with password.",
+ boolean: false
+
+ # runtime - su user
+ option :su_user,
+ long: "--su-user NAME",
+ description: "The su - USER name to perform bootstrap command using a non-root user."
+
+ # runtime - su user password
+ option :su_password,
+ long: "--su-password PASSWORD",
+ description: "The su USER password for authentication."
+
+ # runtime - client_builder
+ option :chef_node_name,
+ short: "-N NAME",
+ long: "--node-name NAME",
+ description: "The node name for your new node."
+
+ # runtime - client_builder - set runlist when creating node
+ 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: []
+
+ # runtime - client_builder - set policy name when creating node
+ option :policy_name,
+ long: "--policy-name POLICY_NAME",
+ description: "Policyfile name to use (--policy-group must also be given).",
+ default: nil
+
+ # runtime - client_builder - set policy group when creating node
+ option :policy_group,
+ long: "--policy-group POLICY_GROUP",
+ description: "Policy group name to use (--policy-name must also be given).",
+ default: nil
+
+ # runtime - client_builder - node tags
+ option :tags,
+ long: "--tags TAGS",
+ description: "Comma separated list of tags to apply to the node.",
+ proc: lambda { |o| o.split(/[\s,]+/) },
+ default: []
+
+ # bootstrap template
+ option :first_boot_attributes,
+ short: "-j JSON_ATTRIBS",
+ long: "--json-attributes",
+ description: "A JSON string to be added to the first run of #{ChefUtils::Dist::Infra::CLIENT}.",
+ proc: lambda { |o| Chef::JSONCompat.parse(o) },
+ default: nil
+
+ # bootstrap template
+ option :first_boot_attributes_from_file,
+ long: "--json-attribute-file FILE",
+ description: "A JSON file to be used to the first run of #{ChefUtils::Dist::Infra::CLIENT}.",
+ proc: lambda { |o| Chef::JSONCompat.parse(File.read(o)) },
+ default: nil
+
+ # bootstrap template
+ # Create ohai hints in /etc/chef/ohai/hints, fname=hintname, content=value
+ option :hints,
+ long: "--hint HINT_NAME[=HINT_FILE]",
+ description: "Specify an Ohai hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
+ proc: Proc.new { |hint, accumulator|
+ accumulator ||= {}
+ name, path = hint.split("=", 2)
+ accumulator[name] = path ? Chef::JSONCompat.parse(::File.read(path)) : {}
+ accumulator
+ }
+
+ # bootstrap override: url of a an installer shell script to use in place of omnitruck
+ # Note that the bootstrap template _only_ references this out of Chef::Config, and not from
+ # the provided options to knife bootstrap, so we set the Chef::Config option here.
+ option :bootstrap_url,
+ long: "--bootstrap-url URL",
+ description: "URL to a custom installation script."
+
+ option :bootstrap_product,
+ long: "--bootstrap-product PRODUCT",
+ description: "Product to install.",
+ default: "chef"
+
+ option :msi_url, # Windows target only
+ short: "-m URL",
+ long: "--msi-url URL",
+ description: "Location of the #{ChefUtils::Dist::Infra::PRODUCT} MSI. The default templates will prefer to download from this location. The MSI will be downloaded from #{ChefUtils::Dist::Org::WEBSITE} if not provided (Windows).",
+ default: ""
+
+ # bootstrap override: Do this instead of our own setup.sh from omnitruck. Causes bootstrap_url to be ignored.
+ option :bootstrap_install_command,
+ long: "--bootstrap-install-command COMMANDS",
+ description: "Custom command to install #{ChefUtils::Dist::Infra::PRODUCT}."
+
+ # bootstrap template: Run this command first in the bootstrap script
+ option :bootstrap_preinstall_command,
+ long: "--bootstrap-preinstall-command COMMANDS",
+ description: "Custom commands to run before installing #{ChefUtils::Dist::Infra::PRODUCT}."
+
+ # bootstrap template
+ option :bootstrap_wget_options,
+ long: "--bootstrap-wget-options OPTIONS",
+ description: "Add options to wget when installing #{ChefUtils::Dist::Infra::PRODUCT}."
+
+ # bootstrap template
+ option :bootstrap_curl_options,
+ long: "--bootstrap-curl-options OPTIONS",
+ description: "Add options to curl when install #{ChefUtils::Dist::Infra::PRODUCT}."
+
+ # chef_vault_handler
+ 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."
+
+ # chef_vault_handler
+ option :bootstrap_vault_json,
+ long: "--bootstrap-vault-json VAULT_JSON",
+ description: "A JSON string with the vault(s) and item(s) to be updated."
+
+ # chef_vault_handler
+ 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, accumulator|
+ (vault, item) = i.split(":")
+ accumulator ||= {}
+ accumulator[vault] ||= []
+ accumulator[vault].push(item)
+ accumulator
+ }
+
+ # Deprecated options. These must be declared after
+ # regular options because they refer to the replacement
+ # option definitions implicitly.
+ deprecated_option :auth_timeout,
+ replacement: :max_wait,
+ long: "--max-wait SECONDS"
+
+ deprecated_option :forward_agent,
+ replacement: :ssh_forward_agent,
+ boolean: true, long: "--forward-agent"
+
+ deprecated_option :host_key_verify,
+ replacement: :ssh_verify_host_key,
+ boolean: true, long: "--[no-]host-key-verify",
+ value_mapper: Proc.new { |verify| verify ? "always" : "never" }
+
+ deprecated_option :prerelease,
+ replacement: :channel,
+ long: "--prerelease",
+ boolean: true, value_mapper: Proc.new { "current" }
+
+ deprecated_option :ssh_user,
+ replacement: :connection_user,
+ long: "--ssh-user USERNAME"
+
+ deprecated_option :ssh_password,
+ replacement: :connection_password,
+ long: "--ssh-password PASSWORD"
+
+ deprecated_option :ssh_port,
+ replacement: :connection_port,
+ long: "--ssh-port PASSWORD"
+
+ deprecated_option :ssl_peer_fingerprint,
+ replacement: :winrm_ssl_peer_fingerprint,
+ long: "--ssl-peer-fingerprint FINGERPRINT"
+
+ deprecated_option :winrm_user,
+ replacement: :connection_user,
+ long: "--winrm-user USERNAME", short: "-x USERNAME"
+
+ deprecated_option :winrm_password,
+ replacement: :connection_password,
+ long: "--winrm-password PASSWORD"
+
+ deprecated_option :winrm_port,
+ replacement: :connection_port,
+ long: "--winrm-port PORT"
+
+ deprecated_option :winrm_authentication_protocol,
+ replacement: :winrm_auth_method,
+ long: "--winrm-authentication-protocol PROTOCOL"
+
+ deprecated_option :winrm_session_timeout,
+ replacement: :session_timeout,
+ long: "--winrm-session-timeout MINUTES"
+
+ deprecated_option :winrm_ssl_verify_mode,
+ replacement: :winrm_no_verify_cert,
+ long: "--winrm-ssl-verify-mode MODE"
+
+ deprecated_option :winrm_transport, replacement: :winrm_ssl,
+ long: "--winrm-transport TRANSPORT",
+ value_mapper: Proc.new { |value| value == "ssl" }
+
+ attr_reader :connection
+
+ deps do
+ require "erubis" unless defined?(Erubis)
+ require "net/ssh" unless defined?(Net::SSH)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+ require_relative "bootstrap/chef_vault_handler"
+ require_relative "bootstrap/client_builder"
+ require_relative "bootstrap/train_connector"
+ end
+
+ banner "knife bootstrap [PROTOCOL://][USER@]FQDN (options)"
+
+ def client_builder
+ @client_builder ||= Chef::Knife::Bootstrap::ClientBuilder.new(
+ chef_config: Chef::Config,
+ config: config,
+ ui: ui
+ )
+ end
+
+ def chef_vault_handler
+ @chef_vault_handler ||= Chef::Knife::Bootstrap::ChefVaultHandler.new(
+ config: config,
+ ui: ui
+ )
+ end
+
+ # Determine if we need to accept the Chef Infra license locally in order to successfully bootstrap
+ # the remote node. Remote 'chef-client' run will fail if it is >= 15 and the license is not accepted locally.
+ def check_license
+ Chef::Log.debug("Checking if we need to accept Chef license to bootstrap node")
+ version = config[:bootstrap_version] || Chef::VERSION.split(".").first
+ acceptor = LicenseAcceptance::Acceptor.new(logger: Chef::Log, provided: Chef::Config[:chef_license])
+ if acceptor.license_required?("chef", version)
+ Chef::Log.debug("License acceptance required for chef version: #{version}")
+ license_id = acceptor.id_from_mixlib("chef")
+ acceptor.check_and_persist(license_id, version)
+ Chef::Config[:chef_license] ||= acceptor.acceptance_value
+ end
+ 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
+ if connection.windows?
+ "windows-chef-client-msi"
+ else
+ "chef-full"
+ end
+ end
+
+ def host_descriptor
+ Array(@name_args).first
+ 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
+ if host_descriptor
+ @server_name ||= host_descriptor.split("@").reverse[0]
+ end
+ end
+
+ # @return [String] The CLI specific bootstrap template or the default
+ def bootstrap_template
+ # Allow passing a bootstrap template or use the default
+ config[:bootstrap_template] || default_bootstrap_template
+ end
+
+ def find_template
+ template = bootstrap_template
+
+ # Use the template directly if it's a path to an actual file
+ if File.exist?(template)
+ Chef::Log.trace("Using the specified bootstrap template: #{File.dirname(template)}")
+ return template
+ end
+
+ # Otherwise search the template directories until we find the right one
+ bootstrap_files = []
+ bootstrap_files << File.join(__dir__, "bootstrap/templates", "#{template}.erb")
+ bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{template}.erb") if Chef::Knife.chef_config_dir
+ ChefConfig::PathHelper.home(".chef", "bootstrap", "#{template}.erb") { |p| bootstrap_files << p }
+ bootstrap_files << Gem.find_files(File.join("chef", "knife", "bootstrap", "#{template}.erb"))
+ bootstrap_files.flatten!
+
+ template_file = Array(bootstrap_files).find do |bootstrap_template|
+ Chef::Log.trace("Looking for bootstrap template in #{File.dirname(bootstrap_template)}")
+ File.exist?(bootstrap_template)
+ end
+
+ unless template_file
+ ui.info("Can not find bootstrap definition for #{template}")
+ raise Errno::ENOENT
+ end
+
+ Chef::Log.trace("Found bootstrap template: #{template_file}")
+
+ template_file
+ end
+
+ def secret
+ @secret ||= encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil
+ end
+
+ # Establish bootstrap context for template rendering.
+ # Requires connection to be a live connection in order to determine
+ # the correct platform.
+ def bootstrap_context
+ @bootstrap_context ||=
+ if connection.windows?
+ require_relative "core/windows_bootstrap_context"
+ Knife::Core::WindowsBootstrapContext.new(config, config[:run_list], Chef::Config, secret)
+ else
+ require_relative "core/bootstrap_context"
+ Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config, secret)
+ end
+ end
+
+ def first_boot_attributes
+ @config[:first_boot_attributes] || @config[:first_boot_attributes_from_file] || {}
+ end
+
+ def render_template
+ @config[:first_boot_attributes] = first_boot_attributes
+ template_file = find_template
+ template = IO.read(template_file).chomp
+ Erubis::Eruby.new(template).evaluate(bootstrap_context)
+ end
+
+ def run
+ check_license if ChefUtils::Dist::Org::ENFORCE_LICENSE
+
+ plugin_setup!
+ validate_name_args!
+ validate_protocol!
+ validate_first_boot_attributes!
+ validate_winrm_transport_opts!
+ validate_policy_options!
+ plugin_validate_options!
+
+ winrm_warn_no_ssl_verification
+ warn_on_short_session_timeout
+
+ plugin_create_instance!
+ $stdout.sync = true
+ connect!
+ register_client
+
+ content = render_template
+ bootstrap_path = upload_bootstrap(content)
+ perform_bootstrap(bootstrap_path)
+ plugin_finalize
+ ensure
+ connection.del_file!(bootstrap_path) if connection && bootstrap_path
+ end
+
+ def register_client
+ # 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? ||
+ (Chef::Config[:validation_key] &&
+ !File.exist?(File.expand_path(Chef::Config[:validation_key])))
+
+ unless config[:chef_node_name]
+ ui.error("You must pass a node name with -N when bootstrapping with user credentials")
+ exit 1
+ end
+ client_builder.run
+ chef_vault_handler.run(client_builder.client)
+
+ bootstrap_context.client_pem = client_builder.client_path
+ else
+ ui.warn "Performing legacy client registration with the validation key at #{Chef::Config[:validation_key]}..."
+ ui.warn "Remove the key file or remove the 'validation_key' configuration option from your config.rb (knife.rb) to use more secure user credentials for client registration."
+ end
+ end
+
+ def perform_bootstrap(remote_bootstrap_script_path)
+ ui.info("Bootstrapping #{ui.color(server_name, :bold)}")
+ cmd = bootstrap_command(remote_bootstrap_script_path)
+ bootstrap_run_command(cmd)
+ end
+
+ # Actual bootstrap command to be run on the node.
+ # Handles recursive calls if su USER failed to authenticate.
+ def bootstrap_run_command(cmd)
+ r = connection.run_command(cmd) do |data, channel|
+ ui.msg("#{ui.color(" [#{connection.hostname}]", :cyan)} #{data}")
+ channel.send_data("#{config[:su_password] || config[:connection_password]}\n") if data.match?("Password:")
+ end
+
+ if r.exit_status != 0
+ ui.error("The following error occurred on #{server_name}:")
+ ui.error("#{r.stdout} #{r.stderr}".strip)
+ exit(r.exit_status)
+ end
+ rescue Train::UserError => e
+ limit ||= 0
+ if e.reason == :bad_su_user_password && limit < 3
+ limit += 1
+ ui.warn("Failed to authenticate su - #{config[:su_user]} to #{server_name}")
+ config[:su_password] = ui.ask("Enter password for su - #{config[:su_user]}@#{server_name}:", echo: false)
+ retry
+ else
+ raise
+ end
+ end
+
+ def connect!
+ ui.info("Connecting to #{ui.color(server_name, :bold)} using #{connection_protocol}")
+ opts ||= connection_opts.dup
+ do_connect(opts)
+ rescue Train::Error => e
+ # We handle these by message text only because train only loads the
+ # transports and protocols that it needs - so the exceptions may not be defined,
+ # and we don't want to require files internal to train.
+ if e.message =~ /fingerprint (\S+) is unknown for "(.+)"/ # Train::Transports::SSHFailed
+ fingerprint = $1
+ hostname, ip = $2.split(",")
+ # TODO: convert the SHA256 base64 value to hex with colons
+ # 'ssh' example output:
+ # RSA key fingerprint is e5:cb:c0:e2:21:3b:12:52:f8:ce:cb:00:24:e2:0c:92.
+ # ECDSA key fingerprint is 5d:67:61:08:a9:d7:01:fd:5e:ae:7e:09:40:ef:c0:3c.
+ # will exit 3 on N
+ ui.confirm <<~EOM
+ The authenticity of host '#{hostname} (#{ip})' can't be established.
+ fingerprint is #{fingerprint}.
+
+ Are you sure you want to continue connecting
+ EOM
+ # FIXME: this should save the key to known_hosts but doesn't appear to be
+ config[:ssh_verify_host_key] = :accept_new
+ conn_opts = connection_opts(reset: true)
+ opts.merge! conn_opts
+ retry
+ elsif (ssh? && e.cause && e.cause.class == Net::SSH::AuthenticationFailed) || (ssh? && e.class == Train::ClientError && e.reason == :no_ssh_password_or_key_available)
+ if connection.password_auth?
+ raise
+ else
+ ui.warn("Failed to authenticate #{opts[:user]} to #{server_name} - trying password auth")
+ password = ui.ask("Enter password for #{opts[:user]}@#{server_name}:", echo: false)
+ end
+
+ opts.merge! force_ssh_password_opts(password)
+ retry
+ else
+ raise
+ end
+ rescue RuntimeError => e
+ if winrm? && e.message == "password is a required option"
+ if connection.password_auth?
+ raise
+ else
+ ui.warn("Failed to authenticate #{opts[:user]} to #{server_name} - trying password auth")
+ password = ui.ask("Enter password for #{opts[:user]}@#{server_name}:", echo: false)
+ end
+
+ opts.merge! force_winrm_password_opts(password)
+ retry
+ else
+ raise
+ end
+ end
+
+ def handle_ssh_error(e); end
+
+ # url values override CLI flags, if you provide both
+ # we'll use the one that you gave in the URL.
+ def connection_protocol
+ return @connection_protocol if @connection_protocol
+
+ from_url = host_descriptor =~ %r{^(.*)://} ? $1 : nil
+ from_knife = config[:connection_protocol]
+ @connection_protocol = from_url || from_knife || "ssh"
+ end
+
+ def do_connect(conn_options)
+ @connection = TrainConnector.new(host_descriptor, connection_protocol, conn_options)
+ connection.connect!
+ rescue Train::UserError => e
+ limit ||= 1
+ if !conn_options.key?(:pty) && e.reason == :sudo_no_tty
+ ui.warn("#{e.message} - trying with pty request")
+ conn_options[:pty] = true # ensure we can talk to systems with requiretty set true in sshd config
+ retry
+ elsif config[:use_sudo_password] && (e.reason == :sudo_password_required || e.reason == :bad_sudo_password) && limit < 3
+ ui.warn("Failed to authenticate #{conn_options[:user]} to #{server_name} - #{e.message} \n sudo: #{limit} incorrect password attempt")
+ sudo_password = ui.ask("Enter sudo password for #{conn_options[:user]}@#{server_name}:", echo: false)
+ limit += 1
+ conn_options[:sudo_password] = sudo_password
+
+ retry
+ else
+ raise
+ end
+ end
+
+ # Fail if both first_boot_attributes and first_boot_attributes_from_file
+ # are set.
+ def validate_first_boot_attributes!
+ if @config[:first_boot_attributes] && @config[:first_boot_attributes_from_file]
+ raise Chef::Exceptions::BootstrapCommandInputError
+ end
+
+ true
+ end
+
+ # FIXME: someone needs to clean this up properly: https://github.com/chef/chef/issues/9645
+ # This code is deliberately left without an abstraction around deprecating the config options to avoid knife plugins from
+ # using those methods (which will need to be deprecated and break them) via inheritance (ruby does not have a true `private`
+ # so the lack of any inheritable implementation is because of that).
+ #
+ def winrm_auth_method
+ config.key?(:winrm_auth_method) ? config[:winrm_auth_method] : config.key?(:winrm_authentications_protocol) ? config[:winrm_authentication_protocol] : "negotiate" # rubocop:disable Style/NestedTernaryOperator
+ end
+
+ def ssh_verify_host_key
+ config.key?(:ssh_verify_host_key) ? config[:ssh_verify_host_key] : config.key?(:host_key_verify) ? config[:host_key_verify] : "always" # rubocop:disable Style/NestedTernaryOperator
+ end
+
+ # Fail if using plaintext auth without ssl because
+ # this can expose keys in plaintext on the wire.
+ # TODO test for this method
+ # TODO check that the protocol is valid.
+ def validate_winrm_transport_opts!
+ return true unless winrm?
+
+ if Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))
+ if winrm_auth_method == "plaintext" &&
+ config[:winrm_ssl] != true
+ ui.error <<~EOM
+ Validatorless bootstrap over unsecure winrm channels could expose your
+ key to network sniffing.
+ Please use a 'winrm_auth_method' other than 'plaintext',
+ or enable ssl on #{server_name} then use the ---winrm-ssl flag
+ to connect.
+ EOM
+
+ exit 1
+ end
+ end
+ true
+ end
+
+ # fail if the server_name is nil
+ def validate_name_args!
+ if server_name.nil?
+ ui.error("Must pass an FQDN or ip to bootstrap")
+ exit 1
+ end
+ end
+
+ # Ensure options are valid by checking policyfile values.
+ #
+ # The method call will cause the program to exit(1) if:
+ # * Only one of --policy-name and --policy-group is specified
+ # * Policyfile options are set and --run-list is set as well
+ #
+ # @return [TrueClass] If options are valid.
+ def validate_policy_options!
+ if incomplete_policyfile_options?
+ ui.error("--policy-name and --policy-group must be specified together")
+ exit 1
+ elsif policyfile_and_run_list_given?
+ ui.error("Policyfile options and --run-list are exclusive")
+ exit 1
+ end
+ end
+
+ # Ensure a valid protocol is provided for target host connection
+ #
+ # The method call will cause the program to exit(1) if:
+ # * Conflicting protocols are given via the target URI and the --protocol option
+ # * The protocol is not a supported protocol
+ #
+ # @return [TrueClass] If options are valid.
+ def validate_protocol!
+ from_cli = config[:connection_protocol]
+ if from_cli && connection_protocol != from_cli
+ # Hanging indent to align with the ERROR: prefix
+ ui.error <<~EOM
+ The URL '#{host_descriptor}' indicates protocol is '#{connection_protocol}'
+ while the --protocol flag specifies '#{from_cli}'. Please include
+ only one or the other.
+ EOM
+ exit 1
+ end
+
+ unless SUPPORTED_CONNECTION_PROTOCOLS.include?(connection_protocol)
+ ui.error <<~EOM
+ Unsupported protocol '#{connection_protocol}'.
+
+ Supported protocols are: #{SUPPORTED_CONNECTION_PROTOCOLS.join(" ")}
+ EOM
+ exit 1
+ end
+ true
+ end
+
+ # Validate any additional options
+ #
+ # Plugins that subclass bootstrap, e.g. knife-ec2, can use this method to validate any additional options before any other actions are executed
+ #
+ # @return [TrueClass] If options are valid or exits
+ def plugin_validate_options!
+ true
+ end
+
+ # Create the server that we will bootstrap, if necessary
+ #
+ # Plugins that subclass bootstrap, e.g. knife-ec2, can use this method to call out to an API to build an instance of the server we wish to bootstrap
+ #
+ # @return [TrueClass] If instance successfully created, or exits
+ def plugin_create_instance!
+ true
+ end
+
+ # Perform any setup necessary by the plugin
+ #
+ # Plugins that subclass bootstrap, e.g. knife-ec2, can use this method to create connection objects
+ #
+ # @return [TrueClass] If instance successfully created, or exits
+ def plugin_setup!; end
+
+ # Perform any teardown or cleanup necessary by the plugin
+ #
+ # Plugins that subclass bootstrap, e.g. knife-ec2, can use this method to display a message or perform any cleanup
+ #
+ # @return [void]
+ def plugin_finalize; end
+
+ # If session_timeout is too short, it is likely
+ # a holdover from "--winrm-session-timeout" which used
+ # minutes as its unit, instead of seconds.
+ # Warn the human so that they are not surprised.
+ #
+ def warn_on_short_session_timeout
+ if session_timeout && session_timeout <= 15
+ ui.warn <<~EOM
+ You provided '--session-timeout #{session_timeout}' second(s).
+ Did you mean '--session-timeout #{session_timeout * 60}' seconds?
+ EOM
+ end
+ end
+
+ def winrm_warn_no_ssl_verification
+ return unless winrm?
+
+ # REVIEWER NOTE
+ # The original check from knife plugin did not include winrm_ssl_peer_fingerprint
+ # Reference:
+ # https://github.com/chef/knife-windows/blob/92d151298142be4a4750c5b54bb264f8d5b81b8a/lib/chef/knife/winrm_knife_base.rb#L271-L273
+ # TODO Seems like we should also do a similar warning if ssh_verify_host == false
+ if config[:ca_trust_file].nil? &&
+ config[:winrm_no_verify_cert] &&
+ config[:winrm_ssl_peer_fingerprint].nil?
+ ui.warn <<~WARN
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ SSL validation of HTTPS requests for the WinRM transport is disabled.
+ HTTPS WinRM connections are still encrypted, but knife is not able
+ to detect forged replies or spoofing attacks.
+
+ To work around this issue you can use the flag `--winrm-no-verify-cert`
+ or add an entry like this to your knife configuration file:
+
+ # Verify all WinRM HTTPS connections
+ knife[:winrm_no_verify_cert] = true
+
+ You can also specify a ca_trust_file via --ca-trust-file,
+ or the expected fingerprint of the target host's certificate
+ via --winrm-ssl-peer-fingerprint.
+ WARN
+ end
+ end
+
+ # @return a configuration hash suitable for connecting to the remote
+ # host via train
+ def connection_opts(reset: false)
+ return @connection_opts unless @connection_opts.nil? || reset == true
+
+ @connection_opts = {}
+ @connection_opts.merge! base_opts
+ @connection_opts.merge! host_verify_opts
+ @connection_opts.merge! gateway_opts
+ @connection_opts.merge! sudo_opts
+ @connection_opts.merge! winrm_opts
+ @connection_opts.merge! ssh_opts
+ @connection_opts.merge! ssh_identity_opts
+ @connection_opts
+ end
+
+ def winrm?
+ connection_protocol == "winrm"
+ end
+
+ def ssh?
+ connection_protocol == "ssh"
+ end
+
+ # Common configuration for all protocols
+ def base_opts
+ port = config_for_protocol(:port)
+ user = config_for_protocol(:user)
+ {}.tap do |opts|
+ opts[:logger] = Chef::Log
+ opts[:password] = config[:connection_password] if config.key?(:connection_password)
+ opts[:user] = user if user
+ opts[:max_wait_until_ready] = config[:max_wait].to_f unless config[:max_wait].nil?
+ # TODO - when would we need to provide rdp_port vs port? Or are they not mutually exclusive?
+ opts[:port] = port if port
+ end
+ end
+
+ def host_verify_opts
+ if winrm?
+ { self_signed: config[:winrm_no_verify_cert] === true }
+ elsif ssh?
+ # Fall back to the old knife config key name for back compat.
+ { verify_host_key: ssh_verify_host_key }
+ else
+ {}
+ end
+ end
+
+ def ssh_opts
+ opts = {}
+ return opts if winrm?
+
+ opts[:non_interactive] = true # Prevent password prompts from underlying net/ssh
+ opts[:forward_agent] = (config[:ssh_forward_agent] === true)
+ opts[:connection_timeout] = session_timeout
+ opts
+ end
+
+ def ssh_identity_opts
+ opts = {}
+ return opts if winrm?
+
+ identity_file = config[:ssh_identity_file]
+ if identity_file
+ opts[:key_files] = [identity_file]
+ # We only set keys_only based on the explicit ssh_identity_file;
+ # someone may use a gateway key and still expect password auth
+ # on the target. Similarly, someone may have a default key specified
+ # in knife config, but have provided a password on the CLI.
+
+ # REVIEW NOTE: this is a new behavior. Originally, ssh_identity_file
+ # could only be populated from CLI options, so there was no need to check
+ # for this. We will also set keys_only to false only if there are keys
+ # and no password.
+ # If both are present, train(via net/ssh) will prefer keys, falling back to password.
+ # Reference: https://github.com/chef/chef/blob/master/lib/chef/knife/ssh.rb#L272
+ opts[:keys_only] = config.key?(:connection_password) == false
+ else
+ opts[:key_files] = []
+ opts[:keys_only] = false
+ end
+
+ gateway_identity_file = config[:ssh_gateway] ? config[:ssh_gateway_identity] : nil
+ unless gateway_identity_file.nil?
+ opts[:key_files] << gateway_identity_file
+ end
+
+ opts
+ end
+
+ def gateway_opts
+ opts = {}
+ if config[:ssh_gateway]
+ split = config[:ssh_gateway].split("@", 2)
+ if split.length == 1
+ gw_host = split[0]
+ else
+ gw_user = split[0]
+ gw_host = split[1]
+ end
+ gw_host, gw_port = gw_host.split(":", 2)
+ # TODO - validate convertible port in config validation?
+ gw_port = Integer(gw_port) rescue nil
+ opts[:bastion_host] = gw_host
+ opts[:bastion_user] = gw_user
+ opts[:bastion_port] = gw_port
+ end
+ opts
+ end
+
+ # use_sudo - tells bootstrap to use the sudo command to run bootstrap
+ # use_sudo_password - tells bootstrap to use the sudo command to run bootstrap
+ # and to use the password specified with --password
+ # TODO: I'd like to make our sudo options sane:
+ # --sudo (bool) - use sudo
+ # --sudo-password PASSWORD (default: :password) - use this password for sudo
+ # --sudo-options "opt,opt,opt" to pass into sudo
+ # --sudo-command COMMAND sudo command other than sudo
+ # REVIEW NOTE: knife bootstrap did not pull sudo values from Chef::Config,
+ # should we change that for consistency?
+ def sudo_opts
+ return {} if winrm?
+
+ opts = { sudo: false }
+ if config[:use_sudo]
+ opts[:sudo] = true
+ if config[:use_sudo_password]
+ opts[:sudo_password] = config[:connection_password]
+ end
+ if config[:preserve_home]
+ opts[:sudo_options] = "-H"
+ end
+ end
+ opts
+ end
+
+ def winrm_opts
+ return {} unless winrm?
+
+ opts = {
+ winrm_transport: winrm_auth_method, # winrm gem and train calls auth method 'transport'
+ winrm_basic_auth_only: config[:winrm_basic_auth_only] || false,
+ ssl: config[:winrm_ssl] === true,
+ ssl_peer_fingerprint: config[:winrm_ssl_peer_fingerprint],
+ }
+
+ if winrm_auth_method == "kerberos"
+ opts[:kerberos_service] = config[:kerberos_service] if config[:kerberos_service]
+ opts[:kerberos_realm] = config[:kerberos_realm] if config[:kerberos_service]
+ end
+
+ if config[:ca_trust_file]
+ opts[:ca_trust_path] = config[:ca_trust_file]
+ end
+
+ opts[:operation_timeout] = session_timeout
+
+ opts
+ end
+
+ # Config overrides to force password auth.
+ def force_ssh_password_opts(password)
+ {
+ password: password,
+ non_interactive: false,
+ keys_only: false,
+ key_files: [],
+ auth_methods: %i{password keyboard_interactive},
+ }
+ end
+
+ def force_winrm_password_opts(password)
+ {
+ password: password,
+ }
+ end
+
+ # This is for deprecating config options. The fallback_key can be used
+ # to pull an old knife config option out of the config file when the
+ # cli value has been renamed. This is different from the deprecated
+ # cli values, since these are for config options that have no corresponding
+ # cli value.
+ #
+ # DO NOT USE - this whole API is considered deprecated
+ #
+ # @api deprecated
+ #
+ def config_value(key, fallback_key = nil, default = nil)
+ Chef.deprecated(:knife_bootstrap_apis, "Use of config_value is deprecated. Knife plugin authors should access the config hash directly, which does correct merging of cli and config options.")
+ if config.key?(key)
+ # the first key is the primary key so we check the merged hash first
+ config[key]
+ elsif config.key?(fallback_key)
+ # we get the old config option here (the deprecated cli option shouldn't exist)
+ config[fallback_key]
+ else
+ default
+ end
+ end
+
+ def upload_bootstrap(content)
+ script_name = connection.windows? ? "bootstrap.bat" : "bootstrap.sh"
+ remote_path = connection.normalize_path(File.join(connection.temp_dir, script_name))
+ connection.upload_file_content!(content, remote_path)
+ remote_path
+ end
+
+ # build the command string for bootstrapping
+ # @return String
+ def bootstrap_command(remote_path)
+ if connection.windows?
+ "cmd.exe /C #{remote_path}"
+ else
+ cmd = "sh #{remote_path}"
+
+ if config[:su_user]
+ # su - USER is subject to required an interactive console
+ # Otherwise, it will raise: su: must be run from a terminal
+ set_transport_options(pty: true)
+ cmd = "su - #{config[:su_user]} -c '#{cmd}'"
+ cmd = "sudo " << cmd if config[:use_sudo]
+ end
+
+ cmd
+ end
+ end
+
+ private
+
+ # To avoid cluttering the CLI options, some flags (such as port and user)
+ # are shared between protocols. However, there is still a need to allow the operator
+ # to specify defaults separately, since they may not be the same values for different
+ # protocols.
+
+ # These keys are available in Chef::Config, and are prefixed with the protocol name.
+ # For example, :user CLI option will map to :winrm_user and :ssh_user Chef::Config keys,
+ # based on the connection protocol in use.
+
+ # @api private
+ def config_for_protocol(option)
+ if option == :port
+ config[:connection_port] || config[knife_key_for_protocol(option)]
+ else
+ config[:connection_user] || config[knife_key_for_protocol(option)]
+ end
+ end
+
+ # @api private
+ def knife_key_for_protocol(option)
+ "#{connection_protocol}_#{option}".to_sym
+ end
+
+ # True if policy_name and run_list are both given
+ def policyfile_and_run_list_given?
+ run_list_given? && policyfile_options_given?
+ end
+
+ def run_list_given?
+ !config[:run_list].nil? && !config[:run_list].empty?
+ end
+
+ def policyfile_options_given?
+ !!config[:policy_name]
+ end
+
+ # True if one of policy_name or policy_group was given, but not both
+ def incomplete_policyfile_options?
+ (!!config[:policy_name] ^ config[:policy_group])
+ end
+
+ # session_timeout option has a default that may not arrive, particularly if
+ # we're being invoked from a plugin that doesn't merge_config.
+ def session_timeout
+ timeout = config[:session_timeout]
+ return options[:session_timeout][:default] if timeout.nil?
+
+ timeout.to_i
+ end
+
+ # Train::Transports::SSH::Connection#transport_options
+ # Append the options to connection transport_options
+ #
+ # @param opts [Hash] the opts to be added to connection transport_options.
+ # @return [Hash] transport_options if the opts contains any option to be set.
+ #
+ def set_transport_options(opts)
+ return unless opts.is_a?(Hash) || !opts.empty?
+
+ connection&.connection&.transport_options&.merge! opts
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/bootstrap/chef_vault_handler.rb b/knife/lib/chef/knife/bootstrap/chef_vault_handler.rb
new file mode 100644
index 0000000000..20759d6fdf
--- /dev/null
+++ b/knife/lib/chef/knife/bootstrap/chef_vault_handler.rb
@@ -0,0 +1,162 @@
+#
+# Author:: Lamont Granquist (<lamont@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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 :config
+
+ # @return [Chef::Knife::UI] ui object for output
+ attr_accessor :ui
+
+ # @return [Chef::ApiClient] vault client
+ attr_reader :client
+
+ # @param config [Hash] knife merged config, typically @config
+ # @param ui [Chef::Knife::UI] ui object for output
+ def initialize(config: {}, knife_config: nil, ui: nil)
+ @config = config
+ unless knife_config.nil?
+ @config = knife_config
+ Chef.deprecated(:knife_bootstrap_apis, "The knife_config option to the Bootstrap::ClientBuilder object is deprecated and has been renamed to just 'config'")
+ end
+ @ui = ui
+ end
+
+ # Updates the chef vault items for the newly created client.
+ #
+ # @param client [Chef::ApiClient] vault client
+ def run(client)
+ return unless doing_chef_vault?
+
+ sanity_check
+
+ @client = 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
+ config[:bootstrap_vault_json]
+ end
+
+ # @return [String] JSON text in a file representing the chef vault items
+ def bootstrap_vault_file
+ config[:bootstrap_vault_file]
+ end
+
+ # @return [Hash] Ruby object representing the chef vault items to create
+ def bootstrap_vault_item
+ config[:bootstrap_vault_item]
+ end
+
+ # Helper to return a ruby object representing 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 || 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(client)
+ 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
+ # @return [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 to very lazily require the chef-vault gem
+ def require_chef_vault!
+ @require_chef_vault ||=
+ begin
+ error_message = "Knife bootstrap requires version 2.6.0 or higher of the chef-vault gem to configure vault items"
+ require "chef-vault"
+ if Gem::Version.new(ChefVault::VERSION) < Gem::Version.new("2.6.0")
+ raise error_message
+ end
+
+ true
+ rescue LoadError
+ raise error_message
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/bootstrap/client_builder.rb b/knife/lib/chef/knife/bootstrap/client_builder.rb
new file mode 100644
index 0000000000..b1e69d90db
--- /dev/null
+++ b/knife/lib/chef/knife/bootstrap/client_builder.rb
@@ -0,0 +1,212 @@
+#
+# Author:: Lamont Granquist (<lamont@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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" unless defined?(Chef::Node)
+require "chef/server_api" unless defined?(Chef::ServerAPI)
+require "chef/api_client" unless defined?(Chef::APIClient)
+require "chef/api_client/registration" unless defined?(Chef::APIClient::Registration)
+require "tmpdir" unless defined?(Dir.mktmpdir)
+
+class Chef
+ class Knife
+ class Bootstrap < Knife
+ class ClientBuilder
+
+ # @return [Hash] knife merged config, typically @config
+ attr_accessor :config
+ # @return [Hash] chef config object
+ attr_accessor :chef_config
+ # @return [Chef::Knife::UI] ui object for output
+ attr_accessor :ui
+ # @return [Chef::ApiClient] client saved on run
+ attr_reader :client
+
+ # @param 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(config: {}, knife_config: nil, chef_config: {}, ui: nil)
+ @config = config
+ unless knife_config.nil?
+ @config = knife_config
+ Chef.deprecated(:knife_bootstrap_apis, "The knife_config option to the Bootstrap::ClientBuilder object is deprecated and has been renamed to just 'config'")
+ end
+ @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}")
+
+ @client = 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 config
+ def node_name
+ config[:chef_node_name]
+ end
+
+ # @return [String] environment from the config
+ def environment
+ config[:environment]
+ end
+
+ # @return [String] run_list from the config
+ def run_list
+ config[:run_list]
+ end
+
+ # @return [String] policy_name from the config
+ def policy_name
+ config[:policy_name]
+ end
+
+ # @return [String] policy_group from the config
+ def policy_group
+ config[:policy_group]
+ end
+
+ # @return [Hash,Array] Object representation of json first-boot attributes from the config
+ def first_boot_attributes
+ 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.policy_name = policy_name if policy_name
+ node.policy_group = policy_group if policy_group
+ (config[:tags] || []).each do |tag|
+ node.tags << tag
+ end
+ 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(relative_path)
+ true
+ rescue Net::HTTPClientException => e
+ raise unless e.response.code == "404"
+
+ false
+ end
+
+ # @return [Chef::ServerAPI] REST client using the client credentials
+ def client_rest
+ @client_rest ||= Chef::ServerAPI.new(chef_server_url, client_name: node_name, signing_key_filename: client_path)
+ end
+
+ # @return [Chef::ServerAPI] REST client using the cli user's knife credentials
+ # this uses the users's credentials
+ def rest
+ @rest ||= Chef::ServerAPI.new(chef_server_url)
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/bootstrap/templates/README.md b/knife/lib/chef/knife/bootstrap/templates/README.md
new file mode 100644
index 0000000000..7f28f8f40f
--- /dev/null
+++ b/knife/lib/chef/knife/bootstrap/templates/README.md
@@ -0,0 +1,11 @@
+This directory contains bootstrap templates which can be used with the -d flag
+to 'knife bootstrap' to install Chef in different ways. To simplify installation,
+and reduce the matrix of common installation patterns to support, we have
+standardized on the [Omnibus](https://github.com/chef/omnibus) built installation
+packages.
+
+The 'chef-full' template downloads a script which is used to determine the correct
+Omnibus package for this system from the [Omnitruck](https://docs.chef.io/api_omnitruck/) API.
+
+You can still utilize custom bootstrap templates on your system if your installation
+needs are unique. Additional information can be found on the [docs site](https://docs.chef.io/knife_bootstrap/#custom-templates).
diff --git a/knife/lib/chef/knife/bootstrap/templates/chef-full.erb b/knife/lib/chef/knife/bootstrap/templates/chef-full.erb
new file mode 100644
index 0000000000..2e0c80eaef
--- /dev/null
+++ b/knife/lib/chef/knife/bootstrap/templates/chef-full.erb
@@ -0,0 +1,242 @@
+<%= "https_proxy=\"#{@config[:bootstrap_proxy]}\" export https_proxy" if @config[:bootstrap_proxy] %>
+<%= "no_proxy=\"#{@config[:bootstrap_no_proxy]}\" export no_proxy" if @config[:bootstrap_no_proxy] %>
+
+if test "x$TMPDIR" = "x"; then
+ tmp="/tmp"
+else
+ tmp=$TMPDIR
+fi
+
+# secure-ish temp dir creation without having mktemp available (DDoS-able but not exploitable)
+tmp_dir="$tmp/install.sh.$$"
+(umask 077 && mkdir $tmp_dir) || exit 1
+
+exists() {
+ if command -v $1 >/dev/null 2>&1
+ then
+ return 0
+ else
+ return 1
+ fi
+}
+
+http_404_error() {
+ echo "ERROR 404: Could not retrieve a valid install.sh!"
+ exit 1
+}
+
+capture_tmp_stderr() {
+ # spool up /tmp/stderr from all the commands we called
+ if test -f "$tmp_dir/stderr"; then
+ output=`cat $tmp_dir/stderr`
+ stderr_results="${stderr_results}\nSTDERR from $1:\n\n$output\n"
+ rm $tmp_dir/stderr
+ fi
+}
+
+# do_wget URL FILENAME
+do_wget() {
+ echo "trying wget..."
+ wget <%= "--proxy=on " if @config[:bootstrap_proxy] %> <%= @config[:bootstrap_wget_options] %> -O "$2" "$1" 2>$tmp_dir/stderr
+ rc=$?
+ # check for 404
+ grep "ERROR 404" $tmp_dir/stderr 2>&1 >/dev/null
+ if test $? -eq 0; then
+ http_404_error
+ fi
+
+ # check for bad return status or empty output
+ if test $rc -ne 0 || test ! -s "$2"; then
+ capture_tmp_stderr "wget"
+ return 1
+ fi
+
+ return 0
+}
+
+# do_curl URL FILENAME
+do_curl() {
+ echo "trying curl..."
+ curl -sL <%= "--proxy \"#{@config[:bootstrap_proxy]}\" " if @config[:bootstrap_proxy] %> <%= @config[:bootstrap_curl_options] %> -D $tmp_dir/stderr -o "$2" "$1" 2>$tmp_dir/stderr
+ rc=$?
+ # check for 404
+ grep "404 Not Found" $tmp_dir/stderr 2>&1 >/dev/null
+ if test $? -eq 0; then
+ http_404_error
+ fi
+
+ # check for bad return status or empty output
+ if test $rc -ne 0 || test ! -s "$2"; then
+ capture_tmp_stderr "curl"
+ return 1
+ fi
+
+ return 0
+}
+
+# do_fetch URL FILENAME
+do_fetch() {
+ echo "trying fetch..."
+ fetch -o "$2" "$1" 2>$tmp_dir/stderr
+ # check for bad return status
+ test $? -ne 0 && return 1
+ return 0
+}
+
+# do_perl URL FILENAME
+do_perl() {
+ echo "trying perl..."
+ perl -e "use LWP::Simple; getprint(shift @ARGV);" "$1" > "$2" 2>$tmp_dir/stderr
+ rc=$?
+ # check for 404
+ grep "404 Not Found" $tmp_dir/stderr 2>&1 >/dev/null
+ if test $? -eq 0; then
+ http_404_error
+ fi
+
+ # check for bad return status or empty output
+ if test $rc -ne 0 || test ! -s "$2"; then
+ capture_tmp_stderr "perl"
+ return 1
+ fi
+
+ return 0
+}
+
+# do_python URL FILENAME
+do_python() {
+ echo "trying python..."
+ python -c "import sys,urllib2 ; sys.stdout.write(urllib2.urlopen(sys.argv[1]).read())" "$1" > "$2" 2>$tmp_dir/stderr
+ rc=$?
+ # check for 404
+ grep "HTTP Error 404" $tmp_dir/stderr 2>&1 >/dev/null
+ if test $? -eq 0; then
+ http_404_error
+ fi
+
+ # check for bad return status or empty output
+ if test $rc -ne 0 || test ! -s "$2"; then
+ capture_tmp_stderr "python"
+ return 1
+ fi
+ return 0
+}
+
+# do_download URL FILENAME
+do_download() {
+ PATH=/opt/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sfw/bin:/sbin:/bin:/usr/sbin:/usr/bin
+ export PATH
+
+ echo "downloading $1"
+ echo " to file $2"
+
+ # we try all of these until we get success.
+ # perl, in particular may be present but LWP::Simple may not be installed
+
+ if exists wget; then
+ do_wget $1 $2 && return 0
+ fi
+
+ if exists curl; then
+ do_curl $1 $2 && return 0
+ fi
+
+ if exists fetch; then
+ do_fetch $1 $2 && return 0
+ fi
+
+ if exists perl; then
+ do_perl $1 $2 && return 0
+ fi
+
+ if exists python; then
+ do_python $1 $2 && return 0
+ fi
+
+ echo ">>>>>> wget, curl, fetch, perl, or python not found on this instance."
+
+ if test "x$stderr_results" != "x"; then
+ echo "\nDEBUG OUTPUT FOLLOWS:\n$stderr_results"
+ fi
+
+ return 16
+}
+
+<%# Run any custom commands before installing chef-client -%>
+<%# Ex. wait for cloud-init to complete -%>
+<% if @config[:bootstrap_preinstall_command] %>
+ <%= @config[:bootstrap_preinstall_command] %>
+<% end %>
+
+<% if @config[:bootstrap_install_command] %>
+ <%= @config[:bootstrap_install_command] %>
+<% else %>
+ install_sh="<%= @config[:bootstrap_url] ? @config[:bootstrap_url] : "https://omnitruck.chef.io/chef/install.sh" %>"
+ if test -f /usr/bin/<%= ChefUtils::Dist::Infra::CLIENT %>; then
+ echo "-----> Existing <%= ChefUtils::Dist::Infra::PRODUCT %> installation detected"
+ else
+ echo "-----> Installing Chef Omnibus (<%= @config[:channel] %>/<%= version_to_install %>)"
+ do_download ${install_sh} $tmp_dir/install.sh
+ sh $tmp_dir/install.sh -P <%= @config[:bootstrap_product] %> -c <%= @config[:channel] %> -v <%= version_to_install %>
+ fi
+<% end %>
+
+if test "x$tmp_dir" != "x"; then
+ rm -r "$tmp_dir"
+fi
+
+mkdir -p /etc/chef
+
+<% if client_pem -%>
+(umask 077 && (cat > /etc/chef/client.pem <<'EOP'
+<%= ::File.read(::File.expand_path(client_pem)) %>
+EOP
+)) || exit 1
+<% end -%>
+
+<% if validation_key -%>
+(umask 077 && (cat > /etc/chef/validation.pem <<'EOP'
+<%= validation_key %>
+EOP
+)) || exit 1
+<% end -%>
+
+<% if encrypted_data_bag_secret -%>
+(umask 077 && (cat > /etc/chef/encrypted_data_bag_secret <<'EOP'
+<%= encrypted_data_bag_secret %>
+EOP
+)) || exit 1
+<% end -%>
+
+<% unless trusted_certs.empty? -%>
+mkdir -p /etc/chef/trusted_certs
+<%= trusted_certs %>
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @config[:hints].nil? || @config[:hints].empty? -%>
+mkdir -p /etc/chef/ohai/hints
+
+<% @config[:hints].each do |name, hash| -%>
+cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP'
+<%= Chef::JSONCompat.to_json(hash) %>
+EOP
+<% end -%>
+<% end -%>
+
+cat > /etc/chef/client.rb <<'EOP'
+<%= config_content %>
+EOP
+
+cat > /etc/chef/first-boot.json <<'EOP'
+<%= Chef::JSONCompat.to_json(first_boot) %>
+EOP
+
+<% unless client_d.empty? -%>
+mkdir -p /etc/chef/client.d
+<%= client_d %>
+<% end -%>
+
+echo "Starting the first <%= ChefUtils::Dist::Infra::PRODUCT %> Client run..."
+
+<%= start_chef %>
diff --git a/knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb b/knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb
new file mode 100644
index 0000000000..7aa7be49f8
--- /dev/null
+++ b/knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb
@@ -0,0 +1,278 @@
+@rem
+@rem Author:: Seth Chisamore (<schisamo@chef.io>)
+@rem Copyright:: Copyright (c) 2011-2019 Chef Software, Inc.
+@rem License:: Apache License, Version 2.0
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem http://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@rem Use delayed environment expansion so that ERRORLEVEL can be evaluated with the
+@rem !ERRORLEVEL! syntax which evaluates at execution of the line of script, not when
+@rem the line is read. See help for the /E switch from cmd.exe /? .
+@setlocal ENABLEDELAYEDEXPANSION
+
+<%= "SETX HTTP_PROXY \"#{@config[:bootstrap_proxy]}\"" if @config[:bootstrap_proxy] %>
+
+@set BOOTSTRAP_DIRECTORY=<%= bootstrap_directory %>
+@echo Checking for existing directory "%BOOTSTRAP_DIRECTORY%"...
+@if NOT EXIST %BOOTSTRAP_DIRECTORY% (
+ @echo Existing directory not found, creating.
+ @mkdir %BOOTSTRAP_DIRECTORY%
+) else (
+ @echo Existing directory found, skipping creation.
+)
+
+> <%= bootstrap_directory %>\wget.vbs (
+ <%= win_wget %>
+)
+
+> <%= bootstrap_directory %>\wget.ps1 (
+ <%= win_wget_ps %>
+)
+
+@rem Determine the version and the architecture
+
+@FOR /F "usebackq tokens=1-8 delims=.[] " %%A IN (`ver`) DO (
+@set WinMajor=%%D
+@set WinMinor=%%E
+@set WinBuild=%%F
+)
+
+@echo Detected Windows Version %WinMajor%.%WinMinor% Build %WinBuild%
+
+@set LATEST_OS_VERSION_MAJOR=10
+@set LATEST_OS_VERSION_MINOR=1
+
+@if /i %WinMajor% GTR %LATEST_OS_VERSION_MAJOR% goto VersionUnknown
+@if /i %WinMajor% EQU %LATEST_OS_VERSION_MAJOR% (
+ @if /i %WinMinor% GTR %LATEST_OS_VERSION_MINOR% goto VersionUnknown
+)
+
+goto Version%WinMajor%.%WinMinor%
+
+:VersionUnknown
+@rem If this is an unknown version of windows set the default
+@set MACHINE_OS=2012r2
+@echo Warning: Unknown version of Windows, assuming default of Windows %MACHINE_OS%
+goto architecture_select
+
+:Version6.0
+@set MACHINE_OS=2008
+goto architecture_select
+
+:Version6.1
+@set MACHINE_OS=2008r2
+goto architecture_select
+
+:Version6.2
+@set MACHINE_OS=2012
+goto architecture_select
+
+@rem Currently Windows Server 2012 R2 is treated as equivalent to Windows Server 2012
+:Version6.3
+@set MACHINE_OS=2012r2
+goto architecture_select
+
+:Version10.0
+@set MACHINE_OS=2016
+goto architecture_select
+
+@rem Currently Windows Server 2019 is treated as equivalent to Windows Server 2016
+:Version10.1
+goto Version10.0
+
+:architecture_select
+<% if @config[:architecture] %>
+ @set MACHINE_ARCH=<%= @config[:architecture] %>
+
+ <% if @config[:architecture] == "x86_64" %>
+ IF "%PROCESSOR_ARCHITECTURE%"=="x86" IF not defined PROCESSOR_ARCHITEW6432 (
+ echo You specified bootstrap_architecture as x86_64 but the target machine is i386. A 64 bit program cannot run on a 32 bit machine. > "&2"
+ echo Exiting without bootstrapping. > "&2"
+ exit /b 1
+ )
+ <% end %>
+<% else %>
+ @set MACHINE_ARCH=x86_64
+ IF "%PROCESSOR_ARCHITECTURE%"=="x86" IF not defined PROCESSOR_ARCHITEW6432 @set MACHINE_ARCH=i686
+<% end %>
+goto chef_installed
+
+:chef_installed
+@echo Checking for existing <%= ChefUtils::Dist::Infra::PRODUCT %> installation
+WHERE <%= ChefUtils::Dist::Infra::CLIENT %> >nul 2>nul
+If !ERRORLEVEL!==0 (
+ @echo Existing <%= ChefUtils::Dist::Infra::PRODUCT %> installation detected, skipping download
+ goto key_create
+) else (
+ @echo No existing installation of <%= ChefUtils::Dist::Infra::PRODUCT %> detected
+ goto install
+)
+
+:install
+@rem If user has provided the custom installation command, execute it
+<% if @config[:bootstrap_install_command] %>
+ <%= @config[:bootstrap_install_command] %>
+<% else %>
+ @rem Install Chef using the MSI installer
+
+ @set "LOCAL_DESTINATION_MSI_PATH=<%= local_download_path %>"
+ @set "CHEF_CLIENT_MSI_LOG_PATH=%TEMP%\<%= ChefUtils::Dist::Infra::CLIENT %>-msi%RANDOM%.log"
+
+ @rem Clear any pre-existing downloads
+ @echo Checking for existing downloaded package at "%LOCAL_DESTINATION_MSI_PATH%"
+ @if EXIST "%LOCAL_DESTINATION_MSI_PATH%" (
+ @echo Found existing downloaded package, deleting.
+ @del /f /q "%LOCAL_DESTINATION_MSI_PATH%"
+ @if ERRORLEVEL 1 (
+ echo Warning: Failed to delete pre-existing package with status code !ERRORLEVEL! > "&2"
+ )
+ ) else (
+ echo No existing downloaded packages to delete.
+ )
+
+ @rem If there is somehow a name collision, remove pre-existing log
+ @if EXIST "%CHEF_CLIENT_MSI_LOG_PATH%" del /f /q "%CHEF_CLIENT_MSI_LOG_PATH%"
+
+ @echo Attempting to download client package using PowerShell if available...
+ @set "REMOTE_SOURCE_MSI_URL=<%= msi_url('%MACHINE_OS%', '%MACHINE_ARCH%', 'PowerShell') %>"
+ @set powershell_download=powershell.exe -ExecutionPolicy Unrestricted -InputFormat None -NoProfile -NonInteractive -File <%= bootstrap_directory %>\wget.ps1 "%REMOTE_SOURCE_MSI_URL%" "%LOCAL_DESTINATION_MSI_PATH%"
+ @echo !powershell_download!
+ @call !powershell_download!
+
+ @set DOWNLOAD_ERROR_STATUS=!ERRORLEVEL!
+
+ @if ERRORLEVEL 1 (
+ @echo Failed PowerShell download with status code !DOWNLOAD_ERROR_STATUS! > "&2"
+ @if !DOWNLOAD_ERROR_STATUS!==0 set DOWNLOAD_ERROR_STATUS=2
+ ) else (
+ @rem Sometimes the error level is not set even when the download failed,
+ @rem so check for the file to be sure it is there -- if it is not, we will retry
+ @if NOT EXIST "%LOCAL_DESTINATION_MSI_PATH%" (
+ echo Failed download: download completed, but downloaded file not found > "&2"
+ set DOWNLOAD_ERROR_STATUS=2
+ ) else (
+ echo Download via PowerShell succeeded.
+ )
+ )
+
+ @if NOT %DOWNLOAD_ERROR_STATUS%==0 (
+ @echo Warning: Failed to download "%REMOTE_SOURCE_MSI_URL%" to "%LOCAL_DESTINATION_MSI_PATH%"
+ @echo Warning: Retrying download with cscript ...
+
+ @if EXIST "%LOCAL_DESTINATION_MSI_PATH%" del /f /q "%LOCAL_DESTINATION_MSI_PATH%"
+
+ @set "REMOTE_SOURCE_MSI_URL=<%= msi_url('%MACHINE_OS%', '%MACHINE_ARCH%') %>"
+ cscript /nologo <%= bootstrap_directory %>\wget.vbs /url:"%REMOTE_SOURCE_MSI_URL%" /path:"%LOCAL_DESTINATION_MSI_PATH%"
+
+ @if NOT ERRORLEVEL 1 (
+ @rem Sometimes the error level is not set even when the download failed,
+ @rem so check for the file to be sure it is there.
+ @if NOT EXIST "%LOCAL_DESTINATION_MSI_PATH%" (
+ echo Failed download: download completed, but downloaded file not found > "&2"
+ echo Exiting without bootstrapping due to download failure. > "&2"
+ exit /b 1
+ ) else (
+ echo Download via cscript succeeded.
+ )
+ ) else (
+ echo Failed to download "%REMOTE_SOURCE_MSI_URL%" with status code !ERRORLEVEL!. > "&2"
+ echo Exiting without bootstrapping due to download failure. > "&2"
+ exit /b 1
+ )
+ )
+
+ @echo Installing downloaded client package...
+
+ <%= install_chef %>
+
+ @if ERRORLEVEL 1 (
+ echo <%= ChefUtils::Dist::Infra::CLIENT %> package failed to install with status code !ERRORLEVEL!. > "&2"
+ echo See installation log for additional detail: %CHEF_CLIENT_MSI_LOG_PATH%. > "&2"
+ ) else (
+ @echo Installation completed successfully
+ del /f /q "%CHEF_CLIENT_MSI_LOG_PATH%"
+ )
+
+<% end %>
+
+@rem This line is required to separate the key_create label from the "block boundary"
+@rem Removing these lines will cause the error "The system cannot find the batch label specified - key_create"
+:key_create
+@endlocal
+
+@echo off
+
+<% if client_pem -%>
+> <%= bootstrap_directory %>\client.pem (
+ <%= escape_and_echo(::File.read(::File.expand_path(client_pem))) %>
+)
+<% end -%>
+
+echo Writing validation key...
+
+<% if validation_key -%>
+> <%= bootstrap_directory %>\validation.pem (
+ <%= escape_and_echo(validation_key) %>
+)
+<% end -%>
+
+echo Validation key written.
+@echo on
+
+<% if secret -%>
+> <%= bootstrap_directory %>\encrypted_data_bag_secret (
+ <%= encrypted_data_bag_secret %>
+)
+<% end -%>
+
+<% unless trusted_certs_script.empty? -%>
+ @if NOT EXIST <%= bootstrap_directory %>\trusted_certs (
+ mkdir <%= bootstrap_directory %>\trusted_certs
+ )
+ )
+
+<%= trusted_certs_script %>
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @config[:hints].nil? || @config[:hints].empty? -%>
+ @if NOT EXIST <%= bootstrap_directory %>\ohai\hints (
+ mkdir <%= bootstrap_directory %>\ohai\hints
+ )
+
+<% @config[:hints].each do |name, hash| -%>
+> <%= bootstrap_directory %>\ohai\hints\<%= name %>.json (
+ <%= escape_and_echo(hash.to_json) %>
+)
+<% end -%>
+<% end -%>
+
+> <%= bootstrap_directory %>\client.rb (
+ <%= config_content %>
+)
+
+> <%= bootstrap_directory %>\first-boot.json (
+ <%= first_boot %>
+)
+
+<% unless client_d.empty? -%>
+ @if NOT EXIST <%= bootstrap_directory %>\client.d (
+ mkdir <%= bootstrap_directory %>\client.d
+ )
+
+ <%= client_d %>
+<% end -%>
+
+@echo Starting <%= ChefUtils::Dist::Infra::CLIENT %> to bootstrap the node...
+<%= start_chef %>
diff --git a/knife/lib/chef/knife/bootstrap/train_connector.rb b/knife/lib/chef/knife/bootstrap/train_connector.rb
new file mode 100644
index 0000000000..a220ece5bc
--- /dev/null
+++ b/knife/lib/chef/knife/bootstrap/train_connector.rb
@@ -0,0 +1,336 @@
+# Copyright:: Copyright (c) Chef Software 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 "train"
+require "tempfile" unless defined?(Tempfile)
+require "uri" unless defined?(URI)
+require "securerandom" unless defined?(SecureRandom)
+
+class Chef
+ class Knife
+ class Bootstrap < Knife
+ class TrainConnector
+ SSH_CONFIG_OVERRIDE_KEYS ||= %i{user port proxy}.freeze
+
+ MKTEMP_WIN_COMMAND ||= <<~EOM.freeze
+ $parent = [System.IO.Path]::GetTempPath();
+ [string] $name = [System.Guid]::NewGuid();
+ $tmp = New-Item -ItemType Directory -Path (Join-Path $parent $name);
+ $tmp.FullName
+ EOM
+
+ DEFAULT_REMOTE_TEMP ||= "/tmp".freeze
+
+ def initialize(host_url, default_protocol, opts)
+ @host_url = host_url
+ @default_protocol = default_protocol
+ @opts_in = opts
+ end
+
+ def config
+ @config ||= begin
+ uri_opts = opts_from_uri(@host_url, @default_protocol)
+ transport_config(@host_url, @opts_in.merge(uri_opts))
+ end
+ end
+
+ def connection
+ @connection ||= begin
+ Train.validate_backend(config)
+ train = Train.create(config[:backend], config)
+ # Note that the train connection is not currently connected
+ # to the remote host, but it's ready to go.
+ train.connection
+ end
+ end
+
+ #
+ # Establish a connection to the configured host.
+ #
+ # @raise [TrainError]
+ # @raise [TrainUserError]
+ #
+ # @return [TrueClass] true if the connection could be established.
+ def connect!
+ # Force connection to establish
+ connection.wait_until_ready
+ true
+ end
+
+ #
+ # @return [String] the configured hostname
+ def hostname
+ config[:host]
+ end
+
+ # Answers the question, "is this connection configured for password auth?"
+ # @return [Boolean] true if the connection is configured with password auth
+ def password_auth?
+ config.key? :password
+ end
+
+ # Answers the question, "Am I connected to a linux host?"
+ #
+ # @return [Boolean] true if the connected host is linux.
+ def linux?
+ connection.platform.linux?
+ end
+
+ # Answers the question, "Am I connected to a unix host?"
+ #
+ # @note this will always return true for a linux host
+ # because train classifies linux as a unix
+ #
+ # @return [Boolean] true if the connected host is unix or linux
+ def unix?
+ connection.platform.unix?
+ end
+
+ #
+ # Answers the question, "Am I connected to a Windows host?"
+ #
+ # @return [Boolean] true if the connected host is Windows
+ def windows?
+ connection.platform.windows?
+ end
+
+ #
+ # Creates a temporary directory on the remote host if it
+ # hasn't already. Caches directory location. For *nix,
+ # it will ensure that the directory is owned by the logged-in user
+ #
+ # @return [String] the temporary path created on the remote host.
+ def temp_dir
+ @tmpdir ||= begin
+ if windows?
+ run_command!(MKTEMP_WIN_COMMAND).stdout.split.last
+ else
+ # Get a 6 chars string using secure random
+ # eg. /tmp/chef_XXXXXX.
+ # Use mkdir to create TEMP dir to get rid of mktemp
+ dir = "#{DEFAULT_REMOTE_TEMP}/chef_#{SecureRandom.alphanumeric(6)}"
+ run_command!("mkdir -p '#{dir}'")
+ # Ensure that dir has the correct owner. We are possibly
+ # running with sudo right now - so this directory would be owned by root.
+ # File upload is performed over SCP as the current logged-in user,
+ # so we'll set ownership to ensure that works.
+ run_command!("chown #{config[:user]} '#{dir}'") if config[:sudo]
+
+ dir
+ end
+ end
+ end
+
+ #
+ # Uploads a file from "local_path" to "remote_path"
+ #
+ # @param local_path [String] The path to a file on the local file system
+ # @param remote_path [String] The destination path on the remote file system.
+ # @return NilClass
+ def upload_file!(local_path, remote_path)
+ connection.upload(local_path, remote_path)
+ nil
+ end
+
+ #
+ # Uploads the provided content into the file "remote_path" on the remote host.
+ #
+ # @param content [String] The content to upload into remote_path
+ # @param remote_path [String] The destination path on the remote file system.
+ # @return NilClass
+ def upload_file_content!(content, remote_path)
+ t = Tempfile.new("chef-content")
+ t.binmode
+ t << content
+ t.close
+ upload_file!(t.path, remote_path)
+ nil
+ ensure
+ t.close
+ t.unlink
+ end
+
+ #
+ # Force-deletes the file at "path" from the remote host.
+ #
+ # @param path [String] The path of the file on the remote host
+ def del_file!(path)
+ if windows?
+ run_command!("If (Test-Path \"#{path}\") { Remove-Item -Force -Path \"#{path}\" }")
+ else
+ run_command!("rm -f \"#{path}\"")
+ end
+ nil
+ end
+
+ #
+ # normalizes path across OS's - always use forward slashes, which
+ # Windows and *nix understand.
+ #
+ # @param path [String] The path to normalize
+ #
+ # @return [String] the normalized path
+ def normalize_path(path)
+ path.tr("\\", "/")
+ end
+
+ #
+ # Runs a command on the remote host.
+ #
+ # @param command [String] The command to run.
+ # @param data_handler [Proc] An optional block. When provided, inbound data will be
+ # published via `data_handler.call(data)`. This can allow
+ # callers to receive and render updates from remote command execution.
+ #
+ # @return [Train::Extras::CommandResult] an object containing stdout, stderr, and exit_status
+ def run_command(command, &data_handler)
+ connection.run_command(command, &data_handler)
+ end
+
+ #
+ # Runs a command the remote host
+ #
+ # @param command [String] The command to run.
+ # @param data_handler [Proc] An optional block. When provided, inbound data will be
+ # published via `data_handler.call(data)`. This can allow
+ # callers to receive and render updates from remote command execution.
+ #
+ # @raise Chef::Knife::Bootstrap::RemoteExecutionFailed if an error occurs (non-zero exit status)
+ # @return [Train::Extras::CommandResult] an object containing stdout, stderr, and exit_status
+ def run_command!(command, &data_handler)
+ result = run_command(command, &data_handler)
+ if result.exit_status != 0
+ raise RemoteExecutionFailed.new(hostname, command, result)
+ end
+
+ result
+ end
+
+ private
+
+ # For a given url and set of options, create a config
+ # hash suitable for passing into train.
+ def transport_config(host_url, opts_in)
+ # These baseline opts are not protocol-specific
+ opts = { target: host_url,
+ www_form_encoded_password: true,
+ transport_retries: 2,
+ transport_retry_sleep: 1,
+ backend: opts_in[:backend],
+ logger: opts_in[:logger] }
+
+ # Accepts options provided by caller if they're not already configured,
+ # but note that they will be constrained to valid options for the backend protocol
+ opts.merge!(opts_from_caller(opts, opts_in))
+
+ # WinRM has some additional computed options
+ opts.merge!(opts_inferred_from_winrm(opts, opts_in))
+
+ # Now that everything is populated, fill in anything missing
+ # that may be found in user ssh config
+ opts.merge!(missing_opts_from_ssh_config(opts, opts_in))
+
+ Train.target_config(opts)
+ end
+
+ # Some winrm options are inferred based on other options.
+ # Return a hash of winrm options based on configuration already built.
+ def opts_inferred_from_winrm(config, opts_in)
+ return {} unless config[:backend] == "winrm"
+
+ opts_out = {}
+
+ if opts_in[:ssl]
+ opts_out[:ssl] = true
+ opts_out[:self_signed] = opts_in[:self_signed] || false
+ end
+
+ # See note here: https://github.com/mwrock/WinRM#example
+ if %w{ssl plaintext}.include?(opts_in[:winrm_auth_method])
+ opts_out[:winrm_disable_sspi] = true
+ end
+ opts_out
+ end
+
+ # Returns a hash containing valid options for the current
+ # transport protocol that are not already present in config
+ def opts_from_caller(config, opts_in)
+ # Train.options gives us the supported config options for the
+ # backend provider (ssh, winrm). We'll use that
+ # to filter out options that don't belong
+ # to the transport type we're using.
+ valid_opts = Train.options(config[:backend])
+ opts_in.select do |key, _v|
+ valid_opts.key?(key) && !config.key?(key)
+ end
+ end
+
+ # Extract any of username/password/host/port/transport
+ # that are in the URI and return them as a config has
+ def opts_from_uri(uri, default_protocol)
+ # Train.unpack_target_from_uri only works for complete URIs in
+ # form of proto://[user[:pass]@]host[:port]/
+ # So we'll add the protocol prefix if it's not supplied.
+ uri_to_check = if URI::DEFAULT_PARSER.make_regexp.match(uri)
+ uri
+ else
+ "#{default_protocol}://#{uri}"
+ end
+
+ Train.unpack_target_from_uri(uri_to_check)
+ end
+
+ # This returns a hash that consists of settings
+ # populated from SSH configuration that are not already present
+ # in the configuration passed in.
+ # This is necessary because train will default these values
+ # itself - causing SSH config data to be ignored
+ def missing_opts_from_ssh_config(config, opts_in)
+ return {} unless config[:backend] == "ssh"
+
+ host_cfg = ssh_config_for_host(config[:host])
+ opts_out = {}
+ opts_in.each do |key, _value|
+ if SSH_CONFIG_OVERRIDE_KEYS.include?(key) && !config.key?(key)
+ opts_out[key] = host_cfg[key]
+ end
+ end
+ opts_out
+ end
+
+ # Having this as a method makes it easier to mock
+ # SSH Config for testing.
+ def ssh_config_for_host(host)
+ require "net/ssh" unless defined?(Net::SSH)
+ Net::SSH::Config.for(host)
+ end
+ end
+
+ class RemoteExecutionFailed < StandardError
+ attr_reader :exit_status, :command, :hostname, :stdout, :stderr
+
+ def initialize(hostname, command, result)
+ @hostname = hostname
+ @exit_status = result.exit_status
+ @stderr = result.stderr
+ @stdout = result.stdout
+ end
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_bulk_delete.rb b/knife/lib/chef/knife/client_bulk_delete.rb
new file mode 100644
index 0000000000..cc200a8bee
--- /dev/null
+++ b/knife/lib/chef/knife/client_bulk_delete.rb
@@ -0,0 +1,104 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class ClientBulkDelete < Knife
+
+ deps do
+ require "chef/api_client_v1" unless defined?(Chef::ApiClientV1)
+ end
+
+ option :delete_validators,
+ short: "-D",
+ long: "--delete-validators",
+ description: "Force deletion of clients if they're validators."
+
+ 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::ApiClientV1.list(true)
+
+ matcher = /#{name_args[0]}/
+ clients_to_delete = {}
+ validators_to_delete = {}
+ all_clients.each do |name, client|
+ next unless name&.match?(matcher)
+
+ if client.validator
+ validators_to_delete[client.name] = client
+ else
+ clients_to_delete[client.name] = client
+ end
+ end
+
+ if clients_to_delete.empty? && validators_to_delete.empty?
+ ui.info "No clients match the expression /#{name_args[0]}/"
+ exit 0
+ end
+
+ check_and_delete_validators(validators_to_delete)
+ check_and_delete_clients(clients_to_delete)
+ end
+
+ def check_and_delete_validators(validators)
+ unless validators.empty?
+ unless config[:delete_validators]
+ ui.msg("The following clients are validators and will not be deleted:")
+ print_clients(validators)
+ ui.msg("You must specify --delete-validators to delete the validator clients")
+ else
+ ui.msg("The following validators will be deleted:")
+ print_clients(validators)
+ if ui.confirm_without_exit("Are you sure you want to delete these validators")
+ destroy_clients(validators)
+ end
+ end
+ end
+ end
+
+ def check_and_delete_clients(clients)
+ unless clients.empty?
+ ui.msg("The following clients will be deleted:")
+ print_clients(clients)
+ ui.confirm("Are you sure you want to delete these clients")
+ destroy_clients(clients)
+ end
+ end
+
+ def destroy_clients(clients)
+ clients.sort.each do |name, client|
+ client.destroy
+ ui.msg("Deleted client #{name}")
+ end
+ end
+
+ def print_clients(clients)
+ ui.msg("")
+ ui.msg(ui.list(clients.keys.sort, :columns_down))
+ ui.msg("")
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_create.rb b/knife/lib/chef/knife/client_create.rb
new file mode 100644
index 0000000000..c79ff25d5e
--- /dev/null
+++ b/knife/lib/chef/knife/client_create.rb
@@ -0,0 +1,101 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ class Knife
+ class ClientCreate < Knife
+
+ deps do
+ require "chef/api_client_v1" unless defined?(Chef::ApiClientV1)
+ end
+
+ option :file,
+ short: "-f FILE",
+ long: "--file FILE",
+ description: "Write the private key to a file if the #{ChefUtils::Dist::Server::PRODUCT} generated one."
+
+ option :validator,
+ long: "--validator",
+ description: "Create the client as a validator.",
+ boolean: true
+
+ option :public_key,
+ short: "-p FILE",
+ long: "--public-key",
+ description: "Set the initial default key for the client from a file on disk (cannot pass with --prevent-keygen)."
+
+ option :prevent_keygen,
+ short: "-k",
+ long: "--prevent-keygen",
+ description: "Prevent #{ChefUtils::Dist::Server::PRODUCT} from generating a default key pair for you. Cannot be passed with --public-key.",
+ boolean: true
+
+ banner "knife client create CLIENTNAME (options)"
+
+ def client
+ @client_field ||= Chef::ApiClientV1.new
+ end
+
+ def create_client(client)
+ # should not be using save :( bad behavior
+ Chef::ApiClientV1.from_hash(client).save
+ end
+
+ def run
+ test_mandatory_field(@name_args[0], "client name")
+ client.name @name_args[0]
+
+ if config[:public_key] && config[:prevent_keygen]
+ show_usage
+ ui.fatal("You cannot pass --public-key and --prevent-keygen")
+ exit 1
+ end
+
+ if !config[:prevent_keygen] && !config[:public_key]
+ client.create_key(true)
+ end
+
+ if config[:validator]
+ client.validator(true)
+ end
+
+ if config[:public_key]
+ client.public_key File.read(File.expand_path(config[:public_key]))
+ end
+
+ output = edit_hash(client)
+ final_client = create_client(output)
+ ui.info("Created #{final_client}")
+
+ # output private_key if one
+ if final_client.private_key
+ if config[:file]
+ File.open(config[:file], "w") do |f|
+ f.print(final_client.private_key)
+ end
+ else
+ puts final_client.private_key
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_delete.rb b/knife/lib/chef/knife/client_delete.rb
new file mode 100644
index 0000000000..874f2ba642
--- /dev/null
+++ b/knife/lib/chef/knife/client_delete.rb
@@ -0,0 +1,62 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class ClientDelete < Knife
+
+ deps do
+ require "chef/api_client_v1" unless defined?(Chef::ApiClientV1)
+ end
+
+ option :delete_validators,
+ short: "-D",
+ long: "--delete-validators",
+ description: "Force deletion of client if it's a validator."
+
+ banner "knife client delete [CLIENT [CLIENT]] (options)"
+
+ def run
+ if @name_args.length == 0
+ show_usage
+ ui.fatal("You must specify at least one client name")
+ exit 1
+ end
+
+ @name_args.each do |client_name|
+ delete_client(client_name)
+ end
+ end
+
+ def delete_client(client_name)
+ delete_object(Chef::ApiClientV1, client_name, "client") do
+ object = Chef::ApiClientV1.load(client_name)
+ if object.validator
+ unless config[:delete_validators]
+ ui.fatal("You must specify --delete-validators to delete the validator client #{client_name}")
+ exit 2
+ end
+ end
+ object.destroy
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_edit.rb b/knife/lib/chef/knife/client_edit.rb
new file mode 100644
index 0000000000..4f58228901
--- /dev/null
+++ b/knife/lib/chef/knife/client_edit.rb
@@ -0,0 +1,52 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class ClientEdit < Knife
+
+ deps do
+ require "chef/api_client_v1" unless defined?(Chef::ApiClientV1)
+ 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
+
+ original_data = Chef::ApiClientV1.load(@client_name).to_h
+ edited_client = edit_hash(original_data)
+ if original_data != edited_client
+ client = Chef::ApiClientV1.from_hash(edited_client)
+ client.save
+ ui.msg("Saved #{client}.")
+ else
+ ui.msg("Client unchanged, not saving.")
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_key_create.rb b/knife/lib/chef/knife/client_key_create.rb
new file mode 100644
index 0000000000..192d724473
--- /dev/null
+++ b/knife/lib/chef/knife/client_key_create.rb
@@ -0,0 +1,73 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "key_create_base"
+
+class Chef
+ class Knife
+ # Implements knife user key create using Chef::Knife::KeyCreate
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the client that this key is for
+ class ClientKeyCreate < Knife
+ include Chef::Knife::KeyCreateBase
+
+ banner "knife client key create CLIENT (options)"
+
+ deps do
+ require_relative "key_create"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def actor_field_name
+ "client"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyCreate.new(@actor, actor_field_name, ui, config)
+ end
+
+ def actor_missing_error
+ "You must specify a client name"
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_key_delete.rb b/knife/lib/chef/knife/client_key_delete.rb
new file mode 100644
index 0000000000..2d486ffcbd
--- /dev/null
+++ b/knife/lib/chef/knife/client_key_delete.rb
@@ -0,0 +1,80 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ # Implements knife client key delete using Chef::Knife::KeyDelete
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the client that this key is for
+ class ClientKeyDelete < Knife
+ banner "knife client key delete CLIENT KEYNAME (options)"
+
+ deps do
+ require_relative "key_delete"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def actor_field_name
+ "client"
+ end
+
+ def actor_missing_error
+ "You must specify a client name"
+ end
+
+ def keyname_missing_error
+ "You must specify a key name"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyDelete.new(@name, @actor, actor_field_name, ui)
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ @name = params[1]
+ if @name.nil?
+ show_usage
+ ui.fatal(keyname_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_key_edit.rb b/knife/lib/chef/knife/client_key_edit.rb
new file mode 100644
index 0000000000..d178aafc17
--- /dev/null
+++ b/knife/lib/chef/knife/client_key_edit.rb
@@ -0,0 +1,83 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "key_edit_base"
+
+class Chef
+ class Knife
+ # Implements knife client key edit using Chef::Knife::KeyEdit
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the client that this key is for
+ class ClientKeyEdit < Knife
+ include Chef::Knife::KeyEditBase
+
+ banner "knife client key edit CLIENT KEYNAME (options)"
+
+ deps do
+ require_relative "key_edit"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def actor_field_name
+ "client"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyEdit.new(@name, @actor, actor_field_name, ui, config)
+ end
+
+ def actor_missing_error
+ "You must specify a client name"
+ end
+
+ def keyname_missing_error
+ "You must specify a key name"
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ @name = params[1]
+ if @name.nil?
+ show_usage
+ ui.fatal(keyname_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_key_list.rb b/knife/lib/chef/knife/client_key_list.rb
new file mode 100644
index 0000000000..afc04335d9
--- /dev/null
+++ b/knife/lib/chef/knife/client_key_list.rb
@@ -0,0 +1,73 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "key_list_base"
+
+class Chef
+ class Knife
+ # Implements knife user key list using Chef::Knife::KeyList
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the client that this key is for
+ class ClientKeyList < Knife
+ include Chef::Knife::KeyListBase
+
+ banner "knife client key list CLIENT (options)"
+
+ deps do
+ require_relative "key_list"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def list_method
+ :list_by_client
+ end
+
+ def actor_missing_error
+ "You must specify a client name"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyList.new(@actor, list_method, ui, config)
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_key_show.rb b/knife/lib/chef/knife/client_key_show.rb
new file mode 100644
index 0000000000..14e1f0ca7a
--- /dev/null
+++ b/knife/lib/chef/knife/client_key_show.rb
@@ -0,0 +1,80 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ # Implements knife client key show using Chef::Knife::KeyShow
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the client that this key is for
+ class ClientKeyShow < Knife
+ banner "knife client key show CLIENT KEYNAME (options)"
+
+ deps do
+ require_relative "key_show"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def load_method
+ :load_by_client
+ end
+
+ def actor_missing_error
+ "You must specify a client name"
+ end
+
+ def keyname_missing_error
+ "You must specify a key name"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyShow.new(@name, @actor, load_method, ui)
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ @name = params[1]
+ if @name.nil?
+ show_usage
+ ui.fatal(keyname_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_list.rb b/knife/lib/chef/knife/client_list.rb
new file mode 100644
index 0000000000..f4a4c7e9ad
--- /dev/null
+++ b/knife/lib/chef/knife/client_list.rb
@@ -0,0 +1,41 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class ClientList < Knife
+
+ deps do
+ require "chef/api_client_v1" unless defined?(Chef::ApiClientV1)
+ 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::ApiClientV1.list))
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_reregister.rb b/knife/lib/chef/knife/client_reregister.rb
new file mode 100644
index 0000000000..3408392d95
--- /dev/null
+++ b/knife/lib/chef/knife/client_reregister.rb
@@ -0,0 +1,58 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class ClientReregister < Knife
+
+ deps do
+ require "chef/api_client_v1" unless defined?(Chef::ApiClientV1)
+ 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::ApiClientV1.reregister(@client_name)
+ Chef::Log.trace("Updated client data: #{client.inspect}")
+ key = client.private_key
+ if config[:file]
+ File.open(config[:file], "w") do |f|
+ f.print(key)
+ end
+ else
+ ui.msg key
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/client_show.rb b/knife/lib/chef/knife/client_show.rb
new file mode 100644
index 0000000000..102ff2c4cc
--- /dev/null
+++ b/knife/lib/chef/knife/client_show.rb
@@ -0,0 +1,48 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class ClientShow < Knife
+
+ include Knife::Core::MultiAttributeReturnOption
+
+ deps do
+ require "chef/api_client_v1" unless defined?(Chef::ApiClientV1)
+ end
+
+ banner "knife client show 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::ApiClientV1.load(@client_name)
+ output(format_for_display(client))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/config_get.rb b/knife/lib/chef/knife/config_get.rb
new file mode 100644
index 0000000000..91e6b7affd
--- /dev/null
+++ b/knife/lib/chef/knife/config_get.rb
@@ -0,0 +1,39 @@
+#
+# Author:: Joshua Timberman <opensource@housepub.org>
+# Copyright:: Copyright (c) 2012, Joshua Timberman
+# Copyright:: Copyright (c) 2018, Noah Kantrowitz
+# 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_relative "../knife"
+require_relative "./config_show"
+
+class Chef
+ class Knife
+ class ConfigGet < ConfigShow
+
+ # Handle the subclassing (knife doesn't do this :()
+ dependency_loaders.concat(superclass.dependency_loaders)
+
+ banner "knife config get [OPTION...] (options)\nDisplays the value of Chef::Config[OPTION] (or all config values)"
+ category "deprecated"
+
+ def run
+ Chef::Log.warn("knife config get has been deprecated in favor of knife config show. This will be removed in the major release version!")
+ super
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/config_get_profile.rb b/knife/lib/chef/knife/config_get_profile.rb
new file mode 100644
index 0000000000..a355c531fe
--- /dev/null
+++ b/knife/lib/chef/knife/config_get_profile.rb
@@ -0,0 +1,37 @@
+#
+# Copyright:: Copyright (c) 2018, Noah Kantrowitz
+# 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_relative "../knife"
+require_relative "./config_use"
+
+class Chef
+ class Knife
+ class ConfigGetProfile < ConfigUse
+
+ # Handle the subclassing (knife doesn't do this :()
+ dependency_loaders.concat(superclass.dependency_loaders)
+
+ banner "knife config get-profile"
+ category "deprecated"
+
+ def run
+ Chef::Log.warn("knife config get-profiles has been deprecated in favor of knife config use. This will be removed in the major release version!")
+ super
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/config_list.rb b/knife/lib/chef/knife/config_list.rb
new file mode 100644
index 0000000000..be80ded3b2
--- /dev/null
+++ b/knife/lib/chef/knife/config_list.rb
@@ -0,0 +1,139 @@
+#
+# Copyright:: Copyright (c) 2018, Noah Kantrowitz
+# 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_relative "../knife"
+
+class Chef
+ class Knife
+ class ConfigList < Knife
+ banner "knife config list (options)"
+
+ TABLE_HEADER ||= [" Profile", "Client", "Key", "Server"].freeze
+
+ deps do
+ require "chef/workstation_config_loader" unless defined?(Chef::WorkstationConfigLoader)
+ require "tty-screen" unless defined?(TTY::Screen)
+ require "tty-table" unless defined?(TTY::Table)
+ end
+
+ option :ignore_knife_rb,
+ short: "-i",
+ long: "--ignore-knife-rb",
+ description: "Ignore the current config.rb/knife.rb configuration.",
+ default: false
+
+ def configure_chef
+ apply_computed_config
+ end
+
+ def run
+ credentials_data = self.class.config_loader.parse_credentials_file
+ if credentials_data.nil? || credentials_data.empty?
+ # Should this just show the ambient knife.rb config as "default" instead?
+ ui.fatal("No profiles found, #{self.class.config_loader.credentials_file_path} does not exist or is empty")
+ exit 1
+ end
+
+ current_profile = self.class.config_loader.credentials_profile(config[:profile])
+ profiles = credentials_data.keys.map do |profile|
+ if config[:ignore_knife_rb]
+ # Don't do any fancy loading nonsense, just the raw data.
+ profile_data = credentials_data[profile]
+ {
+ profile: profile,
+ active: profile == current_profile,
+ client_name: profile_data["client_name"] || profile_data["node_name"],
+ client_key: profile_data["client_key"],
+ server_url: profile_data["chef_server_url"],
+ }
+ else
+ # Fancy loading nonsense so we get what the actual config would be.
+ # Note that this modifies the global config, after this, all bets are
+ # off as to whats in the config.
+ Chef::Config.reset
+ wcl = Chef::WorkstationConfigLoader.new(nil, Chef::Log, profile: profile)
+ wcl.load
+ {
+ profile: profile,
+ active: profile == current_profile,
+ client_name: Chef::Config[:node_name],
+ client_key: Chef::Config[:client_key],
+ server_url: Chef::Config[:chef_server_url],
+ }
+ end
+ end
+
+ # Try to reset the config.
+ unless config[:ignore_knife_rb]
+ Chef::Config.reset
+ apply_computed_config
+ end
+
+ if ui.interchange?
+ # Machine-readable output.
+ ui.output(profiles)
+ else
+ # Table output.
+ ui.output(render_table(profiles))
+ end
+ end
+
+ private
+
+ def render_table(profiles, padding: 1)
+ rows = []
+ # Render the data to a 2D array that will be used for the table.
+ profiles.each do |profile|
+ # Replace the home dir in the client key path with ~.
+ profile[:client_key] = profile[:client_key].to_s.gsub(/^#{Regexp.escape(Dir.home)}/, "~") if profile[:client_key]
+ profile[:profile] = "#{profile[:active] ? "*" : " "}#{profile[:profile]}"
+ rows << profile.values_at(:profile, :client_name, :client_key, :server_url)
+ end
+
+ table = TTY::Table.new(header: TABLE_HEADER, rows: rows)
+
+ # Rotate the table to vertical if the screen width is less than table width.
+ if table.width > TTY::Screen.width
+ table.orientation = :vertical
+ table.rotate
+ # Add a new line after each profile record.
+ table.render do |renderer|
+ renderer.border do
+ separator ->(row) { (row + 1) % TABLE_HEADER.size == 0 }
+ end
+ # Remove the leading space added of the first column.
+ renderer.filter = Proc.new do |val, row_index, col_index|
+ if col_index == 1 || (row_index) % TABLE_HEADER.size == 0
+ val.strip
+ else
+ val
+ end
+ end
+ end
+ else
+ table.render do |renderer|
+ renderer.border do
+ mid "-"
+ end
+ renderer.padding = [0, padding, 0, 0] # pad right with 2 characters
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/config_list_profiles.rb b/knife/lib/chef/knife/config_list_profiles.rb
new file mode 100644
index 0000000000..c037b0de53
--- /dev/null
+++ b/knife/lib/chef/knife/config_list_profiles.rb
@@ -0,0 +1,37 @@
+#
+# Copyright:: Copyright (c) 2018, Noah Kantrowitz
+# 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_relative "../knife"
+require_relative "./config_list"
+
+class Chef
+ class Knife
+ class ConfigListProfiles < ConfigList
+
+ # Handle the subclassing (knife doesn't do this :()
+ dependency_loaders.concat(superclass.dependency_loaders)
+
+ banner "knife config list-profiles (options)"
+ category "deprecated"
+
+ def run
+ Chef::Log.warn("knife config list-profiles has been deprecated in favor of knife config list. This will be removed in the major release version!")
+ super
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/config_show.rb b/knife/lib/chef/knife/config_show.rb
new file mode 100644
index 0000000000..7f28891885
--- /dev/null
+++ b/knife/lib/chef/knife/config_show.rb
@@ -0,0 +1,127 @@
+#
+# Author:: Vivek Singh (<vsingh@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class ConfigShow < Knife
+ banner "knife config show [OPTION...] (options)\nDisplays the value of Chef::Config[OPTION] (or all config values)"
+
+ option :all,
+ short: "-a",
+ long: "--all",
+ description: "Include options that are not set in the configuration.",
+ default: false
+
+ option :raw,
+ short: "-r",
+ long: "--raw",
+ description: "Display a each value with no formatting.",
+ default: false
+
+ def run
+ if config[:format] == "summary" && !config[:raw]
+ # If using the default, human-readable output, also show which config files are being loaded.
+ # Some of this is a bit hacky since it duplicates
+ wcl = self.class.config_loader
+ if wcl.credentials_found
+ loading_from("credentials", ChefConfig::PathHelper.home(".chef", "credentials"))
+ end
+ if wcl.config_location
+ loading_from("configuration", wcl.config_location)
+ end
+
+ if Chef::Config[:config_d_dir]
+ wcl.find_dot_d(Chef::Config[:config_d_dir]).each do |path|
+ loading_from(".d/ configuration", path)
+ end
+ end
+ end
+
+ # Dump the whole config, including defaults is --all was given.
+ config_data = Chef::Config.save(config[:all])
+ # Two special cases, these are set during knife startup but we don't usually care about them.
+ unless config[:all]
+ config_data.delete(:color)
+ # Only keep these if true, false is much less important because it's the default.
+ config_data.delete(:local_mode) unless config_data[:local_mode]
+ config_data.delete(:enforce_default_paths) unless config_data[:enforce_default_paths]
+ config_data.delete(:enforce_path_sanity) unless config_data[:enforce_path_sanity]
+ end
+
+ # Extract the data to show.
+ output_data = {}
+ if @name_args.empty?
+ output_data = config_data
+ else
+ @name_args.each do |filter|
+ if filter =~ %r{^/(.*)/(i?)$}
+ # It's a regex.
+ filter_re = Regexp.new($1, $2 ? Regexp::IGNORECASE : 0)
+ config_data.each do |key, value|
+ output_data[key] = value if key.to_s&.match?(filter_re)
+ end
+ else
+ # It's a dotted path string.
+ filter_parts = filter.split(".")
+ extract = lambda do |memo, filter_part|
+ memo.is_a?(Hash) ? memo[filter_part.to_sym] : nil
+ end
+ # Check against both config_data and all of the data, so that even
+ # in non-all mode, if you ask for a key that isn't in the non-all
+ # data, it will check against the broader set.
+ output_data[filter] = filter_parts.inject(config_data, &extract) || filter_parts.inject(Chef::Config.save(true), &extract)
+ end
+ end
+ end
+
+ # Fix up some values.
+ output_data.each do |key, value|
+ if value == STDOUT
+ output_data[key] = "STDOUT"
+ elsif value == STDERR
+ output_data[key] = "STDERR"
+ end
+ end
+
+ # Show the data.
+ if config[:raw]
+ output_data.each_value do |value|
+ ui.msg(value)
+ end
+ else
+ ui.output(output_data)
+ end
+ end
+
+ private
+
+ # Display a banner about loading from a config file.
+ #
+ # @api private
+ # @param type_of_file [String] Description of the file for the banner.
+ # @param path [String] Path of the file.
+ # @return [nil]
+ def loading_from(type_of_file, path)
+ path = Pathname.new(path).realpath
+ ui.msg(ui.color("Loading from #{type_of_file} file #{path}", :yellow))
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/config_use.rb b/knife/lib/chef/knife/config_use.rb
new file mode 100644
index 0000000000..e944dc210b
--- /dev/null
+++ b/knife/lib/chef/knife/config_use.rb
@@ -0,0 +1,61 @@
+#
+# Author:: Vivek Singh (<vsingh@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class ConfigUse < Knife
+ banner "knife config use [PROFILE]"
+
+ deps do
+ require "fileutils" unless defined?(FileUtils)
+ end
+
+ # Disable normal config loading since this shouldn't fail if the profile
+ # doesn't exist of the config is otherwise corrupted.
+ def configure_chef
+ apply_computed_config
+ end
+
+ def run
+ profile = @name_args[0]&.strip
+ if profile.nil? || profile.empty?
+ ui.msg(self.class.config_loader.credentials_profile(config[:profile]))
+ else
+ credentials_data = self.class.config_loader.parse_credentials_file
+ context_file = ChefConfig::PathHelper.home(".chef", "context").freeze
+
+ if credentials_data.nil? || credentials_data.empty?
+ ui.fatal("No profiles found, #{self.class.config_loader.credentials_file_path} does not exist or is empty")
+ exit 1
+ end
+
+ if credentials_data[profile].nil?
+ raise ChefConfig::ConfigurationError, "Profile #{profile} doesn't exist. Please add it to #{self.class.config_loader.credentials_file_path} and if it is profile with DNS name check that you are not missing single quotes around it as per docs https://docs.chef.io/workstation/knife_setup/#knife-profiles."
+ else
+ # Ensure the .chef/ folder exists.
+ FileUtils.mkdir_p(File.dirname(context_file))
+ IO.write(context_file, "#{profile}\n")
+ ui.msg("Set default profile to #{profile}")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/config_use_profile.rb b/knife/lib/chef/knife/config_use_profile.rb
new file mode 100644
index 0000000000..169bdbef30
--- /dev/null
+++ b/knife/lib/chef/knife/config_use_profile.rb
@@ -0,0 +1,47 @@
+#
+# Copyright:: Copyright (c) 2018, Noah Kantrowitz
+# 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_relative "../knife"
+require_relative "./config_use"
+
+class Chef
+ class Knife
+ class ConfigUseProfile < ConfigUse
+
+ # Handle the subclassing (knife doesn't do this :()
+ dependency_loaders.concat(superclass.dependency_loaders)
+
+ banner "knife config use-profile PROFILE"
+ category "deprecated"
+
+ def run
+ Chef::Log.warn("knife config use-profile has been deprecated in favor of knife config use. This will be removed in the major release version!")
+
+ credentials_data = self.class.config_loader.parse_credentials_file
+ context_file = ChefConfig::PathHelper.home(".chef", "context").freeze
+ profile = @name_args[0]&.strip
+ if profile.nil? || profile.empty?
+ show_usage
+ ui.fatal("You must specify a profile")
+ exit 1
+ end
+
+ super
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/configure.rb b/knife/lib/chef/knife/configure.rb
new file mode 100644
index 0000000000..4a73b6875b
--- /dev/null
+++ b/knife/lib/chef/knife/configure.rb
@@ -0,0 +1,150 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+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 "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+ require_relative "client_create"
+ require_relative "user_create"
+ require "ohai" unless defined?(Ohai)
+ Chef::Knife::ClientCreate.load_deps
+ Chef::Knife::UserCreate.load_deps
+ end
+
+ banner "knife configure (options)"
+
+ option :repository,
+ short: "-r REPO",
+ long: "--repository REPO",
+ description: "The path to the chef-repo."
+
+ option :initial,
+ short: "-i",
+ long: "--initial",
+ boolean: true,
+ description: "Use to create a API client, typically an administrator client on a freshly-installed server."
+
+ option :admin_client_name,
+ long: "--admin-client-name NAME",
+ description: "The name of the client, typically the name of the admin client."
+
+ option :admin_client_key,
+ long: "--admin-client-key PATH",
+ description: "The path to the private key used by the client, typically a file named admin.pem."
+
+ option :validation_client_name,
+ long: "--validation-client-name NAME",
+ description: "The name of the validation client, typically a client named chef-validator."
+
+ option :validation_key,
+ long: "--validation-key PATH",
+ description: "The path to the validation key used by the client, typically 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
+ FileUtils.mkdir_p(chef_config_path)
+
+ ask_user_for_config
+
+ confirm("Overwrite #{config_file_path}") if ::File.exist?(config_file_path)
+
+ ::File.open(config_file_path, "w") do |f|
+ f.puts <<~EOH
+ [default]
+ client_name = '#{new_client_name}'
+ client_key = '#{new_client_key}'
+ chef_server_url = '#{chef_server}'
+ EOH
+ 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
+ user_create = Chef::Knife::UserCreate.new
+ user_create.name_args = [ new_client_name ]
+ user_create.config[:user_password] = config[:user_password] ||
+ ui.ask("Please enter a password for the new user: ", echo: false)
+ user_create.config[:admin] = true
+ user_create.config[:file] = new_client_key
+ user_create.config[:yes] = true
+ user_create.config[:disable_editing] = true
+ user_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("*****")
+ end
+
+ ui.msg("Knife configuration file written to #{config_file_path}")
+ 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: "https://#{server_name}/organizations/myorg")
+ if config[:initial]
+ @new_client_name = config[:node_name] || ask_question("Please enter a name for the new user: ", default: Etc.getlogin)
+ @admin_client_name = config[:admin_client_name] || ask_question("Please enter the existing admin name: ", default: "admin")
+ @admin_client_key = config[:admin_client_key] || ask_question("Please enter the location of the existing admin's private key: ", default: "#{ChefUtils::Dist::Server::CONF_DIR}/admin.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
+
+ @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
+
+ # @return [String] our best guess at what the servername should be using Ohai data and falling back to localhost
+ def guess_servername
+ o = Ohai::System.new
+ o.all_plugins(%w{ os hostname fqdn })
+ o[:fqdn] || o[:machinename] || o[:hostname] || "localhost"
+ end
+
+ # @return [String] the path to the user's .chef directory
+ def chef_config_path
+ @chef_config_path ||= ChefConfig::PathHelper.home(".chef")
+ end
+
+ # @return [String] the full path to the config file (credential file)
+ def config_file_path
+ @config_file_path ||= ::File.expand_path(::File.join(chef_config_path, "credentials"))
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/configure_client.rb b/knife/lib/chef/knife/configure_client.rb
new file mode 100644
index 0000000000..c6f159ec8f
--- /dev/null
+++ b/knife/lib/chef/knife/configure_client.rb
@@ -0,0 +1,48 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../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("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/knife/lib/chef/knife/cookbook_bulk_delete.rb b/knife/lib/chef/knife/cookbook_bulk_delete.rb
new file mode 100644
index 0000000000..d294db842c
--- /dev/null
+++ b/knife/lib/chef/knife/cookbook_bulk_delete.rb
@@ -0,0 +1,71 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class CookbookBulkDelete < Knife
+
+ deps do
+ require_relative "cookbook_delete"
+ require "chef/cookbook_version" unless defined?(Chef::CookbookVersion)
+ 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")
+
+ 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("cookbooks/#{cookbook_name}")[cookbook_name]["versions"].map { |v| v["version"] }.flatten
+ versions.each do |version|
+ rest.delete("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/knife/lib/chef/knife/cookbook_delete.rb b/knife/lib/chef/knife/cookbook_delete.rb
new file mode 100644
index 0000000000..fac23ae336
--- /dev/null
+++ b/knife/lib/chef/knife/cookbook_delete.rb
@@ -0,0 +1,151 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class CookbookDelete < Knife
+
+ attr_accessor :cookbook_name, :version
+
+ deps do
+ require "chef/cookbook_version" unless defined?(Chef::CookbookVersion)
+ end
+
+ option :all, short: "-a", long: "--all", boolean: true, description: "Delete all versions of the cookbook."
+
+ 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("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::HTTPClientException => e
+ if /^404/.match?(e.to_s)
+ 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(&: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(path)
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/cookbook_download.rb b/knife/lib/chef/knife/cookbook_download.rb
new file mode 100644
index 0000000000..dcf7299901
--- /dev/null
+++ b/knife/lib/chef/knife/cookbook_download.rb
@@ -0,0 +1,142 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Christopher Walters (<cw@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class CookbookDownload < Knife
+
+ attr_reader :version
+ attr_accessor :cookbook_name
+
+ deps do
+ require "chef/cookbook_version" unless defined?(Chef::CookbookVersion)
+ 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
+ if @version.nil?
+ ui.fatal("No such cookbook found")
+ exit 1
+ end
+ end
+
+ ui.info("Downloading #{@cookbook_name} cookbook version #{@version}")
+
+ cookbook = Chef::CookbookVersion.load(@cookbook_name, @version)
+ manifest = cookbook.cookbook_manifest
+
+ basedir = File.join(config[:download_directory], "#{@cookbook_name}-#{cookbook.version}")
+ if File.exist?(basedir)
+ if config[:force]
+ Chef::Log.trace("Deleting #{basedir}")
+ FileUtils.rm_rf(basedir)
+ else
+ ui.fatal("Directory #{basedir} exists, use --force to overwrite")
+ exit
+ end
+ end
+
+ manifest.by_parent_directory.each do |segment, files|
+ ui.info("Downloading #{segment}")
+ files.each do |segment_file|
+ dest = File.join(basedir, segment_file["path"].gsub("/", File::SEPARATOR))
+ Chef::Log.trace("Downloading #{segment_file["path"]} to #{dest}")
+ FileUtils.mkdir_p(File.dirname(dest))
+ tempfile = rest.streaming_request(segment_file["url"])
+ FileUtils.mv(tempfile.path, dest)
+ end
+ end
+ ui.info("Cookbook downloaded to #{basedir}")
+ end
+
+ def determine_version
+ if available_versions.nil?
+ nil
+ elsif available_versions.size == 1
+ @version = available_versions.first
+ elsif config[:latest]
+ @version = available_versions.last
+ else
+ ask_which_version
+ end
+ end
+
+ def available_versions
+ @available_versions ||= begin
+ versions = Chef::CookbookVersion.available_versions(@cookbook_name)
+ unless versions.nil?
+ versions.map! { |version| Chef::Version.new(version) }
+ versions.sort!
+ end
+ 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/knife/lib/chef/knife/cookbook_list.rb b/knife/lib/chef/knife/cookbook_list.rb
new file mode 100644
index 0000000000..719c10f893
--- /dev/null
+++ b/knife/lib/chef/knife/cookbook_list.rb
@@ -0,0 +1,47 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Nuo Yan (<nuo@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../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(api_endpoint)
+ ui.output(format_cookbook_list_for_display(cookbook_versions))
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/cookbook_metadata.rb b/knife/lib/chef/knife/cookbook_metadata.rb
new file mode 100644
index 0000000000..854e7a6609
--- /dev/null
+++ b/knife/lib/chef/knife/cookbook_metadata.rb
@@ -0,0 +1,106 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class CookbookMetadata < Knife
+
+ deps do
+ require "chef/cookbook_loader" unless defined?(Chef::CookbookLoader)
+ require "chef/cookbook/metadata" unless defined?(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_key do |cname|
+ 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.exist?(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
+ Chef::Log.trace("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/knife/lib/chef/knife/cookbook_metadata_from_file.rb b/knife/lib/chef/knife/cookbook_metadata_from_file.rb
new file mode 100644
index 0000000000..77a141d426
--- /dev/null
+++ b/knife/lib/chef/knife/cookbook_metadata_from_file.rb
@@ -0,0 +1,49 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Matthew Kent (<mkent@magoazul.com>)
+# Copyright:: Copyright (c) Chef Software Inc.
+# Copyright:: Copyright 2010-2016, 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_relative "../knife"
+
+class Chef
+ class Knife
+ class CookbookMetadataFromFile < Knife
+
+ deps do
+ require "chef/cookbook/metadata" unless defined?(Chef::Cookbook::Metadata)
+ end
+
+ banner "knife cookbook metadata from file FILE (options)"
+
+ def run
+ if @name_args.length < 1
+ show_usage
+ ui.fatal("You must specify the FILE.")
+ exit(1)
+ end
+
+ 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/knife/lib/chef/knife/cookbook_show.rb b/knife/lib/chef/knife/cookbook_show.rb
new file mode 100644
index 0000000000..aac26447b9
--- /dev/null
+++ b/knife/lib/chef/knife/cookbook_show.rb
@@ -0,0 +1,98 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class CookbookShow < Knife
+
+ deps do
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ require "uri" unless defined?(URI)
+ require "chef/cookbook_version" unless defined?(Chef::CookbookVersion)
+ 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
+ cookbook_name, cookbook_version, segment, filename = @name_args
+
+ cookbook = Chef::CookbookVersion.load(cookbook_name, cookbook_version) unless cookbook_version.nil?
+
+ case @name_args.length
+ when 4 # We are showing a specific file
+ node = {}
+ node[:fqdn] = config[:fqdn] if config.key?(:fqdn)
+ node[:platform] = config[:platform] if config.key?(:platform)
+ node[:platform_version] = config[:platform_version] if config.key?(:platform_version)
+
+ class << node
+ def attribute?(name) # rubocop:disable Lint/NestedMethodDefinition
+ key?(name)
+ end
+ end
+
+ manifest_entry = cookbook.preferred_manifest_record(node, segment, filename)
+ temp_file = rest.streaming_request(manifest_entry[:url])
+
+ # 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
+ if segment == "metadata"
+ output(cookbook.metadata)
+ else
+ output(cookbook.files_for(segment))
+ end
+ when 2 # We are showing the whole cookbook
+ output(cookbook.display)
+ when 1 # We are showing the cookbook versions (all of them)
+ env = config[:environment]
+ api_endpoint = env ? "environments/#{env}/cookbooks/#{cookbook_name}" : "cookbooks/#{cookbook_name}"
+ output(format_cookbook_list_for_display(rest.get(api_endpoint)))
+ when 0
+ show_usage
+ ui.fatal("You must specify a cookbook name")
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/cookbook_upload.rb b/knife/lib/chef/knife/cookbook_upload.rb
new file mode 100644
index 0000000000..d9582a3ccc
--- /dev/null
+++ b/knife/lib/chef/knife/cookbook_upload.rb
@@ -0,0 +1,292 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Christopher Walters (<cw@chef.io>)
+# Author:: Nuo Yan (<yan.nuo@gmail.com>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class CookbookUpload < Knife
+ deps do
+ require "chef/mixin/file_class" unless defined?(Chef::Mixin::FileClass)
+ include Chef::Mixin::FileClass
+ require "chef/exceptions" unless defined?(Chef::Exceptions)
+ require "chef/cookbook_loader" unless defined?(Chef::CookbookLoader)
+ require "chef/cookbook_uploader" unless defined?(Chef::CookbookUploader)
+ end
+
+ banner "knife cookbook upload [COOKBOOKS...] (options)"
+
+ option :cookbook_path,
+ short: "-o 'PATH:PATH'",
+ long: "--cookbook-path 'PATH:PATH'",
+ description: "A delimited path to search for cookbooks. On Unix the delimiter is ':', on Windows it is ';'.",
+ proc: lambda { |o| o.split(File::PATH_SEPARATOR) }
+
+ 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 :concurrency,
+ long: "--concurrency NUMBER_OF_THREADS",
+ description: "How many concurrent threads will be used.",
+ default: 10,
+ proc: lambda { |o| o.to_i }
+
+ 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
+ if ! config[:all] && @name_args.empty?
+ show_usage
+ ui.fatal("You must specify the --all flag or at least one cookbook name")
+ exit 1
+ end
+
+ config[:cookbook_path] ||= Chef::Config[:cookbook_path]
+
+ 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(&:size).max.to_i + 2
+
+ cookbooks = []
+ cookbooks_to_upload.each do |cookbook_name, cookbook|
+ raise Chef::Exceptions::MetadataNotFound.new(cookbook.root_paths[0], cookbook_name) unless cookbook.has_metadata_file?
+
+ if cookbook.metadata.name.nil?
+ message = "Cookbook loaded at path [#{cookbook.root_paths[0]}] has invalid metadata: #{cookbook.metadata.errors.join("; ")}"
+ raise Chef::Exceptions::MetadataNotValid, message
+ end
+
+ cookbooks << cookbook
+ end
+
+ if cookbooks.empty?
+ cookbook_path = config[:cookbook_path].respond_to?(:join) ? config[:cookbook_path].join(", ") : config[:cookbook_path]
+ ui.warn("Could not find any cookbooks in your cookbook path: '#{File.expand_path(cookbook_path)}'. Use --cookbook-path to specify the desired path.")
+ else
+ Chef::CookbookLoader.copy_to_tmp_dir_from_array(cookbooks) do |tmp_cl|
+ tmp_cl.load_cookbooks
+ tmp_cl.compile_metadata
+ tmp_cl.freeze_versions if config[:freeze]
+
+ cookbooks_for_upload = []
+ tmp_cl.each do |cookbook_name, cookbook|
+ cookbooks_for_upload << cookbook
+ version_constraints_to_update[cookbook_name] = cookbook.version
+ end
+ if config[:all]
+ if cookbooks_for_upload.any?
+ begin
+ upload(cookbooks_for_upload, justify_width)
+ rescue Chef::Exceptions::CookbookFrozen
+ ui.warn("Not updating version constraints for some cookbooks in the environment as the cookbook is frozen.")
+ ui.error("Uploading of some of the cookbooks must be failed. Remove cookbook whose version is frozen from your cookbooks repo OR use --force option.")
+ upload_failures += 1
+ rescue SystemExit => e
+ raise exit e.status
+ end
+ ui.info("Uploaded all cookbooks.") if upload_failures == 0
+ end
+ else
+ tmp_cl.each do |cookbook_name, cookbook|
+
+ upload([cookbook], justify_width)
+ upload_ok += 1
+ 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
+ rescue SystemExit => e
+ raise exit e.status
+
+ end
+
+ 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
+ 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|
+
+ unless upload_set.key?(cookbook_name)
+ upload_set[cookbook_name] = cookbook_repo[cookbook_name]
+ if config[:depends]
+ upload_set[cookbook_name].metadata.dependencies.each_key { |dep| @name_args << dep }
+ end
+ end
+ rescue Exceptions::CookbookNotFoundInRepo => e
+ ui.error(e.message)
+ Log.debug(e)
+
+ end
+ upload_set
+ end
+ end
+
+ def cookbook_repo
+ @cookbook_loader ||= begin
+ Chef::Cookbook::FileVendor.fetch_from_disk(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
+
+ private
+
+ def assert_environment_valid!
+ environment
+ rescue Net::HTTPClientException => 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, force: config[:force], concurrency: config[:concurrency]).upload_cookbooks
+ rescue Chef::Exceptions::CookbookFrozen => e
+ ui.error e
+ raise
+ 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|
+ !/[0-9a-f]{32,}/.match?(info["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 all dependencies, check if the version is on the server, or
+ # the version is in the cookbooks being uploaded. If not, exit and warn the user.
+ missing_dependencies = cookbook.metadata.dependencies.reject do |cookbook_name, version|
+ check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version)
+ end
+
+ unless missing_dependencies.empty?
+ missing_cookbook_names = missing_dependencies.map { |cookbook_name, version| "'#{cookbook_name}' version '#{version}'" }
+ ui.error "Cookbook #{cookbook.name} depends on cookbooks which are not currently"
+ ui.error "being uploaded and cannot be found on the server."
+ ui.error "The missing cookbook(s) are: #{missing_cookbook_names.join(", ")}"
+ exit 1
+ 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/knife/lib/chef/knife/core/bootstrap_context.rb b/knife/lib/chef/knife/core/bootstrap_context.rb
new file mode 100644
index 0000000000..0d71aa8dc3
--- /dev/null
+++ b/knife/lib/chef/knife/core/bootstrap_context.rb
@@ -0,0 +1,264 @@
+#
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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 "run_list" unless defined?(Chef::RunList)
+require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+require "pathname" unless defined?(Pathname)
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ class Knife
+ module Core
+ # Instances of BootstrapContext are the context objects (i.e., +self+) for
+ # bootstrap templates. For backwards compatibility, they +must+ set the
+ # following instance variables:
+ # * @config - a hash of knife's config values
+ # * @run_list - the run list for the node to bootstrap
+ #
+ class BootstrapContext
+
+ attr_accessor :client_pem
+ attr_accessor :config
+ attr_accessor :chef_config
+
+ def initialize(config, run_list, chef_config, secret = nil)
+ @config = config
+ @run_list = run_list
+ @chef_config = chef_config
+ @secret = secret
+ end
+
+ def bootstrap_environment
+ config[:environment]
+ end
+
+ def validation_key
+ if chef_config[:validation_key] &&
+ File.exist?(File.expand_path(chef_config[:validation_key]))
+ IO.read(File.expand_path(chef_config[:validation_key]))
+ else
+ false
+ end
+ end
+
+ def client_d
+ @client_d ||= client_d_content
+ end
+
+ def encrypted_data_bag_secret
+ @secret
+ end
+
+ # Contains commands and content, see trusted_certs_content
+ # @todo Rename to trusted_certs_script
+ def trusted_certs
+ @trusted_certs ||= trusted_certs_content
+ end
+
+ def get_log_location
+ if !(chef_config[:config_log_location].class == IO ) && (chef_config[:config_log_location].nil? || chef_config[:config_log_location].to_s.empty?)
+ "STDOUT"
+ elsif chef_config[:config_log_location].equal?(:win_evt)
+ raise "The value :win_evt is not supported for config_log_location on Linux Platforms \n"
+ elsif chef_config[:config_log_location].equal?(:syslog)
+ ":syslog"
+ elsif chef_config[:config_log_location].equal?(STDOUT)
+ "STDOUT"
+ elsif chef_config[:config_log_location].equal?(STDERR)
+ "STDERR"
+ elsif chef_config[:config_log_location]
+ %Q{"#{chef_config[:config_log_location]}"}
+ else
+ "STDOUT"
+ end
+ end
+
+ def config_content
+ client_rb = <<~CONFIG
+ chef_server_url "#{chef_config[:chef_server_url]}"
+ validation_client_name "#{chef_config[:validation_client_name]}"
+ CONFIG
+
+ unless chef_config[:chef_license].nil?
+ client_rb << "chef_license \"#{chef_config[:chef_license]}\"\n"
+ end
+
+ unless chef_config[:config_log_level].nil? || chef_config[:config_log_level].empty?
+ client_rb << %Q{log_level :#{chef_config[:config_log_level]}\n}
+ end
+
+ client_rb << "log_location #{get_log_location}\n"
+
+ 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
+
+ # We configure :verify_api_cert only when it's overridden on the CLI
+ # or when specified in the knife config.
+ if !config[:node_verify_api_cert].nil? || config.key?(:verify_api_cert)
+ value = config[:node_verify_api_cert].nil? ? config[:verify_api_cert] : config[:node_verify_api_cert]
+ client_rb << %Q{verify_api_cert #{value}\n}
+ end
+
+ # We configure :ssl_verify_mode only when it's overridden on the CLI
+ # or when specified in the knife config.
+ if config[:node_ssl_verify_mode] || config.key?(:ssl_verify_mode)
+ value = case config[:node_ssl_verify_mode]
+ when "peer"
+ :verify_peer
+ when "none"
+ :verify_none
+ when nil
+ config[:ssl_verify_mode]
+ else
+ nil
+ end
+
+ if value
+ client_rb << %Q{ssl_verify_mode :#{value}\n}
+ end
+ end
+
+ if config[:ssl_verify_mode]
+ client_rb << %Q{ssl_verify_mode :#{config[:ssl_verify_mode]}\n}
+ end
+
+ if config[:bootstrap_proxy]
+ client_rb << %Q{http_proxy "#{config[:bootstrap_proxy]}"\n}
+ client_rb << %Q{https_proxy "#{config[:bootstrap_proxy]}"\n}
+ end
+
+ if config[:bootstrap_proxy_user]
+ client_rb << %Q{http_proxy_user "#{config[:bootstrap_proxy_user]}"\n}
+ client_rb << %Q{https_proxy_user "#{config[:bootstrap_proxy_user]}"\n}
+ end
+
+ if config[:bootstrap_proxy_pass]
+ client_rb << %Q{http_proxy_pass "#{config[:bootstrap_proxy_pass]}"\n}
+ client_rb << %Q{https_proxy_pass "#{config[:bootstrap_proxy_pass]}"\n}
+ end
+
+ if config[:bootstrap_no_proxy]
+ client_rb << %Q{no_proxy "#{config[:bootstrap_no_proxy]}"\n}
+ end
+
+ if encrypted_data_bag_secret
+ client_rb << %Q{encrypted_data_bag_secret "/etc/chef/encrypted_data_bag_secret"\n}
+ end
+
+ unless trusted_certs.empty?
+ client_rb << %Q{trusted_certs_dir "/etc/chef/trusted_certs"\n}
+ end
+
+ if chef_config[:fips]
+ client_rb << "fips true\n"
+ end
+
+ unless chef_config[:file_cache_path].nil?
+ client_rb << "file_cache_path \"#{chef_config[:file_cache_path]}\"\n"
+ end
+
+ unless chef_config[:file_backup_path].nil?
+ client_rb << "file_backup_path \"#{chef_config[:file_backup_path]}\"\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] || ChefUtils::Dist::Infra::CLIENT
+ s = "#{client_path} -j /etc/chef/first-boot.json"
+ if config[:verbosity] && config[:verbosity] >= 3
+ s << " -l trace"
+ elsif config[:verbosity] && config[:verbosity] >= 2
+ s << " -l debug"
+ end
+ s << " -E #{bootstrap_environment}" unless bootstrap_environment.nil?
+ s << " --no-color" unless config[:color]
+ s
+ end
+
+ #
+ # Returns the version of Chef to install (as recognized by the Omnitruck API)
+ #
+ # @return [String] download version string
+ def version_to_install
+ return config[:bootstrap_version] if config[:bootstrap_version]
+
+ if config[:channel] == "stable"
+ Chef::VERSION.split(".").first
+ else
+ "latest"
+ end
+ end
+
+ def first_boot
+ (config[:first_boot_attributes] = Mash.new(config[:first_boot_attributes]) || Mash.new).tap do |attributes|
+ if config[:policy_name] && config[:policy_group]
+ attributes[:policy_name] = config[:policy_name]
+ attributes[:policy_group] = config[:policy_group]
+ else
+ attributes[:run_list] = @run_list
+ end
+ attributes.delete(:run_list) if attributes[:policy_name] && !attributes[:policy_name].empty?
+ attributes.merge!(tags: config[:tags]) if config[:tags] && !config[:tags].empty?
+ end
+ end
+
+ private
+
+ # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped
+ # This string should contain both the commands necessary to both create the files, as well as their content
+ def trusted_certs_content
+ content = ""
+ if chef_config[:trusted_certs_dir]
+ Dir.glob(File.join(ChefConfig::PathHelper.escape_glob_dir(chef_config[:trusted_certs_dir]), "*.{crt,pem}")).each do |cert|
+ content << "cat > /etc/chef/trusted_certs/#{File.basename(cert)} <<'EOP'\n" +
+ IO.read(File.expand_path(cert)) + "\nEOP\n"
+ end
+ end
+ content
+ end
+
+ def client_d_content
+ content = ""
+ if chef_config[:client_d_dir] && File.exist?(chef_config[:client_d_dir])
+ root = Pathname(chef_config[:client_d_dir])
+ root.find do |f|
+ relative = f.relative_path_from(root)
+ if f != root
+ file_on_node = "/etc/chef/client.d/#{relative}"
+ if f.directory?
+ content << "mkdir #{file_on_node}\n"
+ else
+ content << "cat > #{file_on_node} <<'EOP'\n" +
+ f.read.gsub("'", "'\\\\''") + "\nEOP\n"
+ end
+ end
+ end
+ end
+ content
+ end
+
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/cookbook_scm_repo.rb b/knife/lib/chef/knife/core/cookbook_scm_repo.rb
new file mode 100644
index 0000000000..921dadad8b
--- /dev/null
+++ b/knife/lib/chef/knife/core/cookbook_scm_repo.rb
@@ -0,0 +1,159 @@
+#
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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" unless defined?(Chef::Mixin::ShellOut)
+
+class Chef
+ class Knife
+ class CookbookSCMRepo
+
+ DIRTY_REPO = /^\s+M/.freeze
+
+ 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 DIRTY_REPO.match?(cmd.stdout)
+ 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"))
+ true
+ elsif File.dirname(directory) == directory
+ 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/knife/lib/chef/knife/core/cookbook_site_streaming_uploader.rb b/knife/lib/chef/knife/core/cookbook_site_streaming_uploader.rb
new file mode 100644
index 0000000000..85e83af5da
--- /dev/null
+++ b/knife/lib/chef/knife/core/cookbook_site_streaming_uploader.rb
@@ -0,0 +1,249 @@
+#
+# Author:: Stanislav Vitvitskiy
+# Author:: Nuo Yan (nuo@chef.io)
+# Author:: Christopher Walters (<cw@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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.
+#
+
+autoload :URI, "uri"
+module Net
+ autoload :HTTP, "net/http"
+end
+autoload :OpenSSL, "openssl"
+module Mixlib
+ module Authentication
+ autoload :SignedHeaderAuth, "mixlib/authentication/signedheaderauth"
+ end
+end
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+require_relative "../cookbook_metadata"
+class Chef
+ class Knife
+ module Core
+ # == Chef::Knife::Core::CookbookSiteStreamingUploader
+ # A streaming multipart HTTP upload implementation. Used to upload cookbooks
+ # (in tarball form) to https://supermarket.chef.io
+ #
+ # inspired by http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html
+ class CookbookSiteStreamingUploader
+
+ DefaultHeaders = { "accept" => "application/json", "x-chef-version" => ::Chef::VERSION }.freeze # rubocop:disable Naming/ConstantName
+
+ class << self
+
+ def create_build_dir(cookbook)
+ tmp_cookbook_path = Tempfile.new("#{ChefUtils::Dist::Infra::SHORT}-#{cookbook.name}-build")
+ tmp_cookbook_path.close
+ tmp_cookbook_dir = tmp_cookbook_path.path
+ File.unlink(tmp_cookbook_dir)
+ FileUtils.mkdir_p(tmp_cookbook_dir)
+ Chef::Log.trace("Staging at #{tmp_cookbook_dir}")
+ checksums_to_on_disk_paths = cookbook.checksums
+ cookbook.each_file do |manifest_record|
+ path_in_cookbook = manifest_record[:path]
+ on_disk_path = checksums_to_on_disk_paths[manifest_record[:checksum]]
+ dest = File.join(tmp_cookbook_dir, cookbook.name.to_s, path_in_cookbook)
+ FileUtils.mkdir_p(File.dirname(dest))
+ Chef::Log.trace("Staging #{on_disk_path} to #{dest}")
+ FileUtils.cp(on_disk_path, dest)
+ end
+
+ # First, generate metadata
+ Chef::Log.trace("Generating metadata")
+ kcm = Chef::Knife::CookbookMetadata.new
+ kcm.config[:cookbook_path] = [ tmp_cookbook_dir ]
+ kcm.name_args = [ cookbook.name.to_s ]
+ kcm.run
+
+ tmp_cookbook_dir
+ end
+
+ def post(to_url, user_id, secret_key_filename, params = {}, headers = {})
+ make_request(:post, to_url, user_id, secret_key_filename, params, headers)
+ end
+
+ def put(to_url, user_id, secret_key_filename, params = {}, headers = {})
+ make_request(:put, to_url, user_id, secret_key_filename, params, headers)
+ end
+
+ def make_request(http_verb, to_url, user_id, secret_key_filename, params = {}, headers = {})
+ boundary = "----RubyMultipartClient" + rand(1000000).to_s + "ZZZZZ"
+ parts = []
+ content_file = nil
+
+ secret_key = OpenSSL::PKey::RSA.new(File.read(secret_key_filename))
+
+ unless params.nil? || params.empty?
+ params.each do |key, value|
+ if value.is_a?(File)
+ content_file = value
+ filepath = value.path
+ filename = File.basename(filepath)
+ parts << StringPart.new( "--" + boundary + "\r\n" +
+ "Content-Disposition: form-data; name=\"" + key.to_s + "\"; filename=\"" + filename + "\"\r\n" +
+ "Content-Type: application/octet-stream\r\n\r\n")
+ parts << StreamPart.new(value, File.size(filepath))
+ parts << StringPart.new("\r\n")
+ else
+ parts << StringPart.new( "--" + boundary + "\r\n" +
+ "Content-Disposition: form-data; name=\"" + key.to_s + "\"\r\n\r\n")
+ parts << StringPart.new(value.to_s + "\r\n")
+ end
+ end
+ parts << StringPart.new("--" + boundary + "--\r\n")
+ end
+
+ body_stream = MultipartStream.new(parts)
+
+ timestamp = Time.now.utc.iso8601
+
+ url = URI.parse(to_url)
+
+ Chef::Log.logger.debug("Signing: method: #{http_verb}, url: #{url}, file: #{content_file}, User-id: #{user_id}, Timestamp: #{timestamp}")
+
+ # We use the body for signing the request if the file parameter
+ # wasn't a valid file or wasn't included. Extract the body (with
+ # multi-part delimiters intact) to sign the request.
+ # TODO: tim: 2009-12-28: It'd be nice to remove this special case, and
+ # always hash the entire request body. In the file case it would just be
+ # expanded multipart text - the entire body of the POST.
+ content_body = parts.inject("") { |result, part| result + part.read(0, part.size) }
+ content_file.rewind if content_file # we consumed the file for the above operation, so rewind it.
+
+ signing_options = {
+ http_method: http_verb,
+ path: url.path,
+ user_id: user_id,
+ timestamp: timestamp }
+ (content_file && signing_options[:file] = content_file) || (signing_options[:body] = (content_body || ""))
+
+ headers.merge!(Mixlib::Authentication::SignedHeaderAuth.signing_object(signing_options).sign(secret_key))
+
+ content_file.rewind if content_file
+
+ # net/http doesn't like symbols for header keys, so we'll to_s each one just in case
+ headers = DefaultHeaders.merge(Hash[*headers.map { |k, v| [k.to_s, v] }.flatten])
+
+ req = case http_verb
+ when :put
+ Net::HTTP::Put.new(url.path, headers)
+ when :post
+ Net::HTTP::Post.new(url.path, headers)
+ end
+ req.content_length = body_stream.size
+ req.content_type = "multipart/form-data; boundary=" + boundary unless parts.empty?
+ req.body_stream = body_stream
+
+ http = Chef::HTTP::BasicClient.new(url).http_client
+ res = http.request(req)
+
+ # alias status to code and to_s to body for test purposes
+ # TODO: stop the following madness!
+ class << res
+ alias :to_s :body
+
+ # BUG this makes the response compatible with what response_steps expects to test headers (response.headers[] -> response[])
+ def headers # rubocop:disable Lint/NestedMethodDefinition
+ self
+ end
+
+ def status # rubocop:disable Lint/NestedMethodDefinition
+ code.to_i
+ end
+ end
+ res
+ end
+
+ end
+
+ class StreamPart
+ def initialize(stream, size)
+ @stream, @size = stream, size
+ end
+
+ def size
+ @size
+ end
+
+ # read the specified amount from the stream
+ def read(offset, how_much)
+ @stream.read(how_much)
+ end
+ end
+
+ class StringPart
+ def initialize(str)
+ @str = str
+ end
+
+ def size
+ @str.length
+ end
+
+ # read the specified amount from the string starting at the offset
+ def read(offset, how_much)
+ @str[offset, how_much]
+ end
+ end
+
+ class MultipartStream
+ def initialize(parts)
+ @parts = parts
+ @part_no = 0
+ @part_offset = 0
+ end
+
+ def size
+ @parts.inject(0) { |size, part| size + part.size }
+ end
+
+ def read(how_much, dst_buf = nil)
+ if @part_no >= @parts.size
+ dst_buf.replace("") if dst_buf
+ return dst_buf
+ end
+
+ how_much_current_part = @parts[@part_no].size - @part_offset
+
+ how_much_current_part = if how_much_current_part > how_much
+ how_much
+ else
+ how_much_current_part
+ end
+
+ how_much_next_part = how_much - how_much_current_part
+
+ current_part = @parts[@part_no].read(@part_offset, how_much_current_part)
+
+ # recurse into the next part if the current one was not large enough
+ if how_much_next_part > 0
+ @part_no += 1
+ @part_offset = 0
+ next_part = read(how_much_next_part)
+ result = current_part + (next_part || "")
+ else
+ @part_offset += how_much_current_part
+ result = current_part
+ end
+ dst_buf ? dst_buf.replace(result || "") : result
+ end
+ end
+
+ end
+ end
+ end
+end
+
diff --git a/knife/lib/chef/knife/core/formatting_options.rb b/knife/lib/chef/knife/core/formatting_options.rb
new file mode 100644
index 0000000000..cdee2c5989
--- /dev/null
+++ b/knife/lib/chef/knife/core/formatting_options.rb
@@ -0,0 +1,49 @@
+#
+# Author:: Nicolas DUPEUX (<nicolas.dupeux@arkea.com>)
+# Copyright:: Copyright (c) Chef Software 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
+
+ # This module may be included into a knife subcommand class to automatically
+ # add configuration options used by the StatusPresenter and NodePresenter.
+ module FormattingOptions
+ # @private
+ # 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
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/gem_glob_loader.rb b/knife/lib/chef/knife/core/gem_glob_loader.rb
new file mode 100644
index 0000000000..d365602cb4
--- /dev/null
+++ b/knife/lib/chef/knife/core/gem_glob_loader.rb
@@ -0,0 +1,134 @@
+# Author:: Christopher Brown (<cb@chef.io>)
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../version"
+require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+class Chef
+ class Knife
+ class SubcommandLoader
+ class GemGlobLoader < Chef::Knife::SubcommandLoader
+ MATCHES_CHEF_GEM ||= %r{/chef-\d+\.\d+\.\d+}.freeze
+ MATCHES_THIS_CHEF_GEM ||= %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze
+
+ def subcommand_files
+ @subcommand_files ||= (gem_and_builtin_subcommands.values + site_subcommands).flatten.uniq
+ 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
+ require "rubygems" unless defined?(Gem)
+ find_subcommands_via_rubygems
+ rescue LoadError
+ find_subcommands_via_dirglob
+ end
+
+ def find_subcommands_via_rubygems
+ files = find_files_latest_gems "chef/knife/*.rb"
+ version_file_match = /#{Regexp.escape(File.join('chef', 'knife', 'version'))}$/
+ subcommand_files = {}
+ files.each do |file|
+
+ rel_path = file[/(.*)(#{Regexp.escape File.join('chef', 'knife', '')}.*)\.rb/, 2]
+
+ # When not installed as a gem (ChefDK/appbundler in particular), AND
+ # a different version of Chef is installed via gems, `files` will
+ # include some files from the 'other' Chef install. If this contains
+ # a knife command that doesn't exist in this version of Chef, we will
+ # get a LoadError later when we try to require it.
+ next if from_different_chef_version?(file)
+
+ # Exclude knife/chef/version. It's not a knife command, and force-loading
+ # when we load all of these files will emit constant-already-defined warnings
+ next if rel_path =~ version_file_match
+
+ subcommand_files[rel_path] = file
+ end
+
+ subcommand_files.merge(find_subcommands_via_dirglob)
+ end
+
+ private
+
+ def find_files_latest_gems(glob, check_load_path = true)
+ files = []
+
+ if check_load_path
+ files = $LOAD_PATH.map do |load_path|
+ Dir["#{File.expand_path glob, ChefConfig::PathHelper.escape_glob_dir(load_path)}#{Gem.suffix_pattern}"]
+ end.flatten.select { |file| File.file? file.untaint }
+
+ end
+
+ gem_files = latest_gem_specs.map do |spec|
+ # Gem::Specification#matches_for_glob wasn't added until RubyGems 1.8
+ if spec.respond_to? :matches_for_glob
+ spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}")
+ else
+ check_spec_for_glob(spec, glob)
+ end
+ end.flatten
+
+ files.concat gem_files
+ files.uniq! if check_load_path
+
+ files
+ end
+
+ def latest_gem_specs
+ @latest_gem_specs ||= if Gem::Specification.respond_to? :latest_specs
+ Gem::Specification.latest_specs(true) # find prerelease gems
+ else
+ Gem.source_index.latest_specs(true)
+ end
+ end
+
+ def check_spec_for_glob(spec, glob)
+ dirs = if spec.require_paths.size > 1
+ "{#{spec.require_paths.join(",")}}"
+ else
+ spec.require_paths.first
+ end
+
+ glob = File.join(ChefConfig::PathHelper.escape_glob_dir(spec.full_gem_path, dirs), glob)
+
+ Dir[glob].map(&:untaint)
+ end
+
+ def from_different_chef_version?(path)
+ matches_any_chef_gem?(path) && !matches_this_chef_gem?(path)
+ end
+
+ def matches_any_chef_gem?(path)
+ path =~ MATCHES_CHEF_GEM
+ end
+
+ def matches_this_chef_gem?(path)
+ path =~ MATCHES_THIS_CHEF_GEM
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/generic_presenter.rb b/knife/lib/chef/knife/core/generic_presenter.rb
new file mode 100644
index 0000000000..850bfa8b3d
--- /dev/null
+++ b/knife/lib/chef/knife/core/generic_presenter.rb
@@ -0,0 +1,232 @@
+#--
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "text_formatter"
+
+class Chef
+ class Knife
+ module Core
+
+ # Allows includer knife commands to return multiple attributes
+ # @brief knife node show NAME -a ATTR1 -a ATTR2
+ module MultiAttributeReturnOption
+ # @private
+ def self.included(includer)
+ includer.class_eval do
+ option :field_separator,
+ short: "-S SEPARATOR",
+ long: "--field-separator SEPARATOR",
+ description: "Character separator used to delineate nesting in --attribute filters (default \".\")"
+
+ option :attribute,
+ short: "-a ATTR1 [-a ATTR2]",
+ long: "--attribute ATTR1 [--attribute ATTR2] ",
+ description: "Show one or more attributes",
+ proc: Proc.new { |arg, accumulator|
+ accumulator ||= []
+ accumulator << arg
+ accumulator
+ }
+ end
+ end
+ end
+
+ # 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
+
+ # Instantiates 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" unless defined?(YAML)
+ YAML.dump(data)
+ when :pp
+ require "stringio" unless defined?(StringIO)
+ # If you were looking for some attribute and there is only one match
+ # just dump the attribute value
+ if config[:attribute] && 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
+
+ # GenericPresenter is used in contexts where MultiAttributeReturnOption
+ # is not, so we need to set the default value here rather than as part
+ # of the CLI option.
+ def attribute_field_separator
+ config[:field_separator] || "."
+ end
+
+ def extract_nested_value(data, nested_value_spec)
+ nested_value_spec.split(attribute_field_separator).each do |attr|
+ data =
+ if data.is_a?(Array)
+ data[attr.to_i]
+ elsif data.respond_to?(:[], false) && data.respond_to?(:key?) && data.key?(attr)
+ data[attr]
+ elsif data.respond_to?(attr.to_sym, false)
+ # handles -a chef_environment and other things that hang of the node and aren't really attributes
+ data.public_send(attr.to_sym)
+ else
+ nil
+ end
+ end
+ # necessary (?) for coercing objects (the run_list object?) to hashes
+ ( !data.is_a?(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] = {}
+ 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(&: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/knife/lib/chef/knife/core/hashed_command_loader.rb b/knife/lib/chef/knife/core/hashed_command_loader.rb
new file mode 100644
index 0000000000..e419037b67
--- /dev/null
+++ b/knife/lib/chef/knife/core/hashed_command_loader.rb
@@ -0,0 +1,100 @@
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../version"
+class Chef
+ class Knife
+ class SubcommandLoader
+ #
+ # Load a subcommand from a pre-computed path
+ # for the given command.
+ #
+ class HashedCommandLoader < Chef::Knife::SubcommandLoader
+ KEY = "_autogenerated_command_paths".freeze
+
+ attr_accessor :manifest
+
+ def initialize(chef_config_dir, plugin_manifest)
+ super(chef_config_dir)
+ @manifest = plugin_manifest
+ end
+
+ def guess_category(args)
+ category_words = positional_arguments(args)
+ category_words.map! { |w| w.split("-") }.flatten!
+ find_longest_key(manifest[KEY]["plugins_by_category"], category_words, " ")
+ end
+
+ def list_commands(pref_category = nil)
+ if pref_category || manifest[KEY]["plugins_by_category"].key?(pref_category)
+ commands = { pref_category => manifest[KEY]["plugins_by_category"][pref_category] }
+ else
+ commands = manifest[KEY]["plugins_by_category"]
+ end
+ # If any of the specified plugins in the manifest don't have a valid path we will
+ # eventually get an error and the user will need to rehash - instead, lets just
+ # print out 1 error here telling them to rehash
+ errors = {}
+ commands.collect { |k, v| v }.flatten.each do |command|
+ paths = manifest[KEY]["plugins_paths"][command]
+ if paths && paths.is_a?(Array)
+ # It is only an error if all the paths don't exist
+ if paths.all? { |sc| !File.exist?(sc) }
+ errors[command] = paths
+ end
+ end
+ end
+ if errors.empty?
+ commands
+ else
+ Chef::Log.error "There are plugin files specified in the knife cache that cannot be found. Please run knife rehash to update the subcommands cache. If you see this error after rehashing delete the cache at #{Chef::Knife::SubcommandLoader.plugin_manifest_path}"
+ Chef::Log.error "Missing files:\n\t#{errors.values.flatten.join("\n\t")}"
+ {}
+ end
+ end
+
+ def subcommand_files
+ manifest[KEY]["plugins_paths"].values.flatten
+ end
+
+ def load_command(args)
+ paths = manifest[KEY]["plugins_paths"][subcommand_for_args(args)]
+ if paths.nil? || paths.empty? || (! paths.is_a? Array)
+ false
+ else
+ paths.each do |sc|
+ if File.exist?(sc)
+ Kernel.load sc
+ else
+ return false
+ end
+ end
+ true
+ end
+ end
+
+ def subcommand_for_args(args)
+ if manifest[KEY]["plugins_paths"].key?(args)
+ args
+ else
+ find_longest_key(manifest[KEY]["plugins_paths"], args, "_")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/node_editor.rb b/knife/lib/chef/knife/core/node_editor.rb
new file mode 100644
index 0000000000..5980cd888e
--- /dev/null
+++ b/knife/lib/chef/knife/core/node_editor.rb
@@ -0,0 +1,130 @@
+#
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Author:: Jordan Running (<jr@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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" unless defined?(Chef::JSONCompat)
+require "chef/node" unless defined?(Chef::Node)
+
+class Chef
+ class Knife
+ class NodeEditor
+ attr_reader :node, :ui, :config
+ private :node, :ui, :config
+
+ # @param node [Chef::Node]
+ # @param ui [Chef::Knife::UI]
+ # @param config [Hash]
+ def initialize(node, ui, config)
+ @node, @ui, @config = node, ui, config
+ end
+
+ # Opens the node data (as JSON) in the user's editor and returns a new
+ # {Chef::Node} reflecting the user's changes.
+ #
+ # @return [Chef::Node]
+ def edit_node
+ abort "You specified the --disable_editing option, nothing to edit" if config[:disable_editing]
+ assert_editor_set!
+
+ updated_node_data = ui.edit_hash(view)
+ apply_updates(updated_node_data)
+ @updated_node
+ end
+
+ # Returns an array of the names of properties that have been changed or
+ # +false+ if none were changed.
+ #
+ # @return [Array<String>] if any properties have been changed.
+ # @return [false] if no properties have been changed.
+ def updated?
+ return false if @updated_node.nil?
+
+ pristine_copy = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(node))
+ updated_copy = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(@updated_node))
+
+ updated_properties = %w{
+ name
+ chef_environment
+ automatic
+ default
+ normal
+ override
+ policy_name
+ policy_group
+ run_list
+ }.reject do |key|
+ pristine_copy[key] == updated_copy[key]
+ end
+
+ updated_properties.any? && updated_properties
+ end
+
+ # @api private
+ def view
+ result = {
+ "name" => node.name,
+ "chef_environment" => node.chef_environment,
+ "normal" => node.normal_attrs,
+ "policy_name" => node.policy_name,
+ "policy_group" => node.policy_group,
+ "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
+
+ result
+ end
+
+ # @api private
+ def apply_updates(updated_data)
+ if node.name && 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."
+ ui.confirm "Proceed with creation of new node"
+ end
+
+ data = updated_data.dup
+
+ unless config[:all_attributes]
+ data["automatic"] = node.automatic_attrs
+ data["default"] = node.default_attrs
+ data["override"] = node.override_attrs
+ end
+
+ @updated_node = Node.from_hash(data)
+ 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
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/node_presenter.rb b/knife/lib/chef/knife/core/node_presenter.rb
new file mode 100644
index 0000000000..8c948cf76c
--- /dev/null
+++ b/knife/lib/chef/knife/core/node_presenter.rb
@@ -0,0 +1,133 @@
+#
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "text_formatter"
+require_relative "generic_presenter"
+
+class Chef
+ class Knife
+ module Core
+
+ # 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.is_a?(Chef::Node)
+ node = data
+ result = {}
+
+ result["name"] = node.name
+ if node.policy_name.nil? && node.policy_group.nil?
+ result["chef_environment"] = node.chef_environment
+ else
+ result["policy_name"] = node.policy_name
+ result["policy_group"] = node.policy_group
+ end
+ 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 ui object.
+ def summarize(data)
+ if data.is_a?(Chef::Node)
+ node = data
+ # special case clouds with their split horizon thing.
+ ip = (node[:cloud] && node[:cloud][:public_ipv4_addrs] && node[:cloud][:public_ipv4_addrs].first) || node[:ipaddress]
+
+ summarized = <<~SUMMARY
+ #{ui.color("Node Name:", :bold)} #{ui.color(node.name, :bold)}
+ SUMMARY
+ show_policy = !(node.policy_name.nil? && node.policy_group.nil?)
+ if show_policy
+ summarized << <<~POLICY
+ #{key("Policy Name:")} #{node.policy_name}
+ #{key("Policy Group:")} #{node.policy_group}
+ POLICY
+ else
+ summarized << <<~ENV
+ #{key("Environment:")} #{node.chef_environment}
+ ENV
+ end
+ summarized << <<~SUMMARY
+ #{key("FQDN:")} #{node[:fqdn]}
+ #{key("IP:")} #{ip}
+ #{key("Run List:")} #{node.run_list}
+ SUMMARY
+ unless show_policy
+ summarized << <<~ROLES
+ #{key("Roles:")} #{Array(node[:roles]).join(", ")}
+ ROLES
+ end
+ summarized << <<~SUMMARY
+ #{key("Recipes:")} #{Array(node[:recipes]).join(", ")}
+ #{key("Platform:")} #{node[:platform]} #{node[:platform_version]}
+ #{key("Tags:")} #{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/knife/lib/chef/knife/core/object_loader.rb b/knife/lib/chef/knife/core/object_loader.rb
new file mode 100644
index 0000000000..edd8921c11
--- /dev/null
+++ b/knife/lib/chef/knife/core/object_loader.rb
@@ -0,0 +1,116 @@
+#
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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.
+#
+
+autoload :FFI_Yajl, "ffi_yajl"
+require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+require "chef/data_bag_item" unless defined?(Chef::DataBagItem)
+
+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)
+ puts "ZZZ LOoking for: #{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(ChefConfig::PathHelper.escape_glob_dir(File.expand_path(path)), "*")
+ path << ".{json,rb}"
+ objects = Dir.glob(path)
+ objects.map { |o| File.basename(o) }
+ end
+
+ def find_all_object_dirs(path)
+ path = File.join(ChefConfig::PathHelper.escape_glob_dir(File.expand_path(path)), "*")
+ objects = Dir.glob(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 = FFI_Yajl::Parser.parse(IO.read(filename))
+
+ # Chef::DataBagItem doesn't work well with the json_create method
+ if @klass == Chef::DataBagItem
+ r
+ else
+ @klass.from_hash(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.exist?(file) && File.readable?(file)
+ end
+
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/status_presenter.rb b/knife/lib/chef/knife/core/status_presenter.rb
new file mode 100644
index 0000000000..271c71d618
--- /dev/null
+++ b/knife/lib/chef/knife/core/status_presenter.rb
@@ -0,0 +1,147 @@
+#
+# Author:: Nicolas DUPEUX (<nicolas.dupeux@arkea.com>)
+# Copyright:: Copyright (c) Chef Software 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_relative "text_formatter"
+require_relative "generic_presenter"
+
+class Chef
+ class Knife
+ module Core
+
+ # A customized presenter for Chef::Node objects. Supports variable-length
+ # output formats for displaying node data
+ class StatusPresenter < GenericPresenter
+
+ def format(data)
+ if parse_format_option == :json
+ summarize_json(data)
+ else
+ super
+ end
+ end
+
+ def summarize_json(list)
+ result_list = []
+ list.each do |node|
+ result = {}
+
+ result["name"] = node["name"] || node.name
+ result["chef_environment"] = node["chef_environment"]
+ ip = (node["cloud"] && node["cloud"]["public_ipv4_addrs"]&.first) || node["ipaddress"]
+ fqdn = (node["cloud"] && node["cloud"]["public_hostname"]) || node["fqdn"]
+ result["ip"] = ip if ip
+ result["fqdn"] = fqdn if fqdn
+ result["run_list"] = node.run_list if config["run_list"]
+ result["ohai_time"] = node["ohai_time"]
+ result["platform"] = node["platform"] if node["platform"]
+ result["platform_version"] = node["platform_version"] if node["platform_version"]
+
+ if config[:long_output]
+ result["default"] = node.default_attrs
+ result["override"] = node.override_attrs
+ result["automatic"] = node.automatic_attrs
+ end
+ result_list << result
+ end
+
+ Chef::JSONCompat.to_json_pretty(result_list)
+ 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 ui object.
+ def summarize(list)
+ summarized = ""
+ list.each do |data|
+ node = data
+ # special case clouds with their split horizon thing.
+ ip = (node[:cloud] && node[:cloud][:public_ipv4_addrs] && node[:cloud][:public_ipv4_addrs].first) || node[:ipaddress]
+ fqdn = (node[:cloud] && node[:cloud][:public_hostname]) || node[:fqdn]
+ name = node["name"] || node.name
+
+ if config[:run_list]
+ if config[:long_output]
+ run_list = node.run_list.map { |rl| "#{rl.type}[#{rl.name}]" }
+ else
+ run_list = node["run_list"]
+ end
+ end
+
+ line_parts = []
+
+ if node["ohai_time"]
+ hours, minutes, seconds = time_difference_in_hms(node["ohai_time"])
+ hours_text = "#{hours} hour#{hours == 1 ? " " : "s"}"
+ minutes_text = "#{minutes} minute#{minutes == 1 ? " " : "s"}"
+ seconds_text = "#{seconds} second#{seconds == 1 ? " " : "s"}"
+ if hours > 24
+ color = :red
+ text = hours_text
+ elsif hours >= 1
+ color = :yellow
+ text = hours_text
+ elsif minutes >= 1
+ color = :green
+ text = minutes_text
+ else
+ color = :green
+ text = seconds_text
+ end
+ line_parts << @ui.color(text, color) + " ago" << name
+ else
+ line_parts << "Node #{name} has not yet converged"
+ end
+
+ line_parts << fqdn if fqdn
+ line_parts << ip if ip
+ line_parts << run_list.to_s if run_list
+
+ if node["platform"]
+ platform = node["platform"].dup
+ if node["platform_version"]
+ platform << " #{node["platform_version"]}"
+ end
+ line_parts << platform
+ end
+
+ summarized = summarized + line_parts.join(", ") + ".\n"
+ end
+ summarized
+ end
+
+ def key(key_text)
+ ui.color(key_text, :cyan)
+ end
+
+ # @private
+ # @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)
+ [hours, minutes, seconds]
+ end
+
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/subcommand_loader.rb b/knife/lib/chef/knife/core/subcommand_loader.rb
new file mode 100644
index 0000000000..ca7bfcd008
--- /dev/null
+++ b/knife/lib/chef/knife/core/subcommand_loader.rb
@@ -0,0 +1,208 @@
+# Author:: Christopher Brown (<cb@chef.io>)
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../version"
+require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+require "chef/run_list" unless defined?(Chef::RunList)
+require_relative "gem_glob_loader"
+require_relative "hashed_command_loader"
+
+class Chef
+ class Knife
+ #
+ # Public Methods of a Subcommand Loader
+ #
+ # load_commands - loads all available subcommands
+ # load_command(args) - loads subcommands for the given args
+ # list_commands(args) - lists all available subcommands,
+ # optionally filtering by category
+ # subcommand_files - returns an array of all subcommand files
+ # that could be loaded
+ # command_class_from(args) - returns the subcommand class for the
+ # user-requested command
+ #
+ class SubcommandLoader
+ attr_reader :chef_config_dir
+
+ # A small factory method. Eventually, this is the only place
+ # where SubcommandLoader should know about its subclasses, but
+ # to maintain backwards compatibility many of the instance
+ # methods in this base class contain default implementations
+ # of the functions sub classes should otherwise provide
+ # or directly instantiate the appropriate subclass
+ def self.for_config(chef_config_dir)
+ if autogenerated_manifest?
+ Chef::Log.trace("Using autogenerated hashed command manifest #{plugin_manifest_path}")
+ Knife::SubcommandLoader::HashedCommandLoader.new(chef_config_dir, plugin_manifest)
+ else
+ Knife::SubcommandLoader::GemGlobLoader.new(chef_config_dir)
+ end
+ end
+
+ # There are certain situations where we want to shortcut the loader selection
+ # in self.for_config and force using the GemGlobLoader
+ def self.gem_glob_loader(chef_config_dir)
+ Knife::SubcommandLoader::GemGlobLoader.new(chef_config_dir)
+ end
+
+ def self.plugin_manifest?
+ plugin_manifest_path && File.exist?(plugin_manifest_path)
+ end
+
+ def self.autogenerated_manifest?
+ plugin_manifest? && plugin_manifest.key?(HashedCommandLoader::KEY)
+ end
+
+ def self.plugin_manifest
+ Chef::JSONCompat.from_json(File.read(plugin_manifest_path))
+ end
+
+ def self.plugin_manifest_path
+ ChefConfig::PathHelper.home(".chef", "plugin_manifest.json")
+ end
+
+ def self.generate_hash
+ output = if plugin_manifest?
+ plugin_manifest
+ else
+ { Chef::Knife::SubcommandLoader::HashedCommandLoader::KEY => {} }
+ end
+ output[Chef::Knife::SubcommandLoader::HashedCommandLoader::KEY]["plugins_paths"] = Chef::Knife.subcommand_files
+ output[Chef::Knife::SubcommandLoader::HashedCommandLoader::KEY]["plugins_by_category"] = Chef::Knife.subcommands_by_category
+ output
+ end
+
+ def self.write_hash(data)
+ plugin_manifest_dir = File.expand_path("..", plugin_manifest_path)
+ FileUtils.mkdir_p(plugin_manifest_dir) unless File.directory?(plugin_manifest_dir)
+ File.open(plugin_manifest_path, "w") do |f|
+ f.write(Chef::JSONCompat.to_json_pretty(data))
+ end
+ end
+
+ def initialize(chef_config_dir)
+ @chef_config_dir = chef_config_dir
+ end
+
+ # Load all the sub-commands
+ def load_commands
+ return true if @loaded
+
+ subcommand_files.each { |subcommand| Kernel.load subcommand }
+ @loaded = true
+ end
+
+ def force_load
+ @loaded = false
+ load_commands
+ end
+
+ def load_command(_command_args)
+ load_commands
+ end
+
+ def list_commands(pref_cat = nil)
+ load_commands
+ if pref_cat && Chef::Knife.subcommands_by_category.key?(pref_cat)
+ { pref_cat => Chef::Knife.subcommands_by_category[pref_cat] }
+ else
+ Chef::Knife.subcommands_by_category
+ end
+ end
+
+ def command_class_from(args)
+ cmd_words = positional_arguments(args)
+ load_command(cmd_words)
+ result = Chef::Knife.subcommands[find_longest_key(Chef::Knife.subcommands,
+ cmd_words, "_")]
+ result || Chef::Knife.subcommands[args.first.tr("-", "_")]
+ end
+
+ def guess_category(args)
+ category_words = positional_arguments(args)
+ category_words.map! { |w| w.split("-") }.flatten!
+ find_longest_key(Chef::Knife.subcommands_by_category,
+ category_words, " ")
+ end
+
+ #
+ # This is shared between the custom_manifest_loader and the gem_glob_loader
+ def find_subcommands_via_dirglob
+ # The "require paths" of the core knife subcommands bundled with chef
+ files = Dir[File.join(ChefConfig::PathHelper.escape_glob_dir(File.expand_path("../../knife", __dir__)), "*.rb")]
+ version_file_match = /#{Regexp.escape(File.join('chef', 'knife', 'version.rb'))}/
+ subcommand_files = {}
+ files.each do |knife_file|
+ rel_path = knife_file[/#{KNIFE_ROOT}#{Regexp.escape(File::SEPARATOR)}(.*)\.rb/, 1]
+ # Exclude version.rb file for the gem. It's not a knife command, and force-loading it later
+ # because loaded via in subcommand files generates CLI warnings about its consts already having been defined
+ next if knife_file =~ version_file_match
+
+ subcommand_files[rel_path] = knife_file
+ end
+ subcommand_files
+ end
+
+ #
+ # Utility function for finding an element in a hash given an array
+ # of words and a separator. We find the the longest key in the
+ # hash composed of the given words joined by the separator.
+ #
+ def find_longest_key(hash, words, sep = "_")
+ words = words.dup
+ match = nil
+ until match || words.empty?
+ candidate = words.join(sep).tr("-", "_")
+ if hash.key?(candidate)
+ match = candidate
+ else
+ words.pop
+ end
+ end
+ match
+ end
+
+ #
+ # The positional arguments from the argument list provided by the
+ # users. Used to search for subcommands and categories.
+ #
+ # @return [Array<String>]
+ #
+ def positional_arguments(args)
+ args.select { |arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ }
+ 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", ChefConfig::PathHelper.escape_glob_dir(chef_config_dir)))
+ end
+
+ # finally search ~/.chef/plugins/knife/*.rb
+ ChefConfig::PathHelper.home(".chef", "plugins", "knife") do |p|
+ user_specific_files.concat Dir.glob(File.join(ChefConfig::PathHelper.escape_glob_dir(p), "*.rb"))
+ end
+
+ user_specific_files
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/text_formatter.rb b/knife/lib/chef/knife/core/text_formatter.rb
new file mode 100644
index 0000000000..ec97748afb
--- /dev/null
+++ b/knife/lib/chef/knife/core/text_formatter.rb
@@ -0,0 +1,85 @@
+#
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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.is_a?(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.is_a?(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.is_a?(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.is_a?(Array) || value.respond_to?(:keys))
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/ui.rb b/knife/lib/chef/knife/core/ui.rb
new file mode 100644
index 0000000000..782df1ca10
--- /dev/null
+++ b/knife/lib/chef/knife/core/ui.rb
@@ -0,0 +1,338 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Christopher Brown (<cb@chef.io>)
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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" unless defined?(Forwardable)
+require "chef/platform/query_helpers" # NOTE - this require doesn't defined any const we can check.
+require_relative "generic_presenter"
+require "tempfile" unless defined?(Tempfile)
+
+class Chef
+ class Knife
+
+ # 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
+
+ # Creates a new object of class TTY::Prompt
+ # with interrupt as exit so that it can be terminated with status code.
+ def prompt
+ @prompt ||= begin
+ require "tty-prompt"
+ TTY::Prompt.new(interrupt: :exit)
+ end
+ end
+
+ # pastel.decorate is a lightweight replacement for highline.color
+ def pastel
+ @pastel ||= begin
+ require "pastel" unless defined?(Pastel)
+ Pastel.new
+ end
+ end
+
+ # Prints a message to stdout. Aliased as +info+ for compatibility with
+ # the logger API.
+ #
+ # @param message [String] the text string
+ def msg(message)
+ stdout.puts message
+ rescue Errno::EPIPE => e
+ raise e if @config[:verbosity] >= 2
+
+ exit 0
+ end
+
+ # Prints a msg to stderr. Used for info, warn, error, and fatal.
+ #
+ # @param message [String] the text string
+ def log(message)
+ lines = message.split("\n")
+ first_line = lines.shift
+ stderr.puts first_line
+ # If the message is multiple lines,
+ # indent subsequent lines to align with the
+ # log type prefix ("ERROR: ", etc)
+ unless lines.empty?
+ prefix, = first_line.split(":", 2)
+ return if prefix.nil?
+
+ prefix_len = prefix.length
+ prefix_len -= 9 if color? # prefix includes 9 bytes of color escape sequences
+ prefix_len += 2 # include room to align to the ": " following PREFIX
+ padding = " " * prefix_len
+ lines.each do |line|
+ stderr.puts "#{padding}#{line}"
+ end
+ end
+ rescue Errno::EPIPE => e
+ raise e if @config[:verbosity] >= 2
+
+ exit 0
+ end
+
+ alias :info :log
+ alias :err :log
+
+ # Print a Debug
+ #
+ # @param message [String] the text string
+ def debug(message)
+ log("#{color("DEBUG:", :blue, :bold)} #{message}")
+ end
+
+ # Print a warning message
+ #
+ # @param message [String] the text string
+ def warn(message)
+ log("#{color("WARNING:", :yellow, :bold)} #{message}")
+ end
+
+ # Print an error message
+ #
+ # @param message [String] the text string
+ def error(message)
+ log("#{color("ERROR:", :red, :bold)} #{message}")
+ end
+
+ # Print a message describing a fatal error.
+ #
+ # @param message [String] the text string
+ def fatal(message)
+ log("#{color("FATAL:", :red, :bold)} #{message}")
+ end
+
+ # Print a message describing a fatal error and exit 1
+ #
+ # @param message [String] the text string
+ def fatal!(message)
+ fatal(message)
+ exit 1
+ end
+
+ def color(string, *colors)
+ if color?
+ pastel.decorate(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?
+ end
+
+ def ask(*args, **options, &block)
+ prompt.ask(*args, **options, &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 += "[#{opts[:default]}] " if opts[:default]
+
+ if opts[:default] && 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
+ rescue Errno::EPIPE => e
+ raise e if @config[:verbosity] >= 2
+
+ exit 0
+ end
+
+ # Hash -> Hash
+ # Works the same as edit_data but
+ # returns a hash rather than a JSON string/Fully inflated object
+ def edit_hash(hash)
+ raw = edit_data(hash, false)
+ Chef::JSONCompat.parse(raw)
+ end
+
+ def edit_data(data, parse_output = true, object_class: nil)
+ output = Chef::JSONCompat.to_json_pretty(data)
+ unless config[:disable_editing]
+ Tempfile.open([ "knife-edit-", ".json" ]) do |tf|
+ tf.sync = true
+ tf.puts output
+ tf.close
+ raise "Please set EDITOR environment variable. See https://docs.chef.io/knife_setup/ for details." unless system("#{config[:editor]} #{tf.path}")
+
+ output = IO.read(tf.path)
+ end
+ end
+
+ if parse_output
+ if object_class.nil?
+ raise ArgumentError, "Please pass in the object class to hydrate or use #edit_hash"
+ else
+ object_class.from_hash(Chef::JSONCompat.parse(output))
+ end
+ else
+ output
+ end
+ end
+
+ def edit_object(klass, name)
+ object = klass.load(name)
+
+ output = edit_data(object, object_class: klass)
+
+ # 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.parse(Chef::JSONCompat.to_json(object))
+ output_parsed_again = Chef::JSONCompat.parse(Chef::JSONCompat.to_json(output))
+ if object_parsed_again != output_parsed_again
+ output.save
+ msg("Saved #{output}")
+ else
+ msg("Object unchanged, not saving")
+ end
+ output(format_for_display(object)) if config[:print_after]
+ end
+
+ def confirmation_instructions(default_choice)
+ case default_choice
+ when true
+ "? (Y/n) "
+ when false
+ "? (y/N) "
+ else
+ "? (Y/N) "
+ end
+ end
+
+ # See confirm method for argument information
+ def confirm_without_exit(question, append_instructions = true, default_choice = nil)
+ return true if config[:yes]
+
+ stdout.print question
+ stdout.print confirmation_instructions(default_choice) if append_instructions
+
+ answer = stdin.readline
+ answer.chomp!
+
+ case answer
+ when "Y", "y"
+ true
+ when "N", "n"
+ msg("You said no, so I'm done here.")
+ false
+ when ""
+ unless default_choice.nil?
+ default_choice
+ else
+ msg("I have no idea what to do with '#{answer}'")
+ msg("Just say Y or N, please.")
+ confirm_without_exit(question, append_instructions, default_choice)
+ end
+ else
+ msg("I have no idea what to do with '#{answer}'")
+ msg("Just say Y or N, please.")
+ confirm_without_exit(question, append_instructions, default_choice)
+ end
+ end
+
+ #
+ # Not the ideal signature for a function but we need to stick with this
+ # for now until we get a chance to break our API in Chef 12.
+ #
+ # question => Question to print before asking for confirmation
+ # append_instructions => Should print '? (Y/N)' as instructions
+ # default_choice => Set to true for 'Y', and false for 'N' as default answer
+ #
+ def confirm(question, append_instructions = true, default_choice = nil)
+ unless confirm_without_exit(question, append_instructions, default_choice)
+ exit 3
+ end
+ true
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/core/windows_bootstrap_context.rb b/knife/lib/chef/knife/core/windows_bootstrap_context.rb
new file mode 100644
index 0000000000..0ff209a0b7
--- /dev/null
+++ b/knife/lib/chef/knife/core/windows_bootstrap_context.rb
@@ -0,0 +1,406 @@
+#
+# Author:: Seth Chisamore (<schisamo@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "bootstrap_context"
+require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ class Knife
+ module Core
+ # Instances of BootstrapContext are the context objects (i.e., +self+) for
+ # bootstrap templates. For backwards compatibility, they +must+ set the
+ # following instance variables:
+ # * @config - a hash of knife's config values
+ # * @run_list - the run list for the node to bootstrap
+ #
+ class WindowsBootstrapContext < BootstrapContext
+ attr_accessor :config
+ attr_accessor :chef_config
+ attr_accessor :secret
+
+ def initialize(config, run_list, chef_config, secret = nil)
+ @config = config
+ @run_list = run_list
+ @chef_config = chef_config
+ @secret = secret
+ super(config, run_list, chef_config, secret)
+ end
+
+ def 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
+ escape_and_echo(@secret)
+ end
+
+ def trusted_certs_script
+ @trusted_certs_script ||= trusted_certs_content
+ end
+
+ def config_content
+ # The windows: true / windows: false in the block that follows is more than a bit weird. The way to read this is that we need
+ # the e.g. var_chef_dir to be rendered for the windows value ("C:\chef"), but then we are rendering into a file to be read by
+ # ruby, so we don't actually care about forward-vs-backslashes and by rendering into unix we avoid having to deal with the
+ # double-backwhacking of everything. So we expect to see:
+ #
+ # file_cache_path "C:/chef"
+ #
+ # Which is mildly odd, but should be entirely correct as far as ruby cares.
+ #
+ client_rb = <<~CONFIG
+ chef_server_url "#{chef_config[:chef_server_url]}"
+ validation_client_name "#{chef_config[:validation_client_name]}"
+ file_cache_path "#{ChefConfig::PathHelper.escapepath(ChefConfig::Config.var_chef_dir(windows: true))}\\\\cache"
+ file_backup_path "#{ChefConfig::PathHelper.escapepath(ChefConfig::Config.var_chef_dir(windows: true))}\\\\backup"
+ cache_options ({:path => "#{ChefConfig::PathHelper.escapepath(ChefConfig::Config.etc_chef_dir(windows: true))}\\\\cache\\\\checksums", :skip_expires => true})
+ CONFIG
+
+ unless chef_config[:chef_license].nil?
+ client_rb << "chef_license \"#{chef_config[:chef_license]}\"\n"
+ end
+
+ 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 config[:config_log_level]
+ client_rb << %Q{log_level :#{config[:config_log_level]}\n}
+ else
+ client_rb << "log_level :auto\n"
+ end
+
+ client_rb << "log_location #{get_log_location}"
+
+ # We configure :verify_api_cert only when it's overridden on the CLI
+ # or when specified in the knife config.
+ if !config[:node_verify_api_cert].nil? || config.key?(:verify_api_cert)
+ value = config[:node_verify_api_cert].nil? ? config[:verify_api_cert] : config[:node_verify_api_cert]
+ client_rb << %Q{verify_api_cert #{value}\n}
+ end
+
+ # We configure :ssl_verify_mode only when it's overridden on the CLI
+ # or when specified in the knife config.
+ if config[:node_ssl_verify_mode] || config.key?(:ssl_verify_mode)
+ value = case config[:node_ssl_verify_mode]
+ when "peer"
+ :verify_peer
+ when "none"
+ :verify_none
+ when nil
+ config[:ssl_verify_mode]
+ else
+ nil
+ end
+
+ if value
+ client_rb << %Q{ssl_verify_mode :#{value}\n}
+ end
+ end
+
+ if config[:ssl_verify_mode]
+ client_rb << %Q{ssl_verify_mode :#{config[:ssl_verify_mode]}\n}
+ end
+
+ if config[:bootstrap_proxy]
+ client_rb << "\n"
+ client_rb << %Q{http_proxy "#{config[:bootstrap_proxy]}"\n}
+ client_rb << %Q{https_proxy "#{config[:bootstrap_proxy]}"\n}
+ client_rb << %Q{no_proxy "#{config[:bootstrap_no_proxy]}"\n} if config[:bootstrap_no_proxy]
+ end
+
+ if config[:bootstrap_no_proxy]
+ client_rb << %Q{no_proxy "#{config[:bootstrap_no_proxy]}"\n}
+ end
+
+ if secret
+ client_rb << %Q{encrypted_data_bag_secret "#{ChefConfig::PathHelper.escapepath(ChefConfig::Config.etc_chef_dir(windows: true))}\\\\encrypted_data_bag_secret"\n}
+ end
+
+ unless trusted_certs_script.empty?
+ client_rb << %Q{trusted_certs_dir "#{ChefConfig::PathHelper.escapepath(ChefConfig::Config.etc_chef_dir(windows: true))}\\\\trusted_certs"\n}
+ end
+
+ if chef_config[:fips]
+ client_rb << "fips true\n"
+ end
+
+ escape_and_echo(client_rb)
+ end
+
+ def get_log_location
+ if chef_config[:config_log_location].equal?(:win_evt)
+ %Q{:#{chef_config[:config_log_location]}\n}
+ elsif chef_config[:config_log_location].equal?(:syslog)
+ raise "syslog is not supported for log_location on Windows OS\n"
+ elsif chef_config[:config_log_location].equal?(STDOUT)
+ "STDOUT\n"
+ elsif chef_config[:config_log_location].equal?(STDERR)
+ "STDERR\n"
+ elsif chef_config[:config_log_location].nil? || chef_config[:config_log_location].empty?
+ "STDOUT\n"
+ elsif chef_config[:config_log_location]
+ %Q{"#{chef_config[:config_log_location]}"\n}
+ else
+ "STDOUT\n"
+ end
+ end
+
+ def start_chef
+ c_opscode_dir = ChefConfig::PathHelper.cleanpath(ChefConfig::Config.c_opscode_dir, windows: true)
+ client_rb = clean_etc_chef_file("client.rb")
+ first_boot = clean_etc_chef_file("first-boot.json")
+
+ bootstrap_environment_option = bootstrap_environment.nil? ? "" : " -E #{bootstrap_environment}"
+
+ start_chef = "SET \"PATH=%SYSTEM32%;%SystemRoot%;%SYSTEM32%\\Wbem;%SYSTEM32%\\WindowsPowerShell\\v1.0\\;C:\\ruby\\bin;#{c_opscode_dir}\\bin;#{c_opscode_dir}\\embedded\\bin\;%PATH%\"\n"
+ start_chef << "#{ChefUtils::Dist::Infra::CLIENT} -c #{client_rb} -j #{first_boot}#{bootstrap_environment_option}\n"
+ end
+
+ def win_wget
+ # I tried my best to figure out how to properly url decode and switch / to \
+ # but this is VBScript - so I don't really care that badly.
+ win_wget = <<~WGET
+ url = WScript.Arguments.Named("url")
+ path = WScript.Arguments.Named("path")
+ proxy = null
+ '* Vaguely attempt to handle file:// scheme urls by url unescaping and switching all
+ '* / into \. Also assume that file:/// is a local absolute path and that file://<foo>
+ '* is possibly a network file path.
+ If InStr(url, "file://") = 1 Then
+ url = Unescape(url)
+ If InStr(url, "file:///") = 1 Then
+ sourcePath = Mid(url, Len("file:///") + 1)
+ Else
+ sourcePath = Mid(url, Len("file:") + 1)
+ End If
+ sourcePath = Replace(sourcePath, "/", "\\")
+
+ Set objFSO = CreateObject("Scripting.FileSystemObject")
+ If objFSO.Fileexists(path) Then objFSO.DeleteFile path
+ objFSO.CopyFile sourcePath, path, true
+ Set objFSO = Nothing
+
+ Else
+ Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP")
+ Set wshShell = CreateObject( "WScript.Shell" )
+ Set objUserVariables = wshShell.Environment("USER")
+
+ rem http proxy is optional
+ rem attempt to read from HTTP_PROXY env var first
+ On Error Resume Next
+
+ If NOT (objUserVariables("HTTP_PROXY") = "") Then
+ proxy = objUserVariables("HTTP_PROXY")
+
+ rem fall back to named arg
+ ElseIf NOT (WScript.Arguments.Named("proxy") = "") Then
+ proxy = WScript.Arguments.Named("proxy")
+ End If
+
+ If NOT isNull(proxy) Then
+ rem setProxy method is only available on ServerXMLHTTP 6.0+
+ Set objXMLHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0")
+ objXMLHTTP.setProxy 2, proxy
+ End If
+
+ On Error Goto 0
+
+ objXMLHTTP.open "GET", url, false
+ objXMLHTTP.send()
+ If objXMLHTTP.Status = 200 Then
+ Set objADOStream = CreateObject("ADODB.Stream")
+ objADOStream.Open
+ objADOStream.Type = 1
+ objADOStream.Write objXMLHTTP.ResponseBody
+ objADOStream.Position = 0
+ Set objFSO = Createobject("Scripting.FileSystemObject")
+ If objFSO.Fileexists(path) Then objFSO.DeleteFile path
+ Set objFSO = Nothing
+ objADOStream.SaveToFile path
+ objADOStream.Close
+ Set objADOStream = Nothing
+ End If
+ Set objXMLHTTP = Nothing
+ End If
+ WGET
+ escape_and_echo(win_wget)
+ end
+
+ def win_wget_ps
+ win_wget_ps = <<~WGET_PS
+ param(
+ [String] $remoteUrl,
+ [String] $localPath
+ )
+
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+
+ $ProxyUrl = $env:http_proxy;
+ $webClient = new-object System.Net.WebClient;
+
+ if ($ProxyUrl -ne '') {
+ $WebProxy = New-Object System.Net.WebProxy($ProxyUrl,$true)
+ $WebClient.Proxy = $WebProxy
+ }
+
+ $webClient.DownloadFile($remoteUrl, $localPath);
+ WGET_PS
+
+ escape_and_echo(win_wget_ps)
+ end
+
+ def install_chef
+ # The normal install command uses regular double quotes in
+ # the install command, so request such a string from install_command
+ install_command('"') + "\n" + fallback_install_task_command
+ end
+
+ def clean_etc_chef_file(path)
+ ChefConfig::PathHelper.cleanpath(etc_chef_file(path), windows: true)
+ end
+
+ def etc_chef_file(path)
+ "#{bootstrap_directory}/#{path}"
+ end
+
+ def bootstrap_directory
+ ChefConfig::Config.etc_chef_dir(windows: true)
+ end
+
+ def local_download_path
+ "%TEMP%\\#{ChefUtils::Dist::Infra::CLIENT}-latest.msi"
+ end
+
+ # Build a URL to query www.chef.io that will redirect to the correct
+ # Chef Infra msi download.
+ def msi_url(machine_os = nil, machine_arch = nil, download_context = nil)
+ if config[:msi_url].nil? || config[:msi_url].empty?
+ url = "https://www.chef.io/chef/download?p=windows"
+ url += "&pv=#{machine_os}" unless machine_os.nil?
+ url += "&m=#{machine_arch}" unless machine_arch.nil?
+ url += "&DownloadContext=#{download_context}" unless download_context.nil?
+ url += "&channel=#{config[:channel]}"
+ url += "&v=#{version_to_install}"
+ else
+ config[:msi_url]
+ end
+ end
+
+ def first_boot
+ escape_and_echo(super.to_json)
+ end
+
+ # escape WIN BATCH special chars
+ # and prefixes each line with an
+ # echo
+ def escape_and_echo(file_contents)
+ file_contents.gsub(/^(.*)$/, 'echo.\1').gsub(/([(<|>)^])/, '^\1')
+ end
+
+ private
+
+ def install_command(executor_quote)
+ "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}"
+ end
+
+ # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped
+ # This string should contain both the commands necessary to both create the files, as well as their content
+ def trusted_certs_content
+ content = ""
+ if chef_config[:trusted_certs_dir]
+ Dir.glob(File.join(ChefConfig::PathHelper.escape_glob_dir(chef_config[:trusted_certs_dir]), "*.{crt,pem}")).each do |cert|
+ content << "> #{bootstrap_directory}/trusted_certs/#{File.basename(cert)} (\n" +
+ escape_and_echo(IO.read(File.expand_path(cert))) + "\n)\n"
+ end
+ end
+ content
+ end
+
+ def client_d_content
+ content = ""
+ if chef_config[:client_d_dir] && File.exist?(chef_config[:client_d_dir])
+ root = Pathname(chef_config[:client_d_dir])
+ root.find do |f|
+ relative = f.relative_path_from(root)
+ if f != root
+ file_on_node = "#{bootstrap_directory}/client.d/#{relative}".tr("/", "\\")
+ if f.directory?
+ content << "mkdir #{file_on_node}\n"
+ else
+ content << "> #{file_on_node} (\n" +
+ escape_and_echo(IO.read(File.expand_path(f))) + "\n)\n"
+ end
+ end
+ end
+ end
+ content
+ end
+
+ def fallback_install_task_command
+ # This command will be executed by schtasks.exe in the batch
+ # code below. To handle tasks that contain arguments that
+ # need to be double quoted, schtasks allows the use of single
+ # quotes that will later be converted to double quotes
+ command = install_command("'")
+ <<~EOH
+ @set MSIERRORCODE=!ERRORLEVEL!
+ @if ERRORLEVEL 1 (
+ @echo WARNING: Failed to install #{ChefUtils::Dist::Infra::PRODUCT} MSI package in remote context with status code !MSIERRORCODE!.
+ @echo WARNING: This may be due to a defect in operating system update KB2918614: http://support.microsoft.com/kb/2918614
+ @set OLDLOGLOCATION="%CHEF_CLIENT_MSI_LOG_PATH%-fail.log"
+ @move "%CHEF_CLIENT_MSI_LOG_PATH%" "!OLDLOGLOCATION!" > NUL
+ @echo WARNING: Saving installation log of failure at !OLDLOGLOCATION!
+ @echo WARNING: Retrying installation with local context...
+ @schtasks /create /f /sc once /st 00:00:00 /tn chefclientbootstraptask /ru SYSTEM /rl HIGHEST /tr \"cmd /c #{command} & sleep 2 & waitfor /s %computername% /si chefclientinstalldone\"
+
+ @if ERRORLEVEL 1 (
+ @echo ERROR: Failed to create #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task with status code !ERRORLEVEL! > "&2"
+ ) else (
+ @echo Successfully created scheduled task to install #{ChefUtils::Dist::Infra::PRODUCT}.
+ @schtasks /run /tn chefclientbootstraptask
+ @if ERRORLEVEL 1 (
+ @echo ERROR: Failed to execute #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task with status code !ERRORLEVEL!. > "&2"
+ ) else (
+ @echo Successfully started #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task.
+ @echo Waiting for installation to complete -- this may take a few minutes...
+ waitfor chefclientinstalldone /t 600
+ if ERRORLEVEL 1 (
+ @echo ERROR: Timed out waiting for #{ChefUtils::Dist::Infra::PRODUCT} package to install
+ ) else (
+ @echo Finished waiting for #{ChefUtils::Dist::Infra::PRODUCT} package to install.
+ )
+ @schtasks /delete /f /tn chefclientbootstraptask > NUL
+ )
+ )
+ ) else (
+ @echo Successfully installed #{ChefUtils::Dist::Infra::PRODUCT} package.
+ )
+ EOH
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/data_bag_create.rb b/knife/lib/chef/knife/data_bag_create.rb
new file mode 100644
index 0000000000..a8a9caf7e4
--- /dev/null
+++ b/knife/lib/chef/knife/data_bag_create.rb
@@ -0,0 +1,81 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Seth Falcon (<seth@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "data_bag_secret_options"
+
+class Chef
+ class Knife
+ class DataBagCreate < Knife
+ include DataBagSecretOptions
+
+ deps do
+ require "chef/data_bag" unless defined?(Chef::DataBag)
+ require "chef/encrypted_data_bag_item" unless defined?(Chef::EncryptedDataBagItem)
+ end
+
+ banner "knife data bag create BAG [ITEM] (options)"
+ category "data bag"
+
+ 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
+
+ begin
+ Chef::DataBag.validate_name!(@data_bag_name)
+ rescue Chef::Exceptions::InvalidDataBagName => e
+ ui.fatal(e.message)
+ exit(1)
+ end
+
+ # Verify if the data bag exists
+ begin
+ rest.get("data/#{@data_bag_name}")
+ ui.info("Data bag #{@data_bag_name} already exists")
+ rescue Net::HTTPClientException => e
+ raise unless /^404/.match?(e.to_s)
+
+ # if it doesn't exists, try to create it
+ rest.post("data", { "name" => @data_bag_name })
+ ui.info("Created data_bag[#{@data_bag_name}]")
+ 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 encryption_secret_provided?
+ Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret)
+ else
+ output
+ end
+ )
+ item.data_bag(@data_bag_name)
+ rest.post("data/#{@data_bag_name}", item)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/data_bag_delete.rb b/knife/lib/chef/knife/data_bag_delete.rb
new file mode 100644
index 0000000000..a7b5a4b6fd
--- /dev/null
+++ b/knife/lib/chef/knife/data_bag_delete.rb
@@ -0,0 +1,49 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class DataBagDelete < Knife
+
+ deps do
+ require "chef/data_bag" unless defined?(Chef::DataBag)
+ 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("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("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/knife/lib/chef/knife/data_bag_edit.rb b/knife/lib/chef/knife/data_bag_edit.rb
new file mode 100644
index 0000000000..92bff8d7f7
--- /dev/null
+++ b/knife/lib/chef/knife/data_bag_edit.rb
@@ -0,0 +1,74 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Seth Falcon (<seth@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "data_bag_secret_options"
+
+class Chef
+ class Knife
+ class DataBagEdit < Knife
+ include DataBagSecretOptions
+
+ deps do
+ require "chef/data_bag_item" unless defined?(Chef::DataBagItem)
+ require "chef/encrypted_data_bag_item" unless defined?(Chef::EncryptedDataBagItem)
+ end
+
+ banner "knife data bag edit BAG ITEM (options)"
+ category "data bag"
+
+ def load_item(bag, item_name)
+ item = Chef::DataBagItem.load(bag, item_name)
+ if encrypted?(item.raw_data)
+ if encryption_secret_provided_ignore_encrypt_flag?
+ [Chef::EncryptedDataBagItem.new(item, read_secret).to_hash, true]
+ else
+ ui.fatal("You cannot edit an encrypted data bag without providing the secret.")
+ exit(1)
+ end
+ else
+ [item.raw_data, false]
+ 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, was_encrypted = load_item(@name_args[0], @name_args[1])
+ edited_item = edit_hash(item)
+
+ if was_encrypted || encryption_secret_provided?
+ ui.info("Encrypting data bag using provided secret.")
+ item_to_save = Chef::EncryptedDataBagItem.encrypt_data_bag_item(edited_item, read_secret)
+ else
+ ui.info("Saving data bag unencrypted. To encrypt it, provide an appropriate secret.")
+ item_to_save = edited_item
+ end
+
+ rest.put("data/#{@name_args[0]}/#{@name_args[1]}", item_to_save)
+ stdout.puts("Saved data_bag_item[#{@name_args[1]}]")
+ ui.output(edited_item) if config[:print_after]
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/data_bag_from_file.rb b/knife/lib/chef/knife/data_bag_from_file.rb
new file mode 100644
index 0000000000..6c889e1927
--- /dev/null
+++ b/knife/lib/chef/knife/data_bag_from_file.rb
@@ -0,0 +1,113 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Seth Falcon (<seth@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "data_bag_secret_options"
+
+class Chef
+ class Knife
+ class DataBagFromFile < Knife
+ include DataBagSecretOptions
+
+ deps do
+ require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+ require "chef/data_bag" unless defined?(Chef::DataBag)
+ require "chef/data_bag_item" unless defined?(Chef::DataBagItem)
+ require "chef/encrypted_data_bag_item" unless defined?(Chef::EncryptedDataBagItem)
+ require_relative "core/object_loader"
+ end
+
+ banner "knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)"
+ category "data bag"
+
+ option :all,
+ short: "-a",
+ long: "--all",
+ description: "Upload all data bags or all items for specified data bags."
+
+ def loader
+ @loader ||= Knife::Core::ObjectLoader.new(Chef::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).to_s, data_bag, item_path)
+ item = if encryption_secret_provided?
+ Chef::EncryptedDataBagItem.encrypt_data_bag_item(item, read_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 = []
+ args.each do |path|
+ if File.directory?(path)
+ paths.concat(Dir.glob(File.join(ChefConfig::PathHelper.escape_glob_dir(path), "*.json")))
+ else
+ paths << path
+ end
+ end
+ paths
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/data_bag_list.rb b/knife/lib/chef/knife/data_bag_list.rb
new file mode 100644
index 0000000000..8a8e7ba89d
--- /dev/null
+++ b/knife/lib/chef/knife/data_bag_list.rb
@@ -0,0 +1,42 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class DataBagList < Knife
+
+ deps do
+ require "chef/data_bag" unless defined?(Chef::DataBag)
+ 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/knife/lib/chef/knife/data_bag_secret_options.rb b/knife/lib/chef/knife/data_bag_secret_options.rb
new file mode 100644
index 0000000000..4d8ba90929
--- /dev/null
+++ b/knife/lib/chef/knife/data_bag_secret_options.rb
@@ -0,0 +1,122 @@
+#
+# Author:: Tyler Ball (<tball@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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 "mixlib/cli" unless defined?(Mixlib::CLI)
+require "chef/config" unless defined?(Chef::Config)
+require "encrypted_data_bag_item/check_encrypted" unless defined?(Chef::EncryptedDataBagItem::CheckEncrypted)
+
+class Chef
+ class Knife
+ module DataBagSecretOptions
+ include Mixlib::CLI
+ include Chef::EncryptedDataBagItem::CheckEncrypted
+
+ # The config object is populated by knife#merge_configs with knife.rb `knife[:*]` config values, but they do
+ # not overwrite the command line properties. It does mean, however, that `knife[:secret]` and `--secret-file`
+ # passed at the same time populate both `config[:secret]` and `config[:secret_file]`. We cannot differentiate
+ # the valid case (`knife[:secret]` in config file and `--secret-file` on CL) and the invalid case (`--secret`
+ # and `--secret-file` on the CL) - thats why I'm storing the CL options in a different config key if they
+ # are provided.
+
+ def self.included(base)
+ base.option :cl_secret,
+ long: "--secret SECRET",
+ description: "The secret key to use to encrypt data bag item values. Can also be defaulted in your config with the key 'secret'."
+
+ base.option :cl_secret_file,
+ long: "--secret-file SECRET_FILE",
+ description: "A file containing the secret key to use to encrypt data bag item values. Can also be defaulted in your config with the key 'secret_file'."
+
+ base.option :encrypt,
+ long: "--encrypt",
+ description: "If 'secret' or 'secret_file' is present in your config, then encrypt data bags using it.",
+ boolean: true,
+ default: false
+ end
+
+ def encryption_secret_provided?
+ base_encryption_secret_provided?
+ end
+
+ def encryption_secret_provided_ignore_encrypt_flag?
+ base_encryption_secret_provided?(false)
+ end
+
+ def read_secret
+ # Moving the non 'compile-time' requires into here to speed up knife command loading
+ # IE, if we are not running 'knife data bag *' we don't need to load 'chef/encrypted_data_bag_item'
+ require "chef/encrypted_data_bag_item" unless defined?(Chef::EncryptedDataBagItem)
+
+ if config[:cl_secret]
+ config[:cl_secret]
+ elsif config[:cl_secret_file]
+ Chef::EncryptedDataBagItem.load_secret(config[:cl_secret_file])
+ elsif secret = config[:secret]
+ secret
+ else
+ secret_file = config[:secret_file]
+ Chef::EncryptedDataBagItem.load_secret(secret_file)
+ end
+ end
+
+ def validate_secrets
+ if config[:cl_secret] && config[:cl_secret_file]
+ ui.fatal("Please specify only one of --secret, --secret-file")
+ exit(1)
+ end
+
+ if config[:secret] && config[:secret_file]
+ ui.fatal("Please specify only one of 'secret' or 'secret_file' in your config file")
+ exit(1)
+ end
+ end
+
+ private
+
+ ##
+ # Determine if the user has specified an appropriate secret for encrypting data bag items.
+ # @return boolean
+ def base_encryption_secret_provided?(need_encrypt_flag = true)
+ validate_secrets
+
+ return true if config[:cl_secret] || config[:cl_secret_file]
+
+ if need_encrypt_flag
+ if config[:encrypt]
+ unless config[:secret] || config[:secret_file]
+ ui.fatal("No secret or secret_file specified in config, unable to encrypt item.")
+ exit(1)
+ end
+ return true
+ end
+ return false
+ elsif config[:secret] || config[:secret_file]
+ # Certain situations (show and bootstrap) don't need a --encrypt flag to use the config file secret
+ return true
+ end
+ false
+ end
+
+ def knife_config
+ Chef.deprecated(:knife_bootstrap_apis, "The `knife_config` bootstrap helper has been deprecated, use the properly merged `config` helper instead")
+ Chef::Config.key?(:knife) ? Chef::Config[:knife] : {}
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/data_bag_show.rb b/knife/lib/chef/knife/data_bag_show.rb
new file mode 100644
index 0000000000..3270f45ee2
--- /dev/null
+++ b/knife/lib/chef/knife/data_bag_show.rb
@@ -0,0 +1,69 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: Seth Falcon (<seth@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "data_bag_secret_options"
+
+class Chef
+ class Knife
+ class DataBagShow < Knife
+ include DataBagSecretOptions
+
+ deps do
+ require "chef/data_bag" unless defined?(Chef::DataBag)
+ require "chef/encrypted_data_bag_item" unless defined?(Chef::EncryptedDataBagItem)
+ end
+
+ banner "knife data bag show BAG [ITEM] (options)"
+ category "data bag"
+
+ def run
+ display = case @name_args.length
+ when 2 # Bag and Item names provided
+ secret = encryption_secret_provided_ignore_encrypt_flag? ? read_secret : nil
+ raw_data = Chef::DataBagItem.load(@name_args[0], @name_args[1]).raw_data
+ encrypted = encrypted?(raw_data)
+
+ if encrypted && secret
+ # Users do not need to pass --encrypt to read data, we simply try to use the provided secret
+ ui.info("Encrypted data bag detected, decrypting with provided secret.")
+ raw = Chef::EncryptedDataBagItem.load(@name_args[0],
+ @name_args[1],
+ secret)
+ format_for_display(raw.to_h)
+ elsif encrypted && !secret
+ ui.warn("Encrypted data bag detected, but no secret provided for decoding. Displaying encrypted data.")
+ format_for_display(raw_data)
+ else
+ ui.warn("Unencrypted data bag detected, ignoring any provided secret options.") if secret
+ format_for_display(raw_data)
+ end
+
+ when 1 # Only Bag name provided
+ 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/knife/lib/chef/knife/delete.rb b/knife/lib/chef/knife/delete.rb
new file mode 100644
index 0000000000..2853efc21c
--- /dev/null
+++ b/knife/lib/chef/knife/delete.rb
@@ -0,0 +1,125 @@
+#
+# 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_relative "../chef_fs/knife"
+
+class Chef
+ class Knife
+ class Delete < Chef::ChefFS::Knife
+ banner "knife delete [PATTERN1 ... PATTERNn]"
+
+ category "path-based"
+
+ deps do
+ require "chef/chef_fs/file_system" unless defined?(Chef::ChefFS::FileSystem)
+ end
+
+ option :recurse,
+ short: "-r",
+ long: "--[no-]recurse",
+ boolean: true,
+ default: false,
+ description: "Delete directories recursively."
+
+ option :both,
+ long: "--both",
+ boolean: true,
+ default: false,
+ description: "Delete both the local and remote copies."
+
+ option :local,
+ long: "--local",
+ boolean: true,
+ default: false,
+ description: "Delete the local copy (leave the remote copy)."
+
+ def run
+ if name_args.length == 0
+ show_usage
+ ui.fatal("You must specify at least one argument. If you want to delete everything in this directory, run \"knife delete --recurse .\"")
+ exit 1
+ end
+
+ # Get the matches (recursively)
+ error = false
+ if config[:local]
+ pattern_args.each do |pattern|
+ Chef::ChefFS::FileSystem.list(local_fs, pattern).each do |result|
+ if delete_result(result)
+ error = true
+ end
+ end
+ end
+ elsif config[:both]
+ pattern_args.each do |pattern|
+ Chef::ChefFS::FileSystem.list_pairs(pattern, chef_fs, local_fs).each do |chef_result, local_result|
+ if delete_result(chef_result, local_result)
+ error = true
+ end
+ end
+ end
+ else # Remote only
+ pattern_args.each do |pattern|
+ Chef::ChefFS::FileSystem.list(chef_fs, pattern).each do |result|
+ if delete_result(result)
+ error = true
+ end
+ end
+ end
+ end
+
+ if error
+ exit 1
+ end
+ end
+
+ def format_path_with_root(entry)
+ root = entry.root == chef_fs ? " (remote)" : " (local)"
+ "#{format_path(entry)}#{root}"
+ end
+
+ def delete_result(*results)
+ deleted_any = false
+ found_any = false
+ error = false
+ results.each do |result|
+
+ result.delete(config[:recurse])
+ deleted_any = true
+ found_any = true
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ # This is not an error unless *all* of them were not found
+ rescue Chef::ChefFS::FileSystem::MustDeleteRecursivelyError => e
+ ui.error "#{format_path_with_root(e.entry)} must be deleted recursively! Pass -r to knife delete."
+ found_any = true
+ error = true
+ rescue Chef::ChefFS::FileSystem::OperationNotAllowedError => e
+ ui.error "#{format_path_with_root(e.entry)} #{e.reason}."
+ found_any = true
+ error = true
+
+ end
+ if deleted_any
+ output("Deleted #{format_path(results[0])}")
+ elsif !found_any
+ ui.error "#{format_path(results[0])}: No such file or directory"
+ error = true
+ end
+ error
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/deps.rb b/knife/lib/chef/knife/deps.rb
new file mode 100644
index 0000000000..fd419f15f9
--- /dev/null
+++ b/knife/lib/chef/knife/deps.rb
@@ -0,0 +1,156 @@
+#
+# 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_relative "../chef_fs/knife" unless defined?(Chef::ChefFS::Knife)
+
+class Chef
+ class Knife
+ class Deps < Chef::ChefFS::Knife
+ banner "knife deps PATTERN1 [PATTERNn]"
+
+ category "path-based"
+
+ deps do
+ require "chef/chef_fs/file_system" unless defined?(Chef::ChefFS::FileSystem)
+ require "chef/run_list" unless defined?(Chef::RunList)
+ end
+
+ option :recurse,
+ long: "--[no-]recurse",
+ boolean: true,
+ description: "List dependencies recursively (default: true). Only works with --tree."
+
+ option :tree,
+ long: "--tree",
+ boolean: true,
+ description: "Show dependencies in a visual tree. May show duplicates."
+
+ option :remote,
+ long: "--remote",
+ boolean: true,
+ description: "List dependencies on the server instead of the local filesystem."
+
+ attr_accessor :exit_code
+
+ def run
+ if config[:recurse] == false && !config[:tree]
+ ui.error "--no-recurse requires --tree"
+ exit(1)
+ end
+ config[:recurse] = true if config[:recurse].nil?
+
+ @root = config[:remote] ? chef_fs : local_fs
+ dependencies = {}
+ pattern_args.each do |pattern|
+ Chef::ChefFS::FileSystem.list(@root, pattern).each do |entry|
+ if config[:tree]
+ print_dependencies_tree(entry, dependencies)
+ else
+ print_flattened_dependencies(entry, dependencies)
+ end
+ end
+ end
+ exit exit_code if exit_code
+ end
+
+ def print_flattened_dependencies(entry, dependencies)
+ unless dependencies[entry.path]
+ dependencies[entry.path] = get_dependencies(entry)
+ dependencies[entry.path].each do |child|
+ child_entry = Chef::ChefFS::FileSystem.resolve_path(@root, child)
+ print_flattened_dependencies(child_entry, dependencies)
+ end
+ output format_path(entry)
+ end
+ end
+
+ def print_dependencies_tree(entry, dependencies, printed = {}, depth = 0)
+ dependencies[entry.path] = get_dependencies(entry) unless dependencies[entry.path]
+ output "#{" " * depth}#{format_path(entry)}"
+ if !printed[entry.path] && (config[:recurse] || depth == 0)
+ printed[entry.path] = true
+ dependencies[entry.path].each do |child|
+ child_entry = Chef::ChefFS::FileSystem.resolve_path(@root, child)
+ print_dependencies_tree(child_entry, dependencies, printed, depth + 1)
+ end
+ end
+ end
+
+ def get_dependencies(entry)
+ if entry.parent && entry.parent.path == "/cookbooks"
+ entry.chef_object.metadata.dependencies.keys.map { |cookbook| "/cookbooks/#{cookbook}" }
+
+ elsif entry.parent && entry.parent.path == "/nodes"
+ node = Chef::JSONCompat.parse(entry.read)
+ result = []
+ if node["chef_environment"] && node["chef_environment"] != "_default"
+ result << "/environments/#{node["chef_environment"]}.json"
+ end
+ if node["run_list"]
+ result += dependencies_from_runlist(node["run_list"])
+ end
+ result
+
+ elsif entry.parent && entry.parent.path == "/roles"
+ role = Chef::JSONCompat.parse(entry.read)
+ result = []
+ if role["run_list"]
+ dependencies_from_runlist(role["run_list"]).each do |dependency|
+ result << dependency unless result.include?(dependency)
+ end
+ end
+ if role["env_run_lists"]
+ role["env_run_lists"].each_pair do |env, run_list|
+ dependencies_from_runlist(run_list).each do |dependency|
+ result << dependency unless result.include?(dependency)
+ end
+ end
+ end
+ result
+
+ elsif !entry.exists?
+ raise Chef::ChefFS::FileSystem::NotFoundError.new(entry)
+
+ else
+ []
+ end
+ rescue Chef::ChefFS::FileSystem::NotFoundError => e
+ ui.error "#{format_path(e.entry)}: No such file or directory"
+ self.exit_code = 2
+ []
+ end
+
+ def dependencies_from_runlist(run_list)
+ chef_run_list = Chef::RunList.new
+ chef_run_list.reset!(run_list)
+ chef_run_list.map do |run_list_item|
+ case run_list_item.type
+ when :role
+ "/roles/#{run_list_item.name}.json"
+ when :recipe
+ if run_list_item.name =~ /(.+)::[^:]*/
+ "/cookbooks/#{$1}"
+ else
+ "/cookbooks/#{run_list_item.name}"
+ end
+ else
+ raise "Unknown run list item type #{run_list_item.type}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/diff.rb b/knife/lib/chef/knife/diff.rb
new file mode 100644
index 0000000000..971f7aa7f4
--- /dev/null
+++ b/knife/lib/chef/knife/diff.rb
@@ -0,0 +1,83 @@
+#
+# 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_relative "../chef_fs/knife"
+
+class Chef
+ class Knife
+ class Diff < Chef::ChefFS::Knife
+ banner "knife diff PATTERNS"
+
+ category "path-based"
+
+ deps do
+ require "chef/chef_fs/command_line" unless defined?(Chef::ChefFS::CommandLine)
+ end
+
+ 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."
+
+ option :diff_filter,
+ long: "--diff-filter=[(A|D|M|T)...[*]]",
+ description: "Select only files that are Added (A), Deleted (D), Modified (M), or have their type (i.e. regular file, directory) changed (T). Any combination of the filter characters (including none) can be used. When * (All-or-none) is added to the combination, all paths are selected if there is any file that matches other criteria in the comparison; if there is no file that matches other criteria, nothing is selected."
+
+ option :cookbook_version,
+ long: "--cookbook-version VERSION",
+ description: "Version of cookbook to download (if there are multiple versions and cookbook_versions is false)."
+
+ 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)
+ error = false
+ begin
+ patterns.each do |pattern|
+ found_error = Chef::ChefFS::CommandLine.diff_print(pattern, chef_fs, local_fs, config[:recurse] ? nil : 1, output_mode, proc { |entry| format_path(entry) }, config[:diff_filter], ui ) do |diff|
+ stdout.print diff
+ end
+ error = true if found_error
+ end
+ rescue Chef::ChefFS::FileSystem::OperationFailedError => e
+ ui.error "Failed on #{format_path(e.entry)} in #{e.operation}: #{e.message}"
+ error = true
+ end
+
+ if error
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/download.rb b/knife/lib/chef/knife/download.rb
new file mode 100644
index 0000000000..2eda642979
--- /dev/null
+++ b/knife/lib/chef/knife/download.rb
@@ -0,0 +1,84 @@
+#
+# 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_relative "../chef_fs/knife"
+
+class Chef
+ class Knife
+ class Download < Chef::ChefFS::Knife
+ banner "knife download PATTERNS"
+
+ category "path-based"
+
+ deps do
+ require "chef/chef_fs/command_line" unless defined?(Chef::ChefFS::CommandLine)
+ end
+
+ 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 download 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."
+
+ option :diff,
+ long: "--[no-]diff",
+ boolean: true,
+ default: true,
+ description: "Turn off to avoid downloading existing files; only new (and possibly deleted) files with --no-diff."
+
+ option :cookbook_version,
+ long: "--cookbook-version VERSION",
+ description: "Version of cookbook to download (if there are multiple versions and cookbook_versions is false)."
+
+ def run
+ if name_args.length == 0
+ show_usage
+ ui.fatal("You must specify at least one argument. If you want to download everything in this directory, run \"knife download .\"")
+ exit 1
+ end
+
+ error = false
+ pattern_args.each do |pattern|
+ if Chef::ChefFS::FileSystem.copy_to(pattern, chef_fs, local_fs, config[:recurse] ? nil : 1, config, ui, proc { |entry| format_path(entry) })
+ error = true
+ end
+ end
+ if error
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/edit.rb b/knife/lib/chef/knife/edit.rb
new file mode 100644
index 0000000000..45702d168b
--- /dev/null
+++ b/knife/lib/chef/knife/edit.rb
@@ -0,0 +1,88 @@
+#
+# 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_relative "../chef_fs/knife"
+
+class Chef
+ class Knife
+ class Edit < Chef::ChefFS::Knife
+ banner "knife edit [PATTERN1 ... PATTERNn]"
+
+ category "path-based"
+
+ deps do
+ require "chef/chef_fs/file_system" unless defined?(Chef::ChefFS::FileSystem)
+ require "chef/chef_fs/file_system/exceptions" unless defined?(Chef::ChefFS::FileSystem::Exceptions)
+ end
+
+ option :local,
+ long: "--local",
+ boolean: true,
+ description: "Show local files instead of remote."
+
+ def run
+ # Get the matches (recursively)
+ error = false
+ pattern_args.each do |pattern|
+ Chef::ChefFS::FileSystem.list(config[:local] ? local_fs : chef_fs, pattern).each do |result|
+ if result.dir?
+ ui.error "#{format_path(result)}: is a directory" if pattern.exact_path
+ error = true
+ else
+ begin
+ new_value = edit_text(result.read, File.extname(result.name))
+ if new_value
+ result.write(new_value)
+ output "Updated #{format_path(result)}"
+ else
+ output "#{format_path(result)} unchanged"
+ end
+ rescue Chef::ChefFS::FileSystem::OperationNotAllowedError => e
+ ui.error "#{format_path(e.entry)}: #{e.reason}."
+ error = true
+ rescue Chef::ChefFS::FileSystem::NotFoundError => e
+ ui.error "#{format_path(e.entry)}: No such file or directory"
+ error = true
+ end
+ end
+ end
+ end
+ if error
+ exit 1
+ end
+ end
+
+ def edit_text(text, extension)
+ unless config[:disable_editing]
+ Tempfile.open([ "knife-edit-", extension ]) do |file|
+ # Write the text to a temporary file
+ file.write(text)
+ file.close
+
+ # Let the user edit the temporary file
+ unless system("#{config[:editor]} #{file.path}")
+ raise "Please set EDITOR environment variable. See https://docs.chef.io/knife_setup/ for details."
+ end
+
+ result_text = IO.read(file.path)
+
+ return result_text if result_text != text
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/environment_compare.rb b/knife/lib/chef/knife/environment_compare.rb
new file mode 100644
index 0000000000..532d1fc159
--- /dev/null
+++ b/knife/lib/chef/knife/environment_compare.rb
@@ -0,0 +1,128 @@
+#
+# Author:: Sander Botman (<sbotman@schubergphilis.com>)
+# Copyright:: Copyright 2013-2016, Sander Botman.
+# 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_relative "../knife"
+
+class Chef
+ class Knife
+ class EnvironmentCompare < Knife
+
+ deps do
+ require "chef/environment" unless defined?(Chef::Environment)
+ end
+
+ banner "knife environment compare [ENVIRONMENT..] (options)"
+
+ option :all,
+ short: "-a",
+ long: "--all",
+ description: "Show all cookbooks.",
+ boolean: true
+
+ option :mismatch,
+ short: "-m",
+ long: "--mismatch",
+ description: "Only show mismatching versions.",
+ boolean: true
+
+ def run
+ # Get the commandline environments or all if none are provided.
+ environments = environment_list
+
+ # Get a list of all cookbooks that have constraints and their environment.
+ constraints = constraint_list(environments)
+
+ # Get the total list of cookbooks that have constraints
+ cookbooks = cookbook_list(constraints)
+
+ # If we cannot find any cookbooks, we can stop here.
+ if cookbooks.nil? || cookbooks.empty?
+ ui.error "Cannot find any environment cookbook constraints"
+ exit 1
+ end
+
+ # Get all cookbooks so we can compare them all
+ cookbooks = rest.get("/cookbooks?num_versions=1") if config[:all]
+
+ # display matrix view of in the requested format.
+ if config[:format] == "summary"
+ matrix = matrix_output(cookbooks, constraints)
+ ui.output(matrix)
+ else
+ ui.output(constraints)
+ end
+ end
+
+ private
+
+ def environment_list
+ environments = []
+ unless @name_args.nil? || @name_args.empty?
+ @name_args.each { |name| environments << name }
+ else
+ environments = Chef::Environment.list
+ end
+ end
+
+ def constraint_list(environments)
+ constraints = {}
+ environments.each do |env, url| # rubocop:disable Style/HashEachMethods
+ # Because you cannot modify the default environment I filter it out here.
+ unless env == "_default"
+ envdata = Chef::Environment.load(env)
+ ver = envdata.cookbook_versions
+ constraints[env] = ver
+ end
+ end
+ constraints
+ end
+
+ def cookbook_list(constraints)
+ result = {}
+ constraints.each_value { |cb| result.merge!(cb) }
+ result
+ end
+
+ def matrix_output(cookbooks, constraints)
+ rows = [ "" ]
+ environments = []
+ constraints.each_key { |e| environments << e.to_s }
+ columns = environments.count + 1
+ environments.each { |env| rows << ui.color(env, :bold) }
+ cookbooks.each_key do |c|
+ total = []
+ environments.each { |n| total << constraints[n][c] }
+ if total.uniq.count == 1
+ next if config[:mismatch]
+
+ color = :white
+ else
+ color = :yellow
+ end
+ rows << ui.color(c, :bold)
+ environments.each do |e|
+ tag = constraints[e][c] || "latest"
+ rows << ui.color(tag, color)
+ end
+ end
+ ui.list(rows, :uneven_columns_across, columns)
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/environment_create.rb b/knife/lib/chef/knife/environment_create.rb
new file mode 100644
index 0000000000..cfb36957d4
--- /dev/null
+++ b/knife/lib/chef/knife/environment_create.rb
@@ -0,0 +1,52 @@
+#
+# Author:: Stephen Delano (<stephen@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class EnvironmentCreate < Knife
+
+ deps do
+ require "chef/environment" unless defined?(Chef::Environment)
+ 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, object_class: Chef::Environment)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/environment_delete.rb b/knife/lib/chef/knife/environment_delete.rb
new file mode 100644
index 0000000000..65e5a1eb5c
--- /dev/null
+++ b/knife/lib/chef/knife/environment_delete.rb
@@ -0,0 +1,44 @@
+#
+# Author:: Stephen Delano (<stephen@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class EnvironmentDelete < Knife
+
+ deps do
+ require "chef/environment" unless defined?(Chef::Environment)
+ 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/knife/lib/chef/knife/environment_edit.rb b/knife/lib/chef/knife/environment_edit.rb
new file mode 100644
index 0000000000..f2ad842069
--- /dev/null
+++ b/knife/lib/chef/knife/environment_edit.rb
@@ -0,0 +1,44 @@
+#
+# Author:: Stephen Delano (<stephen@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class EnvironmentEdit < Knife
+
+ deps do
+ require "chef/environment" unless defined?(Chef::Environment)
+ 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/knife/lib/chef/knife/environment_from_file.rb b/knife/lib/chef/knife/environment_from_file.rb
new file mode 100644
index 0000000000..4b84abd073
--- /dev/null
+++ b/knife/lib/chef/knife/environment_from_file.rb
@@ -0,0 +1,84 @@
+#
+# Author:: Stephen Delano (<stephen@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class EnvironmentFromFile < Knife
+
+ deps do
+ require "chef/environment" unless defined?(Chef::Environment)
+ require_relative "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/knife/lib/chef/knife/environment_list.rb b/knife/lib/chef/knife/environment_list.rb
new file mode 100644
index 0000000000..7076670fb5
--- /dev/null
+++ b/knife/lib/chef/knife/environment_list.rb
@@ -0,0 +1,41 @@
+#
+# Author:: Stephen Delano (<stephen@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class EnvironmentList < Knife
+
+ deps do
+ require "chef/environment" unless defined?(Chef::Environment)
+ 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/knife/lib/chef/knife/environment_show.rb b/knife/lib/chef/knife/environment_show.rb
new file mode 100644
index 0000000000..0a4000151e
--- /dev/null
+++ b/knife/lib/chef/knife/environment_show.rb
@@ -0,0 +1,47 @@
+#
+# Author:: Stephen Delano (<stephen@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class EnvironmentShow < Knife
+
+ include Knife::Core::MultiAttributeReturnOption
+
+ deps do
+ require "chef/environment" unless defined?(Chef::Environment)
+ end
+
+ 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/knife/lib/chef/knife/exec.rb b/knife/lib/chef/knife/exec.rb
new file mode 100644
index 0000000000..b82476220e
--- /dev/null
+++ b/knife/lib/chef/knife/exec.rb
@@ -0,0 +1,99 @@
+#--
+# Author:: Daniel DeLeo (<dan@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef::Knife::Exec < Chef::Knife
+
+ banner "knife exec [SCRIPT] (options)"
+
+ deps do
+ require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+ end
+
+ option :exec,
+ short: "-E CODE",
+ long: "--exec CODE",
+ description: "A string of #{ChefUtils::Dist::Infra::PRODUCT} 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" unless defined?(Chef::Shell::Extensions)
+ end
+
+ def run
+ config[:script_path] = Array(config[:script_path] || 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
+ ChefConfig::PathHelper.home(".chef", "scripts") { |p| config[:script_path] << p }
+
+ 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
+ puts "An interactive shell is opened"
+ puts
+ puts "Type your script and do:"
+ puts
+ puts "1. To run the script, use 'Ctrl D'"
+ puts "2. To exit, use 'Ctrl/Shift C'"
+ puts
+ puts "Type here a script..."
+ 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.exist?(script)
+
+ # Failing that, try searching the script path. If we can't find
+ # anything, fail gracefully.
+ Chef::Log.trace("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.trace("Testing: #{test}")
+ if File.exist?(test)
+ script = test
+ Chef::Log.trace("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/knife/lib/chef/knife/group_add.rb b/knife/lib/chef/knife/group_add.rb
new file mode 100644
index 0000000000..eccb7dd10c
--- /dev/null
+++ b/knife/lib/chef/knife/group_add.rb
@@ -0,0 +1,55 @@
+#
+# Author:: Seth Falcon (<seth@chef.io>)
+# Author:: Jeremiah Snapp (<jeremiah@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class GroupAdd < Chef::Knife
+ category "group"
+ banner "knife group add MEMBER_TYPE MEMBER_NAME GROUP_NAME"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ member_type, member_name, group_name = name_args
+
+ if name_args.length != 3
+ show_usage
+ ui.fatal "You must specify member type [client|group|user], member name and group name"
+ exit 1
+ end
+
+ validate_member_name!(group_name)
+ validate_member_type!(member_type)
+ validate_member_name!(member_name)
+
+ if group_name.downcase == "users"
+ ui.fatal "knife group can not manage members of Chef Infra Server's 'users' group, which contains all users."
+ exit 1
+ end
+
+ add_to_group!(member_type, member_name, group_name)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/group_create.rb b/knife/lib/chef/knife/group_create.rb
new file mode 100644
index 0000000000..4219188951
--- /dev/null
+++ b/knife/lib/chef/knife/group_create.rb
@@ -0,0 +1,49 @@
+#
+# Author:: Seth Falcon (<seth@chef.io>)
+# Author:: Jeremiah Snapp (<jeremiah@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class GroupCreate < Chef::Knife
+ category "group"
+ banner "knife group create GROUP_NAME"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ group_name = name_args[0]
+
+ if name_args.length != 1
+ show_usage
+ ui.fatal "You must specify group name"
+ exit 1
+ end
+
+ validate_member_name!(group_name)
+
+ ui.msg "Creating '#{group_name}' group"
+ rest.post_rest("groups", { groupname: group_name })
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/group_destroy.rb b/knife/lib/chef/knife/group_destroy.rb
new file mode 100644
index 0000000000..433a5cc627
--- /dev/null
+++ b/knife/lib/chef/knife/group_destroy.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Christopher Maier (<cm@chef.io>)
+# Author:: Jeremiah Snapp (<jeremiah@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class GroupDestroy < Chef::Knife
+ category "group"
+ banner "knife group destroy GROUP_NAME"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ group_name = name_args[0]
+
+ if name_args.length != 1
+ show_usage
+ ui.fatal "You must specify group name"
+ exit 1
+ end
+
+ validate_member_name!(group_name)
+
+ if %w{admins billing-admins clients users}.include?(group_name.downcase)
+ ui.fatal "The '#{group_name}' group is a special group that cannot not be destroyed"
+ exit 1
+ end
+ ui.msg "Destroying '#{group_name}' group"
+ rest.delete_rest("groups/#{group_name}")
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/group_list.rb b/knife/lib/chef/knife/group_list.rb
new file mode 100644
index 0000000000..fc8f00ad6d
--- /dev/null
+++ b/knife/lib/chef/knife/group_list.rb
@@ -0,0 +1,43 @@
+#
+# Author:: Seth Falcon (<seth@chef.io>)
+# Author:: Jeremiah Snapp (<jeremiah@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class GroupList < Chef::Knife
+ category "group"
+ banner "knife group list"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ groups = rest.get_rest("groups").keys.sort
+ ui.output(remove_usags(groups))
+ end
+
+ def remove_usags(groups)
+ groups.select { |gname| !is_usag?(gname) }
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/group_remove.rb b/knife/lib/chef/knife/group_remove.rb
new file mode 100644
index 0000000000..07ab19693f
--- /dev/null
+++ b/knife/lib/chef/knife/group_remove.rb
@@ -0,0 +1,56 @@
+#
+# Author:: Seth Falcon (<seth@chef.io>)
+# Author:: Jeremiah Snapp (<jeremiah@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class GroupRemove < Chef::Knife
+ category "group"
+ banner "knife group remove MEMBER_TYPE MEMBER_NAME GROUP_NAME"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ member_type, member_name, group_name = name_args
+
+ if name_args.length != 3
+ show_usage
+ ui.fatal "You must specify member type [client|group|user], member name and group name"
+ exit 1
+ end
+
+ validate_member_name!(group_name)
+ validate_member_type!(member_type)
+ validate_member_name!(member_name)
+
+ if group_name.downcase == "users"
+ ui.fatal "knife-acl can not manage members of the Users group"
+ ui.fatal "please read knife-acl's README.md for more information"
+ exit 1
+ end
+
+ remove_from_group!(member_type, member_name, group_name)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/group_show.rb b/knife/lib/chef/knife/group_show.rb
new file mode 100644
index 0000000000..6ac53f6b6e
--- /dev/null
+++ b/knife/lib/chef/knife/group_show.rb
@@ -0,0 +1,49 @@
+#
+# Author:: Seth Falcon (<seth@chef.io>)
+# Author:: Jeremiah Snapp (<jeremiah@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class GroupShow < Chef::Knife
+ category "group"
+ banner "knife group show GROUP_NAME"
+
+ deps do
+ require_relative "acl_base"
+ include Chef::Knife::AclBase
+ end
+
+ def run
+ group_name = name_args[0]
+
+ if name_args.length != 1
+ show_usage
+ ui.fatal "You must specify group name"
+ exit 1
+ end
+
+ validate_member_name!(group_name)
+
+ group = rest.get_rest("groups/#{group_name}")
+ ui.output group
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/key_create.rb b/knife/lib/chef/knife/key_create.rb
new file mode 100644
index 0000000000..e1baf08bb6
--- /dev/null
+++ b/knife/lib/chef/knife/key_create.rb
@@ -0,0 +1,112 @@
+#
+# Author:: Tyler Cloke (<tyler@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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/key" unless defined?(Chef::Key)
+require "chef/json_compat" unless defined?(Chef::JSONCompat)
+require "chef/exceptions" unless defined?(Chef::Exceptions)
+
+class Chef
+ class Knife
+ # Service class for UserKeyCreate and ClientKeyCreate,
+ # Implements common functionality of knife [user | org client] key create.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_accessor [Hash] cli input, see UserKeyCreate and ClientKeyCreate for what could populate it
+ class KeyCreate
+
+ attr_accessor :config
+
+ def initialize(actor, actor_field_name, ui, config)
+ @actor = actor
+ @actor_field_name = actor_field_name
+ @ui = ui
+ @config = config
+ end
+
+ def public_key_or_key_name_error_msg
+ <<~EOS
+ You must pass either --public-key or --key-name, or both.
+ If you only pass --public-key, a key name will be generated from the fingerprint of your key.
+ If you only pass --key-name, a key pair will be generated by the server.
+ EOS
+ end
+
+ def edit_data(key)
+ @ui.edit_data(key)
+ end
+
+ def edit_hash(key)
+ @ui.edit_hash(key)
+ end
+
+ def display_info(input)
+ @ui.info(input)
+ end
+
+ def display_private_key(private_key)
+ @ui.msg(private_key)
+ end
+
+ def output_private_key_to_file(private_key)
+ File.open(@config[:file], "w") do |f|
+ f.print(private_key)
+ end
+ end
+
+ def create_key_from_hash(output)
+ Chef::Key.from_hash(output).create
+ end
+
+ def run
+ key = Chef::Key.new(@actor, @actor_field_name)
+ if !@config[:public_key] && !@config[:key_name]
+ raise Chef::Exceptions::KeyCommandInputError, public_key_or_key_name_error_msg
+ elsif !@config[:public_key]
+ key.create_key(true)
+ end
+
+ if @config[:public_key]
+ key.public_key(File.read(File.expand_path(@config[:public_key])))
+ end
+
+ if @config[:key_name]
+ key.name(@config[:key_name])
+ end
+
+ if @config[:expiration_date]
+ key.expiration_date(@config[:expiration_date])
+ else
+ key.expiration_date("infinity")
+ end
+
+ output = edit_hash(key)
+ key = create_key_from_hash(output)
+
+ display_info("Created key: #{key.name}")
+ if key.private_key
+ if @config[:file]
+ output_private_key_to_file(key.private_key)
+ else
+ display_private_key(key.private_key)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/key_create_base.rb b/knife/lib/chef/knife/key_create_base.rb
new file mode 100644
index 0000000000..a1d658e43c
--- /dev/null
+++ b/knife/lib/chef/knife/key_create_base.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Tyler Cloke (<tyler@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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
+ # Extendable module that class_eval's common options into UserKeyCreate and ClientKeyCreate
+ #
+ # @author Tyler Cloke
+ module KeyCreateBase
+ def self.included(includer)
+ includer.class_eval do
+ option :public_key,
+ short: "-p FILENAME",
+ long: "--public-key FILENAME",
+ description: "Public key for newly created key. If not passed, the server will create a key pair for you, but you must pass --key-name NAME in that case."
+
+ option :file,
+ short: "-f FILE",
+ long: "--file FILE",
+ description: "Write the private key to a file, if you requested the server to create one."
+
+ option :key_name,
+ short: "-k NAME",
+ long: "--key-name NAME",
+ description: "The name for your key. If you do not pass a name, you must pass --public-key, and the name will default to the fingerprint of the public key passed."
+
+ option :expiration_date,
+ short: "-e DATE",
+ long: "--expiration-date DATE",
+ description: "Optionally pass the expiration date for the key in ISO 8601 formatted string: YYYY-MM-DDTHH:MM:SSZ e.g. 2013-12-24T21:00:00Z. Defaults to infinity if not passed. UTC timezone assumed."
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/key_delete.rb b/knife/lib/chef/knife/key_delete.rb
new file mode 100644
index 0000000000..83b6a8b535
--- /dev/null
+++ b/knife/lib/chef/knife/key_delete.rb
@@ -0,0 +1,55 @@
+#
+# Author:: Tyler Cloke (<tyler@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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/key" unless defined?(Chef::Key)
+
+class Chef
+ class Knife
+ # Service class for UserKeyDelete and ClientKeyDelete, used to delete keys.
+ # Implements common functionality of knife [user | org client] key delete.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_accessor [Hash] cli input, see UserKeyDelete and ClientKeyDelete for what could populate it
+ class KeyDelete
+ def initialize(name, actor, actor_field_name, ui)
+ @name = name
+ @actor = actor
+ @actor_field_name = actor_field_name
+ @ui = ui
+ end
+
+ def confirm!
+ @ui.confirm("Do you really want to delete the key named #{@name} for the #{@actor_field_name} named #{@actor}")
+ end
+
+ def print_destroyed
+ @ui.info("Deleted key named #{@name} for the #{@actor_field_name} named #{@actor}")
+ end
+
+ def run
+ key = Chef::Key.new(@actor, @actor_field_name)
+ key.name(@name)
+ confirm!
+ key.destroy
+ print_destroyed
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/key_edit.rb b/knife/lib/chef/knife/key_edit.rb
new file mode 100644
index 0000000000..25d7b28437
--- /dev/null
+++ b/knife/lib/chef/knife/key_edit.rb
@@ -0,0 +1,118 @@
+#
+# Author:: Tyler Cloke (<tyler@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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/key" unless defined?(Chef::Key)
+require "chef/json_compat" unless defined?(Chef::JSONCompat)
+require "chef/exceptions" unless defined?(Chef::Exceptions)
+
+class Chef
+ class Knife
+ # Service class for UserKeyEdit and ClientKeyEdit,
+ # Implements common functionality of knife [user | org client] key edit.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_accessor [Hash] cli input, see UserKeyEdit and ClientKeyEdit for what could populate it
+ class KeyEdit
+
+ attr_accessor :config
+
+ def initialize(original_name, actor, actor_field_name, ui, config)
+ @original_name = original_name
+ @actor = actor
+ @actor_field_name = actor_field_name
+ @ui = ui
+ @config = config
+ end
+
+ def public_key_and_create_key_error_msg
+ <<~EOS
+ You passed both --public-key and --create-key. Only pass one, or the other, or neither.
+ Do not pass either if you do not want to change the public_key field of your key.
+ Pass --public-key if you want to update the public_key field of your key from a specific public key.
+ Pass --create-key if you want the server to generate a new key and use that to update the public_key field of your key.
+ EOS
+ end
+
+ def edit_data(key)
+ @ui.edit_data(key)
+ end
+
+ def edit_hash(key)
+ @ui.edit_hash(key)
+ end
+
+ def display_info(input)
+ @ui.info(input)
+ end
+
+ def display_private_key(private_key)
+ @ui.msg(private_key)
+ end
+
+ def output_private_key_to_file(private_key)
+ File.open(@config[:file], "w") do |f|
+ f.print(private_key)
+ end
+ end
+
+ def update_key_from_hash(output)
+ Chef::Key.from_hash(output).update(@original_name)
+ end
+
+ def run
+ key = Chef::Key.new(@actor, @actor_field_name)
+ if @config[:public_key] && @config[:create_key]
+ raise Chef::Exceptions::KeyCommandInputError, public_key_and_create_key_error_msg
+ end
+
+ if @config[:create_key]
+ key.create_key(true)
+ end
+
+ if @config[:public_key]
+ key.public_key(File.read(File.expand_path(@config[:public_key])))
+ end
+
+ if @config[:key_name]
+ key.name(@config[:key_name])
+ else
+ key.name(@original_name)
+ end
+
+ if @config[:expiration_date]
+ key.expiration_date(@config[:expiration_date])
+ end
+
+ output = edit_hash(key)
+ key = update_key_from_hash(output)
+
+ to_display = "Updated key: #{key.name}"
+ to_display << " (formally #{@original_name})" if key.name != @original_name
+ display_info(to_display)
+ if key.private_key
+ if @config[:file]
+ output_private_key_to_file(key.private_key)
+ else
+ display_private_key(key.private_key)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/key_edit_base.rb b/knife/lib/chef/knife/key_edit_base.rb
new file mode 100644
index 0000000000..b094877190
--- /dev/null
+++ b/knife/lib/chef/knife/key_edit_base.rb
@@ -0,0 +1,55 @@
+#
+# Author:: Tyler Cloke (<tyler@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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
+ # Extendable module that class_eval's common options into UserKeyEdit and ClientKeyEdit
+ #
+ # @author Tyler Cloke
+ module KeyEditBase
+ def self.included(includer)
+ includer.class_eval do
+ option :public_key,
+ short: "-p FILENAME",
+ long: "--public-key FILENAME",
+ description: "Replace the public_key field from a file on disk. If not passed, the public_key field will not change."
+
+ option :create_key,
+ short: "-c",
+ long: "--create-key",
+ description: "Replace the public_key field with a key generated by the server. The private key will be returned."
+
+ option :file,
+ short: "-f FILE",
+ long: "--file FILE",
+ description: "Write the private key to a file, if you requested the server to create one via --create-key."
+
+ option :key_name,
+ short: "-k NAME",
+ long: "--key-name NAME",
+ description: "The new name for your key. Pass if you wish to update the name field of your key."
+
+ option :expiration_date,
+ short: "-e DATE",
+ long: "--expiration-date DATE",
+ description: "Updates the expiration_date field of your key if passed. Pass in ISO 8601 formatted string: YYYY-MM-DDTHH:MM:SSZ e.g. 2013-12-24T21:00:00Z or infinity. UTC timezone assumed."
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/key_list.rb b/knife/lib/chef/knife/key_list.rb
new file mode 100644
index 0000000000..e01e2807cf
--- /dev/null
+++ b/knife/lib/chef/knife/key_list.rb
@@ -0,0 +1,90 @@
+#
+# Author:: Tyler Cloke (<tyler@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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/key" unless defined?(Chef::Key)
+require "chef/json_compat" unless defined?(Chef::JSONCompat)
+require "chef/exceptions" unless defined?(Chef::Exceptions)
+
+class Chef
+ class Knife
+ # Service class for UserKeyList and ClientKeyList, used to list keys.
+ # Implements common functionality of knife [user | org client] key list.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_accessor [Hash] cli input, see UserKeyList and ClientKeyList for what could populate it
+ class KeyList
+
+ attr_accessor :config
+
+ def initialize(actor, list_method, ui, config)
+ @actor = actor
+ @list_method = list_method
+ @ui = ui
+ @config = config
+ end
+
+ def expired_and_non_expired_msg
+ <<~EOS
+ You cannot pass both --only-expired and --only-non-expired.
+ Please pass one or none.
+ EOS
+ end
+
+ def display_info(string)
+ @ui.output(string)
+ end
+
+ def colorize(string)
+ @ui.color(string, :cyan)
+ end
+
+ def run
+ if @config[:only_expired] && @config[:only_non_expired]
+ raise Chef::Exceptions::KeyCommandInputError, expired_and_non_expired_msg
+ end
+
+ # call proper list function
+ keys = Chef::Key.send(@list_method, @actor)
+ if @config[:with_details]
+ max_length = 0
+ keys.each do |key|
+ key["name"] = key["name"] + ":"
+ max_length = key["name"].length if key["name"].length > max_length
+ end
+ keys.each do |key|
+ next if !key["expired"] && @config[:only_expired]
+ next if key["expired"] && @config[:only_non_expired]
+
+ display = "#{colorize(key["name"].ljust(max_length))} #{key["uri"]}"
+ display = "#{display} (expired)" if key["expired"]
+ display_info(display)
+ end
+ else
+ keys.each do |key|
+ next if !key["expired"] && @config[:only_expired]
+ next if key["expired"] && @config[:only_non_expired]
+
+ display_info(key["name"])
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/key_list_base.rb b/knife/lib/chef/knife/key_list_base.rb
new file mode 100644
index 0000000000..e06e908b69
--- /dev/null
+++ b/knife/lib/chef/knife/key_list_base.rb
@@ -0,0 +1,45 @@
+#
+# Author:: Tyler Cloke (<tyler@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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
+ # Extendable module that class_eval's common options into UserKeyList and ClientKeyList
+ #
+ # @author Tyler Cloke
+ module KeyListBase
+ def self.included(includer)
+ includer.class_eval do
+ option :with_details,
+ short: "-w",
+ long: "--with-details",
+ description: "Show corresponding URIs and whether the key has expired or not."
+
+ option :only_expired,
+ short: "-e",
+ long: "--only-expired",
+ description: "Only show expired keys."
+
+ option :only_non_expired,
+ short: "-n",
+ long: "--only-non-expired",
+ description: "Only show non-expired keys."
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/key_show.rb b/knife/lib/chef/knife/key_show.rb
new file mode 100644
index 0000000000..719e79fc17
--- /dev/null
+++ b/knife/lib/chef/knife/key_show.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Tyler Cloke (<tyler@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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/key" unless defined?(Chef::Key)
+require "chef/json_compat" unless defined?(Chef::JSONCompat)
+require "chef/exceptions" unless defined?(Chef::Exceptions)
+
+class Chef
+ class Knife
+ # Service class for UserKeyShow and ClientKeyShow, used to show keys.
+ # Implements common functionality of knife [user | org client] key show.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_accessor [Hash] cli input, see UserKeyShow and ClientKeyShow for what could populate it
+ class KeyShow
+
+ attr_accessor :config
+
+ def initialize(name, actor, load_method, ui)
+ @name = name
+ @actor = actor
+ @load_method = load_method
+ @ui = ui
+ end
+
+ def display_output(key)
+ @ui.output(@ui.format_for_display(key))
+ end
+
+ def run
+ key = Chef::Key.send(@load_method, @actor, @name)
+ key.public_key(key.public_key.strip)
+ display_output(key)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/list.rb b/knife/lib/chef/knife/list.rb
new file mode 100644
index 0000000000..7fc2231c5f
--- /dev/null
+++ b/knife/lib/chef/knife/list.rb
@@ -0,0 +1,177 @@
+#
+# 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_relative "../chef_fs/knife"
+
+class Chef
+ class Knife
+ class List < Chef::ChefFS::Knife
+ banner "knife list [-dfR1p] [PATTERN1 ... PATTERNn] (options)"
+
+ category "path-based"
+
+ deps do
+ require "chef/chef_fs/file_system" unless defined?(Chef::ChefFS::FileSystem)
+ require "tty-screen"
+ end
+
+ 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."
+
+ option :local,
+ long: "--local",
+ boolean: true,
+ description: "List local directory instead of remote."
+
+ option :flat,
+ short: "-f",
+ long: "--flat",
+ boolean: true,
+ description: "Show a list of filenames rather than the prettified ls-like output normally produced."
+
+ option :one_column,
+ short: "-1",
+ boolean: true,
+ description: "Show only one column of results."
+
+ option :trailing_slashes,
+ short: "-p",
+ boolean: true,
+ description: "Show trailing slashes after directories."
+
+ attr_accessor :exit_code
+
+ def run
+ patterns = name_args.length == 0 ? [""] : name_args
+
+ # Get the top-level matches
+ all_results = parallelize(pattern_args_from(patterns)) do |pattern|
+ pattern_results = Chef::ChefFS::FileSystem.list(config[:local] ? local_fs : chef_fs, pattern).to_a
+
+ if pattern_results.first && !pattern_results.first.exists? && pattern.exact_path
+ ui.error "#{format_path(pattern_results.first)}: No such file or directory"
+ self.exit_code = 1
+ end
+ pattern_results
+ end.flatten(1).to_a
+
+ # Process directories
+ if !config[:bare_directories]
+ dir_results = parallelize(all_results.select(&:dir?)) do |result|
+ add_dir_result(result)
+ end.flatten(1)
+
+ else
+ dir_results = []
+ end
+
+ # Process all other results
+ results = all_results.select { |result| result.exists? && (!result.dir? || config[:bare_directories]) }.to_a
+
+ # Flatten out directory results if necessary
+ if config[:flat]
+ dir_results.each do |result, children| # rubocop:disable Style/HashEachMethods
+ results += children
+ end
+ dir_results = []
+ end
+
+ # Sort by path for happy output
+ results = results.sort_by(&:path)
+ dir_results = dir_results.sort_by { |result| result[0].path }
+
+ # Print!
+ if results.length == 0 && dir_results.length == 1
+ results = dir_results[0][1]
+ dir_results = []
+ end
+
+ print_result_paths results
+ printed_something = results.length > 0
+ dir_results.each do |result, children|
+ if printed_something
+ output ""
+ else
+ printed_something = true
+ end
+ output "#{format_path(result)}:"
+ print_results(children.map { |result| maybe_add_slash(result.display_name, result.dir?) }.sort, "")
+ end
+
+ exit exit_code if exit_code
+ end
+
+ def add_dir_result(result)
+ begin
+ children = result.children.sort_by(&:name)
+ rescue Chef::ChefFS::FileSystem::NotFoundError => e
+ ui.error "#{format_path(e.entry)}: No such file or directory"
+ return []
+ end
+
+ result = [ [ result, children ] ]
+ if config[:recursive]
+ child_dirs = children.select(&:dir?)
+ result += parallelize(child_dirs) { |child| add_dir_result(child) }.flatten(1).to_a
+ end
+ result
+ end
+
+ def print_result_paths(results, indent = "")
+ print_results(results.map { |result| maybe_add_slash(format_path(result), result.dir?) }, indent)
+ end
+
+ def print_results(results, indent)
+ return if results.length == 0
+
+ print_space = results.map(&:length).max + 2
+ if config[:one_column] || !stdout.isatty
+ columns = 0
+ else
+ columns = TTY::Screen.columns
+ end
+ current_line = ""
+ results.each do |result|
+ if current_line.length > 0 && current_line.length + print_space > columns
+ output current_line.rstrip
+ current_line = ""
+ end
+ if current_line.length == 0
+ current_line << indent
+ end
+ current_line << result
+ current_line << (" " * (print_space - result.length))
+ end
+ output current_line.rstrip if current_line.length > 0
+ end
+
+ def maybe_add_slash(path, is_dir)
+ if config[:trailing_slashes] && is_dir
+ "#{path}/"
+ else
+ path
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/node_bulk_delete.rb b/knife/lib/chef/knife/node_bulk_delete.rb
new file mode 100644
index 0000000000..73975eebc7
--- /dev/null
+++ b/knife/lib/chef/knife/node_bulk_delete.rb
@@ -0,0 +1,75 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodeBulkDelete < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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&.match?(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/knife/lib/chef/knife/node_create.rb b/knife/lib/chef/knife/node_create.rb
new file mode 100644
index 0000000000..ed82cbe7aa
--- /dev/null
+++ b/knife/lib/chef/knife/node_create.rb
@@ -0,0 +1,47 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodeCreate < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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, object_class: Chef::Node)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/node_delete.rb b/knife/lib/chef/knife/node_delete.rb
new file mode 100644
index 0000000000..605d99b57f
--- /dev/null
+++ b/knife/lib/chef/knife/node_delete.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodeDelete < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife node delete [NODE [NODE]] (options)"
+
+ def run
+ if @name_args.length == 0
+ show_usage
+ ui.fatal("You must specify at least one node name")
+ exit 1
+ end
+
+ @name_args.each do |node_name|
+ delete_object(Chef::Node, node_name)
+ end
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/node_edit.rb b/knife/lib/chef/knife/node_edit.rb
new file mode 100644
index 0000000000..ebc98f5bff
--- /dev/null
+++ b/knife/lib/chef/knife/node_edit.rb
@@ -0,0 +1,70 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+
+ class NodeEdit < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ require_relative "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/knife/lib/chef/knife/node_environment_set.rb b/knife/lib/chef/knife/node_environment_set.rb
new file mode 100644
index 0000000000..84d5b3969b
--- /dev/null
+++ b/knife/lib/chef/knife/node_environment_set.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Jimmy McCrory (<jimmy.mccrory@gmail.com>)
+# Copyright:: Copyright 2014-2016, Jimmy McCrory
+# 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodeEnvironmentSet < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ end
+
+ banner "knife node environment set NODE ENVIRONMENT"
+
+ def run
+ if @name_args.size < 2
+ ui.fatal "You must specify a node name and an environment."
+ show_usage
+ exit 1
+ else
+ @node_name = @name_args[0]
+ @environment = @name_args[1]
+ end
+
+ node = Chef::Node.load(@node_name)
+
+ node.chef_environment = @environment
+
+ node.save
+
+ config[:environment] = @environment
+ output(format_for_display(node))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/node_from_file.rb b/knife/lib/chef/knife/node_from_file.rb
new file mode 100644
index 0000000000..4f1935641a
--- /dev/null
+++ b/knife/lib/chef/knife/node_from_file.rb
@@ -0,0 +1,51 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodeFromFile < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ require_relative "core/object_loader"
+ end
+
+ banner "knife node from file FILE (options)"
+
+ def loader
+ @loader ||= Knife::Core::ObjectLoader.new(Chef::Node, ui)
+ end
+
+ def run
+ @name_args.each do |arg|
+ updated = loader.load_from("nodes", arg)
+
+ updated.save
+
+ output(format_for_display(updated)) if config[:print_after]
+
+ ui.info("Updated Node #{updated.name}")
+ end
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/node_list.rb b/knife/lib/chef/knife/node_list.rb
new file mode 100644
index 0000000000..6aae4a617d
--- /dev/null
+++ b/knife/lib/chef/knife/node_list.rb
@@ -0,0 +1,44 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodeList < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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/knife/lib/chef/knife/node_policy_set.rb b/knife/lib/chef/knife/node_policy_set.rb
new file mode 100644
index 0000000000..3f55529b3d
--- /dev/null
+++ b/knife/lib/chef/knife/node_policy_set.rb
@@ -0,0 +1,79 @@
+#
+# Author:: Piyush Awasthi (<piyush.awasthi@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodePolicySet < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife node policy set NODE POLICY_GROUP POLICY_NAME (options)"
+
+ def run
+ validate_node!
+ validate_options!
+ node = Chef::Node.load(@name_args[0])
+ set_policy(node)
+ if node.save
+ ui.info "Successfully set the policy on node #{node.name}"
+ else
+ ui.info "Error in updating node #{node.name}"
+ end
+ end
+
+ private
+
+ # Set policy name and group to node
+ def set_policy(node)
+ policy_group, policy_name = @name_args[1..]
+ node.policy_name = policy_name
+ node.policy_group = policy_group
+ end
+
+ # Validate policy name and policy group
+ def validate_options!
+ if incomplete_policyfile_options?
+ ui.error("Policy group and name must be specified together")
+ exit 1
+ end
+ true
+ end
+
+ # Validate node pass in CLI
+ def validate_node!
+ if @name_args[0].nil?
+ ui.error("You must specify a node name")
+ show_usage
+ exit 1
+ end
+ end
+
+ # True if one of policy_name or policy_group was given, but not both
+ def incomplete_policyfile_options?
+ policy_group, policy_name = @name_args[1..]
+ (policy_group.nil? || policy_name.nil? || @name_args[1..-1].size > 2)
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/node_run_list_add.rb b/knife/lib/chef/knife/node_run_list_add.rb
new file mode 100644
index 0000000000..695344496a
--- /dev/null
+++ b/knife/lib/chef/knife/node_run_list_add.rb
@@ -0,0 +1,104 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodeRunListAdd < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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."
+
+ option :before,
+ short: "-b ITEM",
+ long: "--before ITEM",
+ description: "Place the ENTRY in the run list before 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..].map do |entry|
+ entry.split(",").map(&:strip)
+ end.flatten
+ else
+ # Convert to array and remove the extra spaces
+ entries = @name_args[1].split(",").map(&:strip)
+ end
+
+ if config[:after] && config[:before]
+ ui.fatal("You cannot specify both --before and --after!")
+ exit 1
+ end
+
+ if config[:after]
+ add_to_run_list_after(node, entries, config[:after])
+ elsif config[:before]
+ add_to_run_list_before(node, entries, config[:before])
+ else
+ add_to_run_list_after(node, entries)
+ end
+
+ node.save
+
+ config[:run_list] = true
+
+ output(format_for_display(node))
+ end
+
+ private
+
+ def add_to_run_list_after(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
+
+ def add_to_run_list_before(node, entries, before)
+ nlist = []
+ node.run_list.each do |entry|
+ if entry == before
+ entries.each { |e| nlist << e }
+ end
+ nlist << entry
+ end
+ node.run_list.reset!(nlist)
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/node_run_list_remove.rb b/knife/lib/chef/knife/node_run_list_remove.rb
new file mode 100644
index 0000000000..0c88f8c184
--- /dev/null
+++ b/knife/lib/chef/knife/node_run_list_remove.rb
@@ -0,0 +1,67 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodeRunListRemove < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife node run_list remove [NODE] [ENTRY [ENTRY]] (options)"
+
+ 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..].map do |entry|
+ entry.split(",").map(&:strip)
+ end.flatten
+ else
+ # Convert to array and remove the extra spaces
+ entries = @name_args[1].split(",").map(&:strip)
+ end
+
+ # iterate over the list of things to remove,
+ # warning if one of them was not found
+ entries.each do |e|
+ if node.run_list.find { |rli| e == rli.to_s }
+ node.run_list.remove(e)
+ else
+ ui.warn "#{e} is not in the run list"
+ unless /^(recipe|role)\[/.match?(e)
+ ui.warn "(did you forget recipe[] or role[] around it?)"
+ end
+ end
+ end
+
+ node.save
+
+ config[:run_list] = true
+
+ output(format_for_display(node))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/node_run_list_set.rb b/knife/lib/chef/knife/node_run_list_set.rb
new file mode 100644
index 0000000000..37b9aef3d6
--- /dev/null
+++ b/knife/lib/chef/knife/node_run_list_set.rb
@@ -0,0 +1,66 @@
+#
+# Author:: Mike Fiedler (<miketheman@gmail.com>)
+# Copyright:: Copyright 2013-2016, Mike Fiedler
+# 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_relative "../knife"
+
+class Chef
+ class Knife
+ class NodeRunListSet < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife node run_list set NODE ENTRIES (options)"
+
+ def run
+ if @name_args.size < 2
+ ui.fatal "You must supply both a node name and a run list."
+ show_usage
+ exit 1
+ elsif @name_args.size > 2
+ # Check for nested lists and create a single plain one
+ entries = @name_args[1..].map do |entry|
+ entry.split(",").map(&:strip)
+ end.flatten
+ else
+ # Convert to array and remove the extra spaces
+ entries = @name_args[1].split(",").map(&:strip)
+ end
+ node = Chef::Node.load(@name_args[0])
+
+ set_run_list(node, entries)
+
+ node.save
+
+ config[:run_list] = true
+
+ output(format_for_display(node))
+ end
+
+ # Clears out any existing run_list_items and sets them to the
+ # specified entries
+ def set_run_list(node, entries)
+ node.run_list.run_list_items.clear
+ entries.each { |e| node.run_list << e }
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/node_show.rb b/knife/lib/chef/knife/node_show.rb
new file mode 100644
index 0000000000..bce2ee3fe9
--- /dev/null
+++ b/knife/lib/chef/knife/node_show.rb
@@ -0,0 +1,63 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "core/node_presenter"
+require_relative "core/formatting_options"
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ class Knife
+ class NodeShow < Knife
+
+ include Knife::Core::FormattingOptions
+ include Knife::Core::MultiAttributeReturnOption
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife node show NODE (options)"
+
+ option :run_list,
+ short: "-r",
+ long: "--run-list",
+ description: "Show only the run list."
+
+ option :environment,
+ short: "-E",
+ long: "--environment",
+ description: "Show only the #{ChefUtils::Dist::Infra::PRODUCT} 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))
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/null.rb b/knife/lib/chef/knife/null.rb
new file mode 100644
index 0000000000..7221eee9f5
--- /dev/null
+++ b/knife/lib/chef/knife/null.rb
@@ -0,0 +1,12 @@
+class Chef
+ class Knife
+ class Null < Chef::Knife
+ banner "knife null"
+
+ # setting the category to deprecated keeps it out of help
+ category "deprecated"
+
+ def run; end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/org_create.rb b/knife/lib/chef/knife/org_create.rb
new file mode 100644
index 0000000000..cb5ded26f5
--- /dev/null
+++ b/knife/lib/chef/knife/org_create.rb
@@ -0,0 +1,70 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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 OrgCreate < Knife
+ category "CHEF ORGANIZATION MANAGEMENT"
+ banner "knife org create ORG_SHORT_NAME ORG_FULL_NAME (options)"
+
+ option :filename,
+ long: "--filename FILENAME",
+ short: "-f FILENAME",
+ description: "Write validator private key to FILENAME rather than STDOUT"
+
+ option :association_user,
+ long: "--association_user USERNAME",
+ short: "-a USERNAME",
+ description: "Invite USERNAME to the new organization after creation"
+
+ attr_accessor :org_name, :org_full_name
+
+ deps do
+ require "chef/org" unless defined?(Chef::Org)
+ end
+
+ def run
+ @org_name, @org_full_name = @name_args
+
+ if !org_name || !org_full_name
+ ui.fatal "You must specify an ORG_NAME and an ORG_FULL_NAME"
+ show_usage
+ exit 1
+ end
+
+ org = Chef::Org.from_hash({ "name" => org_name,
+ "full_name" => org_full_name }).create
+ if config[:filename]
+ File.open(config[:filename], "w") do |f|
+ f.print(org.private_key)
+ end
+ else
+ ui.msg org.private_key
+ end
+
+ if config[:association_user]
+ org.associate_user(config[:association_user])
+ org.add_user_to_group("admins", config[:association_user])
+ org.add_user_to_group("billing-admins", config[:association_user])
+ end
+
+ ui.info("Created #{org_name}")
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/org_delete.rb b/knife/lib/chef/knife/org_delete.rb
new file mode 100644
index 0000000000..340f6c529a
--- /dev/null
+++ b/knife/lib/chef/knife/org_delete.rb
@@ -0,0 +1,32 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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 OrgDelete < Knife
+ category "CHEF ORGANIZATION MANAGEMENT"
+ banner "knife org delete ORG_NAME"
+
+ def run
+ org_name = @name_args[0]
+ ui.confirm "Do you want to delete the organization #{org_name}"
+ ui.output root_rest.delete("organizations/#{org_name}")
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/org_edit.rb b/knife/lib/chef/knife/org_edit.rb
new file mode 100644
index 0000000000..1d684ca0b4
--- /dev/null
+++ b/knife/lib/chef/knife/org_edit.rb
@@ -0,0 +1,48 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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 OrgEdit < Knife
+ category "CHEF ORGANIZATION MANAGEMENT"
+ banner "knife org edit ORG"
+
+ def run
+ org_name = @name_args[0]
+
+ if org_name.nil?
+ show_usage
+ ui.fatal("You must specify an organization name")
+ exit 1
+ end
+
+ original_org = root_rest.get("organizations/#{org_name}")
+ edited_org = edit_hash(original_org)
+
+ if original_org == edited_org
+ ui.msg("Organization unchanged, not saving.")
+ exit
+ end
+
+ ui.msg edited_org
+ root_rest.put("organizations/#{org_name}", edited_org)
+ ui.msg("Saved #{org_name}.")
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/org_list.rb b/knife/lib/chef/knife/org_list.rb
new file mode 100644
index 0000000000..85a49ee4c5
--- /dev/null
+++ b/knife/lib/chef/knife/org_list.rb
@@ -0,0 +1,44 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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 OrgList < Knife
+ category "CHEF ORGANIZATION MANAGEMENT"
+ banner "knife org list"
+
+ option :with_uri,
+ long: "--with-uri",
+ short: "-w",
+ description: "Show corresponding URIs"
+
+ option :all_orgs,
+ long: "--all-orgs",
+ short: "-a",
+ description: "Show auto-generated hidden orgs in output"
+
+ def run
+ results = root_rest.get("organizations")
+ unless config[:all_orgs]
+ results = results.select { |k, v| !(k.length == 20 && k =~ /^[a-z]+$/) }
+ end
+ ui.output(ui.format_list_for_display(results))
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/org_show.rb b/knife/lib/chef/knife/org_show.rb
new file mode 100644
index 0000000000..a8bb207c1d
--- /dev/null
+++ b/knife/lib/chef/knife/org_show.rb
@@ -0,0 +1,31 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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 OrgShow < Knife
+ category "CHEF ORGANIZATION MANAGEMENT"
+ banner "knife org show ORGNAME"
+
+ def run
+ org_name = @name_args[0]
+ ui.output root_rest.get("organizations/#{org_name}")
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/org_user_add.rb b/knife/lib/chef/knife/org_user_add.rb
new file mode 100644
index 0000000000..cd0ea88d56
--- /dev/null
+++ b/knife/lib/chef/knife/org_user_add.rb
@@ -0,0 +1,62 @@
+#
+# Author:: Marc Paradise (<marc@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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 OrgUserAdd < Knife
+ category "CHEF ORGANIZATION MANAGEMENT"
+ banner "knife org user add ORG_NAME USER_NAME"
+ attr_accessor :org_name, :username
+
+ option :admin,
+ long: "--admin",
+ short: "-a",
+ description: "Add user to admin group"
+
+ deps do
+ require_relative "../org"
+ end
+
+ def run
+ @org_name, @username = @name_args
+
+ if !org_name || !username
+ ui.fatal "You must specify an ORG_NAME and USER_NAME"
+ show_usage
+ exit 1
+ end
+
+ org = Chef::Org.new(@org_name)
+ begin
+ org.associate_user(@username)
+ rescue Net::HTTPServerException => e
+ if e.response.code == "409"
+ ui.msg "User #{username} already associated with organization #{org_name}"
+ else
+ raise e
+ end
+ end
+ if config[:admin]
+ org.add_user_to_group("admins", @username)
+ org.add_user_to_group("billing-admins", @username)
+ ui.msg "User #{username} is added to admins and billing-admins group"
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/org_user_remove.rb b/knife/lib/chef/knife/org_user_remove.rb
new file mode 100644
index 0000000000..fc78f5767c
--- /dev/null
+++ b/knife/lib/chef/knife/org_user_remove.rb
@@ -0,0 +1,103 @@
+#
+# Author:: Marc Paradise (<marc@getchef.com>)
+# Copyright:: Copyright (c) Chef Software 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 OrgUserRemove < Knife
+ category "CHEF ORGANIZATION MANAGEMENT"
+ banner "knife org user remove ORG_NAME USER_NAME"
+ attr_accessor :org_name, :username
+
+ option :force_remove_from_admins,
+ long: "--force",
+ short: "-f",
+ description: "Force removal of user from the organization's admins and billing-admins group."
+
+ deps do
+ require "chef/org" unless defined?(Chef::Org)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ def run
+ @org_name, @username = @name_args
+
+ if !org_name || !username
+ ui.fatal "You must specify an ORG_NAME and USER_NAME"
+ show_usage
+ exit 1
+ end
+
+ org = Chef::Org.new(@org_name)
+
+ if config[:force_remove_from_admins]
+ if org.actor_delete_would_leave_admins_empty?
+ failure_error_message(org_name, username)
+ ui.msg <<~EOF
+ You ran with --force which force removes the user from the admins and billing-admins groups.
+ However, removing #{username} from the admins group would leave it empty, which breaks the org.
+ Please add another user to org #{org_name} admins group and try again.
+ EOF
+ exit 1
+ end
+ remove_user_from_admin_group(org, org_name, username, "admins")
+ remove_user_from_admin_group(org, org_name, username, "billing-admins")
+ end
+
+ begin
+ org.dissociate_user(@username)
+ rescue Net::HTTPServerException => e
+ if e.response.code == "404"
+ ui.msg "User #{username} is not associated with organization #{org_name}"
+ exit 1
+ elsif e.response.code == "403"
+ body = Chef::JSONCompat.from_json(e.response.body)
+ if body.key?("error") && body["error"] == "Please remove #{username} from this organization's admins group before removing him or her from the organization."
+ failure_error_message(org_name, username)
+ ui.msg <<~EOF
+ User #{username} is in the organization's admin group. Removing users from an organization without removing them from the admins group is not allowed.
+ Re-run this command with --force to remove this user from the admins prior to removing it from the organization.
+ EOF
+ exit 1
+ else
+ raise e
+ end
+ else
+ raise e
+ end
+ end
+ end
+
+ def failure_error_message(org_name, username)
+ ui.error "Error removing user #{username} from organization #{org_name}."
+ end
+
+ def remove_user_from_admin_group(org, org_name, username, admin_group_string)
+ org.remove_user_from_group(admin_group_string, username)
+ rescue Net::HTTPServerException => e
+ if e.response.code == "404"
+ ui.warn <<~EOF
+ User #{username} is not in the #{admin_group_string} group for organization #{org_name}.
+ You probably don't need to pass --force.
+ EOF
+ else
+ raise e
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/raw.rb b/knife/lib/chef/knife/raw.rb
new file mode 100644
index 0000000000..344de9effb
--- /dev/null
+++ b/knife/lib/chef/knife/raw.rb
@@ -0,0 +1,123 @@
+#
+# 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_relative "../knife"
+
+class Chef
+ class Knife
+ class Raw < Chef::Knife
+ banner "knife raw REQUEST_PATH (options)"
+
+ deps do
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ require "chef/config" unless defined?(Chef::Config)
+ require "chef/http" unless defined?(Chef::HTTP)
+ require "chef/http/authenticator" unless defined?(Chef::HTTP::Authenticator)
+ require "chef/http/cookie_manager" unless defined?(Chef::HTTP::CookieManager)
+ require "chef/http/decompressor" unless defined?(Chef::HTTP::Decompressor)
+ require "chef/http/json_output" unless defined?(Chef::HTTP::JSONOutput)
+ end
+
+ option :method,
+ long: "--method METHOD",
+ short: "-m METHOD",
+ default: "GET",
+ description: "Request method (GET, POST, PUT or DELETE). Default: GET."
+
+ option :pretty,
+ long: "--[no-]pretty",
+ boolean: true,
+ default: true,
+ description: "Pretty-print JSON output. Default: true."
+
+ option :input,
+ long: "--input FILE",
+ short: "-i FILE",
+ description: "Name of file to use for PUT or POST."
+
+ option :proxy_auth,
+ long: "--proxy-auth",
+ boolean: true,
+ default: false,
+ description: "Use webui proxy authentication. Client key must be the webui key."
+
+ # We need a custom HTTP client class here because we don't want to even
+ # try to decode the body, in case we get back corrupted JSON or whatnot.
+ class RawInputServerAPI < Chef::HTTP
+ def initialize(options = {})
+ # If making a change here, also update Chef::ServerAPI.
+ options[:client_name] ||= Chef::Config[:node_name]
+ options[:raw_key] ||= Chef::Config[:client_key_contents]
+ options[:signing_key_filename] ||= Chef::Config[:client_key] unless options[:raw_key]
+ options[:ssh_agent_signing] ||= Chef::Config[:ssh_agent_signing]
+ super(Chef::Config[:chef_server_url], options)
+ end
+ use Chef::HTTP::JSONOutput
+ use Chef::HTTP::CookieManager
+ use Chef::HTTP::Decompressor
+ use Chef::HTTP::Authenticator
+ use Chef::HTTP::RemoteRequestID
+ end
+
+ 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("You must specify only a single path")
+ exit(1)
+ end
+
+ path = name_args[0]
+ data = false
+ if config[:input]
+ data = IO.read(config[:input])
+ end
+ begin
+ method = config[:method].to_sym
+
+ headers = { "Content-Type" => "application/json" }
+
+ if config[:proxy_auth]
+ headers["x-ops-request-source"] = "web"
+ end
+
+ if config[:pretty]
+ chef_rest = RawInputServerAPI.new
+ result = chef_rest.request(method, name_args[0], headers, data)
+ unless result.is_a?(String)
+ result = Chef::JSONCompat.to_json_pretty(result)
+ end
+ else
+ chef_rest = RawInputServerAPI.new(raw_output: true)
+ result = chef_rest.request(method, name_args[0], headers, data)
+ end
+ output result
+ rescue Timeout::Error => e
+ ui.error "Server timeout"
+ exit 1
+ rescue Net::HTTPClientException => e
+ ui.error "Server responded with error #{e.response.code} \"#{e.response.message}\""
+ ui.error "Error Body: #{e.response.body}" if e.response.body && e.response.body != ""
+ exit 1
+ end
+ end
+
+ end # class Raw
+ end
+end
diff --git a/knife/lib/chef/knife/recipe_list.rb b/knife/lib/chef/knife/recipe_list.rb
new file mode 100644
index 0000000000..39e040a2f4
--- /dev/null
+++ b/knife/lib/chef/knife/recipe_list.rb
@@ -0,0 +1,32 @@
+#
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+class Chef::Knife::RecipeList < Chef::Knife
+
+ banner "knife recipe list [PATTERN]"
+
+ def run
+ recipes = rest.get("cookbooks/_recipes")
+ if pattern = @name_args.first
+ recipes = recipes.grep(Regexp.new(pattern))
+ end
+ output(recipes)
+ end
+
+end
diff --git a/knife/lib/chef/knife/rehash.rb b/knife/lib/chef/knife/rehash.rb
new file mode 100644
index 0000000000..69ee19229a
--- /dev/null
+++ b/knife/lib/chef/knife/rehash.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Steven Danna <steve@chef.io>
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class Rehash < Chef::Knife
+ banner "knife rehash"
+
+ deps do
+ require_relative "core/subcommand_loader"
+ end
+
+ def run
+ if ! Chef::Knife::SubcommandLoader.autogenerated_manifest?
+ ui.msg "Using knife-rehash will speed up knife's load time by caching the location of subcommands on disk."
+ ui.msg "However, you will need to update the cache by running `knife rehash` anytime you install a new knife plugin."
+ else
+ reload_plugins
+ end
+
+ ui.msg "Knife subcommands are cached in #{Chef::Knife::SubcommandLoader.plugin_manifest_path}. Delete this file to disable the caching."
+ Chef::Knife::SubcommandLoader.write_hash(Chef::Knife::SubcommandLoader.generate_hash)
+ end
+
+ def reload_plugins
+ # The subcommand_loader for this knife command should _always_ be the GemGlobLoader. The GemGlobLoader loads
+ # plugins from disc and ensures the hash we write is always correct. By this point it should also already have
+ # loaded plugins and `load_commands` shouldn't have an effect.
+ Chef::Knife.subcommand_loader.load_commands
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_bulk_delete.rb b/knife/lib/chef/knife/role_bulk_delete.rb
new file mode 100644
index 0000000000..88399bae2c
--- /dev/null
+++ b/knife/lib/chef/knife/role_bulk_delete.rb
@@ -0,0 +1,66 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleBulkDelete < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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&.match?(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/knife/lib/chef/knife/role_create.rb b/knife/lib/chef/knife/role_create.rb
new file mode 100644
index 0000000000..91ff958fe4
--- /dev/null
+++ b/knife/lib/chef/knife/role_create.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleCreate < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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, object_class: Chef::Role)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_delete.rb b/knife/lib/chef/knife/role_delete.rb
new file mode 100644
index 0000000000..91ac7d3172
--- /dev/null
+++ b/knife/lib/chef/knife/role_delete.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleDelete < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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/knife/lib/chef/knife/role_edit.rb b/knife/lib/chef/knife/role_edit.rb
new file mode 100644
index 0000000000..a1818019cb
--- /dev/null
+++ b/knife/lib/chef/knife/role_edit.rb
@@ -0,0 +1,45 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleEdit < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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/knife/lib/chef/knife/role_env_run_list_add.rb b/knife/lib/chef/knife/role_env_run_list_add.rb
new file mode 100644
index 0000000000..a39bdcf5cd
--- /dev/null
+++ b/knife/lib/chef/knife/role_env_run_list_add.rb
@@ -0,0 +1,87 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: William Albenzi (<walbenzi@gmail.com>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleEnvRunListAdd < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife role env_run_list add [ROLE] [ENVIRONMENT] [ENTRY [ENTRY]] (options)"
+
+ option :after,
+ short: "-a ITEM",
+ long: "--after ITEM",
+ description: "Place the ENTRY in the run list after ITEM."
+
+ def add_to_env_run_list(role, environment, entries, after = nil)
+ if after
+ nlist = []
+ unless role.env_run_lists.key?(environment)
+ role.env_run_lists_add(environment => nlist)
+ end
+ role.run_list_for(environment).each do |entry|
+ nlist << entry
+ if entry == after
+ entries.each { |e| nlist << e }
+ end
+ end
+ role.env_run_lists_add(environment => nlist)
+ else
+ nlist = []
+ unless role.env_run_lists.key?(environment)
+ role.env_run_lists_add(environment => nlist)
+ end
+ role.run_list_for(environment).each do |entry|
+ nlist << entry
+ end
+ entries.each { |e| nlist << e }
+ role.env_run_lists_add(environment => nlist)
+ end
+ end
+
+ def run
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = @name_args[1]
+
+ if @name_args.size > 2
+ # Check for nested lists and create a single plain one
+ entries = @name_args[2..].map do |entry|
+ entry.split(",").map(&:strip)
+ end.flatten
+ else
+ # Convert to array and remove the extra spaces
+ entries = @name_args[2].split(",").map(&:strip)
+ end
+
+ add_to_env_run_list(role, environment, entries, config[:after])
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_env_run_list_clear.rb b/knife/lib/chef/knife/role_env_run_list_clear.rb
new file mode 100644
index 0000000000..bb0eeabc16
--- /dev/null
+++ b/knife/lib/chef/knife/role_env_run_list_clear.rb
@@ -0,0 +1,55 @@
+#
+# Author:: Mike Fiedler (<miketheman@gmail.com>)
+# Author:: William Albenzi (<walbenzi@gmail.com>)
+# Copyright:: Copyright 2013-2016, Mike Fiedler
+# 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleEnvRunListClear < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife role env_run_list clear [ROLE] [ENVIRONMENT] (options)"
+ def clear_env_run_list(role, environment)
+ nlist = []
+ role.env_run_lists_add(environment => nlist)
+ end
+
+ def run
+ if @name_args.size > 2
+ ui.fatal "You must not supply an environment run list."
+ show_usage
+ exit 1
+ end
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = @name_args[1]
+
+ clear_env_run_list(role, environment)
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_env_run_list_remove.rb b/knife/lib/chef/knife/role_env_run_list_remove.rb
new file mode 100644
index 0000000000..c1a028340b
--- /dev/null
+++ b/knife/lib/chef/knife/role_env_run_list_remove.rb
@@ -0,0 +1,57 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleEnvRunListRemove < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife role env_run_list remove [ROLE] [ENVIRONMENT] [ENTRIES] (options)"
+
+ def remove_from_env_run_list(role, environment, item_to_remove)
+ nlist = []
+ role.run_list_for(environment).each do |entry|
+ nlist << entry unless entry == item_to_remove
+ # unless entry == @name_args[2]
+ # nlist << entry
+ # end
+ end
+ role.env_run_lists_add(environment => nlist)
+ end
+
+ def run
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = @name_args[1]
+ item_to_remove = @name_args[2]
+
+ remove_from_env_run_list(role, environment, item_to_remove)
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_env_run_list_replace.rb b/knife/lib/chef/knife/role_env_run_list_replace.rb
new file mode 100644
index 0000000000..923a31331f
--- /dev/null
+++ b/knife/lib/chef/knife/role_env_run_list_replace.rb
@@ -0,0 +1,60 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: William Albenzi (<walbenzi@gmail.com>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleEnvRunListReplace < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife role env_run_list replace [ROLE] [ENVIRONMENT] [OLD_ENTRY] [NEW_ENTRY] (options)"
+
+ def replace_in_env_run_list(role, environment, old_entry, new_entry)
+ nlist = []
+ role.run_list_for(environment).each do |entry|
+ if entry == old_entry
+ nlist << new_entry
+ else
+ nlist << entry
+ end
+ end
+ role.env_run_lists_add(environment => nlist)
+ end
+
+ def run
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = @name_args[1]
+ old_entry = @name_args[2]
+ new_entry = @name_args[3]
+
+ replace_in_env_run_list(role, environment, old_entry, new_entry)
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_env_run_list_set.rb b/knife/lib/chef/knife/role_env_run_list_set.rb
new file mode 100644
index 0000000000..55a50c6c0d
--- /dev/null
+++ b/knife/lib/chef/knife/role_env_run_list_set.rb
@@ -0,0 +1,70 @@
+#
+# Author:: Mike Fiedler (<miketheman@gmail.com>)
+# Author:: William Albenzi (<walbenzi@gmail.com>)
+# Copyright:: Copyright 2013-2016, Mike Fiedler
+# 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleEnvRunListSet < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife role env_run_list set [ROLE] [ENVIRONMENT] [ENTRIES] (options)"
+
+ # Clears out any existing env_run_list_items and sets them to the
+ # specified entries
+ def set_env_run_list(role, environment, entries)
+ nlist = []
+ unless role.env_run_lists.key?(environment)
+ role.env_run_lists_add(environment => nlist)
+ end
+ entries.each { |e| nlist << e }
+ role.env_run_lists_add(environment => nlist)
+ end
+
+ def run
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = @name_args[1]
+ if @name_args.size < 2
+ ui.fatal "You must supply both a role name and an environment run list."
+ show_usage
+ exit 1
+ elsif @name_args.size > 2
+ # Check for nested lists and create a single plain one
+ entries = @name_args[2..].map do |entry|
+ entry.split(",").map(&:strip)
+ end.flatten
+ else
+ # Convert to array and remove the extra spaces
+ entries = @name_args[2].split(",").map(&:strip)
+ end
+
+ set_env_run_list(role, environment, entries )
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_from_file.rb b/knife/lib/chef/knife/role_from_file.rb
new file mode 100644
index 0000000000..7b51d8706d
--- /dev/null
+++ b/knife/lib/chef/knife/role_from_file.rb
@@ -0,0 +1,51 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleFromFile < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require_relative "core/object_loader"
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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/knife/lib/chef/knife/role_list.rb b/knife/lib/chef/knife/role_list.rb
new file mode 100644
index 0000000000..723d956b91
--- /dev/null
+++ b/knife/lib/chef/knife/role_list.rb
@@ -0,0 +1,42 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleList < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ 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/knife/lib/chef/knife/role_run_list_add.rb b/knife/lib/chef/knife/role_run_list_add.rb
new file mode 100644
index 0000000000..4276b9ab2d
--- /dev/null
+++ b/knife/lib/chef/knife/role_run_list_add.rb
@@ -0,0 +1,87 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: William Albenzi (<walbenzi@gmail.com>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleRunListAdd < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife role run_list add [ROLE] [ENTRY [ENTRY]] (options)"
+
+ option :after,
+ short: "-a ITEM",
+ long: "--after ITEM",
+ description: "Place the ENTRY in the run list after ITEM."
+
+ def add_to_env_run_list(role, environment, entries, after = nil)
+ if after
+ nlist = []
+ unless role.env_run_lists.key?(environment)
+ role.env_run_lists_add(environment => nlist)
+ end
+ role.run_list_for(environment).each do |entry|
+ nlist << entry
+ if entry == after
+ entries.each { |e| nlist << e }
+ end
+ end
+ role.env_run_lists_add(environment => nlist)
+ else
+ nlist = []
+ unless role.env_run_lists.key?(environment)
+ role.env_run_lists_add(environment => nlist)
+ end
+ role.run_list_for(environment).each do |entry|
+ nlist << entry
+ end
+ entries.each { |e| nlist << e }
+ role.env_run_lists_add(environment => nlist)
+ end
+ end
+
+ def run
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = "_default"
+
+ if @name_args.size > 1
+ # Check for nested lists and create a single plain one
+ entries = @name_args[1..].map do |entry|
+ entry.split(",").map(&:strip)
+ end.flatten
+ else
+ # Convert to array and remove the extra spaces
+ entries = @name_args[1].split(",").map(&:strip)
+ end
+
+ add_to_env_run_list(role, environment, entries, config[:after])
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_run_list_clear.rb b/knife/lib/chef/knife/role_run_list_clear.rb
new file mode 100644
index 0000000000..150dccd7ba
--- /dev/null
+++ b/knife/lib/chef/knife/role_run_list_clear.rb
@@ -0,0 +1,55 @@
+#
+# Author:: Mike Fiedler (<miketheman@gmail.com>)
+# Author:: William Albenzi (<walbenzi@gmail.com>)
+# Copyright:: Copyright 2013-2016, Mike Fiedler
+# 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleRunListClear < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife role run_list clear [ROLE] (options)"
+ def clear_env_run_list(role, environment)
+ nlist = []
+ role.env_run_lists_add(environment => nlist)
+ end
+
+ def run
+ if @name_args.size > 2
+ ui.fatal "You must not supply an environment run list."
+ show_usage
+ exit 1
+ end
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = "_default"
+
+ clear_env_run_list(role, environment)
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_run_list_remove.rb b/knife/lib/chef/knife/role_run_list_remove.rb
new file mode 100644
index 0000000000..7a0f82c092
--- /dev/null
+++ b/knife/lib/chef/knife/role_run_list_remove.rb
@@ -0,0 +1,56 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleRunListRemove < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ end
+
+ banner "knife role run_list remove [ROLE] [ENTRY] (options)"
+
+ def remove_from_env_run_list(role, environment, item_to_remove)
+ nlist = []
+ role.run_list_for(environment).each do |entry|
+ nlist << entry unless entry == item_to_remove
+ # unless entry == @name_args[2]
+ # nlist << entry
+ # end
+ end
+ role.env_run_lists_add(environment => nlist)
+ end
+
+ def run
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = "_default"
+ item_to_remove = @name_args[1]
+
+ remove_from_env_run_list(role, environment, item_to_remove)
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_run_list_replace.rb b/knife/lib/chef/knife/role_run_list_replace.rb
new file mode 100644
index 0000000000..63c7b87199
--- /dev/null
+++ b/knife/lib/chef/knife/role_run_list_replace.rb
@@ -0,0 +1,60 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Author:: William Albenzi (<walbenzi@gmail.com>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleRunListReplace < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife role run_list replace [ROLE] [OLD_ENTRY] [NEW_ENTRY] (options)"
+
+ def replace_in_env_run_list(role, environment, old_entry, new_entry)
+ nlist = []
+ role.run_list_for(environment).each do |entry|
+ if entry == old_entry
+ nlist << new_entry
+ else
+ nlist << entry
+ end
+ end
+ role.env_run_lists_add(environment => nlist)
+ end
+
+ def run
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = "_default"
+ old_entry = @name_args[1]
+ new_entry = @name_args[2]
+
+ replace_in_env_run_list(role, environment, old_entry, new_entry)
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_run_list_set.rb b/knife/lib/chef/knife/role_run_list_set.rb
new file mode 100644
index 0000000000..6cddc7376c
--- /dev/null
+++ b/knife/lib/chef/knife/role_run_list_set.rb
@@ -0,0 +1,69 @@
+#
+# Author:: Mike Fiedler (<miketheman@gmail.com>)
+# Author:: William Albenzi (<walbenzi@gmail.com>)
+# Copyright:: Copyright 2013-2016, Mike Fiedler
+# 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleRunListSet < Knife
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ end
+
+ banner "knife role run_list set [ROLE] [ENTRIES] (options)"
+
+ # Clears out any existing env_run_list_items and sets them to the
+ # specified entries
+ def set_env_run_list(role, environment, entries)
+ nlist = []
+ unless role.env_run_lists.key?(environment)
+ role.env_run_lists_add(environment => nlist)
+ end
+ entries.each { |e| nlist << e }
+ role.env_run_lists_add(environment => nlist)
+ end
+
+ def run
+ role = Chef::Role.load(@name_args[0])
+ role.name(@name_args[0])
+ environment = "_default"
+ if @name_args.size < 1
+ ui.fatal "You must supply both a role name and an environment run list."
+ show_usage
+ exit 1
+ elsif @name_args.size > 1
+ # Check for nested lists and create a single plain one
+ entries = @name_args[1..].map do |entry|
+ entry.split(",").map(&:strip)
+ end.flatten
+ else
+ # Convert to array and remove the extra spaces
+ entries = @name_args[1].split(",").map(&:strip)
+ end
+
+ set_env_run_list(role, environment, entries )
+ role.save
+ config[:env_run_list] = true
+ output(format_for_display(role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/role_show.rb b/knife/lib/chef/knife/role_show.rb
new file mode 100644
index 0000000000..3a2df8b782
--- /dev/null
+++ b/knife/lib/chef/knife/role_show.rb
@@ -0,0 +1,48 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class RoleShow < Knife
+
+ include Knife::Core::MultiAttributeReturnOption
+
+ deps do
+ require "chef/role" unless defined?(Chef::Role)
+ end
+
+ banner "knife role show 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
+
+ role = Chef::Role.load(@role_name)
+ output(format_for_display(config[:environment] ? role.environment(config[:environment]) : role))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/search.rb b/knife/lib/chef/knife/search.rb
new file mode 100644
index 0000000000..306761f109
--- /dev/null
+++ b/knife/lib/chef/knife/search.rb
@@ -0,0 +1,194 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "core/node_presenter"
+require_relative "core/formatting_options"
+
+class Chef
+ class Knife
+ class Search < Knife
+
+ include Knife::Core::MultiAttributeReturnOption
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ require "chef/environment" unless defined?(Chef::Environment)
+ require "chef/api_client" unless defined?(Chef::APIClient)
+ require "chef/search/query" unless defined?(Chef::Search::Query)
+ end
+
+ include Knife::Core::FormattingOptions
+
+ banner "knife search INDEX QUERY (options)"
+
+ 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: nil,
+ proc: lambda { |i| i.to_i }
+
+ 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 -."
+
+ option :filter_result,
+ short: "-f FILTER",
+ long: "--filter-result FILTER",
+ description: "Only return specific attributes of the matching objects; for example: \"ServerName=name, Kernel=kernel.version\"."
+
+ def run
+ read_cli_args
+
+ if @type == "node"
+ ui.use_presenter Knife::Core::NodePresenter
+ end
+
+ q = Chef::Search::Query.new
+
+ result_items = []
+ result_count = 0
+
+ search_args = {}
+ search_args[:fuzz] = true
+ search_args[:start] = config[:start] if config[:start]
+ search_args[:rows] = config[:rows] if config[:rows]
+ if config[:filter_result]
+ search_args[:filter_result] = create_result_filter(config[:filter_result])
+ elsif (not ui.config[:attribute].nil?) && (not ui.config[:attribute].empty?)
+ search_args[:filter_result] = create_result_filter_from_attributes(ui.config[:attribute])
+ elsif config[:id_only]
+ search_args[:filter_result] = create_result_filter_from_attributes([])
+ end
+
+ begin
+ q.search(@type, @query, search_args) do |item|
+ formatted_item = {}
+ if config[:id_only]
+ formatted_item = format_for_display({ "id" => item["__display_name"] })
+ elsif item.is_a?(Hash)
+ # doing a little magic here to set the correct name
+ formatted_item[item["__display_name"]] = item.reject { |k| k == "__display_name" }
+ else
+ formatted_item = format_for_display(item)
+ end
+ result_items << formatted_item
+ result_count += 1
+ end
+ rescue Net::HTTPClientException => e
+ msg = Chef::JSONCompat.from_json(e.response.body)["error"].first
+ ui.error("knife search failed: #{msg}")
+ exit 99
+ end
+
+ if ui.interchange?
+ output({ results: result_count, rows: result_items })
+ else
+ ui.log "#{result_count} items found"
+ ui.log("\n")
+ result_items.each do |item|
+ output(item)
+ unless config[:id_only]
+ ui.msg("\n")
+ end
+ end
+ end
+
+ # return a "failure" code to the shell so that knife search can be used in pipes similar to grep
+ exit 1 if result_count == 0
+ end
+
+ def read_cli_args
+ if config[:query]
+ if @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
+ @type = name_args[0]
+ @query = config[:query]
+ else
+ case name_args.size
+ when 0
+ ui.error "No query specified"
+ ui.msg opt_parser
+ exit 1
+ when 1
+ @type = "node"
+ @query = name_args[0]
+ when 2
+ @type = name_args[0]
+ @query = name_args[1]
+ end
+ end
+ end
+
+ # This method turns a set of key value pairs in a string into the appropriate data structure that the
+ # chef-server search api is expecting.
+ # expected input is in the form of:
+ # -f "return_var1=path.to.attribute, return_var2=shorter.path"
+ #
+ # a more concrete example might be:
+ # -f "env=chef_environment, ruby_platform=languages.ruby.platform"
+ #
+ # The end result is a hash where the key is a symbol in the hash (the return variable)
+ # and the path is an array with the path elements as strings (in order)
+ # See lib/chef/search/query.rb for more examples of this.
+ def create_result_filter(filter_string)
+ final_filter = {}
+ filter_string.delete!(" ")
+ filters = filter_string.split(",")
+ filters.each do |f|
+ return_id, attr_path = f.split("=")
+ final_filter[return_id.to_sym] = attr_path.split(".")
+ end
+ final_filter
+ end
+
+ def create_result_filter_from_attributes(filter_array)
+ final_filter = {}
+ filter_array.each do |f|
+ final_filter[f] = f.split(".")
+ end
+ # adding magic filter so we can actually pull the name as before
+ final_filter["__display_name"] = [ "name" ]
+ final_filter
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/serve.rb b/knife/lib/chef/knife/serve.rb
new file mode 100644
index 0000000000..30e4a28f9a
--- /dev/null
+++ b/knife/lib/chef/knife/serve.rb
@@ -0,0 +1,65 @@
+#
+# 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_relative "../knife"
+require "local_mode" unless defined?(Chef::LocalMode)
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ class Knife
+ class Serve < Knife
+
+ banner "knife serve (options)"
+
+ option :repo_mode,
+ long: "--repo-mode MODE",
+ description: "Specifies the local repository layout. Values: static (only environments/roles/data_bags/cookbooks), everything (includes nodes/clients/users), hosted_everything (includes acls/groups/etc. for Enterprise/Hosted Chef). Default: everything/hosted_everything."
+
+ option :chef_repo_path,
+ long: "--chef-repo-path PATH",
+ description: "Overrides the location of #{ChefUtils::Dist::Infra::PRODUCT} repo. Default is specified by chef_repo_path in the config."
+
+ option :chef_zero_host,
+ long: "--chef-zero-host IP",
+ description: "Overrides the host upon which #{ChefUtils::Dist::Zero::PRODUCT} listens. Default is 127.0.0.1."
+
+ def configure_chef
+ super
+ Chef::Config.local_mode = true
+ Chef::Config[:repo_mode] = config[:repo_mode] if config[:repo_mode]
+
+ # --chef-repo-path forcibly overrides all other paths
+ if config[:chef_repo_path]
+ Chef::Config.chef_repo_path = config[:chef_repo_path]
+ %w{acl client cookbook container data_bag environment group node role user}.each do |variable_name|
+ Chef::Config.delete("#{variable_name}_path".to_sym)
+ end
+ end
+ end
+
+ def run
+ server = Chef::LocalMode.chef_zero_server
+ begin
+ output "Serving files from:\n#{Chef::LocalMode.chef_fs.fs_description}"
+ server.stop
+ server.start(stdout) # to print header
+ ensure
+ server.stop
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/show.rb b/knife/lib/chef/knife/show.rb
new file mode 100644
index 0000000000..cdee271c63
--- /dev/null
+++ b/knife/lib/chef/knife/show.rb
@@ -0,0 +1,72 @@
+#
+# 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_relative "../chef_fs/knife"
+
+class Chef
+ class Knife
+ class Show < Chef::ChefFS::Knife
+ banner "knife show [PATTERN1 ... PATTERNn] (options)"
+
+ category "path-based"
+
+ deps do
+ require "chef/chef_fs/file_system" unless defined?(Chef::ChefFS::FileSystem)
+ require "chef/chef_fs/file_system/exceptions" unless defined?(Chef::ChefFS::FileSystem::Exceptions)
+ end
+
+ option :local,
+ long: "--local",
+ boolean: true,
+ description: "Show local files instead of remote."
+
+ def run
+ # Get the matches (recursively)
+ error = false
+ entry_values = parallelize(pattern_args) do |pattern|
+ parallelize(Chef::ChefFS::FileSystem.list(config[:local] ? local_fs : chef_fs, pattern)) do |entry|
+ if entry.dir?
+ ui.error "#{format_path(entry)}: is a directory" if pattern.exact_path
+ error = true
+ nil
+ else
+ begin
+ [entry, entry.read]
+ rescue Chef::ChefFS::FileSystem::OperationNotAllowedError => e
+ ui.error "#{format_path(e.entry)}: #{e.reason}."
+ error = true
+ nil
+ rescue Chef::ChefFS::FileSystem::NotFoundError => e
+ ui.error "#{format_path(e.entry)}: No such file or directory"
+ error = true
+ nil
+ end
+ end
+ end
+ end.flatten(1)
+ entry_values.each do |entry, value|
+ if entry
+ output "#{format_path(entry)}:"
+ output(format_for_display(value))
+ end
+ end
+ if error
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/ssh.rb b/knife/lib/chef/knife/ssh.rb
new file mode 100644
index 0000000000..e69de62bc2
--- /dev/null
+++ b/knife/lib/chef/knife/ssh.rb
@@ -0,0 +1,645 @@
+#
+# Author:: Adam Jacob (<adam@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class Ssh < Knife
+
+ deps do
+ require "chef/mixin/shell_out" unless defined?(Chef::Mixin::ShellOut)
+ require "net/ssh" unless defined?(Net::SSH)
+ require "net/ssh/multi"
+ require "readline"
+ require "chef/exceptions" unless defined?(Chef::Exceptions)
+ require "chef/search/query" unless defined?(Chef::Search::Query)
+ require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper)
+
+ include Chef::Mixin::ShellOut
+ end
+
+ 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 :ssh_attribute,
+ short: "-a ATTR",
+ long: "--attribute ATTR",
+ description: "The attribute to use for opening the connection - default depends on the context."
+
+ option :manual,
+ short: "-m",
+ long: "--manual-list",
+ boolean: true,
+ description: "QUERY is a space separated list of servers.",
+ default: false
+
+ option :prefix_attribute,
+ long: "--prefix-attribute ATTR",
+ description: "The attribute to use for prefixing the output - default depends on the context."
+
+ 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 - will prompt if flag is specified but no password is given.",
+ # default to a value that can not be a password (boolean)
+ # so we can effectively test if this parameter was specified
+ # without a value
+ default: false
+
+ option :ssh_port,
+ short: "-p PORT",
+ long: "--ssh-port PORT",
+ description: "The ssh port.",
+ proc: Proc.new { |key| key.strip }
+
+ option :ssh_timeout,
+ short: "-t SECONDS",
+ long: "--ssh-timeout SECONDS",
+ description: "The ssh connection timeout.",
+ proc: Proc.new { |key| key.strip.to_i },
+ default: 120
+
+ option :ssh_gateway,
+ short: "-G GATEWAY",
+ long: "--ssh-gateway GATEWAY",
+ description: "The ssh gateway.",
+ proc: Proc.new { |key| key.strip }
+
+ option :ssh_gateway_identity,
+ long: "--ssh-gateway-identity SSH_GATEWAY_IDENTITY",
+ description: "The SSH identity file used for gateway authentication."
+
+ option :forward_agent,
+ short: "-A",
+ long: "--forward-agent",
+ description: "Enable SSH agent forwarding.",
+ boolean: true
+
+ option :ssh_identity_file,
+ short: "-i IDENTITY_FILE",
+ long: "--ssh-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
+
+ option :on_error,
+ short: "-e",
+ long: "--exit-on-error",
+ description: "Immediately exit if an error is encountered.",
+ boolean: true,
+ default: false
+
+ option :duplicated_fqdns,
+ long: "--duplicated-fqdns",
+ description: "Behavior if FQDNs are duplicated, ignored by default.",
+ proc: Proc.new { |key| key.strip.to_sym },
+ default: :ignore
+
+ option :tmux_split,
+ long: "--tmux-split",
+ description: "Split tmux window.",
+ boolean: true,
+ default: false
+
+ def session
+ ssh_error_handler = Proc.new do |server|
+ if config[:on_error]
+ # Net::SSH::Multi magic to force exception to be re-raised.
+ throw :go, :raise
+ else
+ ui.warn "Failed to connect to #{server.host} -- #{$!.class.name}: #{$!.message}"
+ $!.backtrace.each { |l| Chef::Log.debug(l) }
+ end
+ end
+
+ @session ||= Net::SSH::Multi.start(concurrent_connections: config[:concurrency], on_error: ssh_error_handler)
+ end
+
+ def configure_gateway
+ if config[:ssh_gateway]
+ gw_host, gw_user = config[:ssh_gateway].split("@").reverse
+ gw_host, gw_port = gw_host.split(":")
+ gw_opts = session_options(gw_host, gw_port, gw_user, gateway: true)
+ user = gw_opts.delete(:user)
+
+ begin
+ # Try to connect with a key.
+ session.via(gw_host, user, gw_opts)
+ rescue Net::SSH::AuthenticationFailed
+ prompt = "Enter the password for #{user}@#{gw_host}: "
+ gw_opts[:password] = prompt_for_password(prompt)
+ # Try again with a password.
+ session.via(gw_host, user, gw_opts)
+ end
+ end
+ end
+
+ def configure_session
+ list = config[:manual] ? @name_args[0].split(" ") : search_nodes
+ if list.length == 0
+ if @search_count == 0
+ ui.fatal("No nodes returned from search")
+ else
+ ui.fatal("#{@search_count} #{@search_count > 1 ? "nodes" : "node"} found, " +
+ "but does not have the required attribute to establish the connection. " +
+ "Try setting another attribute to open the connection using --attribute.")
+ end
+ exit 10
+ end
+ if %i{warn fatal}.include?(config[:duplicated_fqdns])
+ fqdns = list.map { |v| v[0] }
+ if fqdns.count != fqdns.uniq.count
+ duplicated_fqdns = fqdns.uniq
+ ui.send(config[:duplicated_fqdns],
+ "SSH #{duplicated_fqdns.count > 1 ? "nodes are" : "node is"} " +
+ "duplicated: #{duplicated_fqdns.join(",")}")
+ exit 10 if config[:duplicated_fqdns] == :fatal
+ end
+ end
+ session_from_list(list)
+ end
+
+ def get_prefix_attribute(item)
+ # Order of precedence for prefix
+ # 1) config value (cli or knife config)
+ # 2) nil
+ msg = "Using node attribute '%s' as the prefix: %s"
+ if item["prefix"]
+ Chef::Log.debug(sprintf(msg, config[:prefix_attribute], item["prefix"]))
+ item["prefix"]
+ else
+ nil
+ end
+ end
+
+ def get_ssh_attribute(item)
+ # Order of precedence for ssh target
+ # 1) config value (cli or knife config)
+ # 2) cloud attribute
+ # 3) fqdn
+ msg = "Using node attribute '%s' as the ssh target: %s"
+ if item["target"]
+ Chef::Log.debug(sprintf(msg, config[:ssh_attribute], item["target"]))
+ item["target"]
+ elsif !item.dig("cloud", "public_hostname").to_s.empty?
+ Chef::Log.debug(sprintf(msg, "cloud.public_hostname", item["cloud"]["public_hostname"]))
+ item["cloud"]["public_hostname"]
+ else
+ Chef::Log.debug(sprintf(msg, "fqdn", item["fqdn"]))
+ item["fqdn"]
+ end
+ end
+
+ def search_nodes
+ list = []
+ query = Chef::Search::Query.new
+ required_attributes = { fqdn: ["fqdn"], cloud: ["cloud"] }
+
+ separator = ui.presenter.attribute_field_separator
+
+ if config[:prefix_attribute]
+ required_attributes[:prefix] = config[:prefix_attribute].split(separator)
+ end
+
+ if config[:ssh_attribute]
+ required_attributes[:target] = config[:ssh_attribute].split(separator)
+ end
+
+ @search_count = 0
+ query.search(:node, @name_args[0], filter_result: required_attributes, fuzz: true) do |item|
+ @search_count += 1
+ # we should skip the loop to next iteration if the item
+ # returned by the search is nil
+ next if item.nil?
+
+ # next if we couldn't find the specified attribute in the
+ # returned node object
+ host = get_ssh_attribute(item)
+ next if host.nil?
+
+ prefix = get_prefix_attribute(item)
+ ssh_port = item.dig("cloud", "public_ssh_port")
+ srv = [host, ssh_port, prefix]
+ list.push(srv)
+ end
+
+ list
+ end
+
+ # Net::SSH session options hash for global options. These should be
+ # options that will apply to the gateway connection in addition to the
+ # main one.
+ #
+ # @since 12.5.0
+ # @param host [String] Hostname for this session.
+ # @param port [String] SSH port for this session.
+ # @param user [String] Optional username for this session.
+ # @param gateway [Boolean] Flag: host or gateway key
+ # @return [Hash<Symbol, Object>]
+ def session_options(host, port, user = nil, gateway: false)
+ ssh_config = Net::SSH.configuration_for(host, true)
+ {}.tap do |opts|
+ opts[:user] = user || config[:ssh_user] || ssh_config[:user]
+ if !gateway && config[:ssh_identity_file]
+ opts[:keys] = File.expand_path(config[:ssh_identity_file])
+ opts[:keys_only] = true
+ elsif gateway && config[:ssh_gateway_identity]
+ opts[:keys] = File.expand_path(config[:ssh_gateway_identity])
+ opts[:keys_only] = true
+ elsif config[:ssh_password]
+ opts[:password] = config[:ssh_password]
+ end
+ # Don't set the keys to nil if we don't have them.
+ forward_agent = config[:forward_agent] || ssh_config[:forward_agent]
+ opts[:forward_agent] = forward_agent unless forward_agent.nil?
+ port ||= ssh_config[:port]
+ opts[:port] = port unless port.nil?
+ opts[:logger] = Chef::Log.with_child(subsystem: "net/ssh") if Chef::Log.level == :trace
+ unless config[:host_key_verify]
+ opts[:verify_host_key] = :never
+ opts[:user_known_hosts_file] = "/dev/null"
+ end
+ if ssh_config[:keepalive]
+ opts[:keepalive] = true
+ opts[:keepalive_interval] = ssh_config[:keepalive_interval]
+ end
+ # maintain support for legacy key types / ciphers / key exchange algorithms.
+ # most importantly this adds back support for DSS host keys
+ # See https://github.com/net-ssh/net-ssh/pull/709
+ opts[:append_all_supported_algorithms] = true
+ end
+ end
+
+ def session_from_list(list)
+ list.each do |item|
+ host, ssh_port, prefix = item
+ prefix = host unless prefix
+ Chef::Log.debug("Adding #{host}")
+ session_opts = session_options(host, ssh_port, gateway: false)
+ # Handle port overrides for the main connection.
+ session_opts[:port] = config[:ssh_port] if config[:ssh_port]
+ # Handle connection timeout
+ session_opts[:timeout] = config[:ssh_timeout] if config[:ssh_timeout]
+ # Handle session prefix
+ session_opts[:properties] = { prefix: prefix }
+ # Create the hostspec.
+ hostspec = session_opts[:user] ? "#{session_opts.delete(:user)}@#{host}" : host
+ # Connect a new session on the multi.
+ session.use(hostspec, session_opts)
+
+ @longest = prefix.length if prefix.length > @longest
+ end
+
+ session
+ end
+
+ def fixup_sudo(command)
+ command.sub(/^sudo/, "sudo -p 'knife sudo password: '")
+ end
+
+ def print_data(host, data)
+ @buffers ||= {}
+ if leftover = @buffers[host]
+ @buffers[host] = nil
+ print_data(host, leftover + data)
+ else
+ if newline_index = data.index("\n")
+ line = data.slice!(0...newline_index)
+ data.slice!(0)
+ print_line(host, line)
+ print_data(host, data)
+ else
+ @buffers[host] = data
+ end
+ end
+ end
+
+ def print_line(host, data)
+ padding = @longest - host.length
+ str = ui.color(host, :cyan) + (" " * (padding + 1)) + data
+ ui.msg(str)
+ 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)
+ begin
+ open_session(subsession, command)
+ rescue => e
+ open_session(subsession, command, true)
+ end
+ end
+
+ def open_session(subsession, command, pty = false)
+ stderr = ""
+ exit_status = 0
+ subsession.open_channel do |chan|
+ if config[:on_error] && exit_status != 0
+ chan.close
+ else
+ chan.request_pty if pty
+ chan.exec command do |ch, success|
+ raise ArgumentError, "Cannot execute #{command}" unless success
+
+ ch.on_data do |ichannel, data|
+ print_data(ichannel.connection[:prefix], data)
+ if /^knife sudo password: /.match?(data)
+ print_data(ichannel.connection[:prefix], "\n")
+ ichannel.send_data("#{get_password}\n")
+ end
+ end
+
+ ch.on_extended_data do |_, _type, data|
+ raise ArgumentError if data.eql?("sudo: no tty present and no askpass program specified\n")
+
+ stderr += data
+ end
+
+ ch.on_request "exit-status" do |ichannel, data|
+ exit_status = [exit_status, data.read_long].max
+ end
+ end
+ end
+ end
+ session.loop
+ exit_status
+ end
+
+ def get_password
+ @password ||= prompt_for_password
+ end
+
+ def prompt_for_password(prompt = "Enter your password: ")
+ ui.ask(prompt, 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
+ loop do
+ command = read_line
+ case command
+ when "quit!"
+ puts "Bye!"
+ break
+ when /^on (.+?); (.+)$/
+ raw_list = $1.split(" ")
+ server_list = []
+ 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")
+ ChefConfig::PathHelper.home(".screenrc") do |screenrc_path|
+ if File.exist? screenrc_path
+ tf.puts("source #{screenrc_path}")
+ end
+ 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[:ssh_identity_file]} ") if config[:ssh_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[:ssh_identity_file]} " if config[:ssh_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..].map do |server|
+ if config[:tmux_split]
+ "split-window #{ssh_dest.call(server)}; tmux select-layout tiled"
+ else
+ "new-window -a -n '#{server.host}' #{ssh_dest.call(server)}"
+ end
+ end
+ else
+ []
+ end.join(" \\; ")
+ end
+
+ tmux_name = "'knife ssh #{@name_args[0].tr(":.", "=-")}'"
+ 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" unless defined?(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 cssh
+ cssh_cmd = nil
+ %w{csshX cssh}.each do |cmd|
+
+ # Unix and Mac only
+ cssh_cmd = shell_out!("which #{cmd}").stdout.strip
+ break
+ rescue Mixlib::ShellOut::ShellCommandFailed
+
+ end
+ raise Chef::Exceptions::Exec, "no command found for cssh" unless cssh_cmd
+
+ # pass in the consolidated identity file option to cssh(X)
+ if config[:ssh_identity_file]
+ cssh_cmd << " --ssh_args '-i #{File.expand_path(config[:ssh_identity_file])}'"
+ end
+
+ 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 unless value
+
+ value.strip
+ end
+
+ def configure_user
+ config[:ssh_user] = get_stripped_unfrozen_value(config[:ssh_user] ||
+ Chef::Config[:knife][:ssh_user])
+ end
+
+ def configure_password
+ if config.key?(:ssh_password) && config[:ssh_password].nil?
+ # if we have an actual nil that means someone called "--ssh-password" with no value, so we prompt for a password
+ config[:ssh_password] = get_password
+ else
+ # the false default of ssh_password results in a nil here
+ config[:ssh_password] = get_stripped_unfrozen_value(config[:ssh_password])
+ end
+ end
+
+ def configure_ssh_identity_file
+ config[:ssh_identity_file] = get_stripped_unfrozen_value(config[:ssh_identity_file])
+ end
+
+ def configure_ssh_gateway_identity
+ config[:ssh_gateway_identity] = get_stripped_unfrozen_value(config[:ssh_gateway_identity])
+ end
+
+ def run
+ @longest = 0
+
+ if @name_args.length < 1
+ show_usage
+ ui.fatal("You must specify the SEARCH QUERY.")
+ exit(1)
+ end
+
+ configure_user
+ configure_password
+ @password = config[:ssh_password] if config[:ssh_password]
+
+ # If a password was not given, check for SSH identity file.
+ unless @password
+ configure_ssh_identity_file
+ configure_ssh_gateway_identity
+ end
+
+ configure_gateway
+ configure_session
+
+ exit_status =
+ case @name_args[1]
+ when "interactive"
+ interactive
+ when "screen"
+ screen
+ when "tmux"
+ tmux
+ when "macterm"
+ macterm
+ when "cssh"
+ cssh
+ else
+ ssh_command(@name_args[1..].join(" "))
+ end
+
+ session.close
+ if exit_status && exit_status != 0
+ exit exit_status
+ else
+ exit_status
+ end
+ end
+
+ private :search_nodes
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/ssl_check.rb b/knife/lib/chef/knife/ssl_check.rb
new file mode 100644
index 0000000000..c829e7938b
--- /dev/null
+++ b/knife/lib/chef/knife/ssl_check.rb
@@ -0,0 +1,284 @@
+#
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ class Knife
+ class SslCheck < Chef::Knife
+
+ deps do
+ require "chef/config" unless defined?(Chef::Config)
+ require "pp" unless defined?(PP)
+ require "socket" unless defined?(Socket)
+ require "uri" unless defined?(URI)
+ require "chef/http/ssl_policies" unless defined?(Chef::HTTP::DefaultSSLPolicy)
+ require "openssl" unless defined?(OpenSSL)
+ require "chef/mixin/proxified_socket" unless defined?(Chef::Mixin::ProxifiedSocket)
+ include Chef::Mixin::ProxifiedSocket
+ end
+
+ banner "knife ssl check [URL] (options)"
+
+ def initialize(*args)
+ @host = nil
+ @verify_peer_socket = nil
+ @ssl_policy = HTTP::DefaultSSLPolicy
+ super
+ end
+
+ def uri
+ @uri ||= begin
+ Chef::Log.trace("Checking SSL cert on #{given_uri}")
+ URI.parse(given_uri)
+ end
+ end
+
+ def given_uri
+ (name_args[0] || Chef::Config.chef_server_url)
+ end
+
+ def host
+ uri.host
+ end
+
+ def port
+ uri.port
+ end
+
+ def validate_uri
+ unless host && port
+ invalid_uri!
+ end
+ rescue URI::Error
+ invalid_uri!
+ end
+
+ def invalid_uri!
+ ui.error("Given URI: `#{given_uri}' is invalid")
+ show_usage
+ exit 1
+ end
+
+ def verify_peer_socket
+ @verify_peer_socket ||= begin
+ tcp_connection = proxified_socket(host, port)
+ ssl_client = OpenSSL::SSL::SSLSocket.new(tcp_connection, verify_peer_ssl_context)
+ ssl_client.hostname = host
+ ssl_client
+ end
+ end
+
+ def verify_peer_ssl_context
+ @verify_peer_ssl_context ||= begin
+ verify_peer_context = OpenSSL::SSL::SSLContext.new
+ @ssl_policy.apply_to(verify_peer_context)
+ verify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ verify_peer_context
+ end
+ end
+
+ def noverify_socket
+ @noverify_socket ||= begin
+ tcp_connection = proxified_socket(host, port)
+ OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_ssl_context)
+ end
+ end
+
+ def noverify_peer_ssl_context
+ @noverify_peer_ssl_context ||= begin
+ noverify_peer_context = OpenSSL::SSL::SSLContext.new
+ @ssl_policy.apply_to(noverify_peer_context)
+ noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ noverify_peer_context
+ end
+ end
+
+ def verify_X509
+ cert_debug_msg = ""
+ trusted_certificates.each do |cert_name|
+ message = check_X509_certificate(cert_name)
+ unless message.nil?
+ cert_debug_msg << File.expand_path(cert_name) + ": " + message + "\n"
+ end
+ end
+
+ unless cert_debug_msg.empty?
+ debug_invalid_X509(cert_debug_msg)
+ end
+
+ true # Maybe the bad certs won't hurt...
+ end
+
+ def verify_cert
+ ui.msg("Connecting to host #{host}:#{port}")
+ verify_peer_socket.connect
+ true
+ rescue OpenSSL::SSL::SSLError => e
+ ui.error "The SSL certificate of #{host} could not be verified"
+ Chef::Log.trace e.message
+ debug_invalid_cert
+ false
+ end
+
+ def verify_cert_host
+ verify_peer_socket.post_connection_check(host)
+ true
+ rescue OpenSSL::SSL::SSLError => e
+ ui.error "The SSL cert is signed by a trusted authority but is not valid for the given hostname"
+ Chef::Log.trace(e)
+ debug_invalid_host
+ false
+ end
+
+ def debug_invalid_X509(cert_debug_msg)
+ ui.msg("\n#{ui.color("Configuration Info:", :bold)}\n\n")
+ debug_ssl_settings
+ debug_chef_ssl_config
+
+ ui.warn(<<~BAD_CERTS)
+ There are invalid certificates in your trusted_certs_dir.
+ OpenSSL will not use the following certificates when verifying SSL connections:
+
+ #{cert_debug_msg}
+
+ #{ui.color("TO FIX THESE WARNINGS:", :bold)}
+
+ We are working on documentation for resolving common issues uncovered here.
+
+ * If the certificate is generated by the server, you may try redownloading the
+ server's certificate. By default, the certificate is stored in the following
+ location on the host where your chef-server runs:
+
+ /var/opt/opscode/nginx/ca/SERVER_HOSTNAME.crt
+
+ Copy that file to your trusted_certs_dir (currently: #{configuration.trusted_certs_dir})
+ using SSH/SCP or some other secure method, then re-run this command to confirm
+ that the server's certificate is now trusted.
+
+ BAD_CERTS
+ # @TODO: ^ needs URL once documentation is posted.
+ end
+
+ def debug_invalid_cert
+ noverify_socket.connect
+ issuer_info = noverify_socket.peer_cert.issuer
+ ui.msg("Certificate issuer data: #{issuer_info}")
+
+ ui.msg("\n#{ui.color("Configuration Info:", :bold)}\n\n")
+ debug_ssl_settings
+ debug_chef_ssl_config
+
+ ui.err(<<~ADVICE)
+
+ #{ui.color("TO FIX THIS ERROR:", :bold)}
+
+ If the server you are connecting to uses a self-signed certificate, you must
+ configure #{ChefUtils::Dist::Infra::PRODUCT} to trust that server's certificate.
+
+ By default, the certificate is stored in the following location on the host
+ where your chef-server runs:
+
+ /var/opt/opscode/nginx/ca/SERVER_HOSTNAME.crt
+
+ Copy that file to your trusted_certs_dir (currently: #{configuration.trusted_certs_dir})
+ using SSH/SCP or some other secure method, then re-run this command to confirm
+ that the server's certificate is now trusted.
+
+ ADVICE
+ end
+
+ def debug_invalid_host
+ noverify_socket.connect
+ subject = noverify_socket.peer_cert.subject
+ cn_field_tuple = subject.to_a.find { |field| field[0] == "CN" }
+ cn = cn_field_tuple[1]
+
+ ui.error("You are attempting to connect to: '#{host}'")
+ ui.error("The server's certificate belongs to '#{cn}'")
+ ui.err(<<~ADVICE)
+
+ #{ui.color("TO FIX THIS ERROR:", :bold)}
+
+ The solution for this issue depends on your networking configuration. If you
+ are able to connect to this server using the hostname #{cn}
+ instead of #{host}, then you can resolve this issue by updating chef_server_url
+ in your configuration file.
+
+ If you are not able to connect to the server using the hostname #{cn}
+ you will have to update the certificate on the server to use the correct hostname.
+ ADVICE
+ end
+
+ def debug_ssl_settings
+ ui.err "OpenSSL Configuration:"
+ ui.err "* Version: #{OpenSSL::OPENSSL_VERSION}"
+ ui.err "* Certificate file: #{OpenSSL::X509::DEFAULT_CERT_FILE}"
+ ui.err "* Certificate directory: #{OpenSSL::X509::DEFAULT_CERT_DIR}"
+ end
+
+ def debug_chef_ssl_config
+ ui.err "#{ChefUtils::Dist::Infra::PRODUCT} SSL Configuration:"
+ ui.err "* ssl_ca_path: #{configuration.ssl_ca_path.inspect}"
+ ui.err "* ssl_ca_file: #{configuration.ssl_ca_file.inspect}"
+ ui.err "* trusted_certs_dir: #{configuration.trusted_certs_dir.inspect}"
+ end
+
+ def configuration
+ Chef::Config
+ end
+
+ def run
+ validate_uri
+
+ if verify_X509 && verify_cert && verify_cert_host
+ ui.msg "Successfully verified certificates from `#{host}'"
+ else
+ exit 1
+ end
+ end
+
+ private
+
+ def trusted_certificates
+ if configuration.trusted_certs_dir && Dir.exist?(configuration.trusted_certs_dir)
+ glob_dir = ChefConfig::PathHelper.escape_glob_dir(configuration.trusted_certs_dir)
+ Dir.glob(File.join(glob_dir, "*.{crt,pem}"))
+ else
+ []
+ end
+ end
+
+ def check_X509_certificate(cert_file)
+ store = OpenSSL::X509::Store.new
+ cert = OpenSSL::X509::Certificate.new(IO.read(File.expand_path(cert_file)))
+ begin
+ store.add_cert(cert)
+ # test if the store can verify the cert we just added
+ unless store.verify(cert) # true if verified, false if not
+ return store.error_string
+ end
+ rescue OpenSSL::X509::StoreError => e
+ return e.message
+ end
+ nil
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/ssl_fetch.rb b/knife/lib/chef/knife/ssl_fetch.rb
new file mode 100644
index 0000000000..a005cebe80
--- /dev/null
+++ b/knife/lib/chef/knife/ssl_fetch.rb
@@ -0,0 +1,162 @@
+#
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class SslFetch < Chef::Knife
+
+ deps do
+ require "chef/config" unless defined?(Chef::Config)
+ require "pp" unless defined?(PP)
+ require "socket" unless defined?(Socket)
+ require "uri" unless defined?(URI)
+ require "openssl" unless defined?(OpenSSL)
+ require "chef/mixin/mixin/proxified_socket" unless defined?(Chef::Mixin::ProxifiedSocket)
+
+ include Chef::Mixin::ProxifiedSocket
+ end
+
+ banner "knife ssl fetch [URL] (options)"
+
+ def initialize(*args)
+ super
+ @uri = nil
+ end
+
+ def uri
+ @uri ||= begin
+ Chef::Log.trace("Checking SSL cert on #{given_uri}")
+ URI.parse(given_uri)
+ end
+ end
+
+ def given_uri
+ (name_args[0] || Chef::Config.chef_server_url)
+ end
+
+ def host
+ uri.host
+ end
+
+ def port
+ uri.port
+ end
+
+ def validate_uri
+ unless host && port
+ invalid_uri!
+ end
+ rescue URI::Error
+ invalid_uri!
+ end
+
+ def invalid_uri!
+ ui.error("Given URI: `#{given_uri}' is invalid")
+ show_usage
+ exit 1
+ end
+
+ def remote_cert_chain
+ tcp_connection = proxified_socket(host, port)
+ shady_ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_ssl_context)
+ shady_ssl_connection.connect
+ shady_ssl_connection.peer_cert_chain
+ end
+
+ def noverify_peer_ssl_context
+ @noverify_peer_ssl_context ||= begin
+ noverify_peer_context = OpenSSL::SSL::SSLContext.new
+ noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ noverify_peer_context
+ end
+ end
+
+ def cn_of(certificate)
+ subject = certificate.subject
+ if cn_field_tuple = subject.to_a.find { |field| field[0] == "CN" }
+ cn_field_tuple[1]
+ else
+ nil
+ end
+ end
+
+ # Convert the CN of a certificate into something that will work well as a
+ # filename. To do so, all `*` characters are converted to the string
+ # "wildcard" and then all characters other than alphanumeric and hyphen
+ # characters are converted to underscores.
+ # NOTE: There is some confusion about what the CN will contain when
+ # using internationalized domain names. RFC 6125 mandates that the ascii
+ # representation be used, but it is not clear whether this is followed in
+ # practice.
+ # https://tools.ietf.org/html/rfc6125#section-6.4.2
+ def normalize_cn(cn)
+ cn.gsub("*", "wildcard").gsub(/[^[:alnum:]\-]/, "_")
+ end
+
+ def configuration
+ Chef::Config
+ end
+
+ def trusted_certs_dir
+ configuration.trusted_certs_dir
+ end
+
+ def write_cert(cert)
+ FileUtils.mkdir_p(trusted_certs_dir)
+ cn = cn_of(cert)
+ filename = cn.nil? ? "#{host}_#{Time.new.to_i}" : normalize_cn(cn)
+ full_path = File.join(trusted_certs_dir, "#{filename}.crt")
+ ui.msg("Adding certificate for #{filename} in #{full_path}")
+ File.open(full_path, File::CREAT | File::TRUNC | File::RDWR, 0644) do |f|
+ f.print(cert.to_s)
+ end
+ end
+
+ def run
+ validate_uri
+ ui.warn(<<~TRUST_TRUST)
+ Certificates from #{host} will be fetched and placed in your trusted_cert
+ directory (#{trusted_certs_dir}).
+
+ Knife has no means to verify these are the correct certificates. You should
+ verify the authenticity of these certificates after downloading.
+
+ TRUST_TRUST
+ remote_cert_chain.each do |cert|
+ write_cert(cert)
+ end
+ rescue OpenSSL::SSL::SSLError => e
+ # 'unknown protocol' usually means you tried to connect to a non-ssl
+ # service. We handle that specially here, any other error we let bubble
+ # up (probably a bug of some sort).
+ raise unless e.message.include?("unknown protocol")
+
+ ui.error("The service at the given URI (#{uri}) does not accept SSL connections")
+
+ if uri.scheme == "http"
+ https_uri = uri.to_s.sub(/^http/, "https")
+ ui.error("Perhaps you meant to connect to '#{https_uri}'?")
+ end
+ exit 1
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/status.rb b/knife/lib/chef/knife/status.rb
new file mode 100644
index 0000000000..2e72f0a03b
--- /dev/null
+++ b/knife/lib/chef/knife/status.rb
@@ -0,0 +1,95 @@
+#
+# Author:: Ian Meyer (<ianmmeyer@gmail.com>)
+# Copyright:: Copyright 2010-2020, 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_relative "../knife"
+require_relative "core/status_presenter"
+require_relative "core/formatting_options"
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ class Knife
+ class Status < Knife
+ include Knife::Core::FormattingOptions
+
+ deps do
+ require "chef/search/query" unless defined?(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_by_mins,
+ long: "--hide-by-mins MINS",
+ description: "Hide nodes that have run #{ChefUtils::Dist::Infra::CLIENT} in the last MINS minutes"
+
+ def append_to_query(term)
+ @query << " AND " unless @query.empty?
+ @query << term
+ end
+
+ def run
+ ui.use_presenter Knife::Core::StatusPresenter
+
+ if config[:long_output]
+ opts = {}
+ else
+ opts = { filter_result:
+ { name: ["name"], ipaddress: ["ipaddress"], ohai_time: ["ohai_time"],
+ cloud: ["cloud"], run_list: ["run_list"], platform: ["platform"],
+ platform_version: ["platform_version"], chef_environment: ["chef_environment"] } }
+ end
+
+ @query ||= ""
+ append_to_query(@name_args[0]) if @name_args[0]
+ append_to_query("chef_environment:#{config[:environment]}") if config[:environment]
+
+ if config[:hide_by_mins]
+ hide_by_mins = config[:hide_by_mins].to_i
+ time = Time.now.to_i
+ # AND NOT is not valid lucene syntax, so don't use append_to_query
+ @query << " " unless @query.empty?
+ @query << "NOT ohai_time:[#{(time - hide_by_mins * 60)} TO #{time}]"
+ end
+
+ @query = @query.empty? ? "*:*" : @query
+
+ all_nodes = []
+ q = Chef::Search::Query.new
+ Chef::Log.info("Sending query: #{@query}")
+ q.search(:node, @query, opts) do |node|
+ all_nodes << node
+ end
+
+ all_nodes.sort_by! { |n| n["ohai_time"] || 0 }
+ all_nodes.reverse! if config[:sort_reverse] || config[:sort_status_reverse]
+
+ output(all_nodes)
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/supermarket_download.rb b/knife/lib/chef/knife/supermarket_download.rb
new file mode 100644
index 0000000000..5acd733b78
--- /dev/null
+++ b/knife/lib/chef/knife/supermarket_download.rb
@@ -0,0 +1,121 @@
+#
+# Author:: Christopher Webber (<cwebber@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class SupermarketDownload < Knife
+
+ banner "knife supermarket download COOKBOOK [VERSION] (options)"
+ category "supermarket"
+
+ deps do
+ require "fileutils" unless defined?(FileUtils)
+ end
+
+ option :file,
+ short: "-f FILE",
+ long: "--file FILE",
+ description: "The filename to write to."
+
+ option :force,
+ long: "--force",
+ description: "Force download deprecated version."
+
+ option :supermarket_site,
+ short: "-m SUPERMARKET_SITE",
+ long: "--supermarket-site SUPERMARKET_SITE",
+ description: "The URL of the Supermarket site.",
+ default: "https://supermarket.chef.io"
+
+ def run
+ if current_cookbook_deprecated?
+ message = "DEPRECATION: This cookbook has been deprecated. "
+ replacement = replacement_cookbook
+ if !replacement.to_s.strip.empty?
+ message << "It has been replaced by #{replacement}."
+ else
+ message << "No replacement has been defined."
+ end
+ 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
+ "#{config[:supermarket_site]}/api/v1/cookbooks"
+ end
+
+ def current_cookbook_data
+ @current_cookbook_data ||= begin
+ noauth_rest.get "#{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 uri
+ end
+ end
+
+ def download_cookbook
+ ui.info "Downloading #{@name_args[0]} from Supermarket at version #{version} to #{download_location}"
+ tf = noauth_rest.streaming_request(desired_cookbook_data["file"])
+
+ ::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
+ File.basename(current_cookbook_data["replacement"] || "")
+ end
+
+ def specific_cookbook_version_url
+ "#{cookbooks_api_url}/#{@name_args[0]}/versions/#{@name_args[1].tr(".", "_")}"
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/supermarket_install.rb b/knife/lib/chef/knife/supermarket_install.rb
new file mode 100644
index 0000000000..c979a4d6f4
--- /dev/null
+++ b/knife/lib/chef/knife/supermarket_install.rb
@@ -0,0 +1,192 @@
+#
+# Author:: Christopher Webber (<cwebber@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class SupermarketInstall < Knife
+
+ deps do
+ require "chef/exceptions" unless defined?(Chef::Exceptions)
+ require "shellwords" unless defined?(Shellwords)
+ require "mixlib/archive" unless defined?(Mixlib::Archive)
+ require_relative "core/cookbook_scm_repo"
+ require "chef/cookbook/metadata" unless defined?(Chef::Cookbook::Metadata)
+ end
+
+ banner "knife supermarket install COOKBOOK [VERSION] (options)"
+ category "supermarket"
+
+ 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
+
+ option :supermarket_site,
+ short: "-m SUPERMARKET_SITE",
+ long: "--supermarket-site SUPERMARKET_SITE",
+ description: "The URL of the Supermarket site.",
+ default: "https://supermarket.chef.io"
+
+ attr_reader :cookbook_name
+ attr_reader :vendor_path
+
+ def run
+ 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(Array(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 macOS.
+ 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]
+ preferred_metadata.dependencies.each_key do |cookbook|
+ # 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}$/) && 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::SupermarketDownload.new
+ downloader.config[:file] = download_path
+ downloader.config[:supermarket_site] = config[:supermarket_site]
+ downloader.name_args = name_args
+ downloader.run
+ downloader
+ end
+
+ def extract_cookbook(upstream_file, version)
+ ui.info("Uncompressing #{@cookbook_name} version #{version}.")
+ Mixlib::Archive.new(convert_path(upstream_file)).extract(@install_path, perms: false)
+ end
+
+ def clear_existing_files(cookbook_path)
+ ui.info("Removing pre-existing version.")
+ FileUtils.rmtree(cookbook_path) if File.directory?(cookbook_path)
+ end
+
+ def convert_path(upstream_file)
+ # converts a Windows path (C:\foo) to a mingw path (/c/foo)
+ if ENV["MSYSTEM"] == "MINGW32"
+ upstream_file.sub(/^([[:alpha:]]):/, '/\1')
+ else
+ Shellwords.escape upstream_file
+ end
+ end
+
+ # Get the preferred metadata path on disk. Chef prefers the metadata.rb
+ # over the metadata.json.
+ #
+ # @raise if there is no metadata in the cookbook
+ #
+ # @return [Chef::Cookbook::Metadata]
+ def preferred_metadata
+ md = Chef::Cookbook::Metadata.new
+
+ rb = File.join(@install_path, @cookbook_name, "metadata.rb")
+ if File.exist?(rb)
+ md.from_file(rb)
+ return md
+ end
+
+ json = File.join(@install_path, @cookbook_name, "metadata.json")
+ if File.exist?(json)
+ json = IO.read(json)
+ md.from_json(json)
+ return md
+ end
+
+ raise Chef::Exceptions::MetadataNotFound.new(@install_path, @cookbook_name)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/supermarket_list.rb b/knife/lib/chef/knife/supermarket_list.rb
new file mode 100644
index 0000000000..7dca8d031b
--- /dev/null
+++ b/knife/lib/chef/knife/supermarket_list.rb
@@ -0,0 +1,76 @@
+#
+# Author:: Christopher Webber (<cwebber@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class SupermarketList < Knife
+
+ banner "knife supermarket list (options)"
+ category "supermarket"
+
+ option :with_uri,
+ short: "-w",
+ long: "--with-uri",
+ description: "Show corresponding URIs."
+
+ option :supermarket_site,
+ short: "-m SUPERMARKET_SITE",
+ long: "--supermarket-site SUPERMARKET_SITE",
+ description: "The URL of the Supermarket site.",
+ default: "https://supermarket.chef.io"
+
+ option :sort_by,
+ long: "--sort-by SORT",
+ description: "Use to sort the records",
+ in: %w{recently_updated recently_added most_downloaded most_followed}
+
+ option :owned_by,
+ short: "-o USER",
+ long: "--owned-by USER",
+ description: "Show cookbooks that are owned by the USER"
+
+ def run
+ if config[:with_uri]
+ ui.output(format_for_display(get_cookbook_list))
+ else
+ ui.msg(ui.list(get_cookbook_list.keys, :columns_down))
+ end
+ end
+
+ # In order to avoid pagination items limit set to 9999999
+ def get_cookbook_list(items = 9999999, start = 0, cookbook_collection = {})
+ cookbooks_url = "#{config[:supermarket_site]}/api/v1/cookbooks?items=#{items}&start=#{start}"
+ cookbooks_url << "&order=#{config[:sort_by]}" if config[:sort_by]
+ cookbooks_url << "&user=#{config[:owned_by]}" if config[:owned_by]
+ cr = noauth_rest.get(cookbooks_url)
+
+ cr["items"].each do |cookbook|
+ cookbook_collection[cookbook["cookbook_name"]] = cookbook["cookbook"]
+ end
+ new_start = start + items
+ if new_start < cr["total"]
+ get_cookbook_list(items, new_start, cookbook_collection)
+ else
+ cookbook_collection
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/supermarket_search.rb b/knife/lib/chef/knife/supermarket_search.rb
new file mode 100644
index 0000000000..57befaed35
--- /dev/null
+++ b/knife/lib/chef/knife/supermarket_search.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Christopher Webber (<cwebber@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class SupermarketSearch < Knife
+ banner "knife supermarket search QUERY (options)"
+ category "supermarket"
+
+ option :supermarket_site,
+ short: "-m SUPERMARKET_SITE",
+ long: "--supermarket-site SUPERMARKET_SITE",
+ description: "The URL of the Supermarket site.",
+ default: "https://supermarket.chef.io"
+
+ def run
+ output(search_cookbook(name_args[0]))
+ end
+
+ # In order to avoid pagination items limit set to 9999999
+ def search_cookbook(query, items = 9999999, start = 0, cookbook_collection = {})
+ cookbooks_url = "#{config[:supermarket_site]}/api/v1/search?q=#{query}&items=#{items}&start=#{start}"
+ cr = noauth_rest.get(cookbooks_url)
+ cr["items"].each do |cookbook|
+ cookbook_collection[cookbook["cookbook_name"]] = cookbook
+ end
+ new_start = start + items
+ if new_start < cr["total"]
+ search_cookbook(query, items, new_start, cookbook_collection)
+ else
+ cookbook_collection
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/supermarket_share.rb b/knife/lib/chef/knife/supermarket_share.rb
new file mode 100644
index 0000000000..61fe3b583b
--- /dev/null
+++ b/knife/lib/chef/knife/supermarket_share.rb
@@ -0,0 +1,166 @@
+#
+# Author:: Christopher Webber (<cwebber@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class SupermarketShare < Knife
+
+ include Chef::Mixin::ShellOut
+
+ deps do
+ require "chef/cookbook_loader" unless defined?(Chef::CookbookLoader)
+ require "chef/cookbook_uploader" unless defined?(Chef::CookbookUploader)
+ require "chef/knife/core/cookbook_site_streaming_uploader" unless defined?(Chef::Knife::Core::CookbookSiteStreamingUploader)
+ require "chef/mixin/shell_out" unless defined?(Chef::Mixin::ShellOut)
+ end
+
+ banner "knife supermarket share COOKBOOK [CATEGORY] (options)"
+ category "supermarket"
+
+ 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(":") }
+
+ option :dry_run,
+ long: "--dry-run",
+ short: "-n",
+ boolean: true,
+ default: false,
+ description: "Don't take action, only print what files will be uploaded to Supermarket."
+
+ option :supermarket_site,
+ short: "-m SUPERMARKET_SITE",
+ long: "--supermarket-site SUPERMARKET_SITE",
+ description: "The URL of the Supermarket site.",
+ default: "https://supermarket.chef.io"
+
+ def run
+ config[:cookbook_path] ||= Chef::Config[:cookbook_path]
+
+ if @name_args.length < 1
+ show_usage
+ ui.fatal("You must specify the cookbook name.")
+ exit(1)
+ elsif @name_args.length < 2
+ cookbook_name = @name_args[0]
+ category = get_category(cookbook_name)
+ else
+ cookbook_name = @name_args[0]
+ category = @name_args[1]
+ end
+
+ cl = Chef::CookbookLoader.new(config[:cookbook_path])
+ if cl.cookbook_exists?(cookbook_name)
+ cookbook = cl[cookbook_name]
+ Chef::CookbookUploader.new(cookbook).validate_cookbooks
+ tmp_cookbook_dir = Chef::Knife::Core::CookbookSiteStreamingUploader.create_build_dir(cookbook)
+ begin
+ Chef::Log.trace("Temp cookbook directory is #{tmp_cookbook_dir.inspect}")
+ ui.info("Making tarball #{cookbook_name}.tgz")
+ shell_out!("#{tar_cmd} -czf #{cookbook_name}.tgz #{cookbook_name}", cwd: tmp_cookbook_dir)
+ rescue => e
+ ui.error("Error making tarball #{cookbook_name}.tgz: #{e.message}. Increase log verbosity (-VV) for more information.")
+ Chef::Log.trace("\n#{e.backtrace.join("\n")}")
+ exit(1)
+ end
+
+ if config[:dry_run]
+ ui.info("Not uploading #{cookbook_name}.tgz due to --dry-run flag.")
+ result = shell_out!("#{tar_cmd} -tzf #{cookbook_name}.tgz", cwd: tmp_cookbook_dir)
+ ui.info(result.stdout)
+ FileUtils.rm_rf tmp_cookbook_dir
+ return
+ 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.trace("Removing local staging directory at #{tmp_cookbook_dir}")
+ FileUtils.rm_rf tmp_cookbook_dir
+ rescue => e
+ ui.error("Error uploading cookbook #{cookbook_name} to Supermarket: #{e.message}. Increase log verbosity (-VV) for more information.")
+ Chef::Log.trace("\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 get_category(cookbook_name)
+ data = noauth_rest.get("#{config[:supermarket_site]}/api/v1/cookbooks/#{@name_args[0]}")
+ data["category"]
+ rescue => e
+ return "Other" if e.is_a?(Net::HTTPClientException) && e.response.code == "404"
+
+ ui.fatal("Unable to reach Supermarket: #{e.message}. Increase log verbosity (-VV) for more information.")
+ Chef::Log.trace("\n#{e.backtrace.join("\n")}")
+ exit(1)
+ end
+
+ def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename)
+ uri = "#{config[:supermarket_site]}/api/v1/cookbooks"
+
+ category_string = Chef::JSONCompat.to_json({ "category" => cookbook_category })
+
+ http_resp = Chef::Knife::Core::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 /Version already exists/.match?(res["error_messages"][0])
+ ui.error "The same version of this cookbook already exists on Supermarket."
+ exit(1)
+ else
+ ui.error (res["error_messages"][0]).to_s
+ exit(1)
+ end
+ else
+ ui.error "Unknown error while sharing cookbook"
+ ui.error "Server response: #{http_resp.body}"
+ exit(1)
+ end
+ end
+ res
+ end
+
+ def tar_cmd
+ unless @tar_cmd
+ @tar_cmd = "tar"
+ begin
+ # Unix and Mac only - prefer gnutar
+ if shell_out("which gnutar").exitstatus.equal?(0)
+ @tar_cmd = "gnutar"
+ end
+ rescue Errno::ENOENT
+ end
+ end
+ @tar_cmd
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/supermarket_show.rb b/knife/lib/chef/knife/supermarket_show.rb
new file mode 100644
index 0000000000..7237cf0bc7
--- /dev/null
+++ b/knife/lib/chef/knife/supermarket_show.rb
@@ -0,0 +1,66 @@
+#
+# Author:: Christopher Webber (<cwebber@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class SupermarketShow < Knife
+
+ banner "knife supermarket show COOKBOOK [VERSION] (options)"
+ category "supermarket"
+
+ option :supermarket_site,
+ short: "-m SUPERMARKET_SITE",
+ long: "--supermarket-site SUPERMARKET_SITE",
+ description: "The URL of the Supermarket site.",
+ default: "https://supermarket.chef.io"
+
+ def run
+ output(format_for_display(get_cookbook_data))
+ end
+
+ def supermarket_uri
+ "#{config[:supermarket_site]}/api/v1"
+ end
+
+ def get_cookbook_data
+ case @name_args.length
+ when 1
+ noauth_rest.get("#{supermarket_uri}/cookbooks/#{@name_args[0]}")
+ when 2
+ noauth_rest.get("#{supermarket_uri}/cookbooks/#{@name_args[0]}/versions/#{name_args[1].tr(".", "_")}")
+ end
+ end
+
+ def get_cookbook_list(items = 10, start = 0, cookbook_collection = {})
+ cookbooks_url = "#{supermarket_uri}/cookbooks?items=#{items}&start=#{start}"
+ cr = noauth_rest.get(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/knife/lib/chef/knife/supermarket_unshare.rb b/knife/lib/chef/knife/supermarket_unshare.rb
new file mode 100644
index 0000000000..8c86769804
--- /dev/null
+++ b/knife/lib/chef/knife/supermarket_unshare.rb
@@ -0,0 +1,61 @@
+#
+# Author:: Christopher Webber (<cwebber@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class SupermarketUnshare < Knife
+
+ deps do
+ require "chef/json_compat" unless defined?(Chef::JSONCompat)
+ end
+
+ banner "knife supermarket unshare COOKBOOK"
+ category "supermarket"
+
+ option :supermarket_site,
+ short: "-m SUPERMARKET_SITE",
+ long: "--supermarket-site SUPERMARKET_SITE",
+ description: "The URL of the Supermarket site.",
+ default: "https://supermarket.chef.io"
+
+ 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 all versions of the cookbook #{@cookbook_name}"
+
+ begin
+ rest.delete "#{config[:supermarket_site]}/api/v1/cookbooks/#{@name_args[0]}"
+ rescue Net::HTTPClientException => e
+ raise e unless /Forbidden/.match?(e.message)
+
+ ui.error "Forbidden: You must be the maintainer of #{@cookbook_name} to unshare it."
+ exit 1
+ end
+
+ ui.info "Unshared all versions of the cookbook #{@cookbook_name}"
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/tag_create.rb b/knife/lib/chef/knife/tag_create.rb
new file mode 100644
index 0000000000..ed7d37e7b8
--- /dev/null
+++ b/knife/lib/chef/knife/tag_create.rb
@@ -0,0 +1,52 @@
+#
+# Author:: Ryan Davis (<ryand-ruby@zenspider.com>)
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Author:: Nuo Yan (<nuo@chef.io>)
+# Copyright:: Copyright 2011-2016, 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_relative "../knife"
+
+class Chef
+ class Knife
+ class TagCreate < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ end
+
+ banner "knife tag create NODE TAG ..."
+
+ def run
+ name = @name_args[0]
+ tags = @name_args[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/knife/lib/chef/knife/tag_delete.rb b/knife/lib/chef/knife/tag_delete.rb
new file mode 100644
index 0000000000..539ae39273
--- /dev/null
+++ b/knife/lib/chef/knife/tag_delete.rb
@@ -0,0 +1,60 @@
+#
+# Author:: Ryan Davis (<ryand-ruby@zenspider.com>)
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Author:: Nuo Yan (<nuo@chef.io>)
+# Copyright:: Copyright 2011-2016, 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_relative "../knife"
+
+class Chef
+ class Knife
+ class TagDelete < Knife
+
+ deps do
+ require "chef/node" unless defined?(Chef::Node)
+ end
+
+ banner "knife tag delete NODE TAG ..."
+
+ def run
+ name = @name_args[0]
+ tags = @name_args[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 = []
+ 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/knife/lib/chef/knife/tag_list.rb b/knife/lib/chef/knife/tag_list.rb
new file mode 100644
index 0000000000..3ab960c361
--- /dev/null
+++ b/knife/lib/chef/knife/tag_list.rb
@@ -0,0 +1,47 @@
+#
+# Author:: Ryan Davis (<ryand-ruby@zenspider.com>)
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Author:: Nuo Yan (<nuo@chef.io>)
+# Copyright:: Copyright 2011-2016, 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_relative "../knife"
+
+class Chef
+ class Knife
+ class TagList < Knife
+
+ deps do
+ require "chef/node" unless defined?(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/knife/lib/chef/knife/upload.rb b/knife/lib/chef/knife/upload.rb
new file mode 100644
index 0000000000..e8dd052e77
--- /dev/null
+++ b/knife/lib/chef/knife/upload.rb
@@ -0,0 +1,86 @@
+#
+# 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_relative "../chef_fs/knife"
+
+class Chef
+ class Knife
+ class Upload < Chef::ChefFS::Knife
+ banner "knife upload PATTERNS (options)"
+
+ category "path-based"
+
+ deps do
+ require "chef/chef_fs/command_line" unless defined?(Chef::ChefFS::CommandLine)
+ end
+
+ 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 for many files). Will overwrite frozen cookbooks."
+
+ option :freeze,
+ long: "--[no-]freeze",
+ boolean: true,
+ default: false,
+ description: "Freeze cookbooks that get uploaded."
+
+ option :dry_run,
+ long: "--dry-run",
+ short: "-n",
+ boolean: true,
+ default: false,
+ description: "Don't take action, only print what would happen."
+
+ option :diff,
+ long: "--[no-]diff",
+ boolean: true,
+ default: true,
+ description: "Turn off to avoid uploading existing files; only new (and possibly deleted) files with --no-diff."
+
+ def run
+ if name_args.length == 0
+ show_usage
+ ui.fatal("You must specify at least one argument. If you want to upload everything in this directory, run \"knife upload .\"")
+ exit 1
+ end
+
+ error = false
+ pattern_args.each do |pattern|
+ if Chef::ChefFS::FileSystem.copy_to(pattern, local_fs, chef_fs, config[:recurse] ? nil : 1, config, ui, proc { |entry| format_path(entry) })
+ error = true
+ end
+ end
+ if error
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_create.rb b/knife/lib/chef/knife/user_create.rb
new file mode 100644
index 0000000000..ae1f81628c
--- /dev/null
+++ b/knife/lib/chef/knife/user_create.rb
@@ -0,0 +1,143 @@
+#
+# Author:: Tyler Cloke (<tyler@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require "chef-utils/dist" unless defined?(ChefUtils::Dist)
+
+class Chef
+ class Knife
+ class UserCreate < Knife
+
+ attr_accessor :user_field
+
+ deps do
+ require "chef/user_v1" unless defined?(Chef::UserV1)
+ end
+
+ option :file,
+ short: "-f FILE",
+ long: "--file FILE",
+ description: "Write the private key to a file if the server generated one."
+
+ option :user_key,
+ long: "--user-key FILENAME",
+ description: "Set the initial default key for the user from a file on disk (cannot pass with --prevent-keygen)."
+
+ option :prevent_keygen,
+ short: "-k",
+ long: "--prevent-keygen",
+ description: "API V1 (#{ChefUtils::Dist::Server::PRODUCT} 12.1+) only. Prevent server from generating a default key pair for you. Cannot be passed with --user-key.",
+ boolean: true
+
+ option :orgname,
+ long: "--orgname ORGNAME",
+ short: "-o ORGNAME",
+ description: "Associate new user to an organization matching ORGNAME"
+
+ option :passwordprompt,
+ long: "--prompt-for-password",
+ short: "-p",
+ description: "Prompt for user password"
+
+ banner "knife user create USERNAME DISPLAY_NAME FIRST_NAME LAST_NAME EMAIL PASSWORD (options)"
+
+ def user
+ @user_field ||= Chef::UserV1.new
+ end
+
+ def run
+ test_mandatory_field(@name_args[0], "username")
+ user.username @name_args[0]
+
+ test_mandatory_field(@name_args[1], "display name")
+ user.display_name @name_args[1]
+
+ test_mandatory_field(@name_args[2], "first name")
+ user.first_name @name_args[2]
+
+ test_mandatory_field(@name_args[3], "last name")
+ user.last_name @name_args[3]
+
+ test_mandatory_field(@name_args[4], "email")
+ user.email @name_args[4]
+
+ password = config[:passwordprompt] ? prompt_for_password : @name_args[5]
+ unless password
+ ui.fatal "You must either provide a password or use the --prompt-for-password (-p) option"
+ exit 1
+ end
+
+ if config[:user_key] && config[:prevent_keygen]
+ show_usage
+ ui.fatal("You cannot pass --user-key and --prevent-keygen")
+ exit 1
+ end
+
+ if !config[:prevent_keygen] && !config[:user_key]
+ user.create_key(true)
+ end
+
+ if config[:user_key]
+ user.public_key File.read(File.expand_path(config[:user_key]))
+ end
+
+ user_hash = {
+ username: user.username,
+ first_name: user.first_name,
+ last_name: user.last_name,
+ display_name: "#{user.first_name} #{user.last_name}",
+ email: user.email,
+ password: password,
+ }
+
+ # Check the file before creating the user so the api is more transactional.
+ if config[:file]
+ file = config[:file]
+ unless File.exist?(file) ? File.writable?(file) : File.writable?(File.dirname(file))
+ ui.fatal "File #{config[:file]} is not writable. Check permissions."
+ exit 1
+ end
+ end
+
+ final_user = root_rest.post("users/", user_hash)
+
+ if config[:orgname]
+ request_body = { user: user.username }
+ response = root_rest.post("organizations/#{config[:orgname]}/association_requests", request_body)
+ association_id = response["uri"].split("/").last
+ root_rest.put("users/#{user.username}/association_requests/#{association_id}", { response: "accept" })
+ end
+
+ ui.info("Created #{user.username}")
+ if final_user["private_key"]
+ if config[:file]
+ File.open(config[:file], "w") do |f|
+ f.print(final_user["private_key"])
+ end
+ else
+ ui.msg final_user["private_key"]
+ end
+ end
+ end
+
+ def prompt_for_password
+ ui.ask("Please enter the user's password: ", echo: false)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_delete.rb b/knife/lib/chef/knife/user_delete.rb
new file mode 100644
index 0000000000..c1ab78174b
--- /dev/null
+++ b/knife/lib/chef/knife/user_delete.rb
@@ -0,0 +1,151 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class UserDelete < Knife
+
+ deps do
+ require "chef/org" unless defined? Chef::Org
+ end
+
+ banner "knife user delete USER (options)"
+
+ option :no_disassociate_user,
+ long: "--no-disassociate-user",
+ short: "-d",
+ description: "Don't disassociate the user first"
+
+ option :remove_from_admin_groups,
+ long: "--remove-from-admin-groups",
+ short: "-R",
+ description: "If the user is a member of any org admin groups, attempt to remove from those groups. Ignored if --no-disassociate-user is set."
+
+ attr_reader :username
+
+ def run
+ @username = @name_args[0]
+ admin_memberships = []
+ unremovable_memberships = []
+
+ if @username.nil?
+ show_usage
+ ui.fatal("You must specify a user name")
+ exit 1
+ end
+
+ ui.confirm "Do you want to delete the user #{username}"
+
+ unless config[:no_disassociate_user]
+ ui.stderr.puts("Checking organization memberships...")
+ orgs = org_memberships(username)
+ if orgs.length > 0
+ ui.stderr.puts("Checking admin group memberships for #{orgs.length} org(s).")
+ admin_memberships, unremovable_memberships = admin_group_memberships(orgs, username)
+ end
+
+ unless admin_memberships.empty?
+ unless config[:remove_from_admin_groups]
+ error_exit_admin_group_member!(username, admin_memberships)
+ end
+
+ unless unremovable_memberships.empty?
+ error_exit_cant_remove_admin_membership!(username, unremovable_memberships)
+ end
+ remove_from_admin_groups(admin_memberships, username)
+ end
+ disassociate_user(orgs, username)
+ end
+
+ delete_user(username)
+ end
+
+ def disassociate_user(orgs, username)
+ orgs.each { |org| org.dissociate_user(username) }
+ end
+
+ def org_memberships(username)
+ org_data = root_rest.get("users/#{username}/organizations")
+ org_data.map { |org| Chef::Org.new(org["organization"]["name"]) }
+ end
+
+ def remove_from_admin_groups(admin_of, username)
+ admin_of.each do |org|
+ ui.stderr.puts "Removing #{username} from admins group of '#{org.name}'"
+ org.remove_user_from_group("admins", username)
+ end
+ end
+
+ def admin_group_memberships(orgs, username)
+ admin_of = []
+ unremovable = []
+ orgs.each do |org|
+ if org.user_member_of_group?(username, "admins")
+ admin_of << org
+ if org.actor_delete_would_leave_admins_empty?
+ unremovable << org
+ end
+ end
+ end
+ [admin_of, unremovable]
+ end
+
+ def delete_user(username)
+ ui.stderr.puts "Deleting user #{username}."
+ root_rest.delete("users/#{username}")
+ end
+
+ # Error message that says how to removed from org
+ # admin groups before deleting
+ # Further
+ def error_exit_admin_group_member!(username, admin_of)
+ message = "#{username} is in the 'admins' group of the following organization(s):\n\n"
+ admin_of.each { |org| message << "- #{org.name}\n" }
+ message << <<~EOM
+
+ Run this command again with the --remove-from-admin-groups option to
+ remove the user from these admin group(s) automatically.
+
+ EOM
+ ui.fatal message
+ exit 1
+ end
+
+ def error_exit_cant_remove_admin_membership!(username, only_admin_of)
+ message = <<~EOM
+
+ #{username} is the only member of the 'admins' group of the
+ following organization(s):
+
+ EOM
+ only_admin_of.each { |org| message << "- #{org.name}\n" }
+ message << <<~EOM
+
+ Removing the only administrator of an organization can break it.
+ Assign additional users or groups to the admin group(s) before
+ deleting this user.
+
+ EOM
+ ui.fatal message
+ exit 1
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_dissociate.rb b/knife/lib/chef/knife/user_dissociate.rb
new file mode 100644
index 0000000000..6af1559608
--- /dev/null
+++ b/knife/lib/chef/knife/user_dissociate.rb
@@ -0,0 +1,42 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class UserDissociate < Chef::Knife
+ category "user"
+ banner "knife user dissociate USERNAMES"
+
+ def run
+ if name_args.length < 1
+ show_usage
+ ui.fatal("You must specify a username.")
+ exit 1
+ end
+ users = name_args
+ ui.confirm("Are you sure you want to dissociate the following users: #{users.join(", ")}")
+ users.each do |u|
+ api_endpoint = "users/#{u}"
+ rest.delete_rest(api_endpoint)
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_edit.rb b/knife/lib/chef/knife/user_edit.rb
new file mode 100644
index 0000000000..fff8c6b70f
--- /dev/null
+++ b/knife/lib/chef/knife/user_edit.rb
@@ -0,0 +1,94 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class UserEdit < Knife
+
+ banner "knife user edit USER (options)"
+
+ option :input,
+ long: "--input FILENAME",
+ short: "-i FILENAME",
+ description: "Name of file to use for PUT or POST"
+
+ option :filename,
+ long: "--filename FILENAME",
+ short: "-f FILENAME",
+ description: "Write private key to FILENAME rather than STDOUT"
+
+ def run
+ @user_name = @name_args[0]
+
+ if @user_name.nil?
+ show_usage
+ ui.fatal("You must specify a user name")
+ exit 1
+ end
+ original_user = root_rest.get("users/#{@user_name}")
+ edited_user = get_updated_user(original_user)
+ if original_user != edited_user
+ result = root_rest.put("users/#{@user_name}", edited_user)
+ ui.msg("Saved #{@user_name}.")
+ unless result["private_key"].nil?
+ if config[:filename]
+ File.open(config[:filename], "w") do |f|
+ f.print(result["private_key"])
+ end
+ else
+ ui.msg result["private_key"]
+ end
+ end
+ else
+ ui.msg("User unchanged, not saving.")
+ end
+ end
+ end
+
+ private
+
+ # Check the options for ex: input or filename
+ # Read Or Open file to update user information
+ # return updated user
+ def get_updated_user(original_user)
+ if config[:input]
+ edited_user = JSON.parse(IO.read(config[:input]))
+ elsif config[:filename]
+ file = config[:filename]
+ unless File.exist?(file) ? File.writable?(file) : File.writable?(File.dirname(file))
+ ui.fatal "File #{file} is not writable. Check permissions."
+ exit 1
+ else
+ output = Chef::JSONCompat.to_json_pretty(original_user)
+ File.open(file, "w") do |f|
+ f.sync = true
+ f.puts output
+ f.close
+ raise "Please set EDITOR environment variable. See https://docs.chef.io/knife_setup/ for details." unless system("#{config[:editor]} #{f.path}")
+
+ edited_user = JSON.parse(IO.read(f.path))
+ end
+ end
+ else
+ edited_user = JSON.parse(edit_data(original_user, false))
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_invite_add.rb b/knife/lib/chef/knife/user_invite_add.rb
new file mode 100644
index 0000000000..1690147535
--- /dev/null
+++ b/knife/lib/chef/knife/user_invite_add.rb
@@ -0,0 +1,43 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class UserInviteAdd < Chef::Knife
+ category "user"
+ banner "knife user invite add USERNAMES"
+
+ def run
+ if name_args.length < 1
+ show_usage
+ ui.fatal("You must specify a username.")
+ exit 1
+ end
+
+ users = name_args
+ api_endpoint = "association_requests/"
+ users.each do |u|
+ body = { user: u }
+ rest.post_rest(api_endpoint, body)
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_invite_list.rb b/knife/lib/chef/knife/user_invite_list.rb
new file mode 100644
index 0000000000..831774d1bf
--- /dev/null
+++ b/knife/lib/chef/knife/user_invite_list.rb
@@ -0,0 +1,34 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class UserInviteList < Chef::Knife
+ category "user"
+ banner "knife user invite list"
+
+ def run
+ api_endpoint = "association_requests/"
+ invited_users = rest.get_rest(api_endpoint).map { |i| i["username"] }
+ ui.output(invited_users)
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_invite_rescind.rb b/knife/lib/chef/knife/user_invite_rescind.rb
new file mode 100644
index 0000000000..fd5804e10a
--- /dev/null
+++ b/knife/lib/chef/knife/user_invite_rescind.rb
@@ -0,0 +1,63 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class UserInviteRescind < Chef::Knife
+ category "user"
+ banner "knife user invite rescind [USERNAMES] (options)"
+
+ option :all,
+ short: "-a",
+ long: "--all",
+ description: "Rescind all invites!"
+
+ def run
+ if (name_args.length < 1) && ! config.key?(:all)
+ show_usage
+ ui.fatal("You must specify a username.")
+ exit 1
+ end
+
+ # To rescind we need to send a DELETE to association_requests/INVITE_ID
+ # For user friendliness we look up the invite ID based on username.
+ @invites = {}
+ usernames = name_args
+ rest.get_rest("association_requests").each { |i| @invites[i["username"]] = i["id"] }
+ if config[:all]
+ ui.confirm("Are you sure you want to rescind all association requests")
+ @invites.each do |u, i|
+ rest.delete_rest("association_requests/#{i}")
+ end
+ else
+ ui.confirm("Are you sure you want to rescind the association requests for: #{usernames.join(", ")}")
+ usernames.each do |u|
+ if @invites.key?(u)
+ rest.delete_rest("association_requests/#{@invites[u]}")
+ else
+ ui.fatal("No association request for #{u}.")
+ exit 1
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_key_create.rb b/knife/lib/chef/knife/user_key_create.rb
new file mode 100644
index 0000000000..efc783dd7f
--- /dev/null
+++ b/knife/lib/chef/knife/user_key_create.rb
@@ -0,0 +1,73 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "key_create_base"
+
+class Chef
+ class Knife
+ # Implements knife user key create using Chef::Knife::KeyCreate
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the user that this key is for
+ class UserKeyCreate < Knife
+ include Chef::Knife::KeyCreateBase
+
+ banner "knife user key create USER (options)"
+
+ deps do
+ require_relative "key_create"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def actor_field_name
+ "user"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyCreate.new(@actor, actor_field_name, ui, config)
+ end
+
+ def actor_missing_error
+ "You must specify a user name"
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_key_delete.rb b/knife/lib/chef/knife/user_key_delete.rb
new file mode 100644
index 0000000000..b4f84fdb7b
--- /dev/null
+++ b/knife/lib/chef/knife/user_key_delete.rb
@@ -0,0 +1,80 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ # Implements knife user key delete using Chef::Knife::KeyDelete
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the client that this key is for
+ class UserKeyDelete < Knife
+ banner "knife user key delete USER KEYNAME (options)"
+
+ deps do
+ require_relative "key_delete"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def actor_field_name
+ "user"
+ end
+
+ def actor_missing_error
+ "You must specify a user name"
+ end
+
+ def keyname_missing_error
+ "You must specify a key name"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyDelete.new(@name, @actor, actor_field_name, ui)
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ @name = params[1]
+ if @name.nil?
+ show_usage
+ ui.fatal(keyname_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_key_edit.rb b/knife/lib/chef/knife/user_key_edit.rb
new file mode 100644
index 0000000000..15ef2ada1e
--- /dev/null
+++ b/knife/lib/chef/knife/user_key_edit.rb
@@ -0,0 +1,83 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "key_edit_base"
+
+class Chef
+ class Knife
+ # Implements knife user key edit using Chef::Knife::KeyEdit
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the user that this key is for
+ class UserKeyEdit < Knife
+ include Chef::Knife::KeyEditBase
+
+ banner "knife user key edit USER KEYNAME (options)"
+
+ deps do
+ require_relative "key_edit"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def actor_field_name
+ "user"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyEdit.new(@name, @actor, actor_field_name, ui, config)
+ end
+
+ def actor_missing_error
+ "You must specify a user name"
+ end
+
+ def keyname_missing_error
+ "You must specify a key name"
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ @name = params[1]
+ if @name.nil?
+ show_usage
+ ui.fatal(keyname_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_key_list.rb b/knife/lib/chef/knife/user_key_list.rb
new file mode 100644
index 0000000000..781998b301
--- /dev/null
+++ b/knife/lib/chef/knife/user_key_list.rb
@@ -0,0 +1,73 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+require_relative "key_list_base"
+
+class Chef
+ class Knife
+ # Implements knife user key list using Chef::Knife::KeyList
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the client that this key is for
+ class UserKeyList < Knife
+ include Chef::Knife::KeyListBase
+
+ banner "knife user key list USER (options)"
+
+ deps do
+ require_relative "key_list"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def list_method
+ :list_by_user
+ end
+
+ def actor_missing_error
+ "You must specify a user name"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyList.new(@actor, list_method, ui, config)
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_key_show.rb b/knife/lib/chef/knife/user_key_show.rb
new file mode 100644
index 0000000000..2bf535c792
--- /dev/null
+++ b/knife/lib/chef/knife/user_key_show.rb
@@ -0,0 +1,80 @@
+#
+# Author:: Tyler Cloke (tyler@chef.io)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ # Implements knife user key show using Chef::Knife::KeyShow
+ # as a service class.
+ #
+ # @author Tyler Cloke
+ #
+ # @attr_reader [String] actor the name of the client that this key is for
+ class UserKeyShow < Knife
+ banner "knife user key show USER KEYNAME (options)"
+
+ deps do
+ require_relative "key_show"
+ end
+
+ attr_reader :actor
+
+ def initialize(argv = [])
+ super(argv)
+ @service_object = nil
+ end
+
+ def run
+ apply_params!(@name_args)
+ service_object.run
+ end
+
+ def load_method
+ :load_by_user
+ end
+
+ def actor_missing_error
+ "You must specify a user name"
+ end
+
+ def keyname_missing_error
+ "You must specify a key name"
+ end
+
+ def service_object
+ @service_object ||= Chef::Knife::KeyShow.new(@name, @actor, load_method, ui)
+ end
+
+ def apply_params!(params)
+ @actor = params[0]
+ if @actor.nil?
+ show_usage
+ ui.fatal(actor_missing_error)
+ exit 1
+ end
+ @name = params[1]
+ if @name.nil?
+ show_usage
+ ui.fatal(keyname_missing_error)
+ exit 1
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_list.rb b/knife/lib/chef/knife/user_list.rb
new file mode 100644
index 0000000000..cb3b577541
--- /dev/null
+++ b/knife/lib/chef/knife/user_list.rb
@@ -0,0 +1,43 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class UserList < Knife
+
+ deps do
+ # is not used there, only in knife.
+ require "chef/user_v1" unless defined?(Chef::UserV1)
+ end
+
+ banner "knife user list (options)"
+
+ option :with_uri,
+ short: "-w",
+ long: "--with-uri",
+ description: "Show corresponding URIs."
+
+ def run
+ output(format_list_for_display(Chef::UserV1.list))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_password.rb b/knife/lib/chef/knife/user_password.rb
new file mode 100644
index 0000000000..2da3c3e285
--- /dev/null
+++ b/knife/lib/chef/knife/user_password.rb
@@ -0,0 +1,70 @@
+#
+# Author:: Tyler Cloke (<tyler@getchef.com>)
+# Copyright:: Copyright (c) Chef Software 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 UserPassword < Knife
+ banner "knife user password USERNAME [PASSWORD | --enable-external-auth]"
+
+ option :enable_external_auth,
+ long: "--enable-external-auth",
+ short: "-e",
+ description: "Enable external authentication for this user (such as LDAP)"
+
+ def run
+ # check that correct number of args was passed, should be either
+ # USERNAME PASSWORD or USERNAME --enable-external-auth
+ #
+ # note that you can't pass USERNAME PASSWORD --enable-external-auth
+ unless (@name_args.length == 2 && !config[:enable_external_auth]) || (@name_args.length == 1 && config[:enable_external_auth])
+ show_usage
+ ui.fatal("You must pass two arguments")
+ ui.fatal("Note that --enable-external-auth cannot be passed with a password")
+ exit 1
+ end
+
+ user_name = @name_args[0]
+
+ # note that this will be nil if config[:enable_external_auth] is true
+ password = @name_args[1]
+
+ # since the API does not pass back whether recovery_authentication_enabled is
+ # true or false, there is no way of knowing if the user is using ldap or not,
+ # so we will update the user every time, instead of checking if we are actually
+ # changing anything before we PUT.
+ result = root_rest.get("users/#{user_name}")
+
+ result["password"] = password unless password.nil?
+
+ # if --enable-external-auth was passed, enable it, else disable it.
+ # there is never a situation where we would want to enable ldap
+ # AND change the password. changing the password means that the user
+ # wants to disable ldap and put user in recover (if they are using ldap).
+ result["recovery_authentication_enabled"] = !config[:enable_external_auth]
+
+ begin
+ root_rest.put("users/#{user_name}", result)
+ rescue => e
+ raise e
+ end
+
+ ui.msg("Authentication info updated for #{user_name}.")
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_reregister.rb b/knife/lib/chef/knife/user_reregister.rb
new file mode 100644
index 0000000000..cf2adbceb2
--- /dev/null
+++ b/knife/lib/chef/knife/user_reregister.rb
@@ -0,0 +1,59 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class UserReregister < Knife
+
+ deps do
+ require "chef/user_v1" unless defined?(Chef::UserV1)
+ end
+
+ banner "knife user reregister USER (options)"
+
+ option :file,
+ short: "-f FILE",
+ long: "--file FILE",
+ description: "Write the private key to a file."
+
+ def run
+ @user_name = @name_args[0]
+
+ if @user_name.nil?
+ show_usage
+ ui.fatal("You must specify a user name")
+ exit 1
+ end
+
+ user = Chef::UserV1.load(@user_name)
+ user.reregister
+ Chef::Log.trace("Updated user data: #{user.inspect}")
+ key = user.private_key
+ if config[:file]
+ File.open(config[:file], "w") do |f|
+ f.print(key)
+ end
+ else
+ ui.msg key
+ end
+ end
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/user_show.rb b/knife/lib/chef/knife/user_show.rb
new file mode 100644
index 0000000000..ea2b06b753
--- /dev/null
+++ b/knife/lib/chef/knife/user_show.rb
@@ -0,0 +1,52 @@
+#
+# Author:: Steven Danna (<steve@chef.io>)
+# Copyright:: Copyright (c) Chef Software 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_relative "../knife"
+
+class Chef
+ class Knife
+ class UserShow < Knife
+
+ include Knife::Core::MultiAttributeReturnOption
+
+ banner "knife user show USER (options)"
+
+ option :with_orgs,
+ long: "--with-orgs",
+ short: "-l"
+
+ def run
+ @user_name = @name_args[0]
+
+ if @user_name.nil?
+ show_usage
+ ui.fatal("You must specify a user name")
+ exit 1
+ end
+
+ results = root_rest.get("users/#{@user_name}")
+ if config[:with_orgs]
+ orgs = root_rest.get("users/#{@user_name}/organizations")
+ results["organizations"] = orgs.map { |o| o["organization"]["name"] }
+ end
+ output(format_for_display(results))
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/version.rb b/knife/lib/chef/knife/version.rb
new file mode 100644
index 0000000000..109f033187
--- /dev/null
+++ b/knife/lib/chef/knife/version.rb
@@ -0,0 +1,24 @@
+# Copyright:: Copyright (c) Chef Software 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
+ KNIFE_ROOT = File.expand_path("../..", __dir__)
+ VERSION = "17.0.173".freeze
+ end
+end
+
+
diff --git a/knife/lib/chef/knife/xargs.rb b/knife/lib/chef/knife/xargs.rb
new file mode 100644
index 0000000000..fc82d390cb
--- /dev/null
+++ b/knife/lib/chef/knife/xargs.rb
@@ -0,0 +1,282 @@
+#
+# 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_relative "../chef_fs/knife"
+
+class Chef
+ class Knife
+ class Xargs < Chef::ChefFS::Knife
+ banner "knife xargs [COMMAND] (options)"
+
+ category "path-based"
+
+ deps do
+ require "chef/chef_fs/file_system" unless defined?(Chef::ChefFS::FileSystem)
+ require "chef/chef_fs/file_system/exceptions" unless defined?(Chef::ChefFS::FileSystem::Exceptions)
+ end
+
+ # TODO modify to remote-only / local-only pattern (more like delete)
+ option :local,
+ long: "--local",
+ boolean: true,
+ description: "Xargs local files instead of remote."
+
+ option :patterns,
+ long: "--pattern [PATTERN]",
+ short: "-p [PATTERN]",
+ description: "Pattern on command line (if these are not specified, a list of patterns is expected on standard input). Multiple patterns may be passed in this way.",
+ arg_arity: [1, -1]
+
+ option :diff,
+ long: "--[no-]diff",
+ default: true,
+ boolean: true,
+ description: "Whether to show a diff when files change (default: true)."
+
+ option :dry_run,
+ long: "--dry-run",
+ boolean: true,
+ description: "Prevents changes from actually being uploaded to the server."
+
+ option :force,
+ long: "--[no-]force",
+ boolean: true,
+ default: false,
+ description: "Force upload of files even if they are not changed (quicker and harmless, but doesn't print out what it changed)."
+
+ option :replace_first,
+ long: "--replace-first REPLACESTR",
+ short: "-J REPLACESTR",
+ description: "String to replace with filenames. -J will only replace the FIRST occurrence of the replacement string."
+
+ option :replace_all,
+ long: "--replace REPLACESTR",
+ short: "-I REPLACESTR",
+ description: "String to replace with filenames. -I will replace ALL occurrence of the replacement string."
+
+ option :max_arguments_per_command,
+ long: "--max-args MAXARGS",
+ short: "-n MAXARGS",
+ description: "Maximum number of arguments per command line."
+
+ option :max_command_line,
+ long: "--max-chars LENGTH",
+ short: "-s LENGTH",
+ description: "Maximum size of command line, in characters."
+
+ option :verbose_commands,
+ short: "-t",
+ description: "Print command to be run on the command line."
+
+ option :null_separator,
+ short: "-0",
+ boolean: true,
+ description: "Use the NULL character (\0) as a separator, instead of whitespace."
+
+ def run
+ error = false
+ # Get the matches (recursively)
+ files = []
+ pattern_args_from(get_patterns).each do |pattern|
+ Chef::ChefFS::FileSystem.list(config[:local] ? local_fs : chef_fs, pattern).each do |result|
+ if result.dir?
+ # TODO option to include directories
+ ui.warn "#{format_path(result)}: is a directory. Will not run #{command} on it."
+ else
+ files << result
+ ran = false
+
+ # If the command would be bigger than max command line, back it off a bit
+ # and run a slightly smaller command (with one less arg)
+ if config[:max_command_line]
+ command, tempfiles = create_command(files)
+ begin
+ if command.length > config[:max_command_line].to_i
+ if files.length > 1
+ command, tempfiles_minus_one = create_command(files[0..-2])
+ begin
+ error = true if xargs_files(command, tempfiles_minus_one)
+ files = [ files[-1] ]
+ ran = true
+ ensure
+ destroy_tempfiles(tempfiles)
+ end
+ else
+ error = true if xargs_files(command, tempfiles)
+ files = [ ]
+ ran = true
+ end
+ end
+ ensure
+ destroy_tempfiles(tempfiles)
+ end
+ end
+
+ # If the command has hit the limit for the # of arguments, run it
+ if !ran && config[:max_arguments_per_command] && files.size >= config[:max_arguments_per_command].to_i
+ command, tempfiles = create_command(files)
+ begin
+ error = true if xargs_files(command, tempfiles)
+ files = []
+ ran = true
+ ensure
+ destroy_tempfiles(tempfiles)
+ end
+ end
+ end
+ end
+ end
+
+ # Any leftovers commands shall be run
+ if files.size > 0
+ command, tempfiles = create_command(files)
+ begin
+ error = true if xargs_files(command, tempfiles)
+ ensure
+ destroy_tempfiles(tempfiles)
+ end
+ end
+
+ if error
+ exit 1
+ end
+ end
+
+ def get_patterns
+ if config[:patterns]
+ [ config[:patterns] ].flatten
+ elsif config[:null_separator]
+ stdin.binmode
+ stdin.read.split("\000")
+ else
+ stdin.read.split(/\s+/)
+ end
+ end
+
+ def create_command(files)
+ command = name_args.join(" ")
+
+ # Create the (empty) tempfiles
+ tempfiles = {}
+ begin
+ # Create the temporary files
+ files.each do |file|
+ tempfile = Tempfile.new(file.name)
+ tempfiles[tempfile] = { file: file }
+ end
+ rescue
+ destroy_tempfiles(files)
+ raise
+ end
+
+ # Create the command
+ paths = tempfiles.keys.map(&:path).join(" ")
+ if config[:replace_all]
+ final_command = command.gsub(config[:replace_all], paths)
+ elsif config[:replace_first]
+ final_command = command.sub(config[:replace_first], paths)
+ else
+ final_command = "#{command} #{paths}"
+ end
+
+ [final_command, tempfiles]
+ end
+
+ def destroy_tempfiles(tempfiles)
+ # Unlink the files now that we're done with them
+ tempfiles.each_key(&:close!)
+ end
+
+ def xargs_files(command, tempfiles)
+ error = false
+ # Create the temporary files
+ tempfiles.each_pair do |tempfile, file|
+
+ value = file[:file].read
+ file[:value] = value
+ tempfile.open
+ tempfile.write(value)
+ tempfile.close
+ rescue Chef::ChefFS::FileSystem::OperationNotAllowedError => e
+ ui.error "#{format_path(e.entry)}: #{e.reason}."
+ error = true
+ tempfile.close!
+ tempfiles.delete(tempfile)
+ next
+ rescue Chef::ChefFS::FileSystem::NotFoundError => e
+ ui.error "#{format_path(e.entry)}: No such file or directory"
+ error = true
+ tempfile.close!
+ tempfiles.delete(tempfile)
+ next
+
+ end
+
+ return error if error && tempfiles.size == 0
+
+ # Run the command
+ if config[:verbose_commands] || Chef::Config[:verbosity] && Chef::Config[:verbosity] >= 1
+ output sub_filenames(command, tempfiles)
+ end
+ command_output = `#{command}`
+ command_output = sub_filenames(command_output, tempfiles)
+ stdout.write command_output
+
+ # Check if the output is different
+ tempfiles.each_pair do |tempfile, file|
+ # Read the new output
+ new_value = IO.binread(tempfile.path)
+
+ # Upload the output if different
+ if config[:force] || new_value != file[:value]
+ if config[:dry_run]
+ output "Would update #{format_path(file[:file])}"
+ else
+ file[:file].write(new_value)
+ output "Updated #{format_path(file[:file])}"
+ end
+ end
+
+ # Print a diff of what was uploaded
+ if config[:diff] && new_value != file[:value]
+ old_file = Tempfile.open(file[:file].name)
+ begin
+ old_file.write(file[:value])
+ old_file.close
+
+ diff = `diff -u #{old_file.path} #{tempfile.path}`
+ diff.gsub!(old_file.path, "#{format_path(file[:file])} (old)")
+ diff.gsub!(tempfile.path, "#{format_path(file[:file])} (new)")
+ stdout.write diff
+ ensure
+ old_file.close!
+ end
+ end
+ end
+
+ error
+ end
+
+ def sub_filenames(str, tempfiles)
+ tempfiles.each_pair do |tempfile, file|
+ str = str.gsub(tempfile.path, format_path(file[:file]))
+ end
+ str
+ end
+
+ end
+ end
+end
diff --git a/knife/lib/chef/knife/yaml_convert.rb b/knife/lib/chef/knife/yaml_convert.rb
new file mode 100644
index 0000000000..6bd2d1c0ea
--- /dev/null
+++ b/knife/lib/chef/knife/yaml_convert.rb
@@ -0,0 +1,91 @@
+#
+# Author:: Bryan McLellan <btm@loftninjas.org>
+# Copyright:: Copyright (c) Chef Software 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.
+#
+
+autoload :YAML, "yaml"
+require_relative "../knife"
+class Chef::Knife::YamlConvert < Chef::Knife
+
+ banner "knife yaml convert YAML_FILENAME [RUBY_FILENAME]"
+
+ def run
+ if name_args.empty?
+ ui.fatal!("Please specify the file name of a YAML recipe to convert to Ruby")
+ elsif name_args.size >= 3
+ ui.fatal!("knife yaml convert YAML_FILENAME [RUBY_FILENAME]")
+ end
+
+ yaml_file = @name_args[0]
+ unless ::File.exist?(yaml_file) && ::File.readable?(yaml_file)
+ ui.fatal("Input YAML file '#{yaml_file}' does not exist or is unreadable")
+ end
+
+ ruby_file = if @name_args[1]
+ @name_args[1] # use the specified output filename if provided
+ else
+ if ::File.extname(yaml_file) == ".yml" || ::File.extname(yaml_file) == ".yaml"
+ yaml_file.gsub(/\.(yml|yaml)$/, ".rb")
+ else
+ yaml_file + ".rb" # fall back to putting .rb on the end of whatever the yaml file was named
+ end
+ end
+
+ if ::File.exist?(ruby_file)
+ ui.fatal!("Output Ruby file '#{ruby_file}' already exists")
+ end
+
+ yaml_contents = IO.read(yaml_file)
+
+ # YAML can contain multiple documents (--- is the separator), let's not support that.
+ if ::YAML.load_stream(yaml_contents).length > 1
+ ui.fatal!("YAML recipe '#{yaml_file}' contains multiple documents, only one is supported")
+ end
+
+ # Unfortunately, per the YAML spec, comments are stripped when we load, so we lose them on conversion
+ yaml_hash = ::YAML.safe_load(yaml_contents, permitted_classes: [Symbol])
+ unless yaml_hash.is_a?(Hash) && yaml_hash.key?("resources")
+ ui.fatal!("YAML recipe '#{source_file}' must contain a top-level 'resources' hash (YAML sequence), i.e. 'resources:'")
+ end
+
+ ui.warn("No resources found in '#{yaml_file}'") if yaml_hash["resources"].size == 0
+
+ ::File.open(ruby_file, "w") do |file|
+ file.write(resource_hash_to_string(yaml_hash["resources"], yaml_file))
+ end
+ ui.info("Converted '#{yaml_file}' to '#{ruby_file}'")
+ end
+
+ # Converts a Hash of resources to a Ruby recipe
+ # returns a string ready to be written to a file or stdout
+ def resource_hash_to_string(resource_hash, filename)
+ ruby_contents = []
+ ruby_contents << "# Autoconverted recipe from #{filename}\n"
+
+ resource_hash.each do |r|
+ type = r.delete("type")
+ name = r.delete("name")
+
+ ruby_contents << "#{type} \"#{name}\" do"
+ r.each do |k, v|
+ ruby_contents << " #{k} #{v.inspect}"
+ end
+ ruby_contents << "end\n"
+ end
+
+ ruby_contents.join("\n")
+ end
+end