From 01c6a9e1753bd4b2166efe747151ba8700ebc0f5 Mon Sep 17 00:00:00 2001 From: Chef Expeditor Date: Thu, 11 Mar 2021 17:45:48 +0000 Subject: Move knife to its own gem This moves knife into /knife, in the same way that chef-utils and chef-config are separated. NOTES: == File History == If you see this message as the first message in the history of an knife file, you can see the complete history by using 'git log --follow', 'git config log.follow true' to make it the default behavior in this repository, or 'git config --globa log.follow true' to make it the global default. == API Changes == At the API level, there is one breaking change: CookbookSiteStreamingUploader has been moved out of chef and into knife/core. There were a combination of reasons we chose this path: - CookbookSiteStreamingUploader (CSSU) is only used within Knife. - CookbookSiteStreamingUploader (CSSU) references the command Chef::Knife::CookbookMetadata in order to generate a metadata file for the cookbook to upload - Chef::Knife::CookbookMetadata is no longer available to Chef:: because Knife has been moved to its own gem. Knife gem depends on the Chef gem, so Chef can't depend on something in Knife. A search for usage in related projects (berks, chef-cli) and the Internet at large shows that there are no known external consumers of CSSU. For now, we'll move this class into Knife::Core, as it's going to be faster than splitting off the metadata generation and time is a concern. If we find that we need the metadata generation in chef/ proper, we should evaluate decoupling that functionality from Knife::CookbookMetadata and exposing it via something like `Chef::Cookbook::Metadata#from_cookbook_files` == spec changes == The specs are kept in their existing locations, though we have separated out a `knife_spec_helper` so that we can ensure knife is not directly accessing chef requires; and chef is relying on knife's at all. We also now clear gem paths with each test, to force gem state to reset. This works around a problem where a combination of tests is corrupting the internal Gem state, causing failures in rubygems_spec. See branch `mp/broken-gems` for many more details around findings so far. Signed-off-by: Marc A. Paradise --- .expeditor/config.yml | 1 + .expeditor/update_version.sh | 5 + chef.gemspec | 6 +- habitat/tests/spec.ps1 | 2 +- habitat/tests/test.pester.ps1 | 7 - habitat/tests/test.sh | 4 +- kitchen-tests/Gemfile | 3 +- knife/Gemfile | 29 + knife/LICENSE | 201 ++++ knife/Rakefile | 38 + knife/bin/knife | 24 + knife/knife.gemspec | 65 ++ knife/lib/chef/application/knife.rb | 234 ++++ knife/lib/chef/chef_fs/knife.rb | 160 +++ knife/lib/chef/knife.rb | 675 +++++++++++ knife/lib/chef/knife/acl_add.rb | 57 + knife/lib/chef/knife/acl_base.rb | 183 +++ knife/lib/chef/knife/acl_bulk_add.rb | 78 ++ knife/lib/chef/knife/acl_bulk_remove.rb | 83 ++ knife/lib/chef/knife/acl_remove.rb | 62 + knife/lib/chef/knife/acl_show.rb | 56 + knife/lib/chef/knife/bootstrap.rb | 1191 +++++++++++++++++++ .../lib/chef/knife/bootstrap/chef_vault_handler.rb | 162 +++ knife/lib/chef/knife/bootstrap/client_builder.rb | 212 ++++ knife/lib/chef/knife/bootstrap/templates/README.md | 11 + .../chef/knife/bootstrap/templates/chef-full.erb | 242 ++++ .../templates/windows-chef-client-msi.erb | 278 +++++ knife/lib/chef/knife/bootstrap/train_connector.rb | 336 ++++++ knife/lib/chef/knife/client_bulk_delete.rb | 104 ++ knife/lib/chef/knife/client_create.rb | 101 ++ knife/lib/chef/knife/client_delete.rb | 62 + knife/lib/chef/knife/client_edit.rb | 52 + knife/lib/chef/knife/client_key_create.rb | 73 ++ knife/lib/chef/knife/client_key_delete.rb | 80 ++ knife/lib/chef/knife/client_key_edit.rb | 83 ++ knife/lib/chef/knife/client_key_list.rb | 73 ++ knife/lib/chef/knife/client_key_show.rb | 80 ++ knife/lib/chef/knife/client_list.rb | 41 + knife/lib/chef/knife/client_reregister.rb | 58 + knife/lib/chef/knife/client_show.rb | 48 + knife/lib/chef/knife/config_get.rb | 39 + knife/lib/chef/knife/config_get_profile.rb | 37 + knife/lib/chef/knife/config_list.rb | 139 +++ knife/lib/chef/knife/config_list_profiles.rb | 37 + knife/lib/chef/knife/config_show.rb | 127 +++ knife/lib/chef/knife/config_use.rb | 61 + knife/lib/chef/knife/config_use_profile.rb | 47 + knife/lib/chef/knife/configure.rb | 150 +++ knife/lib/chef/knife/configure_client.rb | 48 + knife/lib/chef/knife/cookbook_bulk_delete.rb | 71 ++ knife/lib/chef/knife/cookbook_delete.rb | 151 +++ knife/lib/chef/knife/cookbook_download.rb | 142 +++ knife/lib/chef/knife/cookbook_list.rb | 47 + knife/lib/chef/knife/cookbook_metadata.rb | 106 ++ .../lib/chef/knife/cookbook_metadata_from_file.rb | 49 + knife/lib/chef/knife/cookbook_show.rb | 98 ++ knife/lib/chef/knife/cookbook_upload.rb | 292 +++++ knife/lib/chef/knife/core/bootstrap_context.rb | 264 +++++ knife/lib/chef/knife/core/cookbook_scm_repo.rb | 159 +++ .../knife/core/cookbook_site_streaming_uploader.rb | 249 ++++ knife/lib/chef/knife/core/formatting_options.rb | 49 + knife/lib/chef/knife/core/gem_glob_loader.rb | 134 +++ knife/lib/chef/knife/core/generic_presenter.rb | 232 ++++ knife/lib/chef/knife/core/hashed_command_loader.rb | 100 ++ knife/lib/chef/knife/core/node_editor.rb | 130 +++ knife/lib/chef/knife/core/node_presenter.rb | 133 +++ knife/lib/chef/knife/core/object_loader.rb | 116 ++ knife/lib/chef/knife/core/status_presenter.rb | 147 +++ knife/lib/chef/knife/core/subcommand_loader.rb | 208 ++++ knife/lib/chef/knife/core/text_formatter.rb | 85 ++ knife/lib/chef/knife/core/ui.rb | 338 ++++++ .../chef/knife/core/windows_bootstrap_context.rb | 406 +++++++ knife/lib/chef/knife/data_bag_create.rb | 81 ++ knife/lib/chef/knife/data_bag_delete.rb | 49 + knife/lib/chef/knife/data_bag_edit.rb | 74 ++ knife/lib/chef/knife/data_bag_from_file.rb | 113 ++ knife/lib/chef/knife/data_bag_list.rb | 42 + knife/lib/chef/knife/data_bag_secret_options.rb | 122 ++ knife/lib/chef/knife/data_bag_show.rb | 69 ++ knife/lib/chef/knife/delete.rb | 125 ++ knife/lib/chef/knife/deps.rb | 156 +++ knife/lib/chef/knife/diff.rb | 83 ++ knife/lib/chef/knife/download.rb | 84 ++ knife/lib/chef/knife/edit.rb | 88 ++ knife/lib/chef/knife/environment_compare.rb | 128 +++ knife/lib/chef/knife/environment_create.rb | 52 + knife/lib/chef/knife/environment_delete.rb | 44 + knife/lib/chef/knife/environment_edit.rb | 44 + knife/lib/chef/knife/environment_from_file.rb | 84 ++ knife/lib/chef/knife/environment_list.rb | 41 + knife/lib/chef/knife/environment_show.rb | 47 + knife/lib/chef/knife/exec.rb | 99 ++ knife/lib/chef/knife/group_add.rb | 55 + knife/lib/chef/knife/group_create.rb | 49 + knife/lib/chef/knife/group_destroy.rb | 53 + knife/lib/chef/knife/group_list.rb | 43 + knife/lib/chef/knife/group_remove.rb | 56 + knife/lib/chef/knife/group_show.rb | 49 + knife/lib/chef/knife/key_create.rb | 112 ++ knife/lib/chef/knife/key_create_base.rb | 50 + knife/lib/chef/knife/key_delete.rb | 55 + knife/lib/chef/knife/key_edit.rb | 118 ++ knife/lib/chef/knife/key_edit_base.rb | 55 + knife/lib/chef/knife/key_list.rb | 90 ++ knife/lib/chef/knife/key_list_base.rb | 45 + knife/lib/chef/knife/key_show.rb | 53 + knife/lib/chef/knife/list.rb | 177 +++ knife/lib/chef/knife/node_bulk_delete.rb | 75 ++ knife/lib/chef/knife/node_create.rb | 47 + knife/lib/chef/knife/node_delete.rb | 46 + knife/lib/chef/knife/node_edit.rb | 70 ++ knife/lib/chef/knife/node_environment_set.rb | 53 + knife/lib/chef/knife/node_from_file.rb | 51 + knife/lib/chef/knife/node_list.rb | 44 + knife/lib/chef/knife/node_policy_set.rb | 79 ++ knife/lib/chef/knife/node_run_list_add.rb | 104 ++ knife/lib/chef/knife/node_run_list_remove.rb | 67 ++ knife/lib/chef/knife/node_run_list_set.rb | 66 ++ knife/lib/chef/knife/node_show.rb | 63 ++ knife/lib/chef/knife/null.rb | 12 + knife/lib/chef/knife/org_create.rb | 70 ++ knife/lib/chef/knife/org_delete.rb | 32 + knife/lib/chef/knife/org_edit.rb | 48 + knife/lib/chef/knife/org_list.rb | 44 + knife/lib/chef/knife/org_show.rb | 31 + knife/lib/chef/knife/org_user_add.rb | 62 + knife/lib/chef/knife/org_user_remove.rb | 103 ++ knife/lib/chef/knife/raw.rb | 123 ++ knife/lib/chef/knife/recipe_list.rb | 32 + knife/lib/chef/knife/rehash.rb | 50 + knife/lib/chef/knife/role_bulk_delete.rb | 66 ++ knife/lib/chef/knife/role_create.rb | 53 + knife/lib/chef/knife/role_delete.rb | 46 + knife/lib/chef/knife/role_edit.rb | 45 + knife/lib/chef/knife/role_env_run_list_add.rb | 87 ++ knife/lib/chef/knife/role_env_run_list_clear.rb | 55 + knife/lib/chef/knife/role_env_run_list_remove.rb | 57 + knife/lib/chef/knife/role_env_run_list_replace.rb | 60 + knife/lib/chef/knife/role_env_run_list_set.rb | 70 ++ knife/lib/chef/knife/role_from_file.rb | 51 + knife/lib/chef/knife/role_list.rb | 42 + knife/lib/chef/knife/role_run_list_add.rb | 87 ++ knife/lib/chef/knife/role_run_list_clear.rb | 55 + knife/lib/chef/knife/role_run_list_remove.rb | 56 + knife/lib/chef/knife/role_run_list_replace.rb | 60 + knife/lib/chef/knife/role_run_list_set.rb | 69 ++ knife/lib/chef/knife/role_show.rb | 48 + knife/lib/chef/knife/search.rb | 194 ++++ knife/lib/chef/knife/serve.rb | 65 ++ knife/lib/chef/knife/show.rb | 72 ++ knife/lib/chef/knife/ssh.rb | 645 +++++++++++ knife/lib/chef/knife/ssl_check.rb | 284 +++++ knife/lib/chef/knife/ssl_fetch.rb | 162 +++ knife/lib/chef/knife/status.rb | 95 ++ knife/lib/chef/knife/supermarket_download.rb | 121 ++ knife/lib/chef/knife/supermarket_install.rb | 192 ++++ knife/lib/chef/knife/supermarket_list.rb | 76 ++ knife/lib/chef/knife/supermarket_search.rb | 53 + knife/lib/chef/knife/supermarket_share.rb | 166 +++ knife/lib/chef/knife/supermarket_show.rb | 66 ++ knife/lib/chef/knife/supermarket_unshare.rb | 61 + knife/lib/chef/knife/tag_create.rb | 52 + knife/lib/chef/knife/tag_delete.rb | 60 + knife/lib/chef/knife/tag_list.rb | 47 + knife/lib/chef/knife/upload.rb | 86 ++ knife/lib/chef/knife/user_create.rb | 143 +++ knife/lib/chef/knife/user_delete.rb | 151 +++ knife/lib/chef/knife/user_dissociate.rb | 42 + knife/lib/chef/knife/user_edit.rb | 94 ++ knife/lib/chef/knife/user_invite_add.rb | 43 + knife/lib/chef/knife/user_invite_list.rb | 34 + knife/lib/chef/knife/user_invite_rescind.rb | 63 ++ knife/lib/chef/knife/user_key_create.rb | 73 ++ knife/lib/chef/knife/user_key_delete.rb | 80 ++ knife/lib/chef/knife/user_key_edit.rb | 83 ++ knife/lib/chef/knife/user_key_list.rb | 73 ++ knife/lib/chef/knife/user_key_show.rb | 80 ++ knife/lib/chef/knife/user_list.rb | 43 + knife/lib/chef/knife/user_password.rb | 70 ++ knife/lib/chef/knife/user_reregister.rb | 59 + knife/lib/chef/knife/user_show.rb | 52 + knife/lib/chef/knife/version.rb | 24 + knife/lib/chef/knife/xargs.rb | 282 +++++ knife/lib/chef/knife/yaml_convert.rb | 91 ++ lib/chef/application/knife.rb | 234 ---- lib/chef/applications.rb | 1 - lib/chef/chef_fs/knife.rb | 160 --- lib/chef/cookbook_site_streaming_uploader.rb | 244 ---- lib/chef/cookbook_uploader.rb | 1 - lib/chef/knife.rb | 672 ----------- lib/chef/knife/acl_add.rb | 57 - lib/chef/knife/acl_base.rb | 183 --- lib/chef/knife/acl_bulk_add.rb | 78 -- lib/chef/knife/acl_bulk_remove.rb | 83 -- lib/chef/knife/acl_remove.rb | 62 - lib/chef/knife/acl_show.rb | 56 - lib/chef/knife/bootstrap.rb | 1192 -------------------- lib/chef/knife/bootstrap/chef_vault_handler.rb | 162 --- lib/chef/knife/bootstrap/client_builder.rb | 212 ---- lib/chef/knife/bootstrap/templates/README.md | 11 - lib/chef/knife/bootstrap/templates/chef-full.erb | 242 ---- .../templates/windows-chef-client-msi.erb | 278 ----- lib/chef/knife/bootstrap/train_connector.rb | 336 ------ lib/chef/knife/client_bulk_delete.rb | 104 -- lib/chef/knife/client_create.rb | 101 -- lib/chef/knife/client_delete.rb | 62 - lib/chef/knife/client_edit.rb | 52 - lib/chef/knife/client_key_create.rb | 73 -- lib/chef/knife/client_key_delete.rb | 80 -- lib/chef/knife/client_key_edit.rb | 83 -- lib/chef/knife/client_key_list.rb | 73 -- lib/chef/knife/client_key_show.rb | 80 -- lib/chef/knife/client_list.rb | 41 - lib/chef/knife/client_reregister.rb | 58 - lib/chef/knife/client_show.rb | 48 - lib/chef/knife/config_get.rb | 39 - lib/chef/knife/config_get_profile.rb | 37 - lib/chef/knife/config_list.rb | 139 --- lib/chef/knife/config_list_profiles.rb | 37 - lib/chef/knife/config_show.rb | 127 --- lib/chef/knife/config_use.rb | 61 - lib/chef/knife/config_use_profile.rb | 47 - lib/chef/knife/configure.rb | 150 --- lib/chef/knife/configure_client.rb | 48 - lib/chef/knife/cookbook_bulk_delete.rb | 71 -- lib/chef/knife/cookbook_delete.rb | 151 --- lib/chef/knife/cookbook_download.rb | 142 --- lib/chef/knife/cookbook_list.rb | 47 - lib/chef/knife/cookbook_metadata.rb | 106 -- lib/chef/knife/cookbook_metadata_from_file.rb | 49 - lib/chef/knife/cookbook_show.rb | 98 -- lib/chef/knife/cookbook_upload.rb | 292 ----- lib/chef/knife/core/bootstrap_context.rb | 264 ----- lib/chef/knife/core/cookbook_scm_repo.rb | 159 --- lib/chef/knife/core/formatting_options.rb | 49 - lib/chef/knife/core/gem_glob_loader.rb | 138 --- lib/chef/knife/core/generic_presenter.rb | 232 ---- lib/chef/knife/core/hashed_command_loader.rb | 100 -- lib/chef/knife/core/node_editor.rb | 130 --- lib/chef/knife/core/node_presenter.rb | 133 --- lib/chef/knife/core/object_loader.rb | 115 -- lib/chef/knife/core/status_presenter.rb | 147 --- lib/chef/knife/core/subcommand_loader.rb | 203 ---- lib/chef/knife/core/text_formatter.rb | 85 -- lib/chef/knife/core/ui.rb | 338 ------ lib/chef/knife/core/windows_bootstrap_context.rb | 406 ------- lib/chef/knife/data_bag_create.rb | 81 -- lib/chef/knife/data_bag_delete.rb | 49 - lib/chef/knife/data_bag_edit.rb | 74 -- lib/chef/knife/data_bag_from_file.rb | 113 -- lib/chef/knife/data_bag_list.rb | 42 - lib/chef/knife/data_bag_secret_options.rb | 122 -- lib/chef/knife/data_bag_show.rb | 69 -- lib/chef/knife/delete.rb | 125 -- lib/chef/knife/deps.rb | 156 --- lib/chef/knife/diff.rb | 83 -- lib/chef/knife/download.rb | 84 -- lib/chef/knife/edit.rb | 88 -- lib/chef/knife/environment_compare.rb | 128 --- lib/chef/knife/environment_create.rb | 52 - lib/chef/knife/environment_delete.rb | 44 - lib/chef/knife/environment_edit.rb | 44 - lib/chef/knife/environment_from_file.rb | 84 -- lib/chef/knife/environment_list.rb | 41 - lib/chef/knife/environment_show.rb | 47 - lib/chef/knife/exec.rb | 99 -- lib/chef/knife/group_add.rb | 55 - lib/chef/knife/group_create.rb | 49 - lib/chef/knife/group_destroy.rb | 53 - lib/chef/knife/group_list.rb | 43 - lib/chef/knife/group_remove.rb | 56 - lib/chef/knife/group_show.rb | 49 - lib/chef/knife/key_create.rb | 112 -- lib/chef/knife/key_create_base.rb | 50 - lib/chef/knife/key_delete.rb | 55 - lib/chef/knife/key_edit.rb | 118 -- lib/chef/knife/key_edit_base.rb | 55 - lib/chef/knife/key_list.rb | 90 -- lib/chef/knife/key_list_base.rb | 45 - lib/chef/knife/key_show.rb | 53 - lib/chef/knife/list.rb | 177 --- lib/chef/knife/node_bulk_delete.rb | 75 -- lib/chef/knife/node_create.rb | 47 - lib/chef/knife/node_delete.rb | 46 - lib/chef/knife/node_edit.rb | 70 -- lib/chef/knife/node_environment_set.rb | 53 - lib/chef/knife/node_from_file.rb | 51 - lib/chef/knife/node_list.rb | 44 - lib/chef/knife/node_policy_set.rb | 79 -- lib/chef/knife/node_run_list_add.rb | 104 -- lib/chef/knife/node_run_list_remove.rb | 67 -- lib/chef/knife/node_run_list_set.rb | 66 -- lib/chef/knife/node_show.rb | 63 -- lib/chef/knife/null.rb | 12 - lib/chef/knife/org_create.rb | 70 -- lib/chef/knife/org_delete.rb | 32 - lib/chef/knife/org_edit.rb | 48 - lib/chef/knife/org_list.rb | 44 - lib/chef/knife/org_show.rb | 31 - lib/chef/knife/org_user_add.rb | 62 - lib/chef/knife/org_user_remove.rb | 103 -- lib/chef/knife/raw.rb | 123 -- lib/chef/knife/recipe_list.rb | 32 - lib/chef/knife/rehash.rb | 50 - lib/chef/knife/role_bulk_delete.rb | 66 -- lib/chef/knife/role_create.rb | 53 - lib/chef/knife/role_delete.rb | 46 - lib/chef/knife/role_edit.rb | 45 - lib/chef/knife/role_env_run_list_add.rb | 87 -- lib/chef/knife/role_env_run_list_clear.rb | 55 - lib/chef/knife/role_env_run_list_remove.rb | 57 - lib/chef/knife/role_env_run_list_replace.rb | 60 - lib/chef/knife/role_env_run_list_set.rb | 70 -- lib/chef/knife/role_from_file.rb | 51 - lib/chef/knife/role_list.rb | 42 - lib/chef/knife/role_run_list_add.rb | 87 -- lib/chef/knife/role_run_list_clear.rb | 55 - lib/chef/knife/role_run_list_remove.rb | 56 - lib/chef/knife/role_run_list_replace.rb | 60 - lib/chef/knife/role_run_list_set.rb | 69 -- lib/chef/knife/role_show.rb | 48 - lib/chef/knife/search.rb | 194 ---- lib/chef/knife/serve.rb | 65 -- lib/chef/knife/show.rb | 72 -- lib/chef/knife/ssh.rb | 645 ----------- lib/chef/knife/ssl_check.rb | 284 ----- lib/chef/knife/ssl_fetch.rb | 161 --- lib/chef/knife/status.rb | 95 -- lib/chef/knife/supermarket_download.rb | 121 -- lib/chef/knife/supermarket_install.rb | 192 ---- lib/chef/knife/supermarket_list.rb | 76 -- lib/chef/knife/supermarket_search.rb | 53 - lib/chef/knife/supermarket_share.rb | 166 --- lib/chef/knife/supermarket_show.rb | 66 -- lib/chef/knife/supermarket_unshare.rb | 61 - lib/chef/knife/tag_create.rb | 52 - lib/chef/knife/tag_delete.rb | 60 - lib/chef/knife/tag_list.rb | 47 - lib/chef/knife/upload.rb | 86 -- lib/chef/knife/user_create.rb | 143 --- lib/chef/knife/user_delete.rb | 151 --- lib/chef/knife/user_dissociate.rb | 42 - lib/chef/knife/user_edit.rb | 94 -- lib/chef/knife/user_invite_add.rb | 43 - lib/chef/knife/user_invite_list.rb | 34 - lib/chef/knife/user_invite_rescind.rb | 63 -- lib/chef/knife/user_key_create.rb | 73 -- lib/chef/knife/user_key_delete.rb | 80 -- lib/chef/knife/user_key_edit.rb | 83 -- lib/chef/knife/user_key_list.rb | 73 -- lib/chef/knife/user_key_show.rb | 80 -- lib/chef/knife/user_list.rb | 38 - lib/chef/knife/user_password.rb | 70 -- lib/chef/knife/user_reregister.rb | 59 - lib/chef/knife/user_show.rb | 52 - lib/chef/knife/xargs.rb | 282 ----- lib/chef/knife/yaml_convert.rb | 91 -- spec/functional/knife/configure_spec.rb | 2 +- spec/functional/knife/cookbook_delete_spec.rb | 2 +- spec/functional/knife/exec_spec.rb | 2 +- spec/functional/knife/rehash_spec.rb | 2 +- spec/functional/knife/ssh_spec.rb | 2 +- spec/functional/knife/version_spec.rb | 26 + spec/functional/version_spec.rb | 2 +- spec/integration/knife/chef_fs_data_store_spec.rb | 2 +- spec/integration/knife/chef_repo_path_spec.rb | 2 +- .../knife/chef_repository_file_system_spec.rb | 2 +- spec/integration/knife/chefignore_spec.rb | 2 +- spec/integration/knife/client_bulk_delete_spec.rb | 2 +- spec/integration/knife/client_create_spec.rb | 2 +- spec/integration/knife/client_delete_spec.rb | 2 +- spec/integration/knife/client_key_create_spec.rb | 2 +- spec/integration/knife/client_key_delete_spec.rb | 2 +- spec/integration/knife/client_key_list_spec.rb | 2 +- spec/integration/knife/client_key_show_spec.rb | 2 +- spec/integration/knife/client_list_spec.rb | 2 +- spec/integration/knife/client_show_spec.rb | 2 +- spec/integration/knife/common_options_spec.rb | 2 +- spec/integration/knife/config_list_spec.rb | 2 +- spec/integration/knife/config_show_spec.rb | 2 +- spec/integration/knife/config_use_spec.rb | 2 +- spec/integration/knife/cookbook_api_ipv6_spec.rb | 6 +- .../integration/knife/cookbook_bulk_delete_spec.rb | 2 +- spec/integration/knife/cookbook_download_spec.rb | 2 +- spec/integration/knife/cookbook_list_spec.rb | 2 +- spec/integration/knife/cookbook_show_spec.rb | 2 +- spec/integration/knife/cookbook_upload_spec.rb | 2 +- spec/integration/knife/data_bag_create_spec.rb | 2 +- spec/integration/knife/data_bag_delete_spec.rb | 2 +- spec/integration/knife/data_bag_edit_spec.rb | 2 +- spec/integration/knife/data_bag_from_file_spec.rb | 2 +- spec/integration/knife/data_bag_list_spec.rb | 2 +- spec/integration/knife/data_bag_show_spec.rb | 2 +- spec/integration/knife/delete_spec.rb | 2 +- spec/integration/knife/deps_spec.rb | 2 +- spec/integration/knife/diff_spec.rb | 2 +- spec/integration/knife/download_spec.rb | 2 +- spec/integration/knife/environment_compare_spec.rb | 2 +- spec/integration/knife/environment_create_spec.rb | 2 +- spec/integration/knife/environment_delete_spec.rb | 2 +- .../knife/environment_from_file_spec.rb | 2 +- spec/integration/knife/environment_list_spec.rb | 2 +- spec/integration/knife/environment_show_spec.rb | 2 +- spec/integration/knife/list_spec.rb | 2 +- spec/integration/knife/node_bulk_delete_spec.rb | 2 +- spec/integration/knife/node_create_spec.rb | 2 +- spec/integration/knife/node_delete_spec.rb | 2 +- .../integration/knife/node_environment_set_spec.rb | 2 +- spec/integration/knife/node_from_file_spec.rb | 2 +- spec/integration/knife/node_list_spec.rb | 2 +- spec/integration/knife/node_run_list_add_spec.rb | 2 +- .../integration/knife/node_run_list_remove_spec.rb | 2 +- spec/integration/knife/node_run_list_set_spec.rb | 2 +- spec/integration/knife/node_show_spec.rb | 2 +- spec/integration/knife/raw_spec.rb | 2 +- spec/integration/knife/redirection_spec.rb | 2 +- spec/integration/knife/role_bulk_delete_spec.rb | 2 +- spec/integration/knife/role_create_spec.rb | 2 +- spec/integration/knife/role_delete_spec.rb | 2 +- spec/integration/knife/role_from_file_spec.rb | 2 +- spec/integration/knife/role_list_spec.rb | 2 +- spec/integration/knife/role_show_spec.rb | 2 +- spec/integration/knife/search_node_spec.rb | 2 +- spec/integration/knife/show_spec.rb | 2 +- spec/integration/knife/upload_spec.rb | 2 +- spec/knife_spec_helper.rb | 245 ++++ spec/spec_helper.rb | 15 +- spec/support/lib/chef/resource/with_state.rb | 1 - spec/support/lib/chef/resource/zen_follower.rb | 1 - spec/support/lib/chef/resource/zen_master.rb | 1 - spec/support/platform_helpers.rb | 2 + .../shared/integration/integration_helper.rb | 1 - spec/unit/application/knife_spec.rb | 2 +- spec/unit/cookbook_site_streaming_uploader_spec.rb | 198 ---- .../knife/bootstrap/chef_vault_handler_spec.rb | 2 +- spec/unit/knife/bootstrap/client_builder_spec.rb | 2 +- spec/unit/knife/bootstrap/train_connector_spec.rb | 2 +- spec/unit/knife/bootstrap_spec.rb | 4 +- spec/unit/knife/client_bulk_delete_spec.rb | 2 +- spec/unit/knife/client_create_spec.rb | 2 +- spec/unit/knife/client_delete_spec.rb | 2 +- spec/unit/knife/client_edit_spec.rb | 2 +- spec/unit/knife/client_list_spec.rb | 2 +- spec/unit/knife/client_reregister_spec.rb | 2 +- spec/unit/knife/client_show_spec.rb | 2 +- spec/unit/knife/configure_client_spec.rb | 2 +- spec/unit/knife/configure_spec.rb | 2 +- spec/unit/knife/cookbook_bulk_delete_spec.rb | 2 +- spec/unit/knife/cookbook_delete_spec.rb | 2 +- spec/unit/knife/cookbook_download_spec.rb | 2 +- spec/unit/knife/cookbook_list_spec.rb | 2 +- .../unit/knife/cookbook_metadata_from_file_spec.rb | 2 +- spec/unit/knife/cookbook_metadata_spec.rb | 2 +- spec/unit/knife/cookbook_show_spec.rb | 2 +- spec/unit/knife/cookbook_upload_spec.rb | 2 +- spec/unit/knife/core/bootstrap_context_spec.rb | 2 +- spec/unit/knife/core/cookbook_scm_repo_spec.rb | 2 +- .../core/cookbook_site_streaming_uploader_spec.rb | 198 ++++ spec/unit/knife/core/gem_glob_loader_spec.rb | 39 +- spec/unit/knife/core/hashed_command_loader_spec.rb | 2 +- spec/unit/knife/core/node_editor_spec.rb | 2 +- spec/unit/knife/core/object_loader_spec.rb | 2 +- spec/unit/knife/core/status_presenter_spec.rb | 2 +- spec/unit/knife/core/subcommand_loader_spec.rb | 2 +- spec/unit/knife/core/ui_spec.rb | 2 +- .../knife/core/windows_bootstrap_context_spec.rb | 2 +- spec/unit/knife/data_bag_create_spec.rb | 2 +- spec/unit/knife/data_bag_edit_spec.rb | 2 +- spec/unit/knife/data_bag_from_file_spec.rb | 2 +- spec/unit/knife/data_bag_secret_options_spec.rb | 2 +- spec/unit/knife/data_bag_show_spec.rb | 2 +- spec/unit/knife/environment_compare_spec.rb | 2 +- spec/unit/knife/environment_create_spec.rb | 2 +- spec/unit/knife/environment_delete_spec.rb | 2 +- spec/unit/knife/environment_edit_spec.rb | 2 +- spec/unit/knife/environment_from_file_spec.rb | 2 +- spec/unit/knife/environment_list_spec.rb | 2 +- spec/unit/knife/environment_show_spec.rb | 2 +- spec/unit/knife/key_create_spec.rb | 2 +- spec/unit/knife/key_delete_spec.rb | 2 +- spec/unit/knife/key_edit_spec.rb | 2 +- spec/unit/knife/key_helper.rb | 2 +- spec/unit/knife/key_list_spec.rb | 2 +- spec/unit/knife/key_show_spec.rb | 2 +- spec/unit/knife/node_bulk_delete_spec.rb | 2 +- spec/unit/knife/node_delete_spec.rb | 2 +- spec/unit/knife/node_edit_spec.rb | 2 +- spec/unit/knife/node_environment_set_spec.rb | 2 +- spec/unit/knife/node_from_file_spec.rb | 2 +- spec/unit/knife/node_list_spec.rb | 2 +- spec/unit/knife/node_policy_set_spec.rb | 2 +- spec/unit/knife/node_run_list_add_spec.rb | 2 +- spec/unit/knife/node_run_list_remove_spec.rb | 2 +- spec/unit/knife/node_run_list_set_spec.rb | 2 +- spec/unit/knife/node_show_spec.rb | 2 +- spec/unit/knife/org_create_spec.rb | 2 +- spec/unit/knife/org_delete_spec.rb | 2 +- spec/unit/knife/org_edit_spec.rb | 2 +- spec/unit/knife/org_list_spec.rb | 2 +- spec/unit/knife/org_show_spec.rb | 2 +- spec/unit/knife/org_user_add_spec.rb | 2 +- spec/unit/knife/raw_spec.rb | 2 +- spec/unit/knife/role_bulk_delete_spec.rb | 2 +- spec/unit/knife/role_create_spec.rb | 2 +- spec/unit/knife/role_delete_spec.rb | 2 +- spec/unit/knife/role_edit_spec.rb | 2 +- spec/unit/knife/role_env_run_list_add_spec.rb | 2 +- spec/unit/knife/role_env_run_list_clear_spec.rb | 2 +- spec/unit/knife/role_env_run_list_remove_spec.rb | 2 +- spec/unit/knife/role_env_run_list_replace_spec.rb | 2 +- spec/unit/knife/role_env_run_list_set_spec.rb | 2 +- spec/unit/knife/role_from_file_spec.rb | 2 +- spec/unit/knife/role_list_spec.rb | 2 +- spec/unit/knife/role_run_list_add_spec.rb | 2 +- spec/unit/knife/role_run_list_clear_spec.rb | 2 +- spec/unit/knife/role_run_list_remove_spec.rb | 2 +- spec/unit/knife/role_run_list_replace_spec.rb | 2 +- spec/unit/knife/role_run_list_set_spec.rb | 2 +- spec/unit/knife/role_show_spec.rb | 2 +- spec/unit/knife/ssh_spec.rb | 2 +- spec/unit/knife/ssl_check_spec.rb | 2 +- spec/unit/knife/ssl_fetch_spec.rb | 2 +- spec/unit/knife/status_spec.rb | 2 +- spec/unit/knife/supermarket_download_spec.rb | 2 +- spec/unit/knife/supermarket_install_spec.rb | 3 +- spec/unit/knife/supermarket_list_spec.rb | 2 +- spec/unit/knife/supermarket_search_spec.rb | 2 +- spec/unit/knife/supermarket_share_spec.rb | 12 +- spec/unit/knife/supermarket_unshare_spec.rb | 2 +- spec/unit/knife/tag_create_spec.rb | 2 +- spec/unit/knife/tag_delete_spec.rb | 2 +- spec/unit/knife/tag_list_spec.rb | 2 +- spec/unit/knife/user_create_spec.rb | 2 +- spec/unit/knife/user_delete_spec.rb | 2 +- spec/unit/knife/user_edit_spec.rb | 2 +- spec/unit/knife/user_list_spec.rb | 2 +- spec/unit/knife/user_password_spec.rb | 2 +- spec/unit/knife/user_reregister_spec.rb | 2 +- spec/unit/knife/user_show_spec.rb | 2 +- spec/unit/knife_spec.rb | 2 +- spec/unit/provider/service/arch_service_spec.rb | 1 + spec/unit/provider/service/debian_service_spec.rb | 1 + tasks/rspec.rb | 22 +- 543 files changed, 19493 insertions(+), 18781 deletions(-) create mode 100644 knife/Gemfile create mode 100644 knife/LICENSE create mode 100644 knife/Rakefile create mode 100755 knife/bin/knife create mode 100644 knife/knife.gemspec create mode 100644 knife/lib/chef/application/knife.rb create mode 100644 knife/lib/chef/chef_fs/knife.rb create mode 100644 knife/lib/chef/knife.rb create mode 100644 knife/lib/chef/knife/acl_add.rb create mode 100644 knife/lib/chef/knife/acl_base.rb create mode 100644 knife/lib/chef/knife/acl_bulk_add.rb create mode 100644 knife/lib/chef/knife/acl_bulk_remove.rb create mode 100644 knife/lib/chef/knife/acl_remove.rb create mode 100644 knife/lib/chef/knife/acl_show.rb create mode 100644 knife/lib/chef/knife/bootstrap.rb create mode 100644 knife/lib/chef/knife/bootstrap/chef_vault_handler.rb create mode 100644 knife/lib/chef/knife/bootstrap/client_builder.rb create mode 100644 knife/lib/chef/knife/bootstrap/templates/README.md create mode 100644 knife/lib/chef/knife/bootstrap/templates/chef-full.erb create mode 100644 knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb create mode 100644 knife/lib/chef/knife/bootstrap/train_connector.rb create mode 100644 knife/lib/chef/knife/client_bulk_delete.rb create mode 100644 knife/lib/chef/knife/client_create.rb create mode 100644 knife/lib/chef/knife/client_delete.rb create mode 100644 knife/lib/chef/knife/client_edit.rb create mode 100644 knife/lib/chef/knife/client_key_create.rb create mode 100644 knife/lib/chef/knife/client_key_delete.rb create mode 100644 knife/lib/chef/knife/client_key_edit.rb create mode 100644 knife/lib/chef/knife/client_key_list.rb create mode 100644 knife/lib/chef/knife/client_key_show.rb create mode 100644 knife/lib/chef/knife/client_list.rb create mode 100644 knife/lib/chef/knife/client_reregister.rb create mode 100644 knife/lib/chef/knife/client_show.rb create mode 100644 knife/lib/chef/knife/config_get.rb create mode 100644 knife/lib/chef/knife/config_get_profile.rb create mode 100644 knife/lib/chef/knife/config_list.rb create mode 100644 knife/lib/chef/knife/config_list_profiles.rb create mode 100644 knife/lib/chef/knife/config_show.rb create mode 100644 knife/lib/chef/knife/config_use.rb create mode 100644 knife/lib/chef/knife/config_use_profile.rb create mode 100644 knife/lib/chef/knife/configure.rb create mode 100644 knife/lib/chef/knife/configure_client.rb create mode 100644 knife/lib/chef/knife/cookbook_bulk_delete.rb create mode 100644 knife/lib/chef/knife/cookbook_delete.rb create mode 100644 knife/lib/chef/knife/cookbook_download.rb create mode 100644 knife/lib/chef/knife/cookbook_list.rb create mode 100644 knife/lib/chef/knife/cookbook_metadata.rb create mode 100644 knife/lib/chef/knife/cookbook_metadata_from_file.rb create mode 100644 knife/lib/chef/knife/cookbook_show.rb create mode 100644 knife/lib/chef/knife/cookbook_upload.rb create mode 100644 knife/lib/chef/knife/core/bootstrap_context.rb create mode 100644 knife/lib/chef/knife/core/cookbook_scm_repo.rb create mode 100644 knife/lib/chef/knife/core/cookbook_site_streaming_uploader.rb create mode 100644 knife/lib/chef/knife/core/formatting_options.rb create mode 100644 knife/lib/chef/knife/core/gem_glob_loader.rb create mode 100644 knife/lib/chef/knife/core/generic_presenter.rb create mode 100644 knife/lib/chef/knife/core/hashed_command_loader.rb create mode 100644 knife/lib/chef/knife/core/node_editor.rb create mode 100644 knife/lib/chef/knife/core/node_presenter.rb create mode 100644 knife/lib/chef/knife/core/object_loader.rb create mode 100644 knife/lib/chef/knife/core/status_presenter.rb create mode 100644 knife/lib/chef/knife/core/subcommand_loader.rb create mode 100644 knife/lib/chef/knife/core/text_formatter.rb create mode 100644 knife/lib/chef/knife/core/ui.rb create mode 100644 knife/lib/chef/knife/core/windows_bootstrap_context.rb create mode 100644 knife/lib/chef/knife/data_bag_create.rb create mode 100644 knife/lib/chef/knife/data_bag_delete.rb create mode 100644 knife/lib/chef/knife/data_bag_edit.rb create mode 100644 knife/lib/chef/knife/data_bag_from_file.rb create mode 100644 knife/lib/chef/knife/data_bag_list.rb create mode 100644 knife/lib/chef/knife/data_bag_secret_options.rb create mode 100644 knife/lib/chef/knife/data_bag_show.rb create mode 100644 knife/lib/chef/knife/delete.rb create mode 100644 knife/lib/chef/knife/deps.rb create mode 100644 knife/lib/chef/knife/diff.rb create mode 100644 knife/lib/chef/knife/download.rb create mode 100644 knife/lib/chef/knife/edit.rb create mode 100644 knife/lib/chef/knife/environment_compare.rb create mode 100644 knife/lib/chef/knife/environment_create.rb create mode 100644 knife/lib/chef/knife/environment_delete.rb create mode 100644 knife/lib/chef/knife/environment_edit.rb create mode 100644 knife/lib/chef/knife/environment_from_file.rb create mode 100644 knife/lib/chef/knife/environment_list.rb create mode 100644 knife/lib/chef/knife/environment_show.rb create mode 100644 knife/lib/chef/knife/exec.rb create mode 100644 knife/lib/chef/knife/group_add.rb create mode 100644 knife/lib/chef/knife/group_create.rb create mode 100644 knife/lib/chef/knife/group_destroy.rb create mode 100644 knife/lib/chef/knife/group_list.rb create mode 100644 knife/lib/chef/knife/group_remove.rb create mode 100644 knife/lib/chef/knife/group_show.rb create mode 100644 knife/lib/chef/knife/key_create.rb create mode 100644 knife/lib/chef/knife/key_create_base.rb create mode 100644 knife/lib/chef/knife/key_delete.rb create mode 100644 knife/lib/chef/knife/key_edit.rb create mode 100644 knife/lib/chef/knife/key_edit_base.rb create mode 100644 knife/lib/chef/knife/key_list.rb create mode 100644 knife/lib/chef/knife/key_list_base.rb create mode 100644 knife/lib/chef/knife/key_show.rb create mode 100644 knife/lib/chef/knife/list.rb create mode 100644 knife/lib/chef/knife/node_bulk_delete.rb create mode 100644 knife/lib/chef/knife/node_create.rb create mode 100644 knife/lib/chef/knife/node_delete.rb create mode 100644 knife/lib/chef/knife/node_edit.rb create mode 100644 knife/lib/chef/knife/node_environment_set.rb create mode 100644 knife/lib/chef/knife/node_from_file.rb create mode 100644 knife/lib/chef/knife/node_list.rb create mode 100644 knife/lib/chef/knife/node_policy_set.rb create mode 100644 knife/lib/chef/knife/node_run_list_add.rb create mode 100644 knife/lib/chef/knife/node_run_list_remove.rb create mode 100644 knife/lib/chef/knife/node_run_list_set.rb create mode 100644 knife/lib/chef/knife/node_show.rb create mode 100644 knife/lib/chef/knife/null.rb create mode 100644 knife/lib/chef/knife/org_create.rb create mode 100644 knife/lib/chef/knife/org_delete.rb create mode 100644 knife/lib/chef/knife/org_edit.rb create mode 100644 knife/lib/chef/knife/org_list.rb create mode 100644 knife/lib/chef/knife/org_show.rb create mode 100644 knife/lib/chef/knife/org_user_add.rb create mode 100644 knife/lib/chef/knife/org_user_remove.rb create mode 100644 knife/lib/chef/knife/raw.rb create mode 100644 knife/lib/chef/knife/recipe_list.rb create mode 100644 knife/lib/chef/knife/rehash.rb create mode 100644 knife/lib/chef/knife/role_bulk_delete.rb create mode 100644 knife/lib/chef/knife/role_create.rb create mode 100644 knife/lib/chef/knife/role_delete.rb create mode 100644 knife/lib/chef/knife/role_edit.rb create mode 100644 knife/lib/chef/knife/role_env_run_list_add.rb create mode 100644 knife/lib/chef/knife/role_env_run_list_clear.rb create mode 100644 knife/lib/chef/knife/role_env_run_list_remove.rb create mode 100644 knife/lib/chef/knife/role_env_run_list_replace.rb create mode 100644 knife/lib/chef/knife/role_env_run_list_set.rb create mode 100644 knife/lib/chef/knife/role_from_file.rb create mode 100644 knife/lib/chef/knife/role_list.rb create mode 100644 knife/lib/chef/knife/role_run_list_add.rb create mode 100644 knife/lib/chef/knife/role_run_list_clear.rb create mode 100644 knife/lib/chef/knife/role_run_list_remove.rb create mode 100644 knife/lib/chef/knife/role_run_list_replace.rb create mode 100644 knife/lib/chef/knife/role_run_list_set.rb create mode 100644 knife/lib/chef/knife/role_show.rb create mode 100644 knife/lib/chef/knife/search.rb create mode 100644 knife/lib/chef/knife/serve.rb create mode 100644 knife/lib/chef/knife/show.rb create mode 100644 knife/lib/chef/knife/ssh.rb create mode 100644 knife/lib/chef/knife/ssl_check.rb create mode 100644 knife/lib/chef/knife/ssl_fetch.rb create mode 100644 knife/lib/chef/knife/status.rb create mode 100644 knife/lib/chef/knife/supermarket_download.rb create mode 100644 knife/lib/chef/knife/supermarket_install.rb create mode 100644 knife/lib/chef/knife/supermarket_list.rb create mode 100644 knife/lib/chef/knife/supermarket_search.rb create mode 100644 knife/lib/chef/knife/supermarket_share.rb create mode 100644 knife/lib/chef/knife/supermarket_show.rb create mode 100644 knife/lib/chef/knife/supermarket_unshare.rb create mode 100644 knife/lib/chef/knife/tag_create.rb create mode 100644 knife/lib/chef/knife/tag_delete.rb create mode 100644 knife/lib/chef/knife/tag_list.rb create mode 100644 knife/lib/chef/knife/upload.rb create mode 100644 knife/lib/chef/knife/user_create.rb create mode 100644 knife/lib/chef/knife/user_delete.rb create mode 100644 knife/lib/chef/knife/user_dissociate.rb create mode 100644 knife/lib/chef/knife/user_edit.rb create mode 100644 knife/lib/chef/knife/user_invite_add.rb create mode 100644 knife/lib/chef/knife/user_invite_list.rb create mode 100644 knife/lib/chef/knife/user_invite_rescind.rb create mode 100644 knife/lib/chef/knife/user_key_create.rb create mode 100644 knife/lib/chef/knife/user_key_delete.rb create mode 100644 knife/lib/chef/knife/user_key_edit.rb create mode 100644 knife/lib/chef/knife/user_key_list.rb create mode 100644 knife/lib/chef/knife/user_key_show.rb create mode 100644 knife/lib/chef/knife/user_list.rb create mode 100644 knife/lib/chef/knife/user_password.rb create mode 100644 knife/lib/chef/knife/user_reregister.rb create mode 100644 knife/lib/chef/knife/user_show.rb create mode 100644 knife/lib/chef/knife/version.rb create mode 100644 knife/lib/chef/knife/xargs.rb create mode 100644 knife/lib/chef/knife/yaml_convert.rb delete mode 100644 lib/chef/application/knife.rb delete mode 100644 lib/chef/chef_fs/knife.rb delete mode 100644 lib/chef/cookbook_site_streaming_uploader.rb delete mode 100644 lib/chef/knife.rb delete mode 100644 lib/chef/knife/acl_add.rb delete mode 100644 lib/chef/knife/acl_base.rb delete mode 100644 lib/chef/knife/acl_bulk_add.rb delete mode 100644 lib/chef/knife/acl_bulk_remove.rb delete mode 100644 lib/chef/knife/acl_remove.rb delete mode 100644 lib/chef/knife/acl_show.rb delete mode 100644 lib/chef/knife/bootstrap.rb delete mode 100644 lib/chef/knife/bootstrap/chef_vault_handler.rb delete mode 100644 lib/chef/knife/bootstrap/client_builder.rb delete mode 100644 lib/chef/knife/bootstrap/templates/README.md delete mode 100644 lib/chef/knife/bootstrap/templates/chef-full.erb delete mode 100644 lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb delete mode 100644 lib/chef/knife/bootstrap/train_connector.rb delete mode 100644 lib/chef/knife/client_bulk_delete.rb delete mode 100644 lib/chef/knife/client_create.rb delete mode 100644 lib/chef/knife/client_delete.rb delete mode 100644 lib/chef/knife/client_edit.rb delete mode 100644 lib/chef/knife/client_key_create.rb delete mode 100644 lib/chef/knife/client_key_delete.rb delete mode 100644 lib/chef/knife/client_key_edit.rb delete mode 100644 lib/chef/knife/client_key_list.rb delete mode 100644 lib/chef/knife/client_key_show.rb delete mode 100644 lib/chef/knife/client_list.rb delete mode 100644 lib/chef/knife/client_reregister.rb delete mode 100644 lib/chef/knife/client_show.rb delete mode 100644 lib/chef/knife/config_get.rb delete mode 100644 lib/chef/knife/config_get_profile.rb delete mode 100644 lib/chef/knife/config_list.rb delete mode 100644 lib/chef/knife/config_list_profiles.rb delete mode 100644 lib/chef/knife/config_show.rb delete mode 100644 lib/chef/knife/config_use.rb delete mode 100644 lib/chef/knife/config_use_profile.rb delete mode 100644 lib/chef/knife/configure.rb delete mode 100644 lib/chef/knife/configure_client.rb delete mode 100644 lib/chef/knife/cookbook_bulk_delete.rb delete mode 100644 lib/chef/knife/cookbook_delete.rb delete mode 100644 lib/chef/knife/cookbook_download.rb delete mode 100644 lib/chef/knife/cookbook_list.rb delete mode 100644 lib/chef/knife/cookbook_metadata.rb delete mode 100644 lib/chef/knife/cookbook_metadata_from_file.rb delete mode 100644 lib/chef/knife/cookbook_show.rb delete mode 100644 lib/chef/knife/cookbook_upload.rb delete mode 100644 lib/chef/knife/core/bootstrap_context.rb delete mode 100644 lib/chef/knife/core/cookbook_scm_repo.rb delete mode 100644 lib/chef/knife/core/formatting_options.rb delete mode 100644 lib/chef/knife/core/gem_glob_loader.rb delete mode 100644 lib/chef/knife/core/generic_presenter.rb delete mode 100644 lib/chef/knife/core/hashed_command_loader.rb delete mode 100644 lib/chef/knife/core/node_editor.rb delete mode 100644 lib/chef/knife/core/node_presenter.rb delete mode 100644 lib/chef/knife/core/object_loader.rb delete mode 100644 lib/chef/knife/core/status_presenter.rb delete mode 100644 lib/chef/knife/core/subcommand_loader.rb delete mode 100644 lib/chef/knife/core/text_formatter.rb delete mode 100644 lib/chef/knife/core/ui.rb delete mode 100644 lib/chef/knife/core/windows_bootstrap_context.rb delete mode 100644 lib/chef/knife/data_bag_create.rb delete mode 100644 lib/chef/knife/data_bag_delete.rb delete mode 100644 lib/chef/knife/data_bag_edit.rb delete mode 100644 lib/chef/knife/data_bag_from_file.rb delete mode 100644 lib/chef/knife/data_bag_list.rb delete mode 100644 lib/chef/knife/data_bag_secret_options.rb delete mode 100644 lib/chef/knife/data_bag_show.rb delete mode 100644 lib/chef/knife/delete.rb delete mode 100644 lib/chef/knife/deps.rb delete mode 100644 lib/chef/knife/diff.rb delete mode 100644 lib/chef/knife/download.rb delete mode 100644 lib/chef/knife/edit.rb delete mode 100644 lib/chef/knife/environment_compare.rb delete mode 100644 lib/chef/knife/environment_create.rb delete mode 100644 lib/chef/knife/environment_delete.rb delete mode 100644 lib/chef/knife/environment_edit.rb delete mode 100644 lib/chef/knife/environment_from_file.rb delete mode 100644 lib/chef/knife/environment_list.rb delete mode 100644 lib/chef/knife/environment_show.rb delete mode 100644 lib/chef/knife/exec.rb delete mode 100644 lib/chef/knife/group_add.rb delete mode 100644 lib/chef/knife/group_create.rb delete mode 100644 lib/chef/knife/group_destroy.rb delete mode 100644 lib/chef/knife/group_list.rb delete mode 100644 lib/chef/knife/group_remove.rb delete mode 100644 lib/chef/knife/group_show.rb delete mode 100644 lib/chef/knife/key_create.rb delete mode 100644 lib/chef/knife/key_create_base.rb delete mode 100644 lib/chef/knife/key_delete.rb delete mode 100644 lib/chef/knife/key_edit.rb delete mode 100644 lib/chef/knife/key_edit_base.rb delete mode 100644 lib/chef/knife/key_list.rb delete mode 100644 lib/chef/knife/key_list_base.rb delete mode 100644 lib/chef/knife/key_show.rb delete mode 100644 lib/chef/knife/list.rb delete mode 100644 lib/chef/knife/node_bulk_delete.rb delete mode 100644 lib/chef/knife/node_create.rb delete mode 100644 lib/chef/knife/node_delete.rb delete mode 100644 lib/chef/knife/node_edit.rb delete mode 100644 lib/chef/knife/node_environment_set.rb delete mode 100644 lib/chef/knife/node_from_file.rb delete mode 100644 lib/chef/knife/node_list.rb delete mode 100644 lib/chef/knife/node_policy_set.rb delete mode 100644 lib/chef/knife/node_run_list_add.rb delete mode 100644 lib/chef/knife/node_run_list_remove.rb delete mode 100644 lib/chef/knife/node_run_list_set.rb delete mode 100644 lib/chef/knife/node_show.rb delete mode 100644 lib/chef/knife/null.rb delete mode 100644 lib/chef/knife/org_create.rb delete mode 100644 lib/chef/knife/org_delete.rb delete mode 100644 lib/chef/knife/org_edit.rb delete mode 100644 lib/chef/knife/org_list.rb delete mode 100644 lib/chef/knife/org_show.rb delete mode 100644 lib/chef/knife/org_user_add.rb delete mode 100644 lib/chef/knife/org_user_remove.rb delete mode 100644 lib/chef/knife/raw.rb delete mode 100644 lib/chef/knife/recipe_list.rb delete mode 100644 lib/chef/knife/rehash.rb delete mode 100644 lib/chef/knife/role_bulk_delete.rb delete mode 100644 lib/chef/knife/role_create.rb delete mode 100644 lib/chef/knife/role_delete.rb delete mode 100644 lib/chef/knife/role_edit.rb delete mode 100644 lib/chef/knife/role_env_run_list_add.rb delete mode 100644 lib/chef/knife/role_env_run_list_clear.rb delete mode 100644 lib/chef/knife/role_env_run_list_remove.rb delete mode 100644 lib/chef/knife/role_env_run_list_replace.rb delete mode 100644 lib/chef/knife/role_env_run_list_set.rb delete mode 100644 lib/chef/knife/role_from_file.rb delete mode 100644 lib/chef/knife/role_list.rb delete mode 100644 lib/chef/knife/role_run_list_add.rb delete mode 100644 lib/chef/knife/role_run_list_clear.rb delete mode 100644 lib/chef/knife/role_run_list_remove.rb delete mode 100644 lib/chef/knife/role_run_list_replace.rb delete mode 100644 lib/chef/knife/role_run_list_set.rb delete mode 100644 lib/chef/knife/role_show.rb delete mode 100644 lib/chef/knife/search.rb delete mode 100644 lib/chef/knife/serve.rb delete mode 100644 lib/chef/knife/show.rb delete mode 100644 lib/chef/knife/ssh.rb delete mode 100644 lib/chef/knife/ssl_check.rb delete mode 100644 lib/chef/knife/ssl_fetch.rb delete mode 100644 lib/chef/knife/status.rb delete mode 100644 lib/chef/knife/supermarket_download.rb delete mode 100644 lib/chef/knife/supermarket_install.rb delete mode 100644 lib/chef/knife/supermarket_list.rb delete mode 100644 lib/chef/knife/supermarket_search.rb delete mode 100644 lib/chef/knife/supermarket_share.rb delete mode 100644 lib/chef/knife/supermarket_show.rb delete mode 100644 lib/chef/knife/supermarket_unshare.rb delete mode 100644 lib/chef/knife/tag_create.rb delete mode 100644 lib/chef/knife/tag_delete.rb delete mode 100644 lib/chef/knife/tag_list.rb delete mode 100644 lib/chef/knife/upload.rb delete mode 100644 lib/chef/knife/user_create.rb delete mode 100644 lib/chef/knife/user_delete.rb delete mode 100644 lib/chef/knife/user_dissociate.rb delete mode 100644 lib/chef/knife/user_edit.rb delete mode 100644 lib/chef/knife/user_invite_add.rb delete mode 100644 lib/chef/knife/user_invite_list.rb delete mode 100644 lib/chef/knife/user_invite_rescind.rb delete mode 100644 lib/chef/knife/user_key_create.rb delete mode 100644 lib/chef/knife/user_key_delete.rb delete mode 100644 lib/chef/knife/user_key_edit.rb delete mode 100644 lib/chef/knife/user_key_list.rb delete mode 100644 lib/chef/knife/user_key_show.rb delete mode 100644 lib/chef/knife/user_list.rb delete mode 100644 lib/chef/knife/user_password.rb delete mode 100644 lib/chef/knife/user_reregister.rb delete mode 100644 lib/chef/knife/user_show.rb delete mode 100644 lib/chef/knife/xargs.rb delete mode 100644 lib/chef/knife/yaml_convert.rb create mode 100644 spec/functional/knife/version_spec.rb create mode 100644 spec/knife_spec_helper.rb delete mode 100644 spec/unit/cookbook_site_streaming_uploader_spec.rb create mode 100644 spec/unit/knife/core/cookbook_site_streaming_uploader_spec.rb diff --git a/.expeditor/config.yml b/.expeditor/config.yml index f87694b89e..c166cd525e 100644 --- a/.expeditor/config.yml +++ b/.expeditor/config.yml @@ -16,6 +16,7 @@ rubygems: - chef-config - chef-bin - chef-utils + - knife pipelines: - verify: diff --git a/.expeditor/update_version.sh b/.expeditor/update_version.sh index 2a75c4b2f9..a6a2c6461d 100755 --- a/.expeditor/update_version.sh +++ b/.expeditor/update_version.sh @@ -16,10 +16,15 @@ VERSION=$(cat VERSION) sed -i -r "s/^(\s*)VERSION = \".+\"/\1VERSION = \"${VERSION}\"/" chef-config/lib/chef-config/version.rb sed -i -r "s/^(\s*)VERSION = \".+\"/\1VERSION = \"${VERSION}\"/" chef-bin/lib/chef-bin/version.rb sed -i -r "s/^(\s*)VERSION = \".+\"/\1VERSION = \"${VERSION}\"/" chef-utils/lib/chef-utils/version.rb +sed -i -r "s/^(\s*)VERSION = \".+\"/\1VERSION = \"${VERSION}\"/" knife/lib/chef/knife/version.rb sed -i -r "s/VersionString\.new\(\".+\"\)/VersionString.new(\"${VERSION}\")/" lib/chef/version.rb # Update the version inside Gemfile.lock bundle update chef chef-config chef-utils --jobs=7 --conservative +# Same for knife. +cd knife +bundle update chef chef-config chef-utils --jobs=7 --conservative + # Once Expeditor finishes executing this script, it will commit the changes and push # the commit as a new tag corresponding to the value in the VERSION file. diff --git a/chef.gemspec b/chef.gemspec index a5e2b5a8dd..b410ea6d44 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -65,11 +65,13 @@ Gem::Specification.new do |s| s.add_dependency "proxifier", "~> 1.0" s.bindir = "bin" - s.executables = %w{ knife } + s.executables = %w{ } s.require_paths = %w{ lib } s.files = %w{Gemfile Rakefile LICENSE README.md} + - Dir.glob("{lib,spec}/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) } + + Dir.glob("{lib,spec}/**/*", File::FNM_DOTMATCH).reject do |f| + File.directory?(f) || File.path(f).match(/knife*/) + end + Dir.glob("*.gemspec") + Dir.glob("tasks/rspec.rb") diff --git a/habitat/tests/spec.ps1 b/habitat/tests/spec.ps1 index 34b3a07beb..0754d31727 100644 --- a/habitat/tests/spec.ps1 +++ b/habitat/tests/spec.ps1 @@ -18,7 +18,7 @@ try { SETX GEM_PATH $($gemPath.Split("=")[1]) /m hab pkg binlink --force $PackageIdentifier - /hab/bin/rspec --tag ~executables --tag ~choco_installed spec/functional + /hab/bin/rspec --tag ~executables --tag ~choco_installed --pattern 'spec/functional/**/*_spec.rb' --exclude-pattern 'spec/functional/knife/**/*.rb' if (-not $?) { throw "functional testing failed"} } finally { Pop-Location diff --git a/habitat/tests/test.pester.ps1 b/habitat/tests/test.pester.ps1 index 56f31e9a2f..c8cf993e2f 100644 --- a/habitat/tests/test.pester.ps1 +++ b/habitat/tests/test.pester.ps1 @@ -52,13 +52,6 @@ Describe "chef-infra-client" { } } - Context "knife" { - It "is an executable" { - hab pkg exec $PackageIdentifier knife.bat --version - $? | Should be $true - } - } - Context "chef-solo" { It "is an executable" { hab pkg exec $PackageIdentifier chef-solo.bat --version diff --git a/habitat/tests/test.sh b/habitat/tests/test.sh index c28ab8b2cf..655e8db2d8 100755 --- a/habitat/tests/test.sh +++ b/habitat/tests/test.sh @@ -28,10 +28,10 @@ echo "--- :mag_right: Testing ${pkg_ident} executables" actual_version=$(hab pkg exec "${pkg_ident}" chef-client -- --version | sed 's/.*: //') [[ "$package_version" = "$actual_version" ]] || error "chef-client is not the expected version. Expected '$package_version', got '$actual_version'" -for executable in 'chef-client' 'ohai' 'chef-shell' 'chef-apply' 'knife' 'chef-solo'; do +for executable in 'chef-client' 'ohai' 'chef-shell' 'chef-apply' 'chef-solo'; do echo -en "\t$executable = " hab pkg exec "${pkg_ident}" "${executable}" -- --version || error "${executable} failed to execute properly" done echo "--- :mag_right: Testing ${pkg_ident} functionality" -hab pkg exec "${pkg_ident}" rspec --tag ~executables spec/functional || error 'failures during rspec tests' +hab pkg exec "${pkg_ident}" rspec --tag ~executables --pattern 'spec/functional/**/*_spec.rb' --exclude-pattern 'spec/functional/knife/**/*.rb' || error 'failures during rspec tests' diff --git a/kitchen-tests/Gemfile b/kitchen-tests/Gemfile index adaaceaf9e..19f6e7d93e 100644 --- a/kitchen-tests/Gemfile +++ b/kitchen-tests/Gemfile @@ -2,9 +2,10 @@ source "https://rubygems.org" gem "rake" # required to build some native extensions gem "chef", path: ".." +gem "knife", path: "../knife" gem "ohai", git: "https://github.com/chef/ohai.git", branch: "master" # avoids failures when we bump chef major gem "berkshelf", git: "https://github.com/berkshelf/berkshelf.git", branch: "master" gem "kitchen-dokken", ">= 2.0" gem "kitchen-inspec", git: "https://github.com/chef/kitchen-inspec.git", branch: "master" gem "inspec" -gem "test-kitchen", git: "https://github.com/test-kitchen/test-kitchen.git", branch: "master" \ No newline at end of file +gem "test-kitchen", git: "https://github.com/test-kitchen/test-kitchen.git", branch: "master" 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 () +# 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 ( 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 () +# 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 () +# Author:: Christopher Brown () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +@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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# Author:: Daniel DeLeo () +# 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 () +# 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 () +# Author:: Christopher Walters () +# 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 () +# Author:: Nuo Yan () +# 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 () +# 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 () +# Author:: Matthew Kent () +# 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 () +# 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 () +# Author:: Christopher Walters () +# Author:: Nuo Yan () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# Author:: Daniel DeLeo () +# 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 () +# 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 () +# 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 () +# Author:: Jordan Running () +# 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] 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 () +# 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 () +# 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] 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 () +# 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 () +# Author:: Daniel DeLeo () +# 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] + # + 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 () +# 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 () +# Author:: Christopher Brown () +# Author:: Daniel DeLeo () +# 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 () +# 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:// + '* 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 () +# Author:: Seth Falcon () +# 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 () +# 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 () +# Author:: Seth Falcon () +# 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 () +# Author:: Seth Falcon () +# 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 () +# 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 () +# 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 () +# Author:: Seth Falcon () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# Author:: Jeremiah Snapp () +# 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 () +# Author:: Jeremiah Snapp () +# 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 () +# Author:: Jeremiah Snapp () +# 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 () +# Author:: Jeremiah Snapp () +# 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 () +# Author:: Jeremiah Snapp () +# 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 () +# Author:: Jeremiah Snapp () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# Author:: William Albenzi () +# 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 () +# Author:: William Albenzi () +# 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 () +# 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 () +# Author:: William Albenzi () +# 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 () +# Author:: William Albenzi () +# 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 () +# 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 () +# 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 () +# Author:: William Albenzi () +# 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 () +# Author:: William Albenzi () +# 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 () +# 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 () +# Author:: William Albenzi () +# 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 () +# Author:: William Albenzi () +# 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 () +# 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 () +# 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 () +# 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] + 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# Author:: Daniel DeLeo () +# Author:: Nuo Yan () +# 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 () +# Author:: Daniel DeLeo () +# Author:: Nuo Yan () +# 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 () +# Author:: Daniel DeLeo () +# Author:: Nuo Yan () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 () +# 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 +# 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 diff --git a/lib/chef/application/knife.rb b/lib/chef/application/knife.rb deleted file mode 100644 index 7906ce6eaa..0000000000 --- a/lib/chef/application/knife.rb +++ /dev/null @@ -1,234 +0,0 @@ -# -# Author:: Adam Jacob ( 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/lib/chef/applications.rb b/lib/chef/applications.rb index a30b765c77..8f7f418d3f 100644 --- a/lib/chef/applications.rb +++ b/lib/chef/applications.rb @@ -1,4 +1,3 @@ require_relative "application/client" -require_relative "application/knife" require_relative "application/solo" require_relative "application/apply" diff --git a/lib/chef/chef_fs/knife.rb b/lib/chef/chef_fs/knife.rb deleted file mode 100644 index ba993beee4..0000000000 --- a/lib/chef/chef_fs/knife.rb +++ /dev/null @@ -1,160 +0,0 @@ -# -# Author:: John Keiser () -# 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_relative "../config" - require_relative "parallelizer" - require_relative "config" - require_relative "file_pattern" - require_relative "path_utils" - 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/lib/chef/cookbook_site_streaming_uploader.rb b/lib/chef/cookbook_site_streaming_uploader.rb deleted file mode 100644 index d7226b79b3..0000000000 --- a/lib/chef/cookbook_site_streaming_uploader.rb +++ /dev/null @@ -1,244 +0,0 @@ -# -# Author:: Stanislav Vitvitskiy -# Author:: Nuo Yan (nuo@chef.io) -# Author:: Christopher Walters () -# 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) - -class Chef - # == Chef::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 diff --git a/lib/chef/cookbook_uploader.rb b/lib/chef/cookbook_uploader.rb index 235a539b94..21a15f706c 100644 --- a/lib/chef/cookbook_uploader.rb +++ b/lib/chef/cookbook_uploader.rb @@ -1,7 +1,6 @@ autoload :Set, "set" require_relative "exceptions" -require_relative "knife/cookbook_metadata" require_relative "digester" require_relative "cookbook_manifest" require_relative "cookbook_version" diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb deleted file mode 100644 index d277e51105..0000000000 --- a/lib/chef/knife.rb +++ /dev/null @@ -1,672 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Christopher Brown () -# 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 "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_relative "workstation_config_loader" -require_relative "mixin/convert_to_class_name" -require_relative "mixin/default_paths" -require_relative "knife/core/subcommand_loader" -require_relative "knife/core/ui" -require_relative "local_mode" -require_relative "server_api" -require_relative "http/authenticator" -require_relative "http/http_request" -require_relative "http" -require "pp" unless defined?(PP) - -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 - require_relative "server_api" - Chef::ServerAPI.new(Chef::Config[:chef_server_url]) - end - end - - def noauth_rest - @rest ||= begin - require_relative "http/simple_json" - 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_relative "server_api" - Chef::ServerAPI.new(Chef::Config[:chef_server_root]) - end - end - end -end diff --git a/lib/chef/knife/acl_add.rb b/lib/chef/knife/acl_add.rb deleted file mode 100644 index 144a18fcb1..0000000000 --- a/lib/chef/knife/acl_add.rb +++ /dev/null @@ -1,57 +0,0 @@ -# -# 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/lib/chef/knife/acl_base.rb b/lib/chef/knife/acl_base.rb deleted file mode 100644 index 0835d1ac05..0000000000 --- a/lib/chef/knife/acl_base.rb +++ /dev/null @@ -1,183 +0,0 @@ -# -# Author:: Steven Danna (steve@chef.io) -# Author:: Jeremiah Snapp () -# 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/lib/chef/knife/acl_bulk_add.rb b/lib/chef/knife/acl_bulk_add.rb deleted file mode 100644 index 4992fe2afa..0000000000 --- a/lib/chef/knife/acl_bulk_add.rb +++ /dev/null @@ -1,78 +0,0 @@ -# -# 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/lib/chef/knife/acl_bulk_remove.rb b/lib/chef/knife/acl_bulk_remove.rb deleted file mode 100644 index 0f35f1e2fb..0000000000 --- a/lib/chef/knife/acl_bulk_remove.rb +++ /dev/null @@ -1,83 +0,0 @@ -# -# 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/lib/chef/knife/acl_remove.rb b/lib/chef/knife/acl_remove.rb deleted file mode 100644 index 13f089ff3e..0000000000 --- a/lib/chef/knife/acl_remove.rb +++ /dev/null @@ -1,62 +0,0 @@ -# -# 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/lib/chef/knife/acl_show.rb b/lib/chef/knife/acl_show.rb deleted file mode 100644 index d3a5002b30..0000000000 --- a/lib/chef/knife/acl_show.rb +++ /dev/null @@ -1,56 +0,0 @@ -# -# 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/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb deleted file mode 100644 index 340ffaecfd..0000000000 --- a/lib/chef/knife/bootstrap.rb +++ /dev/null @@ -1,1192 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../json_compat" - require_relative "../util/path_helper" - 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 - Chef::Util::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/lib/chef/knife/bootstrap/chef_vault_handler.rb b/lib/chef/knife/bootstrap/chef_vault_handler.rb deleted file mode 100644 index 20759d6fdf..0000000000 --- a/lib/chef/knife/bootstrap/chef_vault_handler.rb +++ /dev/null @@ -1,162 +0,0 @@ -# -# Author:: Lamont Granquist () -# 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/lib/chef/knife/bootstrap/client_builder.rb b/lib/chef/knife/bootstrap/client_builder.rb deleted file mode 100644 index d9c3d83d06..0000000000 --- a/lib/chef/knife/bootstrap/client_builder.rb +++ /dev/null @@ -1,212 +0,0 @@ -# -# Author:: Lamont Granquist () -# 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 "../../node" -require_relative "../../server_api" -require_relative "../../api_client/registration" -require_relative "../../api_client" -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/lib/chef/knife/bootstrap/templates/README.md b/lib/chef/knife/bootstrap/templates/README.md deleted file mode 100644 index 7f28f8f40f..0000000000 --- a/lib/chef/knife/bootstrap/templates/README.md +++ /dev/null @@ -1,11 +0,0 @@ -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/lib/chef/knife/bootstrap/templates/chef-full.erb b/lib/chef/knife/bootstrap/templates/chef-full.erb deleted file mode 100644 index 2e0c80eaef..0000000000 --- a/lib/chef/knife/bootstrap/templates/chef-full.erb +++ /dev/null @@ -1,242 +0,0 @@ -<%= "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/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb b/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb deleted file mode 100644 index 7aa7be49f8..0000000000 --- a/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb +++ /dev/null @@ -1,278 +0,0 @@ -@rem -@rem Author:: Seth Chisamore () -@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/lib/chef/knife/bootstrap/train_connector.rb b/lib/chef/knife/bootstrap/train_connector.rb deleted file mode 100644 index a220ece5bc..0000000000 --- a/lib/chef/knife/bootstrap/train_connector.rb +++ /dev/null @@ -1,336 +0,0 @@ -# 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/lib/chef/knife/client_bulk_delete.rb b/lib/chef/knife/client_bulk_delete.rb deleted file mode 100644 index 38d25583b3..0000000000 --- a/lib/chef/knife/client_bulk_delete.rb +++ /dev/null @@ -1,104 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../api_client_v1" - 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/lib/chef/knife/client_create.rb b/lib/chef/knife/client_create.rb deleted file mode 100644 index d6e0eab63b..0000000000 --- a/lib/chef/knife/client_create.rb +++ /dev/null @@ -1,101 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../api_client_v1" - 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/lib/chef/knife/client_delete.rb b/lib/chef/knife/client_delete.rb deleted file mode 100644 index 3ecfa38242..0000000000 --- a/lib/chef/knife/client_delete.rb +++ /dev/null @@ -1,62 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../api_client_v1" - 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/lib/chef/knife/client_edit.rb b/lib/chef/knife/client_edit.rb deleted file mode 100644 index f89f5e38ec..0000000000 --- a/lib/chef/knife/client_edit.rb +++ /dev/null @@ -1,52 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../api_client_v1" - 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/lib/chef/knife/client_key_create.rb b/lib/chef/knife/client_key_create.rb deleted file mode 100644 index 192d724473..0000000000 --- a/lib/chef/knife/client_key_create.rb +++ /dev/null @@ -1,73 +0,0 @@ -# -# 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/lib/chef/knife/client_key_delete.rb b/lib/chef/knife/client_key_delete.rb deleted file mode 100644 index 2d486ffcbd..0000000000 --- a/lib/chef/knife/client_key_delete.rb +++ /dev/null @@ -1,80 +0,0 @@ -# -# 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/lib/chef/knife/client_key_edit.rb b/lib/chef/knife/client_key_edit.rb deleted file mode 100644 index d178aafc17..0000000000 --- a/lib/chef/knife/client_key_edit.rb +++ /dev/null @@ -1,83 +0,0 @@ -# -# 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/lib/chef/knife/client_key_list.rb b/lib/chef/knife/client_key_list.rb deleted file mode 100644 index afc04335d9..0000000000 --- a/lib/chef/knife/client_key_list.rb +++ /dev/null @@ -1,73 +0,0 @@ -# -# 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/lib/chef/knife/client_key_show.rb b/lib/chef/knife/client_key_show.rb deleted file mode 100644 index 14e1f0ca7a..0000000000 --- a/lib/chef/knife/client_key_show.rb +++ /dev/null @@ -1,80 +0,0 @@ -# -# 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/lib/chef/knife/client_list.rb b/lib/chef/knife/client_list.rb deleted file mode 100644 index b4fc46767b..0000000000 --- a/lib/chef/knife/client_list.rb +++ /dev/null @@ -1,41 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../api_client_v1" - 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/lib/chef/knife/client_reregister.rb b/lib/chef/knife/client_reregister.rb deleted file mode 100644 index 6741895b23..0000000000 --- a/lib/chef/knife/client_reregister.rb +++ /dev/null @@ -1,58 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../api_client_v1" - 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/lib/chef/knife/client_show.rb b/lib/chef/knife/client_show.rb deleted file mode 100644 index 9170c73085..0000000000 --- a/lib/chef/knife/client_show.rb +++ /dev/null @@ -1,48 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../api_client_v1" - 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/lib/chef/knife/config_get.rb b/lib/chef/knife/config_get.rb deleted file mode 100644 index 91e6b7affd..0000000000 --- a/lib/chef/knife/config_get.rb +++ /dev/null @@ -1,39 +0,0 @@ -# -# Author:: Joshua Timberman -# 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/lib/chef/knife/config_get_profile.rb b/lib/chef/knife/config_get_profile.rb deleted file mode 100644 index a355c531fe..0000000000 --- a/lib/chef/knife/config_get_profile.rb +++ /dev/null @@ -1,37 +0,0 @@ -# -# 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/lib/chef/knife/config_list.rb b/lib/chef/knife/config_list.rb deleted file mode 100644 index c9f821e2a8..0000000000 --- a/lib/chef/knife/config_list.rb +++ /dev/null @@ -1,139 +0,0 @@ -# -# 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_relative "../workstation_config_loader" - 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/lib/chef/knife/config_list_profiles.rb b/lib/chef/knife/config_list_profiles.rb deleted file mode 100644 index c037b0de53..0000000000 --- a/lib/chef/knife/config_list_profiles.rb +++ /dev/null @@ -1,37 +0,0 @@ -# -# 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/lib/chef/knife/config_show.rb b/lib/chef/knife/config_show.rb deleted file mode 100644 index 7f28891885..0000000000 --- a/lib/chef/knife/config_show.rb +++ /dev/null @@ -1,127 +0,0 @@ -# -# Author:: Vivek Singh () -# 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/lib/chef/knife/config_use.rb b/lib/chef/knife/config_use.rb deleted file mode 100644 index e944dc210b..0000000000 --- a/lib/chef/knife/config_use.rb +++ /dev/null @@ -1,61 +0,0 @@ -# -# Author:: Vivek Singh () -# 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/lib/chef/knife/config_use_profile.rb b/lib/chef/knife/config_use_profile.rb deleted file mode 100644 index 169bdbef30..0000000000 --- a/lib/chef/knife/config_use_profile.rb +++ /dev/null @@ -1,47 +0,0 @@ -# -# 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/lib/chef/knife/configure.rb b/lib/chef/knife/configure.rb deleted file mode 100644 index 2a27fd5d88..0000000000 --- a/lib/chef/knife/configure.rb +++ /dev/null @@ -1,150 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../util/path_helper" - require_relative "client_create" - require_relative "user_create" - require "ohai" unless defined?(Ohai::System) - 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 ||= Chef::Util::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/lib/chef/knife/configure_client.rb b/lib/chef/knife/configure_client.rb deleted file mode 100644 index c6f159ec8f..0000000000 --- a/lib/chef/knife/configure_client.rb +++ /dev/null @@ -1,48 +0,0 @@ -# -# Author:: Adam Jacob () -# 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/lib/chef/knife/cookbook_bulk_delete.rb b/lib/chef/knife/cookbook_bulk_delete.rb deleted file mode 100644 index d6657ccb4f..0000000000 --- a/lib/chef/knife/cookbook_bulk_delete.rb +++ /dev/null @@ -1,71 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Daniel DeLeo () -# 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_relative "../cookbook_version" - end - - option :purge, short: "-p", long: "--purge", boolean: true, description: "Permanently remove files from backing data store." - - banner "knife cookbook bulk delete REGEX (options)" - - def run - unless regex_str = @name_args.first - ui.fatal("You must supply a regular expression to match the results against") - exit 42 - end - - regex = Regexp.new(regex_str) - - all_cookbooks = Chef::CookbookVersion.list - cookbooks_names = all_cookbooks.keys.grep(regex) - cookbooks_to_delete = cookbooks_names.inject({}) { |hash, name| hash[name] = all_cookbooks[name]; hash } - ui.msg "All versions of the following cookbooks will be deleted:" - ui.msg "" - ui.msg ui.list(cookbooks_to_delete.keys.sort, :columns_down) - ui.msg "" - - unless config[:yes] - ui.confirm("Do you really want to delete these cookbooks") - - 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/lib/chef/knife/cookbook_delete.rb b/lib/chef/knife/cookbook_delete.rb deleted file mode 100644 index 04ecb95cf4..0000000000 --- a/lib/chef/knife/cookbook_delete.rb +++ /dev/null @@ -1,151 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../cookbook_version" - 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/lib/chef/knife/cookbook_download.rb b/lib/chef/knife/cookbook_download.rb deleted file mode 100644 index a07b519511..0000000000 --- a/lib/chef/knife/cookbook_download.rb +++ /dev/null @@ -1,142 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Christopher Walters () -# 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_relative "../cookbook_version" - end - - banner "knife cookbook download COOKBOOK [VERSION] (options)" - - option :latest, - short: "-N", - long: "--latest", - description: "The version of the cookbook to download.", - boolean: true - - option :download_directory, - short: "-d DOWNLOAD_DIRECTORY", - long: "--dir DOWNLOAD_DIRECTORY", - description: "The directory to download the cookbook into.", - default: Dir.pwd - - option :force, - short: "-f", - long: "--force", - description: "Force download over the download directory if it exists." - - # TODO: tim/cw: 5-23-2010: need to implement knife-side - # specificity for downloads - need to implement --platform and - # --fqdn here - def run - @cookbook_name, @version = @name_args - - if @cookbook_name.nil? - show_usage - ui.fatal("You must specify a cookbook name") - exit 1 - elsif @version.nil? - @version = determine_version - 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/lib/chef/knife/cookbook_list.rb b/lib/chef/knife/cookbook_list.rb deleted file mode 100644 index 719c10f893..0000000000 --- a/lib/chef/knife/cookbook_list.rb +++ /dev/null @@ -1,47 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Nuo Yan () -# 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/lib/chef/knife/cookbook_metadata.rb b/lib/chef/knife/cookbook_metadata.rb deleted file mode 100644 index 8d8970b1c1..0000000000 --- a/lib/chef/knife/cookbook_metadata.rb +++ /dev/null @@ -1,106 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../cookbook_loader" - require_relative "../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/lib/chef/knife/cookbook_metadata_from_file.rb b/lib/chef/knife/cookbook_metadata_from_file.rb deleted file mode 100644 index d768213384..0000000000 --- a/lib/chef/knife/cookbook_metadata_from_file.rb +++ /dev/null @@ -1,49 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Matthew Kent () -# 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_relative "../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/lib/chef/knife/cookbook_show.rb b/lib/chef/knife/cookbook_show.rb deleted file mode 100644 index 0b97fba139..0000000000 --- a/lib/chef/knife/cookbook_show.rb +++ /dev/null @@ -1,98 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../json_compat" - require "uri" unless defined?(URI) - require_relative "../cookbook_version" - end - - banner "knife cookbook show COOKBOOK [VERSION] [PART] [FILENAME] (options)" - - option :fqdn, - short: "-f FQDN", - long: "--fqdn FQDN", - description: "The FQDN of the host to see the file for." - - option :platform, - short: "-p PLATFORM", - long: "--platform PLATFORM", - description: "The platform to see the file for." - - option :platform_version, - short: "-V VERSION", - long: "--platform-version VERSION", - description: "The platform version to see the file for." - - option :with_uri, - short: "-w", - long: "--with-uri", - description: "Show corresponding URIs." - - def run - 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/lib/chef/knife/cookbook_upload.rb b/lib/chef/knife/cookbook_upload.rb deleted file mode 100644 index 9f6f3c4cb2..0000000000 --- a/lib/chef/knife/cookbook_upload.rb +++ /dev/null @@ -1,292 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Christopher Walters () -# Author:: Nuo Yan () -# 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_relative "../mixin/file_class" - include Chef::Mixin::FileClass - require_relative "../exceptions" - require_relative "../cookbook_loader" - require_relative "../cookbook_uploader" - 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/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb deleted file mode 100644 index 9aa81da82f..0000000000 --- a/lib/chef/knife/core/bootstrap_context.rb +++ /dev/null @@ -1,264 +0,0 @@ -# -# Author:: Daniel DeLeo () -# 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 "../../run_list" -require_relative "../../util/path_helper" -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(Chef::Util::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/lib/chef/knife/core/cookbook_scm_repo.rb b/lib/chef/knife/core/cookbook_scm_repo.rb deleted file mode 100644 index ba194a8a6d..0000000000 --- a/lib/chef/knife/core/cookbook_scm_repo.rb +++ /dev/null @@ -1,159 +0,0 @@ -# -# Author:: Daniel DeLeo () -# 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 "../../mixin/shell_out" - -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/lib/chef/knife/core/formatting_options.rb b/lib/chef/knife/core/formatting_options.rb deleted file mode 100644 index cdee2c5989..0000000000 --- a/lib/chef/knife/core/formatting_options.rb +++ /dev/null @@ -1,49 +0,0 @@ -# -# Author:: Nicolas DUPEUX () -# 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/lib/chef/knife/core/gem_glob_loader.rb b/lib/chef/knife/core/gem_glob_loader.rb deleted file mode 100644 index d058379e71..0000000000 --- a/lib/chef/knife/core/gem_glob_loader.rb +++ /dev/null @@ -1,138 +0,0 @@ -# Author:: Christopher Brown () -# Author:: Daniel DeLeo () -# 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_relative "../../util/path_helper" -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_dirglob - # The "require paths" of the core knife subcommands bundled with chef - files = Dir[File.join(Chef::Util::PathHelper.escape_glob_dir(File.expand_path("../../knife", __dir__)), "*.rb")] - subcommand_files = {} - files.each do |knife_file| - rel_path = knife_file[/#{CHEF_ROOT}#{Regexp.escape(File::SEPARATOR)}(.*)\.rb/, 1] - subcommand_files[rel_path] = knife_file - end - subcommand_files - end - - def find_subcommands_via_rubygems - files = find_files_latest_gems "chef/knife/*.rb" - subcommand_files = {} - files.each do |file| - rel_path = file[/(#{Regexp.escape File.join('chef', 'knife', '')}.*)\.rb/, 1] - - # 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) - - 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, Chef::Util::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(Chef::Util::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/lib/chef/knife/core/generic_presenter.rb b/lib/chef/knife/core/generic_presenter.rb deleted file mode 100644 index 850bfa8b3d..0000000000 --- a/lib/chef/knife/core/generic_presenter.rb +++ /dev/null @@ -1,232 +0,0 @@ -#-- -# Author:: Daniel DeLeo () -# 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/lib/chef/knife/core/hashed_command_loader.rb b/lib/chef/knife/core/hashed_command_loader.rb deleted file mode 100644 index c1d71f3edf..0000000000 --- a/lib/chef/knife/core/hashed_command_loader.rb +++ /dev/null @@ -1,100 +0,0 @@ -# Author:: Steven Danna () -# 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/lib/chef/knife/core/node_editor.rb b/lib/chef/knife/core/node_editor.rb deleted file mode 100644 index 2f9b326d16..0000000000 --- a/lib/chef/knife/core/node_editor.rb +++ /dev/null @@ -1,130 +0,0 @@ -# -# Author:: Daniel DeLeo () -# Author:: Jordan Running () -# 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 "../../json_compat" -require_relative "../../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] 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/lib/chef/knife/core/node_presenter.rb b/lib/chef/knife/core/node_presenter.rb deleted file mode 100644 index 8c948cf76c..0000000000 --- a/lib/chef/knife/core/node_presenter.rb +++ /dev/null @@ -1,133 +0,0 @@ -# -# Author:: Daniel DeLeo () -# 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/lib/chef/knife/core/object_loader.rb b/lib/chef/knife/core/object_loader.rb deleted file mode 100644 index 5421fc46ce..0000000000 --- a/lib/chef/knife/core/object_loader.rb +++ /dev/null @@ -1,115 +0,0 @@ -# -# Author:: Daniel DeLeo () -# 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_relative "../../util/path_helper" -require_relative "../../data_bag_item" - -class Chef - class Knife - module Core - class ObjectLoader - - attr_reader :ui - attr_reader :klass - - class ObjectType - FILE = 1 - FOLDER = 2 - end - - def initialize(klass, ui) - @klass = klass - @ui = ui - end - - def load_from(repo_location, *components) - unless object_file = find_file(repo_location, *components) - ui.error "Could not find or open file '#{components.last}' in current directory or in '#{repo_location}/#{components.join("/")}'" - exit 1 - end - object_from_file(object_file) - end - - # When someone makes this awesome, please update the above error message. - def find_file(repo_location, *components) - if file_exists_and_is_readable?(File.expand_path( components.last )) - File.expand_path( components.last ) - else - relative_path = File.join(Dir.pwd, repo_location, *components) - if file_exists_and_is_readable?(relative_path) - relative_path - else - nil - end - end - end - - # Find all objects in the given location - # If the object type is File it will look for all *.{json,rb} - # files, otherwise it will lookup for folders only (useful for - # data_bags) - # - # @param [String] path - base look up location - # - # @return [Array] basenames of the found objects - # - # @api public - def find_all_objects(path) - path = File.join(Chef::Util::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(Chef::Util::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/lib/chef/knife/core/status_presenter.rb b/lib/chef/knife/core/status_presenter.rb deleted file mode 100644 index 271c71d618..0000000000 --- a/lib/chef/knife/core/status_presenter.rb +++ /dev/null @@ -1,147 +0,0 @@ -# -# Author:: Nicolas DUPEUX () -# 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/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb deleted file mode 100644 index 26d7e0277c..0000000000 --- a/lib/chef/knife/core/subcommand_loader.rb +++ /dev/null @@ -1,203 +0,0 @@ -# Author:: Christopher Brown () -# Author:: Daniel DeLeo () -# 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_relative "../../util/path_helper" -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 - Chef::Util::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(Chef::Util::PathHelper.escape_glob_dir(File.expand_path("../../knife", __dir__)), "*.rb")] - subcommand_files = {} - files.each do |knife_file| - rel_path = knife_file[/#{CHEF_ROOT}#{Regexp.escape(File::SEPARATOR)}(.*)\.rb/, 1] - subcommand_files[rel_path] = knife_file - end - subcommand_files - end - - # - # 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] - # - 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", Chef::Util::PathHelper.escape_glob_dir(chef_config_dir))) - end - - # finally search ~/.chef/plugins/knife/*.rb - Chef::Util::PathHelper.home(".chef", "plugins", "knife") do |p| - user_specific_files.concat Dir.glob(File.join(Chef::Util::PathHelper.escape_glob_dir(p), "*.rb")) - end - - user_specific_files - end - end - end -end diff --git a/lib/chef/knife/core/text_formatter.rb b/lib/chef/knife/core/text_formatter.rb deleted file mode 100644 index ec97748afb..0000000000 --- a/lib/chef/knife/core/text_formatter.rb +++ /dev/null @@ -1,85 +0,0 @@ -# -# Author:: Daniel DeLeo () -# 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/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb deleted file mode 100644 index aa84537064..0000000000 --- a/lib/chef/knife/core/ui.rb +++ /dev/null @@ -1,338 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Christopher Brown () -# Author:: Daniel DeLeo () -# 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 "../../platform/query_helpers" -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/lib/chef/knife/core/windows_bootstrap_context.rb b/lib/chef/knife/core/windows_bootstrap_context.rb deleted file mode 100644 index fa8b43f383..0000000000 --- a/lib/chef/knife/core/windows_bootstrap_context.rb +++ /dev/null @@ -1,406 +0,0 @@ -# -# Author:: Seth Chisamore () -# 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_relative "../../util/path_helper" -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:// - '* 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(Chef::Util::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/lib/chef/knife/data_bag_create.rb b/lib/chef/knife/data_bag_create.rb deleted file mode 100644 index 11448c69b7..0000000000 --- a/lib/chef/knife/data_bag_create.rb +++ /dev/null @@ -1,81 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Seth Falcon () -# 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_relative "../data_bag" - require_relative "../encrypted_data_bag_item" - 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/lib/chef/knife/data_bag_delete.rb b/lib/chef/knife/data_bag_delete.rb deleted file mode 100644 index ab38244e91..0000000000 --- a/lib/chef/knife/data_bag_delete.rb +++ /dev/null @@ -1,49 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../data_bag" - end - - banner "knife data bag delete BAG [ITEM] (options)" - category "data bag" - - def run - if @name_args.length == 2 - delete_object(Chef::DataBagItem, @name_args[1], "data_bag_item") do - rest.delete("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/lib/chef/knife/data_bag_edit.rb b/lib/chef/knife/data_bag_edit.rb deleted file mode 100644 index 1935f2149e..0000000000 --- a/lib/chef/knife/data_bag_edit.rb +++ /dev/null @@ -1,74 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Seth Falcon () -# 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_relative "../data_bag_item" - require_relative "../encrypted_data_bag_item" - 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/lib/chef/knife/data_bag_from_file.rb b/lib/chef/knife/data_bag_from_file.rb deleted file mode 100644 index 5f48b0a934..0000000000 --- a/lib/chef/knife/data_bag_from_file.rb +++ /dev/null @@ -1,113 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Seth Falcon () -# 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_relative "../util/path_helper" - require_relative "../data_bag" - require_relative "../data_bag_item" - require_relative "core/object_loader" - require_relative "../encrypted_data_bag_item" - 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(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(Chef::Util::PathHelper.escape_glob_dir(path), "*.json"))) - else - paths << path - end - end - paths - end - end - end -end diff --git a/lib/chef/knife/data_bag_list.rb b/lib/chef/knife/data_bag_list.rb deleted file mode 100644 index 801bf588b4..0000000000 --- a/lib/chef/knife/data_bag_list.rb +++ /dev/null @@ -1,42 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../data_bag" - end - - banner "knife data bag list (options)" - category "data bag" - - option :with_uri, - short: "-w", - long: "--with-uri", - description: "Show corresponding URIs." - - def run - output(format_list_for_display(Chef::DataBag.list)) - end - end - end -end diff --git a/lib/chef/knife/data_bag_secret_options.rb b/lib/chef/knife/data_bag_secret_options.rb deleted file mode 100644 index 8f9f96502f..0000000000 --- a/lib/chef/knife/data_bag_secret_options.rb +++ /dev/null @@ -1,122 +0,0 @@ -# -# Author:: Tyler Ball () -# 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_relative "../config" -require_relative "../encrypted_data_bag_item/check_encrypted" - -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_relative "../encrypted_data_bag_item" - - 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/lib/chef/knife/data_bag_show.rb b/lib/chef/knife/data_bag_show.rb deleted file mode 100644 index cb7b56c333..0000000000 --- a/lib/chef/knife/data_bag_show.rb +++ /dev/null @@ -1,69 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: Seth Falcon () -# 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_relative "../data_bag" - require_relative "../encrypted_data_bag_item" - 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/lib/chef/knife/delete.rb b/lib/chef/knife/delete.rb deleted file mode 100644 index 3e5c545017..0000000000 --- a/lib/chef/knife/delete.rb +++ /dev/null @@ -1,125 +0,0 @@ -# -# 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_relative "../chef_fs/file_system" - 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/lib/chef/knife/deps.rb b/lib/chef/knife/deps.rb deleted file mode 100644 index f620b53bfa..0000000000 --- a/lib/chef/knife/deps.rb +++ /dev/null @@ -1,156 +0,0 @@ -# -# 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 Deps < Chef::ChefFS::Knife - banner "knife deps PATTERN1 [PATTERNn]" - - category "path-based" - - deps do - require_relative "../chef_fs/file_system" - require_relative "../run_list" - 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/lib/chef/knife/diff.rb b/lib/chef/knife/diff.rb deleted file mode 100644 index 3e9336aacc..0000000000 --- a/lib/chef/knife/diff.rb +++ /dev/null @@ -1,83 +0,0 @@ -# -# 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_relative "../chef_fs/command_line" - 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/lib/chef/knife/download.rb b/lib/chef/knife/download.rb deleted file mode 100644 index ab8c92a1c0..0000000000 --- a/lib/chef/knife/download.rb +++ /dev/null @@ -1,84 +0,0 @@ -# -# 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_relative "../chef_fs/command_line" - 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/lib/chef/knife/edit.rb b/lib/chef/knife/edit.rb deleted file mode 100644 index caca201566..0000000000 --- a/lib/chef/knife/edit.rb +++ /dev/null @@ -1,88 +0,0 @@ -# -# 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_relative "../chef_fs/file_system" - require_relative "../chef_fs/file_system/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/lib/chef/knife/environment_compare.rb b/lib/chef/knife/environment_compare.rb deleted file mode 100644 index 22abee59c8..0000000000 --- a/lib/chef/knife/environment_compare.rb +++ /dev/null @@ -1,128 +0,0 @@ -# -# Author:: Sander Botman () -# 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_relative "../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/lib/chef/knife/environment_create.rb b/lib/chef/knife/environment_create.rb deleted file mode 100644 index a724f72d4f..0000000000 --- a/lib/chef/knife/environment_create.rb +++ /dev/null @@ -1,52 +0,0 @@ -# -# Author:: Stephen Delano () -# 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_relative "../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/lib/chef/knife/environment_delete.rb b/lib/chef/knife/environment_delete.rb deleted file mode 100644 index ec1b7cb8d8..0000000000 --- a/lib/chef/knife/environment_delete.rb +++ /dev/null @@ -1,44 +0,0 @@ -# -# Author:: Stephen Delano () -# 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_relative "../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/lib/chef/knife/environment_edit.rb b/lib/chef/knife/environment_edit.rb deleted file mode 100644 index 7c6105a6c0..0000000000 --- a/lib/chef/knife/environment_edit.rb +++ /dev/null @@ -1,44 +0,0 @@ -# -# Author:: Stephen Delano () -# 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_relative "../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/lib/chef/knife/environment_from_file.rb b/lib/chef/knife/environment_from_file.rb deleted file mode 100644 index a5011a3abf..0000000000 --- a/lib/chef/knife/environment_from_file.rb +++ /dev/null @@ -1,84 +0,0 @@ -# -# Author:: Stephen Delano () -# 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_relative "../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/lib/chef/knife/environment_list.rb b/lib/chef/knife/environment_list.rb deleted file mode 100644 index 7bcdeb6084..0000000000 --- a/lib/chef/knife/environment_list.rb +++ /dev/null @@ -1,41 +0,0 @@ -# -# Author:: Stephen Delano () -# 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_relative "../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/lib/chef/knife/environment_show.rb b/lib/chef/knife/environment_show.rb deleted file mode 100644 index e336b2d392..0000000000 --- a/lib/chef/knife/environment_show.rb +++ /dev/null @@ -1,47 +0,0 @@ -# -# Author:: Stephen Delano () -# 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_relative "../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/lib/chef/knife/exec.rb b/lib/chef/knife/exec.rb deleted file mode 100644 index d3ce2cee24..0000000000 --- a/lib/chef/knife/exec.rb +++ /dev/null @@ -1,99 +0,0 @@ -#-- -# Author:: Daniel DeLeo () -# Author:: Jeremiah Snapp () -# 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/lib/chef/knife/group_create.rb b/lib/chef/knife/group_create.rb deleted file mode 100644 index 4219188951..0000000000 --- a/lib/chef/knife/group_create.rb +++ /dev/null @@ -1,49 +0,0 @@ -# -# Author:: Seth Falcon () -# Author:: Jeremiah Snapp () -# 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/lib/chef/knife/group_destroy.rb b/lib/chef/knife/group_destroy.rb deleted file mode 100644 index 433a5cc627..0000000000 --- a/lib/chef/knife/group_destroy.rb +++ /dev/null @@ -1,53 +0,0 @@ -# -# Author:: Christopher Maier () -# Author:: Jeremiah Snapp () -# 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/lib/chef/knife/group_list.rb b/lib/chef/knife/group_list.rb deleted file mode 100644 index fc8f00ad6d..0000000000 --- a/lib/chef/knife/group_list.rb +++ /dev/null @@ -1,43 +0,0 @@ -# -# Author:: Seth Falcon () -# Author:: Jeremiah Snapp () -# 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/lib/chef/knife/group_remove.rb b/lib/chef/knife/group_remove.rb deleted file mode 100644 index 07ab19693f..0000000000 --- a/lib/chef/knife/group_remove.rb +++ /dev/null @@ -1,56 +0,0 @@ -# -# Author:: Seth Falcon () -# Author:: Jeremiah Snapp () -# 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/lib/chef/knife/group_show.rb b/lib/chef/knife/group_show.rb deleted file mode 100644 index 6ac53f6b6e..0000000000 --- a/lib/chef/knife/group_show.rb +++ /dev/null @@ -1,49 +0,0 @@ -# -# Author:: Seth Falcon () -# Author:: Jeremiah Snapp () -# 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/lib/chef/knife/key_create.rb b/lib/chef/knife/key_create.rb deleted file mode 100644 index 6129cab683..0000000000 --- a/lib/chef/knife/key_create.rb +++ /dev/null @@ -1,112 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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 "../key" -require_relative "../json_compat" -require_relative "../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/lib/chef/knife/key_create_base.rb b/lib/chef/knife/key_create_base.rb deleted file mode 100644 index a1d658e43c..0000000000 --- a/lib/chef/knife/key_create_base.rb +++ /dev/null @@ -1,50 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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/lib/chef/knife/key_delete.rb b/lib/chef/knife/key_delete.rb deleted file mode 100644 index 10f1235924..0000000000 --- a/lib/chef/knife/key_delete.rb +++ /dev/null @@ -1,55 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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 "../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/lib/chef/knife/key_edit.rb b/lib/chef/knife/key_edit.rb deleted file mode 100644 index 3f8918f1a9..0000000000 --- a/lib/chef/knife/key_edit.rb +++ /dev/null @@ -1,118 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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 "../key" -require_relative "../json_compat" -require_relative "../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/lib/chef/knife/key_edit_base.rb b/lib/chef/knife/key_edit_base.rb deleted file mode 100644 index b094877190..0000000000 --- a/lib/chef/knife/key_edit_base.rb +++ /dev/null @@ -1,55 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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/lib/chef/knife/key_list.rb b/lib/chef/knife/key_list.rb deleted file mode 100644 index 076b39d251..0000000000 --- a/lib/chef/knife/key_list.rb +++ /dev/null @@ -1,90 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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 "../key" -require_relative "../json_compat" -require_relative "../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/lib/chef/knife/key_list_base.rb b/lib/chef/knife/key_list_base.rb deleted file mode 100644 index e06e908b69..0000000000 --- a/lib/chef/knife/key_list_base.rb +++ /dev/null @@ -1,45 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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/lib/chef/knife/key_show.rb b/lib/chef/knife/key_show.rb deleted file mode 100644 index 8b3d980004..0000000000 --- a/lib/chef/knife/key_show.rb +++ /dev/null @@ -1,53 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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 "../key" -require_relative "../json_compat" -require_relative "../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/lib/chef/knife/list.rb b/lib/chef/knife/list.rb deleted file mode 100644 index 1cc398e01a..0000000000 --- a/lib/chef/knife/list.rb +++ /dev/null @@ -1,177 +0,0 @@ -# -# 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_relative "../chef_fs/file_system" - 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/lib/chef/knife/node_bulk_delete.rb b/lib/chef/knife/node_bulk_delete.rb deleted file mode 100644 index 874509b730..0000000000 --- a/lib/chef/knife/node_bulk_delete.rb +++ /dev/null @@ -1,75 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - end - - banner "knife node bulk delete REGEX (options)" - - def run - if name_args.length < 1 - ui.fatal("You must supply a regular expression to match the results against") - exit 42 - end - - nodes_to_delete = {} - matcher = /#{name_args[0]}/ - - all_nodes.each do |name, node| - next unless name&.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/lib/chef/knife/node_create.rb b/lib/chef/knife/node_create.rb deleted file mode 100644 index c0db667b25..0000000000 --- a/lib/chef/knife/node_create.rb +++ /dev/null @@ -1,47 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - end - - banner "knife node create NODE (options)" - - def run - @node_name = @name_args[0] - - if @node_name.nil? - show_usage - ui.fatal("You must specify a node name") - exit 1 - end - - node = Chef::Node.new - node.name(@node_name) - create_object(node, object_class: Chef::Node) - end - end - end -end diff --git a/lib/chef/knife/node_delete.rb b/lib/chef/knife/node_delete.rb deleted file mode 100644 index 7c0c6f0a21..0000000000 --- a/lib/chef/knife/node_delete.rb +++ /dev/null @@ -1,46 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - 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/lib/chef/knife/node_edit.rb b/lib/chef/knife/node_edit.rb deleted file mode 100644 index a2585391ea..0000000000 --- a/lib/chef/knife/node_edit.rb +++ /dev/null @@ -1,70 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - 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/lib/chef/knife/node_environment_set.rb b/lib/chef/knife/node_environment_set.rb deleted file mode 100644 index 644b6138b6..0000000000 --- a/lib/chef/knife/node_environment_set.rb +++ /dev/null @@ -1,53 +0,0 @@ -# -# Author:: Jimmy McCrory () -# 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_relative "../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/lib/chef/knife/node_from_file.rb b/lib/chef/knife/node_from_file.rb deleted file mode 100644 index 86d602ae7c..0000000000 --- a/lib/chef/knife/node_from_file.rb +++ /dev/null @@ -1,51 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - 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/lib/chef/knife/node_list.rb b/lib/chef/knife/node_list.rb deleted file mode 100644 index a8b57aedc5..0000000000 --- a/lib/chef/knife/node_list.rb +++ /dev/null @@ -1,44 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - end - - banner "knife node list (options)" - - option :with_uri, - short: "-w", - long: "--with-uri", - description: "Show corresponding URIs." - - def run - env = Chef::Config[:environment] - output(format_list_for_display( env ? Chef::Node.list_by_environment(env) : Chef::Node.list )) - end - - end - end -end diff --git a/lib/chef/knife/node_policy_set.rb b/lib/chef/knife/node_policy_set.rb deleted file mode 100644 index d34ebd9478..0000000000 --- a/lib/chef/knife/node_policy_set.rb +++ /dev/null @@ -1,79 +0,0 @@ -# -# Author:: Piyush Awasthi () -# 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_relative "../node" - require_relative "../json_compat" - 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/lib/chef/knife/node_run_list_add.rb b/lib/chef/knife/node_run_list_add.rb deleted file mode 100644 index 40476371eb..0000000000 --- a/lib/chef/knife/node_run_list_add.rb +++ /dev/null @@ -1,104 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - end - - banner "knife node run_list add [NODE] [ENTRY [ENTRY]] (options)" - - option :after, - short: "-a ITEM", - long: "--after ITEM", - description: "Place the ENTRY in the run list after ITEM." - - 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/lib/chef/knife/node_run_list_remove.rb b/lib/chef/knife/node_run_list_remove.rb deleted file mode 100644 index 484e575475..0000000000 --- a/lib/chef/knife/node_run_list_remove.rb +++ /dev/null @@ -1,67 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - 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/lib/chef/knife/node_run_list_set.rb b/lib/chef/knife/node_run_list_set.rb deleted file mode 100644 index f356b39d95..0000000000 --- a/lib/chef/knife/node_run_list_set.rb +++ /dev/null @@ -1,66 +0,0 @@ -# -# Author:: Mike Fiedler () -# 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_relative "../node" - require_relative "../json_compat" - 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/lib/chef/knife/node_show.rb b/lib/chef/knife/node_show.rb deleted file mode 100644 index 173348dc41..0000000000 --- a/lib/chef/knife/node_show.rb +++ /dev/null @@ -1,63 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - 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/lib/chef/knife/null.rb b/lib/chef/knife/null.rb deleted file mode 100644 index 7221eee9f5..0000000000 --- a/lib/chef/knife/null.rb +++ /dev/null @@ -1,12 +0,0 @@ -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/lib/chef/knife/org_create.rb b/lib/chef/knife/org_create.rb deleted file mode 100644 index 3c1354ae22..0000000000 --- a/lib/chef/knife/org_create.rb +++ /dev/null @@ -1,70 +0,0 @@ -# -# Author:: Steven Danna () -# 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_relative "../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/lib/chef/knife/org_delete.rb b/lib/chef/knife/org_delete.rb deleted file mode 100644 index 340f6c529a..0000000000 --- a/lib/chef/knife/org_delete.rb +++ /dev/null @@ -1,32 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/org_edit.rb b/lib/chef/knife/org_edit.rb deleted file mode 100644 index 1d684ca0b4..0000000000 --- a/lib/chef/knife/org_edit.rb +++ /dev/null @@ -1,48 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/org_list.rb b/lib/chef/knife/org_list.rb deleted file mode 100644 index 85a49ee4c5..0000000000 --- a/lib/chef/knife/org_list.rb +++ /dev/null @@ -1,44 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/org_show.rb b/lib/chef/knife/org_show.rb deleted file mode 100644 index a8bb207c1d..0000000000 --- a/lib/chef/knife/org_show.rb +++ /dev/null @@ -1,31 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/org_user_add.rb b/lib/chef/knife/org_user_add.rb deleted file mode 100644 index cd0ea88d56..0000000000 --- a/lib/chef/knife/org_user_add.rb +++ /dev/null @@ -1,62 +0,0 @@ -# -# Author:: Marc Paradise () -# 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/lib/chef/knife/org_user_remove.rb b/lib/chef/knife/org_user_remove.rb deleted file mode 100644 index 50a1471443..0000000000 --- a/lib/chef/knife/org_user_remove.rb +++ /dev/null @@ -1,103 +0,0 @@ -# -# Author:: Marc Paradise () -# 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_relative "../org" - require "chef/json_compat" - 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/lib/chef/knife/raw.rb b/lib/chef/knife/raw.rb deleted file mode 100644 index 5adb36ea70..0000000000 --- a/lib/chef/knife/raw.rb +++ /dev/null @@ -1,123 +0,0 @@ -# -# 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_relative "../json_compat" - require_relative "../config" - require_relative "../http" - require_relative "../http/authenticator" - require_relative "../http/cookie_manager" - require_relative "../http/decompressor" - require_relative "../http/json_output" - 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/lib/chef/knife/recipe_list.rb b/lib/chef/knife/recipe_list.rb deleted file mode 100644 index 39e040a2f4..0000000000 --- a/lib/chef/knife/recipe_list.rb +++ /dev/null @@ -1,32 +0,0 @@ -# -# Author:: Daniel DeLeo () -# 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/lib/chef/knife/rehash.rb b/lib/chef/knife/rehash.rb deleted file mode 100644 index 69ee19229a..0000000000 --- a/lib/chef/knife/rehash.rb +++ /dev/null @@ -1,50 +0,0 @@ -# -# Author:: Steven Danna -# 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/lib/chef/knife/role_bulk_delete.rb b/lib/chef/knife/role_bulk_delete.rb deleted file mode 100644 index f57ac79619..0000000000 --- a/lib/chef/knife/role_bulk_delete.rb +++ /dev/null @@ -1,66 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../role" - require_relative "../json_compat" - end - - banner "knife role bulk delete REGEX (options)" - - def run - if @name_args.length < 1 - ui.error("You must supply a regular expression to match the results against") - exit 1 - end - - all_roles = Chef::Role.list(true) - - matcher = /#{@name_args[0]}/ - roles_to_delete = {} - all_roles.each do |name, role| - next unless name&.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/lib/chef/knife/role_create.rb b/lib/chef/knife/role_create.rb deleted file mode 100644 index 295445554d..0000000000 --- a/lib/chef/knife/role_create.rb +++ /dev/null @@ -1,53 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../role" - require_relative "../json_compat" - end - - banner "knife role create ROLE (options)" - - option :description, - short: "-d DESC", - long: "--description DESC", - description: "The role description." - - def run - @role_name = @name_args[0] - - if @role_name.nil? - show_usage - ui.fatal("You must specify a role name") - exit 1 - end - - role = Chef::Role.new - role.name(@role_name) - role.description(config[:description]) if config[:description] - create_object(role, object_class: Chef::Role) - end - end - end -end diff --git a/lib/chef/knife/role_delete.rb b/lib/chef/knife/role_delete.rb deleted file mode 100644 index c46e265c5e..0000000000 --- a/lib/chef/knife/role_delete.rb +++ /dev/null @@ -1,46 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../role" - require_relative "../json_compat" - end - - banner "knife role delete ROLE (options)" - - def run - @role_name = @name_args[0] - - if @role_name.nil? - show_usage - ui.fatal("You must specify a role name") - exit 1 - end - - delete_object(Chef::Role, @role_name) - end - - end - end -end diff --git a/lib/chef/knife/role_edit.rb b/lib/chef/knife/role_edit.rb deleted file mode 100644 index 1925336646..0000000000 --- a/lib/chef/knife/role_edit.rb +++ /dev/null @@ -1,45 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../role" - require_relative "../json_compat" - end - - banner "knife role edit ROLE (options)" - - def run - @role_name = @name_args[0] - - if @role_name.nil? - show_usage - ui.fatal("You must specify a role name") - exit 1 - end - - ui.edit_object(Chef::Role, @role_name) - end - end - end -end diff --git a/lib/chef/knife/role_env_run_list_add.rb b/lib/chef/knife/role_env_run_list_add.rb deleted file mode 100644 index b5753b46fc..0000000000 --- a/lib/chef/knife/role_env_run_list_add.rb +++ /dev/null @@ -1,87 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: William Albenzi () -# 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_relative "../role" - require_relative "../json_compat" - 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/lib/chef/knife/role_env_run_list_clear.rb b/lib/chef/knife/role_env_run_list_clear.rb deleted file mode 100644 index dda523e809..0000000000 --- a/lib/chef/knife/role_env_run_list_clear.rb +++ /dev/null @@ -1,55 +0,0 @@ -# -# Author:: Mike Fiedler () -# Author:: William Albenzi () -# 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_relative "../role" - require_relative "../json_compat" - 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/lib/chef/knife/role_env_run_list_remove.rb b/lib/chef/knife/role_env_run_list_remove.rb deleted file mode 100644 index 57363610ce..0000000000 --- a/lib/chef/knife/role_env_run_list_remove.rb +++ /dev/null @@ -1,57 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../role" - require_relative "../json_compat" - 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/lib/chef/knife/role_env_run_list_replace.rb b/lib/chef/knife/role_env_run_list_replace.rb deleted file mode 100644 index e76680661e..0000000000 --- a/lib/chef/knife/role_env_run_list_replace.rb +++ /dev/null @@ -1,60 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: William Albenzi () -# 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_relative "../role" - require_relative "../json_compat" - 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/lib/chef/knife/role_env_run_list_set.rb b/lib/chef/knife/role_env_run_list_set.rb deleted file mode 100644 index 0f1ce62a5d..0000000000 --- a/lib/chef/knife/role_env_run_list_set.rb +++ /dev/null @@ -1,70 +0,0 @@ -# -# Author:: Mike Fiedler () -# Author:: William Albenzi () -# 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_relative "../role" - require_relative "../json_compat" - 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/lib/chef/knife/role_from_file.rb b/lib/chef/knife/role_from_file.rb deleted file mode 100644 index 16e38eeb63..0000000000 --- a/lib/chef/knife/role_from_file.rb +++ /dev/null @@ -1,51 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../role" - require_relative "core/object_loader" - require_relative "../json_compat" - end - - banner "knife role from file FILE [FILE..] (options)" - - def loader - @loader ||= Knife::Core::ObjectLoader.new(Chef::Role, ui) - end - - def run - @name_args.each do |arg| - updated = loader.load_from("roles", arg) - - updated.save - - output(format_for_display(updated)) if config[:print_after] - - ui.info("Updated Role #{updated.name}") - end - end - - end - end -end diff --git a/lib/chef/knife/role_list.rb b/lib/chef/knife/role_list.rb deleted file mode 100644 index d6aad053c1..0000000000 --- a/lib/chef/knife/role_list.rb +++ /dev/null @@ -1,42 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../json_compat" - end - - banner "knife role list (options)" - - option :with_uri, - short: "-w", - long: "--with-uri", - description: "Show corresponding URIs." - - def run - output(format_list_for_display(Chef::Role.list)) - end - end - end -end diff --git a/lib/chef/knife/role_run_list_add.rb b/lib/chef/knife/role_run_list_add.rb deleted file mode 100644 index 76633ff5f6..0000000000 --- a/lib/chef/knife/role_run_list_add.rb +++ /dev/null @@ -1,87 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: William Albenzi () -# 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_relative "../role" - require_relative "../json_compat" - 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/lib/chef/knife/role_run_list_clear.rb b/lib/chef/knife/role_run_list_clear.rb deleted file mode 100644 index b7106233f0..0000000000 --- a/lib/chef/knife/role_run_list_clear.rb +++ /dev/null @@ -1,55 +0,0 @@ -# -# Author:: Mike Fiedler () -# Author:: William Albenzi () -# 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_relative "../role" - require_relative "../json_compat" - 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/lib/chef/knife/role_run_list_remove.rb b/lib/chef/knife/role_run_list_remove.rb deleted file mode 100644 index 884f3bc28d..0000000000 --- a/lib/chef/knife/role_run_list_remove.rb +++ /dev/null @@ -1,56 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../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/lib/chef/knife/role_run_list_replace.rb b/lib/chef/knife/role_run_list_replace.rb deleted file mode 100644 index 16f789fbef..0000000000 --- a/lib/chef/knife/role_run_list_replace.rb +++ /dev/null @@ -1,60 +0,0 @@ -# -# Author:: Adam Jacob () -# Author:: William Albenzi () -# 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_relative "../role" - require_relative "../json_compat" - 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/lib/chef/knife/role_run_list_set.rb b/lib/chef/knife/role_run_list_set.rb deleted file mode 100644 index ad1a5e2923..0000000000 --- a/lib/chef/knife/role_run_list_set.rb +++ /dev/null @@ -1,69 +0,0 @@ -# -# Author:: Mike Fiedler () -# Author:: William Albenzi () -# 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_relative "../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/lib/chef/knife/role_show.rb b/lib/chef/knife/role_show.rb deleted file mode 100644 index ee90352e50..0000000000 --- a/lib/chef/knife/role_show.rb +++ /dev/null @@ -1,48 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../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/lib/chef/knife/search.rb b/lib/chef/knife/search.rb deleted file mode 100644 index 620cfb971d..0000000000 --- a/lib/chef/knife/search.rb +++ /dev/null @@ -1,194 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../node" - require_relative "../environment" - require_relative "../api_client" - require_relative "../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/lib/chef/knife/serve.rb b/lib/chef/knife/serve.rb deleted file mode 100644 index d79e05aa85..0000000000 --- a/lib/chef/knife/serve.rb +++ /dev/null @@ -1,65 +0,0 @@ -# -# 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 "../local_mode" -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/lib/chef/knife/show.rb b/lib/chef/knife/show.rb deleted file mode 100644 index 0e5ab9d0fe..0000000000 --- a/lib/chef/knife/show.rb +++ /dev/null @@ -1,72 +0,0 @@ -# -# 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_relative "../chef_fs/file_system" - require_relative "../chef_fs/file_system/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/lib/chef/knife/ssh.rb b/lib/chef/knife/ssh.rb deleted file mode 100644 index 8681fdfd02..0000000000 --- a/lib/chef/knife/ssh.rb +++ /dev/null @@ -1,645 +0,0 @@ -# -# Author:: Adam Jacob () -# 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_relative "../mixin/shell_out" - require "net/ssh" unless defined?(Net::SSH) - require "net/ssh/multi" - require "readline" - require_relative "../exceptions" - require_relative "../search/query" - require_relative "../util/path_helper" - - 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] - 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") - Chef::Util::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/lib/chef/knife/ssl_check.rb b/lib/chef/knife/ssl_check.rb deleted file mode 100644 index 0cc4141d42..0000000000 --- a/lib/chef/knife/ssl_check.rb +++ /dev/null @@ -1,284 +0,0 @@ -# -# Author:: Daniel DeLeo () -# 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_relative "../config" - require "pp" unless defined?(PP) - require "socket" unless defined?(Socket) - require "uri" unless defined?(URI) - require_relative "../http/ssl_policies" - require "openssl" unless defined?(OpenSSL) - require_relative "../mixin/proxified_socket" - 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/lib/chef/knife/ssl_fetch.rb b/lib/chef/knife/ssl_fetch.rb deleted file mode 100644 index cfbbc823b2..0000000000 --- a/lib/chef/knife/ssl_fetch.rb +++ /dev/null @@ -1,161 +0,0 @@ -# -# Author:: Daniel DeLeo () -# 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_relative "../config" - require "pp" unless defined?(PP) - require "socket" unless defined?(Socket) - require "uri" unless defined?(URI) - require "openssl" unless defined?(OpenSSL) - require_relative "../mixin/proxified_socket" - 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/lib/chef/knife/status.rb b/lib/chef/knife/status.rb deleted file mode 100644 index 34692d6da7..0000000000 --- a/lib/chef/knife/status.rb +++ /dev/null @@ -1,95 +0,0 @@ -# -# Author:: Ian Meyer () -# 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_relative "../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/lib/chef/knife/supermarket_download.rb b/lib/chef/knife/supermarket_download.rb deleted file mode 100644 index 5acd733b78..0000000000 --- a/lib/chef/knife/supermarket_download.rb +++ /dev/null @@ -1,121 +0,0 @@ -# -# Author:: Christopher Webber () -# 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/lib/chef/knife/supermarket_install.rb b/lib/chef/knife/supermarket_install.rb deleted file mode 100644 index a3d3aa7a5d..0000000000 --- a/lib/chef/knife/supermarket_install.rb +++ /dev/null @@ -1,192 +0,0 @@ -# -# Author:: Christopher Webber () -# 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_relative "../exceptions" - require "shellwords" unless defined?(Shellwords) - require "mixlib/archive" unless defined?(Mixlib::Archive) - require_relative "core/cookbook_scm_repo" - require_relative "../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/lib/chef/knife/supermarket_list.rb b/lib/chef/knife/supermarket_list.rb deleted file mode 100644 index 7dca8d031b..0000000000 --- a/lib/chef/knife/supermarket_list.rb +++ /dev/null @@ -1,76 +0,0 @@ -# -# Author:: Christopher Webber () -# 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/lib/chef/knife/supermarket_search.rb b/lib/chef/knife/supermarket_search.rb deleted file mode 100644 index 57befaed35..0000000000 --- a/lib/chef/knife/supermarket_search.rb +++ /dev/null @@ -1,53 +0,0 @@ -# -# Author:: Christopher Webber () -# 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/lib/chef/knife/supermarket_share.rb b/lib/chef/knife/supermarket_share.rb deleted file mode 100644 index 49b3474566..0000000000 --- a/lib/chef/knife/supermarket_share.rb +++ /dev/null @@ -1,166 +0,0 @@ -# -# Author:: Christopher Webber () -# 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_relative "../cookbook_loader" - require_relative "../cookbook_uploader" - require_relative "../cookbook_site_streaming_uploader" - require_relative "../mixin/shell_out" - 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::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::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/lib/chef/knife/supermarket_show.rb b/lib/chef/knife/supermarket_show.rb deleted file mode 100644 index 7237cf0bc7..0000000000 --- a/lib/chef/knife/supermarket_show.rb +++ /dev/null @@ -1,66 +0,0 @@ -# -# Author:: Christopher Webber () -# 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/lib/chef/knife/supermarket_unshare.rb b/lib/chef/knife/supermarket_unshare.rb deleted file mode 100644 index 686d95f47a..0000000000 --- a/lib/chef/knife/supermarket_unshare.rb +++ /dev/null @@ -1,61 +0,0 @@ -# -# Author:: Christopher Webber () -# 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_relative "../json_compat" - 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/lib/chef/knife/tag_create.rb b/lib/chef/knife/tag_create.rb deleted file mode 100644 index 2f0d302e74..0000000000 --- a/lib/chef/knife/tag_create.rb +++ /dev/null @@ -1,52 +0,0 @@ -# -# Author:: Ryan Davis () -# Author:: Daniel DeLeo () -# Author:: Nuo Yan () -# 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_relative "../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/lib/chef/knife/tag_delete.rb b/lib/chef/knife/tag_delete.rb deleted file mode 100644 index 85fa6a9e27..0000000000 --- a/lib/chef/knife/tag_delete.rb +++ /dev/null @@ -1,60 +0,0 @@ -# -# Author:: Ryan Davis () -# Author:: Daniel DeLeo () -# Author:: Nuo Yan () -# 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_relative "../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/lib/chef/knife/tag_list.rb b/lib/chef/knife/tag_list.rb deleted file mode 100644 index 8b91034609..0000000000 --- a/lib/chef/knife/tag_list.rb +++ /dev/null @@ -1,47 +0,0 @@ -# -# Author:: Ryan Davis () -# Author:: Daniel DeLeo () -# Author:: Nuo Yan () -# 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_relative "../node" - end - - banner "knife tag list NODE" - - def run - name = @name_args[0] - - if name.nil? - show_usage - ui.fatal("You must specify a node name.") - exit 1 - end - - node = Chef::Node.load(name) - output(node.tags) - end - end - end -end diff --git a/lib/chef/knife/upload.rb b/lib/chef/knife/upload.rb deleted file mode 100644 index 190549d86a..0000000000 --- a/lib/chef/knife/upload.rb +++ /dev/null @@ -1,86 +0,0 @@ -# -# 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_relative "../chef_fs/command_line" - 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/lib/chef/knife/user_create.rb b/lib/chef/knife/user_create.rb deleted file mode 100644 index aa1d4d54f2..0000000000 --- a/lib/chef/knife/user_create.rb +++ /dev/null @@ -1,143 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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_relative "../user_v1" - 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/lib/chef/knife/user_delete.rb b/lib/chef/knife/user_delete.rb deleted file mode 100644 index 64d729c951..0000000000 --- a/lib/chef/knife/user_delete.rb +++ /dev/null @@ -1,151 +0,0 @@ -# -# Author:: Steven Danna () -# 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_relative "../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/lib/chef/knife/user_dissociate.rb b/lib/chef/knife/user_dissociate.rb deleted file mode 100644 index 6af1559608..0000000000 --- a/lib/chef/knife/user_dissociate.rb +++ /dev/null @@ -1,42 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/user_edit.rb b/lib/chef/knife/user_edit.rb deleted file mode 100644 index fff8c6b70f..0000000000 --- a/lib/chef/knife/user_edit.rb +++ /dev/null @@ -1,94 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/user_invite_add.rb b/lib/chef/knife/user_invite_add.rb deleted file mode 100644 index 1690147535..0000000000 --- a/lib/chef/knife/user_invite_add.rb +++ /dev/null @@ -1,43 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/user_invite_list.rb b/lib/chef/knife/user_invite_list.rb deleted file mode 100644 index 831774d1bf..0000000000 --- a/lib/chef/knife/user_invite_list.rb +++ /dev/null @@ -1,34 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/user_invite_rescind.rb b/lib/chef/knife/user_invite_rescind.rb deleted file mode 100644 index fd5804e10a..0000000000 --- a/lib/chef/knife/user_invite_rescind.rb +++ /dev/null @@ -1,63 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/user_key_create.rb b/lib/chef/knife/user_key_create.rb deleted file mode 100644 index efc783dd7f..0000000000 --- a/lib/chef/knife/user_key_create.rb +++ /dev/null @@ -1,73 +0,0 @@ -# -# 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/lib/chef/knife/user_key_delete.rb b/lib/chef/knife/user_key_delete.rb deleted file mode 100644 index b4f84fdb7b..0000000000 --- a/lib/chef/knife/user_key_delete.rb +++ /dev/null @@ -1,80 +0,0 @@ -# -# 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/lib/chef/knife/user_key_edit.rb b/lib/chef/knife/user_key_edit.rb deleted file mode 100644 index 15ef2ada1e..0000000000 --- a/lib/chef/knife/user_key_edit.rb +++ /dev/null @@ -1,83 +0,0 @@ -# -# 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/lib/chef/knife/user_key_list.rb b/lib/chef/knife/user_key_list.rb deleted file mode 100644 index 781998b301..0000000000 --- a/lib/chef/knife/user_key_list.rb +++ /dev/null @@ -1,73 +0,0 @@ -# -# 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/lib/chef/knife/user_key_show.rb b/lib/chef/knife/user_key_show.rb deleted file mode 100644 index 2bf535c792..0000000000 --- a/lib/chef/knife/user_key_show.rb +++ /dev/null @@ -1,80 +0,0 @@ -# -# 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/lib/chef/knife/user_list.rb b/lib/chef/knife/user_list.rb deleted file mode 100644 index 3284964a47..0000000000 --- a/lib/chef/knife/user_list.rb +++ /dev/null @@ -1,38 +0,0 @@ -# -# Author:: Steven Danna () -# 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 - - banner "knife user list (options)" - - option :with_uri, - short: "-w", - long: "--with-uri", - description: "Show corresponding URIs." - - def run - results = root_rest.get("users") - output(format_list_for_display(results)) - end - end - end -end diff --git a/lib/chef/knife/user_password.rb b/lib/chef/knife/user_password.rb deleted file mode 100644 index 2da3c3e285..0000000000 --- a/lib/chef/knife/user_password.rb +++ /dev/null @@ -1,70 +0,0 @@ -# -# Author:: Tyler Cloke () -# 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/lib/chef/knife/user_reregister.rb b/lib/chef/knife/user_reregister.rb deleted file mode 100644 index ee58c19d9f..0000000000 --- a/lib/chef/knife/user_reregister.rb +++ /dev/null @@ -1,59 +0,0 @@ -# -# Author:: Steven Danna () -# 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_relative "../user_v1" - 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/lib/chef/knife/user_show.rb b/lib/chef/knife/user_show.rb deleted file mode 100644 index ea2b06b753..0000000000 --- a/lib/chef/knife/user_show.rb +++ /dev/null @@ -1,52 +0,0 @@ -# -# Author:: Steven Danna () -# 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/lib/chef/knife/xargs.rb b/lib/chef/knife/xargs.rb deleted file mode 100644 index 9dcc724d38..0000000000 --- a/lib/chef/knife/xargs.rb +++ /dev/null @@ -1,282 +0,0 @@ -# -# 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_relative "../chef_fs/file_system" - require_relative "../chef_fs/file_system/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/lib/chef/knife/yaml_convert.rb b/lib/chef/knife/yaml_convert.rb deleted file mode 100644 index 6bd2d1c0ea..0000000000 --- a/lib/chef/knife/yaml_convert.rb +++ /dev/null @@ -1,91 +0,0 @@ -# -# Author:: Bryan McLellan -# 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 diff --git a/spec/functional/knife/configure_spec.rb b/spec/functional/knife/configure_spec.rb index 8f2a5b4d6e..402e988132 100644 --- a/spec/functional/knife/configure_spec.rb +++ b/spec/functional/knife/configure_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/configure" diff --git a/spec/functional/knife/cookbook_delete_spec.rb b/spec/functional/knife/cookbook_delete_spec.rb index 650db0ede5..f25999f0fc 100644 --- a/spec/functional/knife/cookbook_delete_spec.rb +++ b/spec/functional/knife/cookbook_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "tiny_server" describe Chef::Knife::CookbookDelete do diff --git a/spec/functional/knife/exec_spec.rb b/spec/functional/knife/exec_spec.rb index 3905798317..267fe8492e 100644 --- a/spec/functional/knife/exec_spec.rb +++ b/spec/functional/knife/exec_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "tiny_server" describe Chef::Knife::Exec do diff --git a/spec/functional/knife/rehash_spec.rb b/spec/functional/knife/rehash_spec.rb index 8f59eec270..a4b7e5507c 100644 --- a/spec/functional/knife/rehash_spec.rb +++ b/spec/functional/knife/rehash_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/rehash" require "chef/knife/core/subcommand_loader" diff --git a/spec/functional/knife/ssh_spec.rb b/spec/functional/knife/ssh_spec.rb index 1d4aff15b5..5a29f995f8 100644 --- a/spec/functional/knife/ssh_spec.rb +++ b/spec/functional/knife/ssh_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "tiny_server" describe Chef::Knife::Ssh do diff --git a/spec/functional/knife/version_spec.rb b/spec/functional/knife/version_spec.rb new file mode 100644 index 0000000000..b024cc1cda --- /dev/null +++ b/spec/functional/knife/version_spec.rb @@ -0,0 +1,26 @@ +# 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 "knife_spec_helper" +require "chef/mixin/shell_out" + +describe "Knife Version", :executables do + include Chef::Mixin::ShellOut + let(:knife_dir) { File.join(__dir__, "..", "..", "..", "knife") } + xit "should be sane" do + expect(shell_out!("bundle exec knife -v", cwd: knife_dir).stdout.chomp).to match(/.*: #{Chef::Knife::VERSION}/) + end +end + diff --git a/spec/functional/version_spec.rb b/spec/functional/version_spec.rb index 5d0f0fce43..3b348ed024 100644 --- a/spec/functional/version_spec.rb +++ b/spec/functional/version_spec.rb @@ -25,7 +25,7 @@ describe "Chef Versions", :executables do include Chef::Mixin::ShellOut let(:chef_dir) { File.join(__dir__, "..", "..") } - binaries = [ ChefUtils::Dist::Infra::CLIENT, "chef-shell", "chef-apply", "knife", ChefUtils::Dist::Solo::EXEC ] + binaries = [ ChefUtils::Dist::Infra::CLIENT, "chef-shell", "chef-apply", ChefUtils::Dist::Solo::EXEC ] binaries.each do |binary| it "#{binary} version should be sane" do diff --git a/spec/integration/knife/chef_fs_data_store_spec.rb b/spec/integration/knife/chef_fs_data_store_spec.rb index 6e04684c33..fda06164a4 100644 --- a/spec/integration/knife/chef_fs_data_store_spec.rb +++ b/spec/integration/knife/chef_fs_data_store_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "chef/knife/list" require "chef/knife/delete" diff --git a/spec/integration/knife/chef_repo_path_spec.rb b/spec/integration/knife/chef_repo_path_spec.rb index ac7dae15f0..27b45ac428 100644 --- a/spec/integration/knife/chef_repo_path_spec.rb +++ b/spec/integration/knife/chef_repo_path_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/list" diff --git a/spec/integration/knife/chef_repository_file_system_spec.rb b/spec/integration/knife/chef_repository_file_system_spec.rb index 295efc0c3a..9a129dcb98 100644 --- a/spec/integration/knife/chef_repository_file_system_spec.rb +++ b/spec/integration/knife/chef_repository_file_system_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "chef/knife/list" require "chef/knife/show" diff --git a/spec/integration/knife/chefignore_spec.rb b/spec/integration/knife/chefignore_spec.rb index eccd38d928..f111cd56e1 100644 --- a/spec/integration/knife/chefignore_spec.rb +++ b/spec/integration/knife/chefignore_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "chef/knife/list" require "chef/knife/show" diff --git a/spec/integration/knife/client_bulk_delete_spec.rb b/spec/integration/knife/client_bulk_delete_spec.rb index 5c0ff94867..b7733f638d 100644 --- a/spec/integration/knife/client_bulk_delete_spec.rb +++ b/spec/integration/knife/client_bulk_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/client_create_spec.rb b/spec/integration/knife/client_create_spec.rb index 2e48cde7ab..3898ff9d24 100644 --- a/spec/integration/knife/client_create_spec.rb +++ b/spec/integration/knife/client_create_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "openssl" diff --git a/spec/integration/knife/client_delete_spec.rb b/spec/integration/knife/client_delete_spec.rb index 76a3b9a686..057561eaea 100644 --- a/spec/integration/knife/client_delete_spec.rb +++ b/spec/integration/knife/client_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/client_key_create_spec.rb b/spec/integration/knife/client_key_create_spec.rb index b9838d6718..29b960111c 100644 --- a/spec/integration/knife/client_key_create_spec.rb +++ b/spec/integration/knife/client_key_create_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "openssl" diff --git a/spec/integration/knife/client_key_delete_spec.rb b/spec/integration/knife/client_key_delete_spec.rb index 2730ee8cae..8c15377986 100644 --- a/spec/integration/knife/client_key_delete_spec.rb +++ b/spec/integration/knife/client_key_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/client_key_list_spec.rb b/spec/integration/knife/client_key_list_spec.rb index 773445eca9..01e5b78585 100644 --- a/spec/integration/knife/client_key_list_spec.rb +++ b/spec/integration/knife/client_key_list_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "date" diff --git a/spec/integration/knife/client_key_show_spec.rb b/spec/integration/knife/client_key_show_spec.rb index ee17fc3e5a..05024d40b2 100644 --- a/spec/integration/knife/client_key_show_spec.rb +++ b/spec/integration/knife/client_key_show_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "date" diff --git a/spec/integration/knife/client_list_spec.rb b/spec/integration/knife/client_list_spec.rb index f7875b44af..7668b9e455 100644 --- a/spec/integration/knife/client_list_spec.rb +++ b/spec/integration/knife/client_list_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/client_show_spec.rb b/spec/integration/knife/client_show_spec.rb index 1520575e48..39a107e37f 100644 --- a/spec/integration/knife/client_show_spec.rb +++ b/spec/integration/knife/client_show_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/common_options_spec.rb b/spec/integration/knife/common_options_spec.rb index 468b7af8be..7796bf9923 100644 --- a/spec/integration/knife/common_options_spec.rb +++ b/spec/integration/knife/common_options_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "chef/knife/raw" diff --git a/spec/integration/knife/config_list_spec.rb b/spec/integration/knife/config_list_spec.rb index b05350ed87..5193608f36 100644 --- a/spec/integration/knife/config_list_spec.rb +++ b/spec/integration/knife/config_list_spec.rb @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/config_show_spec.rb b/spec/integration/knife/config_show_spec.rb index 9e6ff73aa1..e11d001df9 100644 --- a/spec/integration/knife/config_show_spec.rb +++ b/spec/integration/knife/config_show_spec.rb @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/config_use_spec.rb b/spec/integration/knife/config_use_spec.rb index 0431729b25..4a982bc0bd 100644 --- a/spec/integration/knife/config_use_spec.rb +++ b/spec/integration/knife/config_use_spec.rb @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/cookbook_api_ipv6_spec.rb b/spec/integration/knife/cookbook_api_ipv6_spec.rb index b65cdc697b..5d0ce0707f 100644 --- a/spec/integration/knife/cookbook_api_ipv6_spec.rb +++ b/spec/integration/knife/cookbook_api_ipv6_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "chef/mixin/shell_out" @@ -62,7 +62,7 @@ describe "Knife cookbook API integration with IPv6", :workstation, :not_supporte Dir.mktmpdir end - let(:chef_dir) { File.join(__dir__, "..", "..", "..", "bin") } + let(:chef_dir) { File.join(__dir__, "..", "..", "..", "knife", "bin") } let(:knife) { "ruby '#{chef_dir}/knife'" } let(:knife_config_flag) { "-c '#{path_to("config/knife.rb")}'" } @@ -102,7 +102,7 @@ describe "Knife cookbook API integration with IPv6", :workstation, :not_supporte end it "downloads the cookbook" do - shell_out!("knife cookbook download apache2 #{knife_config_flag} -d #{cache_path}", cwd: chef_dir) + shell_out!("#{knife} cookbook download apache2 #{knife_config_flag} -d #{cache_path}", cwd: chef_dir) expect(Dir["#{cache_path}/*"].map { |entry| File.basename(entry) }).to include("apache2-0.0.1") end end diff --git a/spec/integration/knife/cookbook_bulk_delete_spec.rb b/spec/integration/knife/cookbook_bulk_delete_spec.rb index 677a6aaa31..0e791f5a1e 100644 --- a/spec/integration/knife/cookbook_bulk_delete_spec.rb +++ b/spec/integration/knife/cookbook_bulk_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/cookbook_bulk_delete" diff --git a/spec/integration/knife/cookbook_download_spec.rb b/spec/integration/knife/cookbook_download_spec.rb index 1cc05c909a..589417126c 100644 --- a/spec/integration/knife/cookbook_download_spec.rb +++ b/spec/integration/knife/cookbook_download_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/cookbook_download" diff --git a/spec/integration/knife/cookbook_list_spec.rb b/spec/integration/knife/cookbook_list_spec.rb index c94df52272..e712ae3235 100644 --- a/spec/integration/knife/cookbook_list_spec.rb +++ b/spec/integration/knife/cookbook_list_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/cookbook_list" diff --git a/spec/integration/knife/cookbook_show_spec.rb b/spec/integration/knife/cookbook_show_spec.rb index 57701d4426..d8c2e38f64 100644 --- a/spec/integration/knife/cookbook_show_spec.rb +++ b/spec/integration/knife/cookbook_show_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/cookbook_show" diff --git a/spec/integration/knife/cookbook_upload_spec.rb b/spec/integration/knife/cookbook_upload_spec.rb index 7139f0accd..f42683b2a3 100644 --- a/spec/integration/knife/cookbook_upload_spec.rb +++ b/spec/integration/knife/cookbook_upload_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/cookbook_upload" diff --git a/spec/integration/knife/data_bag_create_spec.rb b/spec/integration/knife/data_bag_create_spec.rb index ca01a2d8ab..439d69507c 100644 --- a/spec/integration/knife/data_bag_create_spec.rb +++ b/spec/integration/knife/data_bag_create_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/data_bag_create" diff --git a/spec/integration/knife/data_bag_delete_spec.rb b/spec/integration/knife/data_bag_delete_spec.rb index c0a17779b9..a7fac7e2ee 100644 --- a/spec/integration/knife/data_bag_delete_spec.rb +++ b/spec/integration/knife/data_bag_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/data_bag_delete" diff --git a/spec/integration/knife/data_bag_edit_spec.rb b/spec/integration/knife/data_bag_edit_spec.rb index 1063b5d14f..1071df2a78 100644 --- a/spec/integration/knife/data_bag_edit_spec.rb +++ b/spec/integration/knife/data_bag_edit_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/data_bag_edit" diff --git a/spec/integration/knife/data_bag_from_file_spec.rb b/spec/integration/knife/data_bag_from_file_spec.rb index 93801226d0..bb8bd192f0 100644 --- a/spec/integration/knife/data_bag_from_file_spec.rb +++ b/spec/integration/knife/data_bag_from_file_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/data_bag_list_spec.rb b/spec/integration/knife/data_bag_list_spec.rb index 0216b90c5d..1e7734db64 100644 --- a/spec/integration/knife/data_bag_list_spec.rb +++ b/spec/integration/knife/data_bag_list_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/data_bag_list" diff --git a/spec/integration/knife/data_bag_show_spec.rb b/spec/integration/knife/data_bag_show_spec.rb index b332b1b114..91ebf605f1 100644 --- a/spec/integration/knife/data_bag_show_spec.rb +++ b/spec/integration/knife/data_bag_show_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/data_bag_show" diff --git a/spec/integration/knife/delete_spec.rb b/spec/integration/knife/delete_spec.rb index 851c492a66..e00949e7f4 100644 --- a/spec/integration/knife/delete_spec.rb +++ b/spec/integration/knife/delete_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "chef/knife/delete" require "chef/knife/list" diff --git a/spec/integration/knife/deps_spec.rb b/spec/integration/knife/deps_spec.rb index 77505e6332..9875277f14 100644 --- a/spec/integration/knife/deps_spec.rb +++ b/spec/integration/knife/deps_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/deps" diff --git a/spec/integration/knife/diff_spec.rb b/spec/integration/knife/diff_spec.rb index 41ae5ea519..c69573735a 100644 --- a/spec/integration/knife/diff_spec.rb +++ b/spec/integration/knife/diff_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "chef/knife/diff" diff --git a/spec/integration/knife/download_spec.rb b/spec/integration/knife/download_spec.rb index 7bdec7b356..29200d66f2 100644 --- a/spec/integration/knife/download_spec.rb +++ b/spec/integration/knife/download_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "chef/knife/download" require "chef/knife/diff" diff --git a/spec/integration/knife/environment_compare_spec.rb b/spec/integration/knife/environment_compare_spec.rb index 7a623adf4c..a8d207466a 100644 --- a/spec/integration/knife/environment_compare_spec.rb +++ b/spec/integration/knife/environment_compare_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/environment_create_spec.rb b/spec/integration/knife/environment_create_spec.rb index 66ba9ed6e6..496828073d 100644 --- a/spec/integration/knife/environment_create_spec.rb +++ b/spec/integration/knife/environment_create_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/environment_delete_spec.rb b/spec/integration/knife/environment_delete_spec.rb index f55a1c96bd..93427aaf2f 100644 --- a/spec/integration/knife/environment_delete_spec.rb +++ b/spec/integration/knife/environment_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/environment_from_file_spec.rb b/spec/integration/knife/environment_from_file_spec.rb index f9d35f4d47..e5ba056bb7 100644 --- a/spec/integration/knife/environment_from_file_spec.rb +++ b/spec/integration/knife/environment_from_file_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/environment_list_spec.rb b/spec/integration/knife/environment_list_spec.rb index dba685a82e..f74b2b6360 100644 --- a/spec/integration/knife/environment_list_spec.rb +++ b/spec/integration/knife/environment_list_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/environment_show_spec.rb b/spec/integration/knife/environment_show_spec.rb index de6ad1efd4..b961e85734 100644 --- a/spec/integration/knife/environment_show_spec.rb +++ b/spec/integration/knife/environment_show_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/list_spec.rb b/spec/integration/knife/list_spec.rb index 4c711f3306..8228ba6056 100644 --- a/spec/integration/knife/list_spec.rb +++ b/spec/integration/knife/list_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/list" diff --git a/spec/integration/knife/node_bulk_delete_spec.rb b/spec/integration/knife/node_bulk_delete_spec.rb index dcaa71ef58..8784b5ea8a 100644 --- a/spec/integration/knife/node_bulk_delete_spec.rb +++ b/spec/integration/knife/node_bulk_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/node_create_spec.rb b/spec/integration/knife/node_create_spec.rb index e8f6d71694..d3debb8f00 100644 --- a/spec/integration/knife/node_create_spec.rb +++ b/spec/integration/knife/node_create_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "openssl" diff --git a/spec/integration/knife/node_delete_spec.rb b/spec/integration/knife/node_delete_spec.rb index c743d6e03f..3cece6ebaf 100644 --- a/spec/integration/knife/node_delete_spec.rb +++ b/spec/integration/knife/node_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/node_environment_set_spec.rb b/spec/integration/knife/node_environment_set_spec.rb index 16a86dbc30..51b288fe39 100644 --- a/spec/integration/knife/node_environment_set_spec.rb +++ b/spec/integration/knife/node_environment_set_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/node_from_file_spec.rb b/spec/integration/knife/node_from_file_spec.rb index 6f7e0780f0..5dcaaaa463 100644 --- a/spec/integration/knife/node_from_file_spec.rb +++ b/spec/integration/knife/node_from_file_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/node_list_spec.rb b/spec/integration/knife/node_list_spec.rb index 8d3bc29a5a..65c201be3f 100644 --- a/spec/integration/knife/node_list_spec.rb +++ b/spec/integration/knife/node_list_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/node_run_list_add_spec.rb b/spec/integration/knife/node_run_list_add_spec.rb index f13e584526..72b5328b17 100644 --- a/spec/integration/knife/node_run_list_add_spec.rb +++ b/spec/integration/knife/node_run_list_add_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/node_run_list_remove_spec.rb b/spec/integration/knife/node_run_list_remove_spec.rb index 55f224b5ac..19aeb81806 100644 --- a/spec/integration/knife/node_run_list_remove_spec.rb +++ b/spec/integration/knife/node_run_list_remove_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/node_run_list_set_spec.rb b/spec/integration/knife/node_run_list_set_spec.rb index e642afc1ce..d83e74dd04 100644 --- a/spec/integration/knife/node_run_list_set_spec.rb +++ b/spec/integration/knife/node_run_list_set_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/node_show_spec.rb b/spec/integration/knife/node_show_spec.rb index cf3f166699..be63011ef8 100644 --- a/spec/integration/knife/node_show_spec.rb +++ b/spec/integration/knife/node_show_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/raw_spec.rb b/spec/integration/knife/raw_spec.rb index ba26def473..8e7e913b02 100644 --- a/spec/integration/knife/raw_spec.rb +++ b/spec/integration/knife/raw_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/raw" diff --git a/spec/integration/knife/redirection_spec.rb b/spec/integration/knife/redirection_spec.rb index 34d5fe6efc..eea5556cff 100644 --- a/spec/integration/knife/redirection_spec.rb +++ b/spec/integration/knife/redirection_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "tiny_server" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/role_bulk_delete_spec.rb b/spec/integration/knife/role_bulk_delete_spec.rb index 6810cebc91..76745d9b6a 100644 --- a/spec/integration/knife/role_bulk_delete_spec.rb +++ b/spec/integration/knife/role_bulk_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/role_create_spec.rb b/spec/integration/knife/role_create_spec.rb index 80ef1d9a9f..03f59d4b99 100644 --- a/spec/integration/knife/role_create_spec.rb +++ b/spec/integration/knife/role_create_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/role_delete_spec.rb b/spec/integration/knife/role_delete_spec.rb index c4c6498c51..22b36e5572 100644 --- a/spec/integration/knife/role_delete_spec.rb +++ b/spec/integration/knife/role_delete_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/role_from_file_spec.rb b/spec/integration/knife/role_from_file_spec.rb index 4a2912935c..ae296122ce 100644 --- a/spec/integration/knife/role_from_file_spec.rb +++ b/spec/integration/knife/role_from_file_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/role_list_spec.rb b/spec/integration/knife/role_list_spec.rb index 9e4b983698..39aa28783f 100644 --- a/spec/integration/knife/role_list_spec.rb +++ b/spec/integration/knife/role_list_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/role_show_spec.rb b/spec/integration/knife/role_show_spec.rb index dfa989bf69..a4ecea1d61 100644 --- a/spec/integration/knife/role_show_spec.rb +++ b/spec/integration/knife/role_show_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/search_node_spec.rb b/spec/integration/knife/search_node_spec.rb index 8eaa30f7fa..9e7935b83c 100644 --- a/spec/integration/knife/search_node_spec.rb +++ b/spec/integration/knife/search_node_spec.rb @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" diff --git a/spec/integration/knife/show_spec.rb b/spec/integration/knife/show_spec.rb index 4bee492e7b..6913494916 100644 --- a/spec/integration/knife/show_spec.rb +++ b/spec/integration/knife/show_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "support/shared/context/config" require "chef/knife/show" diff --git a/spec/integration/knife/upload_spec.rb b/spec/integration/knife/upload_spec.rb index 37cfcefa32..e4bb44ad7e 100644 --- a/spec/integration/knife/upload_spec.rb +++ b/spec/integration/knife/upload_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "support/shared/integration/integration_helper" require "chef/knife/upload" require "chef/knife/diff" diff --git a/spec/knife_spec_helper.rb b/spec/knife_spec_helper.rb new file mode 100644 index 0000000000..fd40f4002e --- /dev/null +++ b/spec/knife_spec_helper.rb @@ -0,0 +1,245 @@ +# +# Author:: Adam Jacob () +# 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. + +# If you need to add anything in here, don't. +# Add it to one of the files in spec/support + +# Abuse ruby's constant lookup to avoid undefined constant errors + +$LOAD_PATH.unshift File.expand_path("..", __dir__) +$LOAD_PATH.unshift File.expand_path("../../chef-config/lib", __dir__) +$LOAD_PATH.unshift File.expand_path("../../chef-utils/lib", __dir__) + +require "rubygems" +require "rspec/mocks" +require "rexml/document" +require "webmock/rspec" + +require "chef/knife" + +# cwd is knife/ +Dir["lib/chef/knife/**/*.rb"] + .map { |f| f.gsub("lib/", "") } + .map { |f| f.gsub(/\.rb$/, "") } + .each { |f| require f } + +require "chef/resource_resolver" +require "chef/provider_resolver" + +require "chef/mixins" +require "chef/dsl" + +require "chef/shell" +require "chef/util/file_edit" + +require "chef/config" + +require "chef/chef_fs/file_system_cache" + +require "chef/api_client_v1" + +require "chef/mixin/versioned_api" +require "chef/server_api_versions" + +# MPTD relevant for knife? +if ENV["CHEF_FIPS"] == "1" + Chef::Config.init_openssl +end + +# If you want to load anything into the testing environment +# without versioning it, add it to spec/support/local_gems.rb +require "spec/support/local_gems" if File.exist?(File.join(File.dirname(__FILE__), "support", "local_gems.rb")) + +# Explicitly require spec helpers that need to load first +require "spec/support/platform_helpers" +require "spec/support/shared/unit/mock_shellout" +require "spec/support/recipe_dsl_helper" + +# MPTD - I had to add each of these manually, but I'm not clear why they were +# _not_ required before splitting the gem. +require "spec/support/key_helpers" +require "spec/support/shared/unit/knife_shared" +require "spec/support/shared/functional/knife" +require "spec/support/shared/integration/knife_support" +require "spec/support/shared/matchers/exit_with_code" +require "spec/support/shared/matchers/match_environment_variable" + +# Autoloads support files +# Excludes support/platforms by default +# Do not change the gsub. +Dir["spec/support/**/*.rb"] + .reject { |f| f =~ %r{^spec/support/platforms} } + .reject { |f| f =~ %r{^spec/support/pedant} } + .map { |f| f.gsub(/.rb$/, "") } + .map { |f| f.gsub(%r{spec/}, "") } + .each { |f| require f } + +OHAI_SYSTEM = Ohai::System.new +OHAI_SYSTEM.all_plugins(["platform", "hostname", "languages/powershell", "uptime"]) + +test_node = Chef::Node.new +test_node.automatic["os"] = (OHAI_SYSTEM["os"] || "unknown_os").dup.freeze +test_node.automatic["platform_family"] = (OHAI_SYSTEM["platform_family"] || "unknown_platform_family").dup.freeze +test_node.automatic["platform"] = (OHAI_SYSTEM["platform"] || "unknown_platform").dup.freeze +test_node.automatic["platform_version"] = (OHAI_SYSTEM["platform_version"] || "unknown_platform_version").dup.freeze +TEST_NODE = test_node.freeze +TEST_OS = TEST_NODE["os"] +TEST_PLATFORM = TEST_NODE["platform"] +TEST_PLATFORM_VERSION = TEST_NODE["platform_version"] +TEST_PLATFORM_FAMILY = TEST_NODE["platform_family"] + +provider_priority_map ||= nil +resource_priority_map ||= nil +provider_handler_map ||= nil +resource_handler_map ||= nil + +class UnexpectedSystemExit < RuntimeError + def self.from(system_exit) + new(system_exit.message).tap { |e| e.set_backtrace(system_exit.backtrace) } + end +end + +RSpec.configure do |config| + config.include(RSpec::Matchers) + config.include(MockShellout::RSpec) + config.filter_run focus: true + config.filter_run_excluding external: true + config.raise_on_warning = true + + # Explicitly disable :should syntax + # And set max_formatted_output_length to nil to prevent RSpec from doing truncation. + config.expect_with :rspec do |c| + c.syntax = :expect + c.max_formatted_output_length = nil + end + config.mock_with :rspec do |c| + c.syntax = :expect + c.allow_message_expectations_on_nil = false + end + + # TODO - which if any of these filters apply to knife tests? + # + # Only run these tests on platforms that are also chef workstations + config.filter_run_excluding :workstation if solaris? || aix? + + # Tests that randomly fail, but may have value. + config.filter_run_excluding volatile: true + config.filter_run_excluding volatile_on_solaris: true if solaris? + config.filter_run_excluding volatile_from_verify: false + + config.filter_run_excluding skip_buildkite: true if ENV["BUILDKITE"] + + config.filter_run_excluding windows_only: true unless windows? + config.filter_run_excluding unix_only: true unless unix? + + # check for particular binaries we need + + running_platform_arch = `uname -m`.strip unless windows? + + config.filter_run_excluding arch: lambda { |target_arch| + running_platform_arch != target_arch + } + + config.run_all_when_everything_filtered = true + + config.before(:each) do + # it'd be nice to run this with connections blocked or only to localhost, but we do make lots + # of real connections, so cannot. we reset it to allow connections every time to avoid + # tests setting connections to be disabled and that state leaking into other tests. + WebMock.allow_net_connect! + Chef.reset! + Chef::ChefFS::FileSystemCache.instance.reset! + Chef::Config.reset + Chef::Log.setup! + Chef::ServerAPIVersions.instance.reset! + Chef::Config[:log_level] = :fatal + Chef::Log.level(Chef::Config[:log_level]) + + # By default, treat deprecation warnings as errors in tests. + # and set environment variable so the setting persists in child processes + Chef::Config.treat_deprecation_warnings_as_errors(true) + ENV["CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS"] = "1" + end + + # This bit of jankiness guards against specs which accidentally drop privs when running as + # root -- which are nearly impossible to debug and so we bail out very hard if this + # condition ever happens. If a spec stubs Process.[e]uid this can throw a false positive + # which the spec must work around by unmocking Process.[e]uid to and_call_original in its + # after block. + # Should not be a problem with knife which does not escalate local privs, but + # it seems wise to continue to guard against. + if Process.euid == 0 && Process.uid == 0 + config.after(:each) do + if Process.uid != 0 + RSpec.configure { |c| c.fail_fast = true } + raise "rspec was invoked as root, but the last test dropped real uid to #{Process.uid}" + end + if Process.euid != 0 + RSpec.configure { |c| c.fail_fast = true } + raise "rspec was invoked as root, but the last test dropped effective uid to #{Process.euid}" + end + end + end + + # raise if anyone commits any test to CI with :focus set on it + if ENV["CI"] + config.before(:example, :focus) do + raise "This example was committed with `:focus` and should not have been" + end + end + + config.before(:suite) do + ARGV.clear + end + + # Protect Rspec from accidental exit(0) causing rspec to terminate without error + config.around(:example) do |ex| + ex.run + rescue SystemExit => e + raise UnexpectedSystemExit.from(e) + + end +end + +require "webrick/utils" +# Webrick uses a centralized/synchronized timeout manager. It works by +# starting a thread to check for timeouts on an interval. The timeout +# checker thread cannot be stopped or canceled in any easy way, and it +# makes calls to Time.new, which fail when rspec is in the process of +# creating a method stub for that method. Since our tests don't rely on +# any timeout behavior enforced by webrick, disable the timeout manager +# via a monkey patch. +# +# Hopefully this fails loudly if the webrick code should change. As of this +# writing, the relevant code is in webrick/utils, which can be located on +# your system with: +# +# $ gem which webrick/utils +module WEBrick + module Utils + class TimeoutHandler + def initialize; end + + def register(*args); end + + def cancel(*args); end + end + end +end + +# Enough stuff needs json serialization that I'm just adding it here for equality asserts +require "chef/json_compat" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 83cec749a7..2924b2abdb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -35,12 +35,6 @@ require "rexml/document" require "webmock/rspec" require "chef" -require "chef/knife" - -Dir["lib/chef/knife/**/*.rb"] - .map { |f| f.gsub("lib/", "") } - .map { |f| f.gsub(/\.rb$/, "") } - .each { |f| require f } require "chef/resource_resolver" require "chef/provider_resolver" @@ -82,6 +76,7 @@ require "spec/support/recipe_dsl_helper" Dir["spec/support/**/*.rb"] .reject { |f| f =~ %r{^spec/support/platforms} } .reject { |f| f =~ %r{^spec/support/pedant} } + .reject { |f| f =~ %r{^spec/support/shared/integration/knife_support} } .map { |f| f.gsub(/.rb$/, "") } .map { |f| f.gsub(%r{spec/}, "") } .each { |f| require f } @@ -232,6 +227,14 @@ RSpec.configure do |config| Chef.reset! + # Hack warning: + # + # Something across gem_installer_spec and mixlib_cli specs are polluting gem state so that the 'unmockening' test in rubygems_spec fails. + # This works around that until we can understand root cause. + # + # To explore the minimal test case around that and see more detailed notes, see branch `mp/broken-gems` + Gem.clear_paths + Chef::ChefFS::FileSystemCache.instance.reset! Chef::Config.reset diff --git a/spec/support/lib/chef/resource/with_state.rb b/spec/support/lib/chef/resource/with_state.rb index 98e4033e01..1977fb3f6a 100644 --- a/spec/support/lib/chef/resource/with_state.rb +++ b/spec/support/lib/chef/resource/with_state.rb @@ -16,7 +16,6 @@ # limitations under the License. # -require "chef/knife" require "chef/json_compat" class Chef diff --git a/spec/support/lib/chef/resource/zen_follower.rb b/spec/support/lib/chef/resource/zen_follower.rb index 44de913f8b..c7d01dcebb 100644 --- a/spec/support/lib/chef/resource/zen_follower.rb +++ b/spec/support/lib/chef/resource/zen_follower.rb @@ -15,7 +15,6 @@ # limitations under the License. # -require "chef/knife" require "chef/json_compat" class Chef diff --git a/spec/support/lib/chef/resource/zen_master.rb b/spec/support/lib/chef/resource/zen_master.rb index ba2f950bed..0e0853d0b3 100644 --- a/spec/support/lib/chef/resource/zen_master.rb +++ b/spec/support/lib/chef/resource/zen_master.rb @@ -16,7 +16,6 @@ # limitations under the License. # -require "chef/knife" require "chef/json_compat" class Chef diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index 83f2fcf172..4cc9943709 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -2,6 +2,8 @@ require "fcntl" require "chef/mixin/shell_out" require "ohai/mixin/http_helper" require "ohai/mixin/gce_metadata" +# MPTD - why has nobody had to do this before now? +require "spec/support/chef_helpers" class ShellHelpers extend Chef::Mixin::ShellOut diff --git a/spec/support/shared/integration/integration_helper.rb b/spec/support/shared/integration/integration_helper.rb index 41f2b46995..c42a04004a 100644 --- a/spec/support/shared/integration/integration_helper.rb +++ b/spec/support/shared/integration/integration_helper.rb @@ -22,7 +22,6 @@ require "fileutils" require "chef/config" require "chef/json_compat" require "chef/server_api" -require "support/shared/integration/knife_support" require "cheffish/rspec/chef_run_support" module Cheffish diff --git a/spec/unit/application/knife_spec.rb b/spec/unit/application/knife_spec.rb index bce6b19366..ad705ab1e5 100644 --- a/spec/unit/application/knife_spec.rb +++ b/spec/unit/application/knife_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" require "#{CHEF_SPEC_DATA}/knife_subcommand/test_yourself" describe Chef::Application::Knife do diff --git a/spec/unit/cookbook_site_streaming_uploader_spec.rb b/spec/unit/cookbook_site_streaming_uploader_spec.rb deleted file mode 100644 index af714094d0..0000000000 --- a/spec/unit/cookbook_site_streaming_uploader_spec.rb +++ /dev/null @@ -1,198 +0,0 @@ -# -# Author:: Xabier de Zuazo (xabier@onddo.com) -# Copyright:: Copyright 2013-2016, Onddo Labs, SL. -# 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 "spec_helper" - -require "chef/cookbook_site_streaming_uploader" - -class FakeTempfile - def initialize(basename) - @basename = basename - end - - def close; end - - def path - "#{@basename}.ZZZ" - end - -end - -describe Chef::CookbookSiteStreamingUploader do - - describe "create_build_dir" do - - before(:each) do - @cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) - @loader = Chef::CookbookLoader.new(@cookbook_repo) - @loader.load_cookbooks - allow(File).to receive(:unlink) - end - - it "should create the cookbook tmp dir" do - cookbook = @loader[:openldap] - files_count = Dir.glob(File.join(@cookbook_repo, cookbook.name.to_s, "**", "*"), File::FNM_DOTMATCH).count { |file| File.file?(file) } - - expect(Tempfile).to receive(:new).with("chef-#{cookbook.name}-build").and_return(FakeTempfile.new("chef-#{cookbook.name}-build")) - expect(FileUtils).to receive(:mkdir_p).exactly(files_count + 1).times - expect(FileUtils).to receive(:cp).exactly(files_count).times - Chef::CookbookSiteStreamingUploader.create_build_dir(cookbook) - end - - end # create_build_dir - - describe "make_request" do - - before(:each) do - @uri = "http://cookbooks.dummy.com/api/v1/cookbooks" - @secret_filename = File.join(CHEF_SPEC_DATA, "ssl/private_key.pem") - @rsa_key = File.read(@secret_filename) - response = Net::HTTPResponse.new("1.0", "200", "OK") - allow_any_instance_of(Net::HTTP).to receive(:request).and_return(response) - end - - it "should send an http request" do - expect_any_instance_of(Net::HTTP).to receive(:request) - Chef::CookbookSiteStreamingUploader.make_request(:post, @uri, "bill", @secret_filename) - end - - it "should read the private key file" do - expect(File).to receive(:read).with(@secret_filename).and_return(@rsa_key) - Chef::CookbookSiteStreamingUploader.make_request(:post, @uri, "bill", @secret_filename) - end - - it "should add the authentication signed header" do - expect_any_instance_of(Mixlib::Authentication::SigningObject).to receive(:sign).and_return({}) - Chef::CookbookSiteStreamingUploader.make_request(:post, @uri, "bill", @secret_filename) - end - - it "should be able to send post requests" do - post = Net::HTTP::Post.new(@uri, {}) - - expect(Net::HTTP::Post).to receive(:new).once.and_return(post) - expect(Net::HTTP::Put).not_to receive(:new) - expect(Net::HTTP::Get).not_to receive(:new) - Chef::CookbookSiteStreamingUploader.make_request(:post, @uri, "bill", @secret_filename) - end - - it "should be able to send put requests" do - put = Net::HTTP::Put.new(@uri, {}) - - expect(Net::HTTP::Post).not_to receive(:new) - expect(Net::HTTP::Put).to receive(:new).once.and_return(put) - expect(Net::HTTP::Get).not_to receive(:new) - Chef::CookbookSiteStreamingUploader.make_request(:put, @uri, "bill", @secret_filename) - end - - it "should be able to receive files to attach as argument" do - Chef::CookbookSiteStreamingUploader.make_request(:put, @uri, "bill", @secret_filename, { - myfile: File.new(File.join(CHEF_SPEC_DATA, "config.rb")), # a dummy file - }) - end - - it "should be able to receive strings to attach as argument" do - Chef::CookbookSiteStreamingUploader.make_request(:put, @uri, "bill", @secret_filename, { - mystring: "Lorem ipsum", - }) - end - - it "should be able to receive strings and files as argument at the same time" do - Chef::CookbookSiteStreamingUploader.make_request(:put, @uri, "bill", @secret_filename, { - myfile1: File.new(File.join(CHEF_SPEC_DATA, "config.rb")), - mystring1: "Lorem ipsum", - myfile2: File.new(File.join(CHEF_SPEC_DATA, "config.rb")), - mystring2: "Dummy text", - }) - end - - end # make_request - - describe "StreamPart" do - before(:each) do - @file = File.new(File.join(CHEF_SPEC_DATA, "config.rb")) - @stream_part = Chef::CookbookSiteStreamingUploader::StreamPart.new(@file, File.size(@file)) - end - - it "should create a StreamPart" do - expect(@stream_part).to be_instance_of(Chef::CookbookSiteStreamingUploader::StreamPart) - end - - it "should expose its size" do - expect(@stream_part.size).to eql(File.size(@file)) - end - - it "should read with offset and how_much" do - content = @file.read(4) - @file.rewind - expect(@stream_part.read(0, 4)).to eql(content) - end - - end # StreamPart - - describe "StringPart" do - before(:each) do - @str = "What a boring string" - @string_part = Chef::CookbookSiteStreamingUploader::StringPart.new(@str) - end - - it "should create a StringPart" do - expect(@string_part).to be_instance_of(Chef::CookbookSiteStreamingUploader::StringPart) - end - - it "should expose its size" do - expect(@string_part.size).to eql(@str.size) - end - - it "should read with offset and how_much" do - expect(@string_part.read(2, 4)).to eql(@str[2, 4]) - end - - end # StringPart - - describe "MultipartStream" do - before(:each) do - @string1 = "stream1" - @string2 = "stream2" - @stream1 = Chef::CookbookSiteStreamingUploader::StringPart.new(@string1) - @stream2 = Chef::CookbookSiteStreamingUploader::StringPart.new(@string2) - @parts = [ @stream1, @stream2 ] - - @multipart_stream = Chef::CookbookSiteStreamingUploader::MultipartStream.new(@parts) - end - - it "should create a MultipartStream" do - expect(@multipart_stream).to be_instance_of(Chef::CookbookSiteStreamingUploader::MultipartStream) - end - - it "should expose its size" do - expect(@multipart_stream.size).to eql(@stream1.size + @stream2.size) - end - - it "should read with how_much" do - expect(@multipart_stream.read(10)).to eql("#{@string1}#{@string2}"[0, 10]) - end - - it "should read receiving destination buffer as second argument (CHEF-4456: Ruby 2 compat)" do - dst_buf = "" - @multipart_stream.read(10, dst_buf) - expect(dst_buf).to eql("#{@string1}#{@string2}"[0, 10]) - end - - end # MultipartStream - -end diff --git a/spec/unit/knife/bootstrap/chef_vault_handler_spec.rb b/spec/unit/knife/bootstrap/chef_vault_handler_spec.rb index 4d36208be0..a4d007611e 100644 --- a/spec/unit/knife/bootstrap/chef_vault_handler_spec.rb +++ b/spec/unit/knife/bootstrap/chef_vault_handler_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::Bootstrap::ChefVaultHandler do diff --git a/spec/unit/knife/bootstrap/client_builder_spec.rb b/spec/unit/knife/bootstrap/client_builder_spec.rb index 10edd13882..cf6999b093 100644 --- a/spec/unit/knife/bootstrap/client_builder_spec.rb +++ b/spec/unit/knife/bootstrap/client_builder_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::Bootstrap::ClientBuilder do diff --git a/spec/unit/knife/bootstrap/train_connector_spec.rb b/spec/unit/knife/bootstrap/train_connector_spec.rb index 4c384100fa..0a1091fa8d 100644 --- a/spec/unit/knife/bootstrap/train_connector_spec.rb +++ b/spec/unit/knife/bootstrap/train_connector_spec.rb @@ -15,7 +15,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "ostruct" require "chef/knife/bootstrap/train_connector" diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb index a3dd714094..f2f2f48f98 100644 --- a/spec/unit/knife/bootstrap_spec.rb +++ b/spec/unit/knife/bootstrap_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" Chef::Knife::Bootstrap.load_deps @@ -185,7 +185,7 @@ describe Chef::Knife::Bootstrap do context "when :bootstrap_template config is set to a template name" do let(:bootstrap_template) { "example" } - let(:builtin_template_path) { File.expand_path(File.join(__dir__, "../../../lib/chef/knife/bootstrap/templates", "example.erb")) } + let(:builtin_template_path) { File.expand_path(File.join(__dir__, "../../../knife/lib/chef/knife/bootstrap/templates", "example.erb")) } let(:chef_config_dir_template_path) { "/knife/chef/config/bootstrap/example.erb" } diff --git a/spec/unit/knife/client_bulk_delete_spec.rb b/spec/unit/knife/client_bulk_delete_spec.rb index 435eb888aa..86d69ff4d6 100644 --- a/spec/unit/knife/client_bulk_delete_spec.rb +++ b/spec/unit/knife/client_bulk_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::ClientBulkDelete do let(:stdout_io) { StringIO.new } diff --git a/spec/unit/knife/client_create_spec.rb b/spec/unit/knife/client_create_spec.rb index d8b67de101..48a7e71df5 100644 --- a/spec/unit/knife/client_create_spec.rb +++ b/spec/unit/knife/client_create_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" Chef::Knife::ClientCreate.load_deps diff --git a/spec/unit/knife/client_delete_spec.rb b/spec/unit/knife/client_delete_spec.rb index 41a83b05e4..ec20878ade 100644 --- a/spec/unit/knife/client_delete_spec.rb +++ b/spec/unit/knife/client_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::ClientDelete do before(:each) do diff --git a/spec/unit/knife/client_edit_spec.rb b/spec/unit/knife/client_edit_spec.rb index e7c9030883..03ac450b3e 100644 --- a/spec/unit/knife/client_edit_spec.rb +++ b/spec/unit/knife/client_edit_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/api_client_v1" describe Chef::Knife::ClientEdit do diff --git a/spec/unit/knife/client_list_spec.rb b/spec/unit/knife/client_list_spec.rb index d1b379a787..b6a205e6b1 100644 --- a/spec/unit/knife/client_list_spec.rb +++ b/spec/unit/knife/client_list_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::ClientList do before(:each) do diff --git a/spec/unit/knife/client_reregister_spec.rb b/spec/unit/knife/client_reregister_spec.rb index 6b6519d44f..f1217edee4 100644 --- a/spec/unit/knife/client_reregister_spec.rb +++ b/spec/unit/knife/client_reregister_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::ClientReregister do before(:each) do diff --git a/spec/unit/knife/client_show_spec.rb b/spec/unit/knife/client_show_spec.rb index 47b4b6ccb0..39928a6289 100644 --- a/spec/unit/knife/client_show_spec.rb +++ b/spec/unit/knife/client_show_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::ClientShow do before(:each) do diff --git a/spec/unit/knife/configure_client_spec.rb b/spec/unit/knife/configure_client_spec.rb index b104718c89..f88ffb31ed 100644 --- a/spec/unit/knife/configure_client_spec.rb +++ b/spec/unit/knife/configure_client_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::ConfigureClient do before do diff --git a/spec/unit/knife/configure_spec.rb b/spec/unit/knife/configure_spec.rb index 7d6c840d1f..f9d1bea8fd 100644 --- a/spec/unit/knife/configure_spec.rb +++ b/spec/unit/knife/configure_spec.rb @@ -1,4 +1,4 @@ -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::Configure do before do diff --git a/spec/unit/knife/cookbook_bulk_delete_spec.rb b/spec/unit/knife/cookbook_bulk_delete_spec.rb index 3527d39bd8..52f9c1eeb9 100644 --- a/spec/unit/knife/cookbook_bulk_delete_spec.rb +++ b/spec/unit/knife/cookbook_bulk_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::CookbookBulkDelete do before(:each) do diff --git a/spec/unit/knife/cookbook_delete_spec.rb b/spec/unit/knife/cookbook_delete_spec.rb index f2aa7e1be0..b05006f2d3 100644 --- a/spec/unit/knife/cookbook_delete_spec.rb +++ b/spec/unit/knife/cookbook_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::CookbookDelete do before(:each) do diff --git a/spec/unit/knife/cookbook_download_spec.rb b/spec/unit/knife/cookbook_download_spec.rb index c8903dea5b..b3dbc81205 100644 --- a/spec/unit/knife/cookbook_download_spec.rb +++ b/spec/unit/knife/cookbook_download_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::CookbookDownload do before(:each) do diff --git a/spec/unit/knife/cookbook_list_spec.rb b/spec/unit/knife/cookbook_list_spec.rb index 4cf806c6f0..42c3ef1bfd 100644 --- a/spec/unit/knife/cookbook_list_spec.rb +++ b/spec/unit/knife/cookbook_list_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::CookbookList do before do diff --git a/spec/unit/knife/cookbook_metadata_from_file_spec.rb b/spec/unit/knife/cookbook_metadata_from_file_spec.rb index f9bbffae2d..c595aef96f 100644 --- a/spec/unit/knife/cookbook_metadata_from_file_spec.rb +++ b/spec/unit/knife/cookbook_metadata_from_file_spec.rb @@ -18,7 +18,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::CookbookMetadataFromFile do before(:each) do diff --git a/spec/unit/knife/cookbook_metadata_spec.rb b/spec/unit/knife/cookbook_metadata_spec.rb index 732cf78421..1a274cc6f4 100644 --- a/spec/unit/knife/cookbook_metadata_spec.rb +++ b/spec/unit/knife/cookbook_metadata_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::CookbookMetadata do let(:knife) do diff --git a/spec/unit/knife/cookbook_show_spec.rb b/spec/unit/knife/cookbook_show_spec.rb index defc243de3..94e080cb15 100644 --- a/spec/unit/knife/cookbook_show_spec.rb +++ b/spec/unit/knife/cookbook_show_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::CookbookShow do before do diff --git a/spec/unit/knife/cookbook_upload_spec.rb b/spec/unit/knife/cookbook_upload_spec.rb index dbed8b8a67..0893f6a6b3 100644 --- a/spec/unit/knife/cookbook_upload_spec.rb +++ b/spec/unit/knife/cookbook_upload_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/cookbook_uploader" require "timeout" diff --git a/spec/unit/knife/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb index a55047a739..79fddc8184 100644 --- a/spec/unit/knife/core/bootstrap_context_spec.rb +++ b/spec/unit/knife/core/bootstrap_context_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/core/bootstrap_context" describe Chef::Knife::Core::BootstrapContext do diff --git a/spec/unit/knife/core/cookbook_scm_repo_spec.rb b/spec/unit/knife/core/cookbook_scm_repo_spec.rb index 316cbdfaa6..68a155bbbe 100644 --- a/spec/unit/knife/core/cookbook_scm_repo_spec.rb +++ b/spec/unit/knife/core/cookbook_scm_repo_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/core/cookbook_scm_repo" describe Chef::Knife::CookbookSCMRepo do diff --git a/spec/unit/knife/core/cookbook_site_streaming_uploader_spec.rb b/spec/unit/knife/core/cookbook_site_streaming_uploader_spec.rb new file mode 100644 index 0000000000..f40626990a --- /dev/null +++ b/spec/unit/knife/core/cookbook_site_streaming_uploader_spec.rb @@ -0,0 +1,198 @@ +# +# Author:: Xabier de Zuazo (xabier@onddo.com) +# Copyright:: Copyright 2013-2016, Onddo Labs, SL. +# 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 "knife_spec_helper" + +require "chef/knife/core/cookbook_site_streaming_uploader" + +class FakeTempfile + def initialize(basename) + @basename = basename + end + + def close; end + + def path + "#{@basename}.ZZZ" + end + +end + +describe Chef::Knife::Core::CookbookSiteStreamingUploader do + + let(:subject) { Chef::Knife::Core::CookbookSiteStreamingUploader } + describe "create_build_dir" do + before(:each) do + @cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) + @loader = Chef::CookbookLoader.new(@cookbook_repo) + @loader.load_cookbooks + allow(File).to receive(:unlink) + end + + it "should create the cookbook tmp dir" do + cookbook = @loader[:openldap] + files_count = Dir.glob(File.join(@cookbook_repo, cookbook.name.to_s, "**", "*"), File::FNM_DOTMATCH).count { |file| File.file?(file) } + + expect(Tempfile).to receive(:new).with("chef-#{cookbook.name}-build").and_return(FakeTempfile.new("chef-#{cookbook.name}-build")) + expect(FileUtils).to receive(:mkdir_p).exactly(files_count + 1).times + expect(FileUtils).to receive(:cp).exactly(files_count).times + subject.create_build_dir(cookbook) + end + + end # create_build_dir + + describe "make_request" do + + before(:each) do + @uri = "http://cookbooks.dummy.com/api/v1/cookbooks" + @secret_filename = File.join(CHEF_SPEC_DATA, "ssl/private_key.pem") + @rsa_key = File.read(@secret_filename) + response = Net::HTTPResponse.new("1.0", "200", "OK") + allow_any_instance_of(Net::HTTP).to receive(:request).and_return(response) + end + + it "should send an http request" do + expect_any_instance_of(Net::HTTP).to receive(:request) + subject.make_request(:post, @uri, "bill", @secret_filename) + end + + it "should read the private key file" do + expect(File).to receive(:read).with(@secret_filename).and_return(@rsa_key) + subject.make_request(:post, @uri, "bill", @secret_filename) + end + + it "should add the authentication signed header" do + expect_any_instance_of(Mixlib::Authentication::SigningObject).to receive(:sign).and_return({}) + subject.make_request(:post, @uri, "bill", @secret_filename) + end + + it "should be able to send post requests" do + post = Net::HTTP::Post.new(@uri, {}) + + expect(Net::HTTP::Post).to receive(:new).once.and_return(post) + expect(Net::HTTP::Put).not_to receive(:new) + expect(Net::HTTP::Get).not_to receive(:new) + subject.make_request(:post, @uri, "bill", @secret_filename) + end + + it "should be able to send put requests" do + put = Net::HTTP::Put.new(@uri, {}) + + expect(Net::HTTP::Post).not_to receive(:new) + expect(Net::HTTP::Put).to receive(:new).once.and_return(put) + expect(Net::HTTP::Get).not_to receive(:new) + subject.make_request(:put, @uri, "bill", @secret_filename) + end + + it "should be able to receive files to attach as argument" do + subject.make_request(:put, @uri, "bill", @secret_filename, { + myfile: File.new(File.join(CHEF_SPEC_DATA, "config.rb")), # a dummy file + }) + end + + it "should be able to receive strings to attach as argument" do + subject.make_request(:put, @uri, "bill", @secret_filename, { + mystring: "Lorem ipsum", + }) + end + + it "should be able to receive strings and files as argument at the same time" do + subject.make_request(:put, @uri, "bill", @secret_filename, { + myfile1: File.new(File.join(CHEF_SPEC_DATA, "config.rb")), + mystring1: "Lorem ipsum", + myfile2: File.new(File.join(CHEF_SPEC_DATA, "config.rb")), + mystring2: "Dummy text", + }) + end + + end # make_request + + describe "StreamPart" do + before(:each) do + @file = File.new(File.join(CHEF_SPEC_DATA, "config.rb")) + @stream_part = Chef::Knife::Core::CookbookSiteStreamingUploader::StreamPart.new(@file, File.size(@file)) + end + + it "should create a StreamPart" do + expect(@stream_part).to be_instance_of(Chef::Knife::Core::CookbookSiteStreamingUploader::StreamPart) + end + + it "should expose its size" do + expect(@stream_part.size).to eql(File.size(@file)) + end + + it "should read with offset and how_much" do + content = @file.read(4) + @file.rewind + expect(@stream_part.read(0, 4)).to eql(content) + end + + end # StreamPart + + describe "StringPart" do + before(:each) do + @str = "What a boring string" + @string_part = Chef::Knife::Core::CookbookSiteStreamingUploader::StringPart.new(@str) + end + + it "should create a StringPart" do + expect(@string_part).to be_instance_of(Chef::Knife::Core::CookbookSiteStreamingUploader::StringPart) + end + + it "should expose its size" do + expect(@string_part.size).to eql(@str.size) + end + + it "should read with offset and how_much" do + expect(@string_part.read(2, 4)).to eql(@str[2, 4]) + end + + end # StringPart + + describe "MultipartStream" do + before(:each) do + @string1 = "stream1" + @string2 = "stream2" + @stream1 = Chef::Knife::Core::CookbookSiteStreamingUploader::StringPart.new(@string1) + @stream2 = Chef::Knife::Core::CookbookSiteStreamingUploader::StringPart.new(@string2) + @parts = [ @stream1, @stream2 ] + + @multipart_stream = Chef::Knife::Core::CookbookSiteStreamingUploader::MultipartStream.new(@parts) + end + + it "should create a MultipartStream" do + expect(@multipart_stream).to be_instance_of(Chef::Knife::Core::CookbookSiteStreamingUploader::MultipartStream) + end + + it "should expose its size" do + expect(@multipart_stream.size).to eql(@stream1.size + @stream2.size) + end + + it "should read with how_much" do + expect(@multipart_stream.read(10)).to eql("#{@string1}#{@string2}"[0, 10]) + end + + it "should read receiving destination buffer as second argument (CHEF-4456: Ruby 2 compat)" do + dst_buf = "" + @multipart_stream.read(10, dst_buf) + expect(dst_buf).to eql("#{@string1}#{@string2}"[0, 10]) + end + + end # MultipartStream + +end diff --git a/spec/unit/knife/core/gem_glob_loader_spec.rb b/spec/unit/knife/core/gem_glob_loader_spec.rb index f0c29b86a0..a6a94cc57a 100644 --- a/spec/unit/knife/core/gem_glob_loader_spec.rb +++ b/spec/unit/knife/core/gem_glob_loader_spec.rb @@ -15,7 +15,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::SubcommandLoader::GemGlobLoader do let(:loader) { Chef::Knife::SubcommandLoader::GemGlobLoader.new(File.join(CHEF_SPEC_DATA, "knife-site-subcommands")) } @@ -24,11 +24,11 @@ describe Chef::Knife::SubcommandLoader::GemGlobLoader do before do allow(ChefUtils).to receive(:windows?) { false } - Chef::Util::PathHelper.class_variable_set(:@@home_dir, home) + ChefConfig::PathHelper.class_variable_set(:@@home_dir, home) end after do - Chef::Util::PathHelper.class_variable_set(:@@home_dir, nil) + ChefConfig::PathHelper.class_variable_set(:@@home_dir, nil) end it "builds a list of the core subcommand file require paths" do @@ -62,12 +62,42 @@ describe Chef::Knife::SubcommandLoader::GemGlobLoader do expect(loader).to receive(:find_subcommands_via_dirglob).and_return({}) expect(loader.subcommand_files.select { |file| file.include?("knife-ec2") }.sort).to eq(gem_files) end + it "excludes knife version file if loaded from a gem" do + gems = [ double("knife-ec2-0.5.12") ] + gem_files = [ + "/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_base.rb", + "/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_otherstuff.rb", + "/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/version.rb", + ] + expected_files = [ + "/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_base.rb", + "/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_otherstuff.rb" + ] + + expect($LOAD_PATH).to receive(:map).and_return([]) + if Gem::Specification.respond_to? :latest_specs + expect(Gem::Specification).to receive(:latest_specs).with(true).and_return(gems) + expect(gems[0]).to receive(:matches_for_glob).with(%r{chef/knife/\*\.rb\{(.*),\.rb,(.*)\}}).and_return(gem_files) + else + expect(Gem.source_index).to receive(:latest_specs).with(true).and_return(gems) + expect(gems[0]).to receive(:require_paths).twice.and_return(["lib"]) + expect(gems[0]).to receive(:full_gem_path).and_return("/usr/lib/ruby/gems/knife-ec2-0.5.12") + expect(Dir).to receive(:[]).with("/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/*.rb").and_return(gem_files) + end + expect(loader).to receive(:find_subcommands_via_dirglob).and_return({}) + expect(loader.subcommand_files.select { |file| file.include?("knife-ec2") }.sort).to eq(expected_files) + end it "finds files using a dirglob when rubygems is not available" do expect(loader.find_subcommands_via_dirglob).to include("chef/knife/node_create") loader.find_subcommands_via_dirglob.each_value { |abs_path| expect(abs_path).to match(%r{chef/knife/.+}) } end + it "excludes chef/knife/version.rb using a dirglob when rubygems is not available" do + expect(loader.find_subcommands_via_dirglob).to_not include("chef/knife/version") + loader.find_subcommands_via_dirglob.each_value { |abs_path| expect(abs_path).to match(%r{chef/knife/.+}) } + end + it "finds user-specific subcommands in the user's ~/.chef directory" do expected_command = File.join(home, ".chef", "plugins", "knife", "example_home_subcommand.rb") expect(loader.site_subcommands).to include(expected_command) @@ -86,6 +116,9 @@ describe Chef::Knife::SubcommandLoader::GemGlobLoader do # source tree of the "primary" chef install, it can be loaded and cause an # error. We also want to ensure that we only load builtin commands from the # "primary" chef install. + # + # NOTE - we need to revisit coverage now that we're moving knife to its own gem; + # or remove this test if it's no longer a supported scenario. context "when a different version of chef is also installed as a gem" do let(:all_found_commands) do diff --git a/spec/unit/knife/core/hashed_command_loader_spec.rb b/spec/unit/knife/core/hashed_command_loader_spec.rb index c88656945b..305c928309 100644 --- a/spec/unit/knife/core/hashed_command_loader_spec.rb +++ b/spec/unit/knife/core/hashed_command_loader_spec.rb @@ -15,7 +15,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::SubcommandLoader::HashedCommandLoader do before do diff --git a/spec/unit/knife/core/node_editor_spec.rb b/spec/unit/knife/core/node_editor_spec.rb index d8e5c86d2c..f4fbe76695 100644 --- a/spec/unit/knife/core/node_editor_spec.rb +++ b/spec/unit/knife/core/node_editor_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/core/node_editor" describe Chef::Knife::NodeEditor do diff --git a/spec/unit/knife/core/object_loader_spec.rb b/spec/unit/knife/core/object_loader_spec.rb index 0dcabff46d..00a9ed4553 100644 --- a/spec/unit/knife/core/object_loader_spec.rb +++ b/spec/unit/knife/core/object_loader_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/core/object_loader" describe Chef::Knife::Core::ObjectLoader do diff --git a/spec/unit/knife/core/status_presenter_spec.rb b/spec/unit/knife/core/status_presenter_spec.rb index 377c581bfc..a3f297045b 100644 --- a/spec/unit/knife/core/status_presenter_spec.rb +++ b/spec/unit/knife/core/status_presenter_spec.rb @@ -14,7 +14,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::Core::StatusPresenter do describe "#summarize_json" do diff --git a/spec/unit/knife/core/subcommand_loader_spec.rb b/spec/unit/knife/core/subcommand_loader_spec.rb index e8bc045946..ad53a422fe 100644 --- a/spec/unit/knife/core/subcommand_loader_spec.rb +++ b/spec/unit/knife/core/subcommand_loader_spec.rb @@ -15,7 +15,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::SubcommandLoader do let(:loader) { Chef::Knife::SubcommandLoader.new(File.join(CHEF_SPEC_DATA, "knife-site-subcommands")) } diff --git a/spec/unit/knife/core/ui_spec.rb b/spec/unit/knife/core/ui_spec.rb index 3bbe799267..d5d09c0fdf 100644 --- a/spec/unit/knife/core/ui_spec.rb +++ b/spec/unit/knife/core/ui_spec.rb @@ -19,7 +19,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::UI do before do diff --git a/spec/unit/knife/core/windows_bootstrap_context_spec.rb b/spec/unit/knife/core/windows_bootstrap_context_spec.rb index 76b90c955e..1243dd1559 100644 --- a/spec/unit/knife/core/windows_bootstrap_context_spec.rb +++ b/spec/unit/knife/core/windows_bootstrap_context_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/core/windows_bootstrap_context" describe Chef::Knife::Core::WindowsBootstrapContext do let(:config) { {} } diff --git a/spec/unit/knife/data_bag_create_spec.rb b/spec/unit/knife/data_bag_create_spec.rb index 93082c190e..6700d886fc 100644 --- a/spec/unit/knife/data_bag_create_spec.rb +++ b/spec/unit/knife/data_bag_create_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "tempfile" describe Chef::Knife::DataBagCreate do diff --git a/spec/unit/knife/data_bag_edit_spec.rb b/spec/unit/knife/data_bag_edit_spec.rb index 6ebcaf4945..1be75ba014 100644 --- a/spec/unit/knife/data_bag_edit_spec.rb +++ b/spec/unit/knife/data_bag_edit_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "tempfile" describe Chef::Knife::DataBagEdit do diff --git a/spec/unit/knife/data_bag_from_file_spec.rb b/spec/unit/knife/data_bag_from_file_spec.rb index 12211eede3..4d7c506d0d 100644 --- a/spec/unit/knife/data_bag_from_file_spec.rb +++ b/spec/unit/knife/data_bag_from_file_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/data_bag_item" require "chef/encrypted_data_bag_item" diff --git a/spec/unit/knife/data_bag_secret_options_spec.rb b/spec/unit/knife/data_bag_secret_options_spec.rb index e8f99c3f79..9946b82110 100644 --- a/spec/unit/knife/data_bag_secret_options_spec.rb +++ b/spec/unit/knife/data_bag_secret_options_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife" require "chef/config" require "tempfile" diff --git a/spec/unit/knife/data_bag_show_spec.rb b/spec/unit/knife/data_bag_show_spec.rb index 2b806b8a65..0cc0bdf766 100644 --- a/spec/unit/knife/data_bag_show_spec.rb +++ b/spec/unit/knife/data_bag_show_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/data_bag_item" require "chef/encrypted_data_bag_item" diff --git a/spec/unit/knife/environment_compare_spec.rb b/spec/unit/knife/environment_compare_spec.rb index bfaeed0c82..001c725624 100644 --- a/spec/unit/knife/environment_compare_spec.rb +++ b/spec/unit/knife/environment_compare_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::EnvironmentCompare do before(:each) do diff --git a/spec/unit/knife/environment_create_spec.rb b/spec/unit/knife/environment_create_spec.rb index d54cab8dc9..0535276e9c 100644 --- a/spec/unit/knife/environment_create_spec.rb +++ b/spec/unit/knife/environment_create_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::EnvironmentCreate do before(:each) do diff --git a/spec/unit/knife/environment_delete_spec.rb b/spec/unit/knife/environment_delete_spec.rb index 643bf1cc13..e088f6a791 100644 --- a/spec/unit/knife/environment_delete_spec.rb +++ b/spec/unit/knife/environment_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::EnvironmentDelete do before(:each) do diff --git a/spec/unit/knife/environment_edit_spec.rb b/spec/unit/knife/environment_edit_spec.rb index 1feb1c05fd..f05de2cb2f 100644 --- a/spec/unit/knife/environment_edit_spec.rb +++ b/spec/unit/knife/environment_edit_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::EnvironmentEdit do before(:each) do diff --git a/spec/unit/knife/environment_from_file_spec.rb b/spec/unit/knife/environment_from_file_spec.rb index 2090ec7bbd..fb9329eb57 100644 --- a/spec/unit/knife/environment_from_file_spec.rb +++ b/spec/unit/knife/environment_from_file_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" Chef::Knife::EnvironmentFromFile.load_deps diff --git a/spec/unit/knife/environment_list_spec.rb b/spec/unit/knife/environment_list_spec.rb index 7bb0e723aa..4f44a93f60 100644 --- a/spec/unit/knife/environment_list_spec.rb +++ b/spec/unit/knife/environment_list_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::EnvironmentList do before(:each) do diff --git a/spec/unit/knife/environment_show_spec.rb b/spec/unit/knife/environment_show_spec.rb index 8f67e593bc..536afcc058 100644 --- a/spec/unit/knife/environment_show_spec.rb +++ b/spec/unit/knife/environment_show_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::EnvironmentShow do before(:each) do diff --git a/spec/unit/knife/key_create_spec.rb b/spec/unit/knife/key_create_spec.rb index 12826ae7e2..91d3fc0f69 100644 --- a/spec/unit/knife/key_create_spec.rb +++ b/spec/unit/knife/key_create_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/user_key_create" require "chef/knife/client_key_create" require "chef/knife/key_create" diff --git a/spec/unit/knife/key_delete_spec.rb b/spec/unit/knife/key_delete_spec.rb index fd39c7381a..ff669446bb 100644 --- a/spec/unit/knife/key_delete_spec.rb +++ b/spec/unit/knife/key_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/user_key_delete" require "chef/knife/client_key_delete" require "chef/knife/key_delete" diff --git a/spec/unit/knife/key_edit_spec.rb b/spec/unit/knife/key_edit_spec.rb index b42503af59..ae58d281b7 100644 --- a/spec/unit/knife/key_edit_spec.rb +++ b/spec/unit/knife/key_edit_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/user_key_edit" require "chef/knife/client_key_edit" require "chef/knife/key_edit" diff --git a/spec/unit/knife/key_helper.rb b/spec/unit/knife/key_helper.rb index 6dbfb567f4..c58d383703 100644 --- a/spec/unit/knife/key_helper.rb +++ b/spec/unit/knife/key_helper.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" shared_examples_for "a knife key command" do let(:stderr) { StringIO.new } diff --git a/spec/unit/knife/key_list_spec.rb b/spec/unit/knife/key_list_spec.rb index 51ed73b64f..3cb8a1c58d 100644 --- a/spec/unit/knife/key_list_spec.rb +++ b/spec/unit/knife/key_list_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/user_key_list" require "chef/knife/client_key_list" require "chef/knife/key_list" diff --git a/spec/unit/knife/key_show_spec.rb b/spec/unit/knife/key_show_spec.rb index 6d1ca2ccc7..ace6dad990 100644 --- a/spec/unit/knife/key_show_spec.rb +++ b/spec/unit/knife/key_show_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/user_key_show" require "chef/knife/client_key_show" require "chef/knife/key_show" diff --git a/spec/unit/knife/node_bulk_delete_spec.rb b/spec/unit/knife/node_bulk_delete_spec.rb index e23f286999..cf38d542fa 100644 --- a/spec/unit/knife/node_bulk_delete_spec.rb +++ b/spec/unit/knife/node_bulk_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::NodeBulkDelete do before(:each) do diff --git a/spec/unit/knife/node_delete_spec.rb b/spec/unit/knife/node_delete_spec.rb index e6c677c041..92932c0b6f 100644 --- a/spec/unit/knife/node_delete_spec.rb +++ b/spec/unit/knife/node_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::NodeDelete do before(:each) do diff --git a/spec/unit/knife/node_edit_spec.rb b/spec/unit/knife/node_edit_spec.rb index 7b2ebb5b2c..e89322d415 100644 --- a/spec/unit/knife/node_edit_spec.rb +++ b/spec/unit/knife/node_edit_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" Chef::Knife::NodeEdit.load_deps describe Chef::Knife::NodeEdit do diff --git a/spec/unit/knife/node_environment_set_spec.rb b/spec/unit/knife/node_environment_set_spec.rb index 6a6d48cc2f..c2d55d0ab1 100644 --- a/spec/unit/knife/node_environment_set_spec.rb +++ b/spec/unit/knife/node_environment_set_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::NodeEnvironmentSet do before(:each) do diff --git a/spec/unit/knife/node_from_file_spec.rb b/spec/unit/knife/node_from_file_spec.rb index 00d6dd5d1a..359b9726b6 100644 --- a/spec/unit/knife/node_from_file_spec.rb +++ b/spec/unit/knife/node_from_file_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" Chef::Knife::NodeFromFile.load_deps diff --git a/spec/unit/knife/node_list_spec.rb b/spec/unit/knife/node_list_spec.rb index d594fffc14..baa79cb81f 100644 --- a/spec/unit/knife/node_list_spec.rb +++ b/spec/unit/knife/node_list_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::NodeList do before(:each) do diff --git a/spec/unit/knife/node_policy_set_spec.rb b/spec/unit/knife/node_policy_set_spec.rb index 40b1d2617d..5815da29df 100644 --- a/spec/unit/knife/node_policy_set_spec.rb +++ b/spec/unit/knife/node_policy_set_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::NodePolicySet do let(:node) do diff --git a/spec/unit/knife/node_run_list_add_spec.rb b/spec/unit/knife/node_run_list_add_spec.rb index 0148711fac..87b75d9818 100644 --- a/spec/unit/knife/node_run_list_add_spec.rb +++ b/spec/unit/knife/node_run_list_add_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::NodeRunListAdd do before(:each) do diff --git a/spec/unit/knife/node_run_list_remove_spec.rb b/spec/unit/knife/node_run_list_remove_spec.rb index 1974821728..0eff7c6d27 100644 --- a/spec/unit/knife/node_run_list_remove_spec.rb +++ b/spec/unit/knife/node_run_list_remove_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::NodeRunListRemove do before(:each) do diff --git a/spec/unit/knife/node_run_list_set_spec.rb b/spec/unit/knife/node_run_list_set_spec.rb index 6246dfce6a..35fdd63e4d 100644 --- a/spec/unit/knife/node_run_list_set_spec.rb +++ b/spec/unit/knife/node_run_list_set_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::NodeRunListSet do before(:each) do diff --git a/spec/unit/knife/node_show_spec.rb b/spec/unit/knife/node_show_spec.rb index 037672501e..c26ae94f40 100644 --- a/spec/unit/knife/node_show_spec.rb +++ b/spec/unit/knife/node_show_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::NodeShow do diff --git a/spec/unit/knife/org_create_spec.rb b/spec/unit/knife/org_create_spec.rb index 3c33817b55..f45ade2df7 100644 --- a/spec/unit/knife/org_create_spec.rb +++ b/spec/unit/knife/org_create_spec.rb @@ -15,7 +15,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/org" describe Chef::Knife::OrgCreate do diff --git a/spec/unit/knife/org_delete_spec.rb b/spec/unit/knife/org_delete_spec.rb index baa102f8c8..33311bd678 100644 --- a/spec/unit/knife/org_delete_spec.rb +++ b/spec/unit/knife/org_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/org" describe Chef::Knife::OrgDelete do diff --git a/spec/unit/knife/org_edit_spec.rb b/spec/unit/knife/org_edit_spec.rb index 05339e8f21..af9dae2c49 100644 --- a/spec/unit/knife/org_edit_spec.rb +++ b/spec/unit/knife/org_edit_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::OrgEdit do let(:knife) { Chef::Knife::OrgEdit.new } diff --git a/spec/unit/knife/org_list_spec.rb b/spec/unit/knife/org_list_spec.rb index de77b4b0c7..aa5fca5099 100644 --- a/spec/unit/knife/org_list_spec.rb +++ b/spec/unit/knife/org_list_spec.rb @@ -15,7 +15,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/org" describe Chef::Knife::OrgList do diff --git a/spec/unit/knife/org_show_spec.rb b/spec/unit/knife/org_show_spec.rb index 2f5246dd84..364b879a7c 100644 --- a/spec/unit/knife/org_show_spec.rb +++ b/spec/unit/knife/org_show_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/org" describe Chef::Knife::OrgShow do diff --git a/spec/unit/knife/org_user_add_spec.rb b/spec/unit/knife/org_user_add_spec.rb index 20e28d6919..72ee1d0607 100644 --- a/spec/unit/knife/org_user_add_spec.rb +++ b/spec/unit/knife/org_user_add_spec.rb @@ -15,7 +15,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/org" describe Chef::Knife::OrgUserAdd do diff --git a/spec/unit/knife/raw_spec.rb b/spec/unit/knife/raw_spec.rb index 1f88195e65..90a09a31e6 100644 --- a/spec/unit/knife/raw_spec.rb +++ b/spec/unit/knife/raw_spec.rb @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::Raw do let(:rest) do diff --git a/spec/unit/knife/role_bulk_delete_spec.rb b/spec/unit/knife/role_bulk_delete_spec.rb index 5af7c51584..f68efba57c 100644 --- a/spec/unit/knife/role_bulk_delete_spec.rb +++ b/spec/unit/knife/role_bulk_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleBulkDelete do before(:each) do diff --git a/spec/unit/knife/role_create_spec.rb b/spec/unit/knife/role_create_spec.rb index 0d563e40dd..13f47492b1 100644 --- a/spec/unit/knife/role_create_spec.rb +++ b/spec/unit/knife/role_create_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleCreate do before(:each) do diff --git a/spec/unit/knife/role_delete_spec.rb b/spec/unit/knife/role_delete_spec.rb index d43f99689d..658da5299d 100644 --- a/spec/unit/knife/role_delete_spec.rb +++ b/spec/unit/knife/role_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleDelete do before(:each) do diff --git a/spec/unit/knife/role_edit_spec.rb b/spec/unit/knife/role_edit_spec.rb index faf9cf7d84..adade177a7 100644 --- a/spec/unit/knife/role_edit_spec.rb +++ b/spec/unit/knife/role_edit_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleEdit do before(:each) do diff --git a/spec/unit/knife/role_env_run_list_add_spec.rb b/spec/unit/knife/role_env_run_list_add_spec.rb index 13a05db33e..b42ec6141f 100644 --- a/spec/unit/knife/role_env_run_list_add_spec.rb +++ b/spec/unit/knife/role_env_run_list_add_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleEnvRunListAdd do before(:each) do diff --git a/spec/unit/knife/role_env_run_list_clear_spec.rb b/spec/unit/knife/role_env_run_list_clear_spec.rb index d4b9625550..ad88d1ae37 100644 --- a/spec/unit/knife/role_env_run_list_clear_spec.rb +++ b/spec/unit/knife/role_env_run_list_clear_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleEnvRunListClear do before(:each) do diff --git a/spec/unit/knife/role_env_run_list_remove_spec.rb b/spec/unit/knife/role_env_run_list_remove_spec.rb index 7f9b41475c..8755ce452b 100644 --- a/spec/unit/knife/role_env_run_list_remove_spec.rb +++ b/spec/unit/knife/role_env_run_list_remove_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleEnvRunListRemove do before(:each) do diff --git a/spec/unit/knife/role_env_run_list_replace_spec.rb b/spec/unit/knife/role_env_run_list_replace_spec.rb index 93b233efdc..457f4efbd7 100644 --- a/spec/unit/knife/role_env_run_list_replace_spec.rb +++ b/spec/unit/knife/role_env_run_list_replace_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleEnvRunListReplace do before(:each) do diff --git a/spec/unit/knife/role_env_run_list_set_spec.rb b/spec/unit/knife/role_env_run_list_set_spec.rb index d35e4dbb17..34233398f5 100644 --- a/spec/unit/knife/role_env_run_list_set_spec.rb +++ b/spec/unit/knife/role_env_run_list_set_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleEnvRunListSet do before(:each) do diff --git a/spec/unit/knife/role_from_file_spec.rb b/spec/unit/knife/role_from_file_spec.rb index 51e94d31e3..6e2fdf7cfb 100644 --- a/spec/unit/knife/role_from_file_spec.rb +++ b/spec/unit/knife/role_from_file_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" Chef::Knife::RoleFromFile.load_deps diff --git a/spec/unit/knife/role_list_spec.rb b/spec/unit/knife/role_list_spec.rb index dea2e874a4..f37a85b6dc 100644 --- a/spec/unit/knife/role_list_spec.rb +++ b/spec/unit/knife/role_list_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleList do before(:each) do diff --git a/spec/unit/knife/role_run_list_add_spec.rb b/spec/unit/knife/role_run_list_add_spec.rb index 6f222ee80a..7b038c2e81 100644 --- a/spec/unit/knife/role_run_list_add_spec.rb +++ b/spec/unit/knife/role_run_list_add_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleRunListAdd do before(:each) do diff --git a/spec/unit/knife/role_run_list_clear_spec.rb b/spec/unit/knife/role_run_list_clear_spec.rb index 327a9979b0..5479b01811 100644 --- a/spec/unit/knife/role_run_list_clear_spec.rb +++ b/spec/unit/knife/role_run_list_clear_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleRunListClear do before(:each) do diff --git a/spec/unit/knife/role_run_list_remove_spec.rb b/spec/unit/knife/role_run_list_remove_spec.rb index 200a559c08..353ae36c1a 100644 --- a/spec/unit/knife/role_run_list_remove_spec.rb +++ b/spec/unit/knife/role_run_list_remove_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleRunListRemove do before(:each) do diff --git a/spec/unit/knife/role_run_list_replace_spec.rb b/spec/unit/knife/role_run_list_replace_spec.rb index 1957403fb1..e59b704f00 100644 --- a/spec/unit/knife/role_run_list_replace_spec.rb +++ b/spec/unit/knife/role_run_list_replace_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleRunListReplace do before(:each) do diff --git a/spec/unit/knife/role_run_list_set_spec.rb b/spec/unit/knife/role_run_list_set_spec.rb index 06098c585e..b75f1ab377 100644 --- a/spec/unit/knife/role_run_list_set_spec.rb +++ b/spec/unit/knife/role_run_list_set_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleRunListSet do before(:each) do diff --git a/spec/unit/knife/role_show_spec.rb b/spec/unit/knife/role_show_spec.rb index fe48e2f940..a79cb40e81 100644 --- a/spec/unit/knife/role_show_spec.rb +++ b/spec/unit/knife/role_show_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::RoleShow do let(:role) { "base" } diff --git a/spec/unit/knife/ssh_spec.rb b/spec/unit/knife/ssh_spec.rb index 8606045e8c..59015f024a 100644 --- a/spec/unit/knife/ssh_spec.rb +++ b/spec/unit/knife/ssh_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "net/ssh" require "net/ssh/multi" diff --git a/spec/unit/knife/ssl_check_spec.rb b/spec/unit/knife/ssl_check_spec.rb index 1165da4539..4412ee0be9 100644 --- a/spec/unit/knife/ssl_check_spec.rb +++ b/spec/unit/knife/ssl_check_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "stringio" describe Chef::Knife::SslCheck do diff --git a/spec/unit/knife/ssl_fetch_spec.rb b/spec/unit/knife/ssl_fetch_spec.rb index 2184994dc0..c2dc5bdade 100644 --- a/spec/unit/knife/ssl_fetch_spec.rb +++ b/spec/unit/knife/ssl_fetch_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/ssl_fetch" describe Chef::Knife::SslFetch do diff --git a/spec/unit/knife/status_spec.rb b/spec/unit/knife/status_spec.rb index 838e4c9600..f3b31c1897 100644 --- a/spec/unit/knife/status_spec.rb +++ b/spec/unit/knife/status_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::Status do before(:each) do diff --git a/spec/unit/knife/supermarket_download_spec.rb b/spec/unit/knife/supermarket_download_spec.rb index 5d15e74966..3796140d61 100644 --- a/spec/unit/knife/supermarket_download_spec.rb +++ b/spec/unit/knife/supermarket_download_spec.rb @@ -18,7 +18,7 @@ # require "chef/knife/supermarket_download" -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::SupermarketDownload do diff --git a/spec/unit/knife/supermarket_install_spec.rb b/spec/unit/knife/supermarket_install_spec.rb index 03cc5d1992..6ebbbc005c 100644 --- a/spec/unit/knife/supermarket_install_spec.rb +++ b/spec/unit/knife/supermarket_install_spec.rb @@ -16,8 +16,9 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/supermarket_install" +require "mixlib/archive" describe Chef::Knife::SupermarketInstall do let(:knife) { Chef::Knife::SupermarketInstall.new } diff --git a/spec/unit/knife/supermarket_list_spec.rb b/spec/unit/knife/supermarket_list_spec.rb index a1acccaaaa..bbaf733f44 100644 --- a/spec/unit/knife/supermarket_list_spec.rb +++ b/spec/unit/knife/supermarket_list_spec.rb @@ -17,7 +17,7 @@ # require "chef/knife/supermarket_list" -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::SupermarketList do let(:knife) { described_class.new } diff --git a/spec/unit/knife/supermarket_search_spec.rb b/spec/unit/knife/supermarket_search_spec.rb index cba2f615aa..18092f52c8 100644 --- a/spec/unit/knife/supermarket_search_spec.rb +++ b/spec/unit/knife/supermarket_search_spec.rb @@ -17,7 +17,7 @@ # require "chef/knife/supermarket_search" -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::SupermarketSearch do let(:knife) { described_class.new } diff --git a/spec/unit/knife/supermarket_share_spec.rb b/spec/unit/knife/supermarket_share_spec.rb index 088cef9dfd..d362edf3c0 100644 --- a/spec/unit/knife/supermarket_share_spec.rb +++ b/spec/unit/knife/supermarket_share_spec.rb @@ -16,10 +16,10 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/supermarket_share" require "chef/cookbook_uploader" -require "chef/cookbook_site_streaming_uploader" +require "chef/knife/core/cookbook_site_streaming_uploader" describe Chef::Knife::SupermarketShare do @@ -42,7 +42,7 @@ describe Chef::Knife::SupermarketShare do @cookbook_uploader = Chef::CookbookUploader.new("herpderp", rest: "norest") allow(Chef::CookbookUploader).to receive(:new).and_return(@cookbook_uploader) allow(@cookbook_uploader).to receive(:validate_cookbooks).and_return(true) - allow(Chef::CookbookSiteStreamingUploader).to receive(:create_build_dir).and_return(Dir.mktmpdir) + allow(Chef::Knife::Core::CookbookSiteStreamingUploader).to receive(:create_build_dir).and_return(Dir.mktmpdir) allow(@knife).to receive(:shell_out!).and_return(true) @stdout = StringIO.new @@ -140,7 +140,7 @@ describe Chef::Knife::SupermarketShare do context "when the --dry-run flag is specified" do before do - allow(Chef::CookbookSiteStreamingUploader).to receive(:create_build_dir).and_return("/var/tmp/dummy") + allow(Chef::Knife::Core::CookbookSiteStreamingUploader).to receive(:create_build_dir).and_return("/var/tmp/dummy") @knife.config = { dry_run: true } @so = instance_double("Mixlib::ShellOut") allow(@knife).to receive(:shell_out!).and_return(@so) @@ -165,7 +165,7 @@ describe Chef::Knife::SupermarketShare do before(:each) do @upload_response = double("Net::HTTPResponse") - allow(Chef::CookbookSiteStreamingUploader).to receive(:post).and_return(@upload_response) + allow(Chef::Knife::Core::CookbookSiteStreamingUploader).to receive(:post).and_return(@upload_response) allow(File).to receive(:open).and_return(true) end @@ -174,7 +174,7 @@ describe Chef::Knife::SupermarketShare do response_text = Chef::JSONCompat.to_json({ uri: "https://supermarket.chef.io/cookbooks/cookbook_name" }) allow(@upload_response).to receive(:body).and_return(response_text) allow(@upload_response).to receive(:code).and_return(201) - expect(Chef::CookbookSiteStreamingUploader).to receive(:post).with(/supermarket\.chef\.io/, anything, anything, anything) + expect(Chef::Knife::Core::CookbookSiteStreamingUploader).to receive(:post).with(/supermarket\.chef\.io/, anything, anything, anything) @knife.run end diff --git a/spec/unit/knife/supermarket_unshare_spec.rb b/spec/unit/knife/supermarket_unshare_spec.rb index 8ae4d03cb5..63682a663c 100644 --- a/spec/unit/knife/supermarket_unshare_spec.rb +++ b/spec/unit/knife/supermarket_unshare_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/knife/supermarket_unshare" describe Chef::Knife::SupermarketUnshare do diff --git a/spec/unit/knife/tag_create_spec.rb b/spec/unit/knife/tag_create_spec.rb index a1a4923871..290b925f4e 100644 --- a/spec/unit/knife/tag_create_spec.rb +++ b/spec/unit/knife/tag_create_spec.rb @@ -1,4 +1,4 @@ -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::TagCreate do before(:each) do diff --git a/spec/unit/knife/tag_delete_spec.rb b/spec/unit/knife/tag_delete_spec.rb index 4201196de0..dd01fba50f 100644 --- a/spec/unit/knife/tag_delete_spec.rb +++ b/spec/unit/knife/tag_delete_spec.rb @@ -1,4 +1,4 @@ -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::TagDelete do before(:each) do diff --git a/spec/unit/knife/tag_list_spec.rb b/spec/unit/knife/tag_list_spec.rb index dceec9a5ea..5da7803e09 100644 --- a/spec/unit/knife/tag_list_spec.rb +++ b/spec/unit/knife/tag_list_spec.rb @@ -1,4 +1,4 @@ -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::TagList do before(:each) do diff --git a/spec/unit/knife/user_create_spec.rb b/spec/unit/knife/user_create_spec.rb index c69a668f7e..fb6a2c3ba0 100644 --- a/spec/unit/knife/user_create_spec.rb +++ b/spec/unit/knife/user_create_spec.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" Chef::Knife::UserCreate.load_deps diff --git a/spec/unit/knife/user_delete_spec.rb b/spec/unit/knife/user_delete_spec.rb index 4dd2665cda..57d4072c50 100644 --- a/spec/unit/knife/user_delete_spec.rb +++ b/spec/unit/knife/user_delete_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/org" Chef::Knife::UserDelete.load_deps diff --git a/spec/unit/knife/user_edit_spec.rb b/spec/unit/knife/user_edit_spec.rb index 2fde328c0c..12e2f19561 100644 --- a/spec/unit/knife/user_edit_spec.rb +++ b/spec/unit/knife/user_edit_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::UserEdit do let(:knife) { Chef::Knife::UserEdit.new } diff --git a/spec/unit/knife/user_list_spec.rb b/spec/unit/knife/user_list_spec.rb index 63df590591..01013de352 100644 --- a/spec/unit/knife/user_list_spec.rb +++ b/spec/unit/knife/user_list_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" Chef::Knife::UserList.load_deps diff --git a/spec/unit/knife/user_password_spec.rb b/spec/unit/knife/user_password_spec.rb index 098597a14c..139ed242de 100644 --- a/spec/unit/knife/user_password_spec.rb +++ b/spec/unit/knife/user_password_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/org" Chef::Knife::UserDelete.load_deps diff --git a/spec/unit/knife/user_reregister_spec.rb b/spec/unit/knife/user_reregister_spec.rb index 481415e432..9178996abf 100644 --- a/spec/unit/knife/user_reregister_spec.rb +++ b/spec/unit/knife/user_reregister_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" describe Chef::Knife::UserReregister do let(:knife) { Chef::Knife::UserReregister.new } diff --git a/spec/unit/knife/user_show_spec.rb b/spec/unit/knife/user_show_spec.rb index 30742d8c21..3bcbbcb648 100644 --- a/spec/unit/knife/user_show_spec.rb +++ b/spec/unit/knife/user_show_spec.rb @@ -16,7 +16,7 @@ # limitations under the License. # -require "spec_helper" +require "knife_spec_helper" require "chef/org" Chef::Knife::UserShow.load_deps diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb index 88f36a3973..f3315f3cf5 100644 --- a/spec/unit/knife_spec.rb +++ b/spec/unit/knife_spec.rb @@ -21,7 +21,7 @@ module KnifeSpecs end -require "spec_helper" +require "knife_spec_helper" require "uri" require "chef/knife/core/gem_glob_loader" diff --git a/spec/unit/provider/service/arch_service_spec.rb b/spec/unit/provider/service/arch_service_spec.rb index 026db3dc75..c92af83de5 100644 --- a/spec/unit/provider/service/arch_service_spec.rb +++ b/spec/unit/provider/service/arch_service_spec.rb @@ -44,6 +44,7 @@ describe Chef::Provider::Service::Arch, "load_current_resource" do describe "when first created" do it "should set the current resources service name to the new resources service name" do + allow(@provider).to receive(:determine_current_status!) allow(@provider).to receive(:shell_out).and_return(OpenStruct.new(exitstatus: 0, stdout: "")) @provider.load_current_resource expect(@provider.current_resource.service_name).to eq("chef") diff --git a/spec/unit/provider/service/debian_service_spec.rb b/spec/unit/provider/service/debian_service_spec.rb index d0cd048c4f..0ae1d28cd3 100644 --- a/spec/unit/provider/service/debian_service_spec.rb +++ b/spec/unit/provider/service/debian_service_spec.rb @@ -33,6 +33,7 @@ describe Chef::Provider::Service::Debian do @pid, @stdin, @stdout, @stderr = nil, nil, nil, nil allow(File).to receive(:exist?).with("/etc/init.d/chef").and_return true + allow(@provider).to receive(:determine_current_status!) end let(:init_lines) do diff --git a/tasks/rspec.rb b/tasks/rspec.rb index 929e0f91b0..050c40be5c 100644 --- a/tasks/rspec.rb +++ b/tasks/rspec.rb @@ -25,8 +25,9 @@ begin desc "Run specs for Chef's Gem Components" task :component_specs do - %w{chef-utils chef-config}.each do |gem| + %w{chef-utils chef-config knife}.each do |gem| Dir.chdir(gem) do + puts "--- Running #{gem} specs" Bundler.with_unbundled_env do sh("bundle install --jobs=3 --retry=3") sh("bundle exec rake spec") @@ -39,19 +40,24 @@ begin task spec: :component_specs - desc "Run all specs in spec directory" + desc "Run all chef specs in spec directory" RSpec::Core::RakeTask.new(:spec) do |t| t.verbose = false t.rspec_opts = %w{--profile} - t.pattern = FileList["spec/**/*_spec.rb"] + t.pattern = FileList["spec/**/*_spec.rb"].reject do |path| + path =~ /knife.*/ + end end namespace :spec do - desc "Run all specs in spec directory" + desc "Run all chef specs in spec directory" RSpec::Core::RakeTask.new(:all) do |t| t.verbose = false t.rspec_opts = %w{--profile} t.pattern = FileList["spec/**/*_spec.rb"] + t.pattern = FileList["spec/**/*_spec.rb"].reject do |path| + path =~ /knife.*/ + end end desc "Print Specdoc for all specs" @@ -61,7 +67,7 @@ begin t.pattern = FileList["spec/**/*_spec.rb"] end - desc "Run the specs under spec/unit with activesupport loaded" + desc "Run chef's node and role unit specs with activesupport loaded" RSpec::Core::RakeTask.new(:activesupport) do |t| t.verbose = false t.rspec_opts = %w{--require active_support/core_ext --profile} @@ -70,11 +76,13 @@ begin end %i{unit functional integration stress}.each do |sub| - desc "Run the specs under spec/#{sub}" + desc "Run the chef specs under spec/#{sub}" RSpec::Core::RakeTask.new(sub) do |t| t.verbose = false t.rspec_opts = %w{--profile} - t.pattern = FileList["spec/#{sub}/**/*_spec.rb"] + t.pattern = FileList["spec/#{sub}/**/*_spec.rb"].reject do |path| + path =~ /knife.*/ + end end end end -- cgit v1.2.1