summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorSeth Chisamore <schisamo@opscode.com>2012-10-30 10:39:35 -0400
committerSeth Chisamore <schisamo@opscode.com>2012-10-30 10:39:35 -0400
commit24dc69a9a97e82a6e4207de68d6dcc664178249b (patch)
tree19bb289c9f88b4bbab066bc56b95d6d222fd5c35 /lib
parent9348c1c9c80ee757354d624b7dc1b78ebc7605c4 (diff)
downloadchef-24dc69a9a97e82a6e4207de68d6dcc664178249b.tar.gz
[OC-3564] move core Chef to the repo root \o/ \m/
The opscode/chef repository now only contains the core Chef library code used by chef-client, knife and chef-solo!
Diffstat (limited to 'lib')
-rw-r--r--lib/chef.rb39
-rw-r--r--lib/chef/api_client.rb181
-rw-r--r--lib/chef/application.rb162
-rw-r--r--lib/chef/application/agent.rb18
-rw-r--r--lib/chef/application/client.rb327
-rw-r--r--lib/chef/application/knife.rb183
-rw-r--r--lib/chef/application/solo.rb254
-rw-r--r--lib/chef/application/windows_service.rb237
-rw-r--r--lib/chef/applications.rb4
-rw-r--r--lib/chef/checksum/storage.rb18
-rw-r--r--lib/chef/checksum/storage/filesystem.rb56
-rw-r--r--lib/chef/checksum_cache.rb190
-rw-r--r--lib/chef/chef_fs.rb11
-rw-r--r--lib/chef/chef_fs/command_line.rb232
-rw-r--r--lib/chef/chef_fs/file_pattern.rb312
-rw-r--r--lib/chef/chef_fs/file_system.rb358
-rw-r--r--lib/chef/chef_fs/file_system/base_fs_dir.rb47
-rw-r--r--lib/chef/chef_fs/file_system/base_fs_object.rb121
-rw-r--r--lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb109
-rw-r--r--lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb31
-rw-r--r--lib/chef/chef_fs/file_system/chef_server_root_dir.rb84
-rw-r--r--lib/chef/chef_fs/file_system/cookbook_dir.rb188
-rw-r--r--lib/chef/chef_fs/file_system/cookbook_file.rb78
-rw-r--r--lib/chef/chef_fs/file_system/cookbook_subdir.rb54
-rw-r--r--lib/chef/chef_fs/file_system/cookbooks_dir.rb68
-rw-r--r--lib/chef/chef_fs/file_system/data_bag_dir.rb78
-rw-r--r--lib/chef/chef_fs/file_system/data_bag_item.rb59
-rw-r--r--lib/chef/chef_fs/file_system/data_bags_dir.rb66
-rw-r--r--lib/chef/chef_fs/file_system/file_system_entry.rb90
-rw-r--r--lib/chef/chef_fs/file_system/file_system_error.rb31
-rw-r--r--lib/chef/chef_fs/file_system/file_system_root_dir.rb31
-rw-r--r--lib/chef/chef_fs/file_system/must_delete_recursively_error.rb31
-rw-r--r--lib/chef/chef_fs/file_system/nodes_dir.rb47
-rw-r--r--lib/chef/chef_fs/file_system/nonexistent_fs_object.rb40
-rw-r--r--lib/chef/chef_fs/file_system/not_found_error.rb31
-rw-r--r--lib/chef/chef_fs/file_system/rest_list_dir.rb84
-rw-r--r--lib/chef/chef_fs/file_system/rest_list_entry.rb123
-rw-r--r--lib/chef/chef_fs/knife.rb77
-rw-r--r--lib/chef/chef_fs/path_utils.rb64
-rw-r--r--lib/chef/client.rb505
-rw-r--r--lib/chef/config.rb310
-rw-r--r--lib/chef/cookbook/chefignore.rb66
-rw-r--r--lib/chef/cookbook/cookbook_collection.rb45
-rw-r--r--lib/chef/cookbook/cookbook_version_loader.rb173
-rw-r--r--lib/chef/cookbook/file_system_file_vendor.rb56
-rw-r--r--lib/chef/cookbook/file_vendor.rb48
-rw-r--r--lib/chef/cookbook/metadata.rb629
-rw-r--r--lib/chef/cookbook/remote_file_vendor.rb84
-rw-r--r--lib/chef/cookbook/synchronizer.rb216
-rw-r--r--lib/chef/cookbook/syntax_check.rb136
-rw-r--r--lib/chef/cookbook_loader.rb134
-rw-r--r--lib/chef/cookbook_site_streaming_uploader.rb244
-rw-r--r--lib/chef/cookbook_uploader.rb163
-rw-r--r--lib/chef/cookbook_version.rb782
-rw-r--r--lib/chef/daemon.rb172
-rw-r--r--lib/chef/data_bag.rb145
-rw-r--r--lib/chef/data_bag_item.rb214
-rw-r--r--lib/chef/dsl.rb5
-rw-r--r--lib/chef/dsl/data_query.rb66
-rw-r--r--lib/chef/dsl/include_attribute.rb60
-rw-r--r--lib/chef/dsl/include_recipe.rb42
-rw-r--r--lib/chef/dsl/platform_introspection.rb215
-rw-r--r--lib/chef/dsl/recipe.rb84
-rw-r--r--lib/chef/encrypted_data_bag_item.rb139
-rw-r--r--lib/chef/environment.rb287
-rw-r--r--lib/chef/event_dispatch/base.rb311
-rw-r--r--lib/chef/event_dispatch/dispatcher.rb42
-rw-r--r--lib/chef/exceptions.rb260
-rw-r--r--lib/chef/file_access_control.rb75
-rw-r--r--lib/chef/file_access_control/unix.rb216
-rw-r--r--lib/chef/file_access_control/windows.rb310
-rw-r--r--lib/chef/file_cache.rb220
-rw-r--r--lib/chef/formatters/base.rb247
-rw-r--r--lib/chef/formatters/doc.rb236
-rw-r--r--lib/chef/formatters/error_descriptor.rb66
-rw-r--r--lib/chef/formatters/error_inspectors.rb19
-rw-r--r--lib/chef/formatters/error_inspectors/api_error_formatting.rb111
-rw-r--r--lib/chef/formatters/error_inspectors/compile_error_inspector.rb106
-rw-r--r--lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb146
-rw-r--r--lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb80
-rw-r--r--lib/chef/formatters/error_inspectors/node_load_error_inspector.rb125
-rw-r--r--lib/chef/formatters/error_inspectors/registration_error_inspector.rb137
-rw-r--r--lib/chef/formatters/error_inspectors/resource_failure_inspector.rb108
-rw-r--r--lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb118
-rw-r--r--lib/chef/formatters/error_mapper.rb85
-rw-r--r--lib/chef/formatters/minimal.rb235
-rw-r--r--lib/chef/handler.rb235
-rw-r--r--lib/chef/handler/error_report.rb33
-rw-r--r--lib/chef/handler/json_file.rb64
-rw-r--r--lib/chef/json_compat.rb53
-rw-r--r--lib/chef/knife.rb537
-rw-r--r--lib/chef/knife/bootstrap.rb234
-rw-r--r--lib/chef/knife/bootstrap/archlinux-gems.erb75
-rw-r--r--lib/chef/knife/bootstrap/centos5-gems.erb71
-rw-r--r--lib/chef/knife/bootstrap/chef-full.erb73
-rw-r--r--lib/chef/knife/bootstrap/fedora13-gems.erb58
-rw-r--r--lib/chef/knife/bootstrap/ubuntu10.04-apt.erb65
-rw-r--r--lib/chef/knife/bootstrap/ubuntu10.04-gems.erb65
-rw-r--r--lib/chef/knife/bootstrap/ubuntu12.04-gems.erb60
-rw-r--r--lib/chef/knife/client_bulk_delete.rb65
-rw-r--r--lib/chef/knife/client_create.rb80
-rw-r--r--lib/chef/knife/client_delete.rb46
-rw-r--r--lib/chef/knife/client_edit.rb45
-rw-r--r--lib/chef/knife/client_list.rb42
-rw-r--r--lib/chef/knife/client_reregister.rb58
-rw-r--r--lib/chef/knife/client_show.rb52
-rw-r--r--lib/chef/knife/configure.rb168
-rw-r--r--lib/chef/knife/configure_client.rb50
-rw-r--r--lib/chef/knife/cookbook_bulk_delete.rb72
-rw-r--r--lib/chef/knife/cookbook_create.rb297
-rw-r--r--lib/chef/knife/cookbook_delete.rb151
-rw-r--r--lib/chef/knife/cookbook_download.rb137
-rw-r--r--lib/chef/knife/cookbook_list.rb47
-rw-r--r--lib/chef/knife/cookbook_metadata.rb108
-rw-r--r--lib/chef/knife/cookbook_metadata_from_file.rb44
-rw-r--r--lib/chef/knife/cookbook_show.rb102
-rw-r--r--lib/chef/knife/cookbook_site_download.rb109
-rw-r--r--lib/chef/knife/cookbook_site_install.rb155
-rw-r--r--lib/chef/knife/cookbook_site_list.rb62
-rw-r--r--lib/chef/knife/cookbook_site_search.rb51
-rw-r--r--lib/chef/knife/cookbook_site_share.rb114
-rw-r--r--lib/chef/knife/cookbook_site_show.rb60
-rw-r--r--lib/chef/knife/cookbook_site_unshare.rb56
-rw-r--r--lib/chef/knife/cookbook_site_vendor.rb46
-rw-r--r--lib/chef/knife/cookbook_test.rb95
-rw-r--r--lib/chef/knife/cookbook_upload.rb295
-rw-r--r--lib/chef/knife/core/bootstrap_context.rb106
-rw-r--r--lib/chef/knife/core/cookbook_scm_repo.rb160
-rw-r--r--lib/chef/knife/core/generic_presenter.rb204
-rw-r--r--lib/chef/knife/core/node_editor.rb130
-rw-r--r--lib/chef/knife/core/node_presenter.rb137
-rw-r--r--lib/chef/knife/core/object_loader.rb112
-rw-r--r--lib/chef/knife/core/subcommand_loader.rb112
-rw-r--r--lib/chef/knife/core/text_formatter.rb86
-rw-r--r--lib/chef/knife/core/ui.rb219
-rw-r--r--lib/chef/knife/data_bag_create.rb93
-rw-r--r--lib/chef/knife/data_bag_delete.rb51
-rw-r--r--lib/chef/knife/data_bag_edit.rb94
-rw-r--r--lib/chef/knife/data_bag_from_file.rb136
-rw-r--r--lib/chef/knife/data_bag_list.rb46
-rw-r--r--lib/chef/knife/data_bag_show.rb81
-rw-r--r--lib/chef/knife/delete.rb33
-rw-r--r--lib/chef/knife/diff.rb46
-rw-r--r--lib/chef/knife/download.rb47
-rw-r--r--lib/chef/knife/environment_create.rb53
-rw-r--r--lib/chef/knife/environment_delete.rb45
-rw-r--r--lib/chef/knife/environment_edit.rb45
-rw-r--r--lib/chef/knife/environment_from_file.rb83
-rw-r--r--lib/chef/knife/environment_list.rb42
-rw-r--r--lib/chef/knife/environment_show.rb53
-rw-r--r--lib/chef/knife/exec.rb86
-rw-r--r--lib/chef/knife/help.rb103
-rw-r--r--lib/chef/knife/help_topics.rb4
-rw-r--r--lib/chef/knife/index_rebuild.rb50
-rw-r--r--lib/chef/knife/list.rb109
-rw-r--r--lib/chef/knife/node_bulk_delete.rb80
-rw-r--r--lib/chef/knife/node_create.rb50
-rw-r--r--lib/chef/knife/node_delete.rb47
-rw-r--r--lib/chef/knife/node_edit.rb72
-rw-r--r--lib/chef/knife/node_from_file.rb50
-rw-r--r--lib/chef/knife/node_list.rb46
-rw-r--r--lib/chef/knife/node_run_list_add.rb75
-rw-r--r--lib/chef/knife/node_run_list_remove.rb48
-rw-r--r--lib/chef/knife/node_show.rb73
-rw-r--r--lib/chef/knife/raw.rb108
-rw-r--r--lib/chef/knife/recipe_list.rb32
-rw-r--r--lib/chef/knife/role_bulk_delete.rb70
-rw-r--r--lib/chef/knife/role_create.rb55
-rw-r--r--lib/chef/knife/role_delete.rb47
-rw-r--r--lib/chef/knife/role_edit.rb48
-rw-r--r--lib/chef/knife/role_from_file.rb56
-rw-r--r--lib/chef/knife/role_list.rb43
-rw-r--r--lib/chef/knife/role_show.rb54
-rw-r--r--lib/chef/knife/search.rb141
-rw-r--r--lib/chef/knife/show.rb32
-rw-r--r--lib/chef/knife/ssh.rb444
-rw-r--r--lib/chef/knife/status.rb119
-rw-r--r--lib/chef/knife/tag_create.rb52
-rw-r--r--lib/chef/knife/tag_delete.rb60
-rw-r--r--lib/chef/knife/tag_list.rb47
-rw-r--r--lib/chef/knife/upload.rb47
-rw-r--r--lib/chef/log.rb39
-rw-r--r--lib/chef/mash.rb225
-rw-r--r--lib/chef/mixin/check_helper.rb31
-rw-r--r--lib/chef/mixin/checksum.rb32
-rw-r--r--lib/chef/mixin/command.rb164
-rw-r--r--lib/chef/mixin/command/unix.rb220
-rw-r--r--lib/chef/mixin/command/windows.rb76
-rw-r--r--lib/chef/mixin/convert_to_class_name.rb65
-rw-r--r--lib/chef/mixin/create_path.rb57
-rw-r--r--lib/chef/mixin/deep_merge.rb142
-rw-r--r--lib/chef/mixin/deprecation.rb65
-rw-r--r--lib/chef/mixin/enforce_ownership_and_permissions.rb39
-rw-r--r--lib/chef/mixin/file_class.rb46
-rw-r--r--lib/chef/mixin/from_file.rb50
-rw-r--r--lib/chef/mixin/get_source_from_package.rb42
-rw-r--r--lib/chef/mixin/language.rb36
-rw-r--r--lib/chef/mixin/language_include_attribute.rb29
-rw-r--r--lib/chef/mixin/language_include_recipe.rb26
-rw-r--r--lib/chef/mixin/params_validate.rb225
-rw-r--r--lib/chef/mixin/path_sanity.rb67
-rw-r--r--lib/chef/mixin/recipe_definition_dsl_core.rb33
-rw-r--r--lib/chef/mixin/securable.rb180
-rw-r--r--lib/chef/mixin/shell_out.rb69
-rw-r--r--lib/chef/mixin/template.rb100
-rw-r--r--lib/chef/mixin/why_run.rb339
-rw-r--r--lib/chef/mixin/xml_escape.rb140
-rw-r--r--lib/chef/mixins.rb14
-rw-r--r--lib/chef/monkey_patches/dir.rb36
-rw-r--r--lib/chef/monkey_patches/moneta.rb50
-rw-r--r--lib/chef/monkey_patches/net_http.rb22
-rw-r--r--lib/chef/monkey_patches/numeric.rb15
-rw-r--r--lib/chef/monkey_patches/object.rb9
-rw-r--r--lib/chef/monkey_patches/regexp.rb34
-rw-r--r--lib/chef/monkey_patches/string.rb49
-rw-r--r--lib/chef/monkey_patches/tempfile.rb64
-rw-r--r--lib/chef/nil_argument.rb3
-rw-r--r--lib/chef/node.rb476
-rw-r--r--lib/chef/node/attribute.rb254
-rw-r--r--lib/chef/node/attribute_collections.rb191
-rw-r--r--lib/chef/node/immutable_collections.rb387
-rw-r--r--lib/chef/platform.rb499
-rw-r--r--lib/chef/provider.rb233
-rw-r--r--lib/chef/provider/breakpoint.rb36
-rw-r--r--lib/chef/provider/cookbook_file.rb84
-rw-r--r--lib/chef/provider/cron.rb214
-rw-r--r--lib/chef/provider/cron/solaris.rb56
-rw-r--r--lib/chef/provider/deploy.rb480
-rw-r--r--lib/chef/provider/deploy/revision.rb80
-rw-r--r--lib/chef/provider/deploy/timestamped.rb32
-rw-r--r--lib/chef/provider/directory.rb128
-rw-r--r--lib/chef/provider/env.rb152
-rw-r--r--lib/chef/provider/env/windows.rb75
-rw-r--r--lib/chef/provider/erl_call.rb106
-rw-r--r--lib/chef/provider/execute.rb68
-rw-r--r--lib/chef/provider/file.rb338
-rw-r--r--lib/chef/provider/git.rb260
-rw-r--r--lib/chef/provider/group.rb159
-rw-r--r--lib/chef/provider/group/aix.rb70
-rw-r--r--lib/chef/provider/group/dscl.rb129
-rw-r--r--lib/chef/provider/group/gpasswd.rb65
-rw-r--r--lib/chef/provider/group/groupadd.rb96
-rw-r--r--lib/chef/provider/group/groupmod.rb120
-rw-r--r--lib/chef/provider/group/pw.rb93
-rw-r--r--lib/chef/provider/group/suse.rb60
-rw-r--r--lib/chef/provider/group/usermod.rb68
-rw-r--r--lib/chef/provider/group/windows.rb79
-rw-r--r--lib/chef/provider/http_request.rb136
-rw-r--r--lib/chef/provider/ifconfig.rb214
-rw-r--r--lib/chef/provider/link.rb130
-rw-r--r--lib/chef/provider/log.rb54
-rw-r--r--lib/chef/provider/mdadm.rb92
-rw-r--r--lib/chef/provider/mount.rb128
-rw-r--r--lib/chef/provider/mount/mount.rb252
-rw-r--r--lib/chef/provider/mount/windows.rb81
-rw-r--r--lib/chef/provider/ohai.rb47
-rw-r--r--lib/chef/provider/package.rb229
-rw-r--r--lib/chef/provider/package/apt.rb147
-rw-r--r--lib/chef/provider/package/dpkg.rb128
-rw-r--r--lib/chef/provider/package/easy_install.rb136
-rw-r--r--lib/chef/provider/package/freebsd.rb149
-rw-r--r--lib/chef/provider/package/ips.rb101
-rw-r--r--lib/chef/provider/package/macports.rb105
-rw-r--r--lib/chef/provider/package/pacman.rb111
-rw-r--r--lib/chef/provider/package/portage.rb138
-rw-r--r--lib/chef/provider/package/rpm.rb121
-rw-r--r--lib/chef/provider/package/rubygems.rb548
-rw-r--r--lib/chef/provider/package/smartos.rb84
-rw-r--r--lib/chef/provider/package/solaris.rb139
-rw-r--r--lib/chef/provider/package/yum-dump.py287
-rw-r--r--lib/chef/provider/package/yum.rb1214
-rw-r--r--lib/chef/provider/package/zypper.rb144
-rw-r--r--lib/chef/provider/remote_directory.rb174
-rw-r--r--lib/chef/provider/remote_file.rb138
-rw-r--r--lib/chef/provider/resource_update.rb55
-rw-r--r--lib/chef/provider/route.rb223
-rw-r--r--lib/chef/provider/ruby_block.rb42
-rw-r--r--lib/chef/provider/script.rb57
-rw-r--r--lib/chef/provider/service.rb158
-rw-r--r--lib/chef/provider/service/arch.rb113
-rw-r--r--lib/chef/provider/service/debian.rb152
-rw-r--r--lib/chef/provider/service/freebsd.rb175
-rw-r--r--lib/chef/provider/service/gentoo.rb67
-rw-r--r--lib/chef/provider/service/init.rb87
-rw-r--r--lib/chef/provider/service/insserv.rb52
-rw-r--r--lib/chef/provider/service/invokercd.rb35
-rw-r--r--lib/chef/provider/service/macosx.rb144
-rw-r--r--lib/chef/provider/service/redhat.rb77
-rw-r--r--lib/chef/provider/service/simple.rb172
-rw-r--r--lib/chef/provider/service/solaris.rb86
-rw-r--r--lib/chef/provider/service/systemd.rb115
-rw-r--r--lib/chef/provider/service/upstart.rb232
-rw-r--r--lib/chef/provider/service/windows.rb163
-rw-r--r--lib/chef/provider/subversion.rb214
-rw-r--r--lib/chef/provider/template.rb117
-rw-r--r--lib/chef/provider/user.rb207
-rw-r--r--lib/chef/provider/user/dscl.rb288
-rw-r--r--lib/chef/provider/user/pw.rb113
-rw-r--r--lib/chef/provider/user/useradd.rb144
-rw-r--r--lib/chef/provider/user/windows.rb124
-rw-r--r--lib/chef/providers.rb100
-rw-r--r--lib/chef/recipe.rb133
-rw-r--r--lib/chef/reserved_names.rb9
-rw-r--r--lib/chef/resource.rb853
-rw-r--r--lib/chef/resource/apt_package.rb43
-rw-r--r--lib/chef/resource/bash.rb33
-rw-r--r--lib/chef/resource/breakpoint.rb35
-rw-r--r--lib/chef/resource/chef_gem.rb53
-rw-r--r--lib/chef/resource/conditional.rb101
-rw-r--r--lib/chef/resource/cookbook_file.rb52
-rw-r--r--lib/chef/resource/cron.rb202
-rw-r--r--lib/chef/resource/csh.rb33
-rw-r--r--lib/chef/resource/deploy.rb403
-rw-r--r--lib/chef/resource/deploy_revision.rb40
-rw-r--r--lib/chef/resource/directory.rb65
-rw-r--r--lib/chef/resource/dpkg_package.rb34
-rw-r--r--lib/chef/resource/easy_install_package.rb57
-rw-r--r--lib/chef/resource/env.rb63
-rw-r--r--lib/chef/resource/erl_call.rb86
-rw-r--r--lib/chef/resource/execute.rb132
-rw-r--r--lib/chef/resource/file.rb96
-rw-r--r--lib/chef/resource/freebsd_package.rb35
-rw-r--r--lib/chef/resource/gem_package.rb53
-rw-r--r--lib/chef/resource/git.rb46
-rw-r--r--lib/chef/resource/group.rb83
-rw-r--r--lib/chef/resource/http_request.rb64
-rw-r--r--lib/chef/resource/ifconfig.rb149
-rw-r--r--lib/chef/resource/ips_package.rb42
-rw-r--r--lib/chef/resource/link.rb92
-rw-r--r--lib/chef/resource/log.rb65
-rw-r--r--lib/chef/resource/macports_package.rb29
-rw-r--r--lib/chef/resource/mdadm.rb105
-rw-r--r--lib/chef/resource/mount.rb139
-rw-r--r--lib/chef/resource/ohai.rb54
-rw-r--r--lib/chef/resource/package.rb84
-rw-r--r--lib/chef/resource/pacman_package.rb33
-rw-r--r--lib/chef/resource/perl.rb33
-rw-r--r--lib/chef/resource/portage_package.rb33
-rw-r--r--lib/chef/resource/python.rb33
-rw-r--r--lib/chef/resource/remote_directory.rb125
-rw-r--r--lib/chef/resource/remote_file.rb81
-rw-r--r--lib/chef/resource/route.rb140
-rw-r--r--lib/chef/resource/rpm_package.rb34
-rw-r--r--lib/chef/resource/ruby.rb33
-rw-r--r--lib/chef/resource/ruby_block.rb51
-rw-r--r--lib/chef/resource/scm.rb151
-rw-r--r--lib/chef/resource/script.rb63
-rw-r--r--lib/chef/resource/service.rb164
-rw-r--r--lib/chef/resource/smartos_package.rb36
-rw-r--r--lib/chef/resource/solaris_package.rb36
-rw-r--r--lib/chef/resource/subversion.rb37
-rw-r--r--lib/chef/resource/template.rb76
-rw-r--r--lib/chef/resource/timestamped_deploy.rb31
-rw-r--r--lib/chef/resource/user.rb134
-rw-r--r--lib/chef/resource/yum_package.rb63
-rw-r--r--lib/chef/resource_collection.rb217
-rw-r--r--lib/chef/resource_collection/stepable_iterator.rb124
-rw-r--r--lib/chef/resource_definition.rb67
-rw-r--r--lib/chef/resource_definition_list.rb38
-rw-r--r--lib/chef/resource_platform_map.rb151
-rw-r--r--lib/chef/resource_reporter.rb272
-rw-r--r--lib/chef/resources.rb67
-rw-r--r--lib/chef/rest.rb526
-rw-r--r--lib/chef/rest/auth_credentials.rb57
-rw-r--r--lib/chef/rest/cookie_jar.rb31
-rw-r--r--lib/chef/rest/rest_request.rb229
-rw-r--r--lib/chef/role.rb253
-rw-r--r--lib/chef/run_context.rb290
-rw-r--r--lib/chef/run_list.rb163
-rw-r--r--lib/chef/run_list/run_list_expansion.rb191
-rw-r--r--lib/chef/run_list/run_list_item.rb99
-rw-r--r--lib/chef/run_list/versioned_recipe_list.rb68
-rw-r--r--lib/chef/run_lock.rb84
-rw-r--r--lib/chef/run_status.rb124
-rw-r--r--lib/chef/runner.rb118
-rw-r--r--lib/chef/scan_access_control.rb135
-rw-r--r--lib/chef/search/query.rb65
-rw-r--r--lib/chef/shef/ext.rb19
-rw-r--r--lib/chef/shell.rb327
-rw-r--r--lib/chef/shell/ext.rb593
-rw-r--r--lib/chef/shell/model_wrapper.rb120
-rw-r--r--lib/chef/shell/shell_rest.rb28
-rw-r--r--lib/chef/shell/shell_session.rb298
-rw-r--r--lib/chef/shell_out.rb13
-rw-r--r--lib/chef/streaming_cookbook_uploader.rb201
-rw-r--r--lib/chef/tasks/chef_repo.rake334
-rw-r--r--lib/chef/util/file_edit.rb132
-rw-r--r--lib/chef/util/windows.rb56
-rw-r--r--lib/chef/util/windows/net_group.rb101
-rw-r--r--lib/chef/util/windows/net_use.rb121
-rw-r--r--lib/chef/util/windows/net_user.rb198
-rw-r--r--lib/chef/util/windows/volume.rb59
-rw-r--r--lib/chef/version.rb23
-rw-r--r--lib/chef/version_class.rb70
-rw-r--r--lib/chef/version_constraint.rb116
-rw-r--r--lib/chef/win32/api.rb364
-rw-r--r--lib/chef/win32/api/error.rb921
-rw-r--r--lib/chef/win32/api/file.rb535
-rw-r--r--lib/chef/win32/api/memory.rb105
-rw-r--r--lib/chef/win32/api/process.rb40
-rw-r--r--lib/chef/win32/api/psapi.rb51
-rw-r--r--lib/chef/win32/api/security.rb341
-rw-r--r--lib/chef/win32/api/system.rb192
-rw-r--r--lib/chef/win32/api/unicode.rb178
-rw-r--r--lib/chef/win32/error.rb73
-rw-r--r--lib/chef/win32/file.rb167
-rw-r--r--lib/chef/win32/file/info.rb100
-rw-r--r--lib/chef/win32/handle.rb48
-rw-r--r--lib/chef/win32/memory.rb101
-rw-r--r--lib/chef/win32/process.rb84
-rw-r--r--lib/chef/win32/security.rb489
-rw-r--r--lib/chef/win32/security/ace.rb125
-rw-r--r--lib/chef/win32/security/acl.rb101
-rw-r--r--lib/chef/win32/security/securable_object.rb109
-rw-r--r--lib/chef/win32/security/security_descriptor.rb93
-rw-r--r--lib/chef/win32/security/sid.rb199
-rw-r--r--lib/chef/win32/security/token.rb64
-rw-r--r--lib/chef/win32/unicode.rb43
-rw-r--r--lib/chef/win32/version.rb119
419 files changed, 53709 insertions, 0 deletions
diff --git a/lib/chef.rb b/lib/chef.rb
new file mode 100644
index 0000000000..ef173c4212
--- /dev/null
+++ b/lib/chef.rb
@@ -0,0 +1,39 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/version'
+require 'chef/nil_argument'
+require 'chef/mash'
+require 'chef/exceptions'
+require 'chef/log'
+require 'chef/config'
+require 'chef/providers'
+require 'chef/resources'
+require 'chef/shell_out'
+
+require 'chef/daemon'
+
+require 'chef/run_status'
+require 'chef/handler'
+require 'chef/handler/json_file'
+
+require 'chef/monkey_patches/tempfile'
+require 'chef/monkey_patches/dir'
+require 'chef/monkey_patches/string'
+require 'chef/monkey_patches/numeric'
+require 'chef/monkey_patches/object'
diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb
new file mode 100644
index 0000000000..da05939c24
--- /dev/null
+++ b/lib/chef/api_client.rb
@@ -0,0 +1,181 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/from_file'
+require 'chef/mash'
+require 'chef/json_compat'
+require 'chef/search/query'
+
+class Chef
+ class ApiClient
+
+ include Chef::Mixin::FromFile
+ include Chef::Mixin::ParamsValidate
+
+ # Create a new Chef::ApiClient object.
+ def initialize
+ @name = ''
+ @public_key = nil
+ @private_key = nil
+ @admin = false
+ end
+
+ # Gets or sets the client name.
+ #
+ # @params [Optional String] The name must be alpha-numeric plus - and _.
+ # @return [String] The current value of the name.
+ def name(arg=nil)
+ set_or_return(
+ :name,
+ arg,
+ :regex => /^[\-[:alnum:]_\.]+$/
+ )
+ end
+
+ # Gets or sets whether this client is an admin.
+ #
+ # @params [Optional True/False] Should be true or false - default is false.
+ # @return [True/False] The current value
+ def admin(arg=nil)
+ set_or_return(
+ :admin,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ # Gets or sets the public key.
+ #
+ # @params [Optional String] The string representation of the public key.
+ # @return [String] The current value.
+ def public_key(arg=nil)
+ set_or_return(
+ :public_key,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ # Gets or sets the private key.
+ #
+ # @params [Optional String] The string representation of the private key.
+ # @return [String] The current value.
+ def private_key(arg=nil)
+ set_or_return(
+ :private_key,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ # The hash representation of the object. Includes the name and public_key,
+ # but never the private key.
+ #
+ # @return [Hash]
+ def to_hash
+ result = {
+ "name" => @name,
+ "public_key" => @public_key,
+ "admin" => @admin,
+ 'json_class' => self.class.name,
+ "chef_type" => "client"
+ }
+ result
+ end
+
+ # The JSON representation of the object.
+ #
+ # @return [String] the JSON string.
+ def to_json(*a)
+ to_hash.to_json(*a)
+ end
+
+ def self.json_create(o)
+ client = Chef::ApiClient.new
+ client.name(o["name"] || o["clientname"])
+ client.public_key(o["public_key"])
+ client.admin(o["admin"])
+ client
+ end
+
+ def self.list(inflate=false)
+ if inflate
+ response = Hash.new
+ Chef::Search::Query.new.search(:client) do |n|
+ n = self.json_create(n) if n.instance_of?(Hash)
+ response[n.name] = n
+ end
+ response
+ else
+ Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("clients")
+ end
+ end
+
+ # Load a client by name via the API
+ def self.load(name)
+ response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("clients/#{name}")
+ if response.kind_of?(Chef::ApiClient)
+ response
+ else
+ client = Chef::ApiClient.new
+ client.name(response['clientname'])
+ client
+ end
+ end
+
+ # Remove this client via the REST API
+ def destroy
+ Chef::REST.new(Chef::Config[:chef_server_url]).delete_rest("clients/#{@name}")
+ end
+
+ # Save this client via the REST API, returns a hash including the private key
+ def save(new_key=false, validation=false)
+ if validation
+ r = Chef::REST.new(Chef::Config[:chef_server_url], Chef::Config[:validation_client_name], Chef::Config[:validation_key])
+ else
+ r = Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+ # First, try and create a new registration
+ begin
+ r.post_rest("clients", {:name => self.name, :admin => self.admin })
+ rescue Net::HTTPServerException => e
+ # If that fails, go ahead and try and update it
+ if e.response.code == "409"
+ r.put_rest("clients/#{name}", { :name => self.name, :admin => self.admin, :private_key => new_key })
+ else
+ raise e
+ end
+ end
+ end
+
+ # Create the client via the REST API
+ def create
+ Chef::REST.new(Chef::Config[:chef_server_url]).post_rest("clients", self)
+ end
+
+ # As a string
+ def to_s
+ "client[#{@name}]"
+ end
+
+ end
+end
+
diff --git a/lib/chef/application.rb b/lib/chef/application.rb
new file mode 100644
index 0000000000..328a81a2eb
--- /dev/null
+++ b/lib/chef/application.rb
@@ -0,0 +1,162 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Author:: Mark Mzyk (mmzyk@opscode.com)
+# Copyright:: Copyright (c) 2008 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 'socket'
+require 'chef/config'
+require 'chef/exceptions'
+require 'chef/log'
+require 'chef/platform'
+require 'mixlib/cli'
+require 'tmpdir'
+require 'rbconfig'
+
+class Chef::Application
+ include Mixlib::CLI
+
+ class Wakeup < Exception
+ end
+
+ def initialize
+ super
+
+ trap("TERM") do
+ Chef::Application.fatal!("SIGTERM received, stopping", 1)
+ end
+
+ trap("INT") do
+ Chef::Application.fatal!("SIGINT received, stopping", 2)
+ end
+
+ unless Chef::Platform.windows?
+ trap("QUIT") do
+ Chef::Log.info("SIGQUIT received, call stack:\n " + caller.join("\n "))
+ end
+
+ trap("HUP") do
+ Chef::Log.info("SIGHUP received, reconfiguring")
+ reconfigure
+ end
+ end
+
+ # Always switch to a readable directory. Keeps subsequent Dir.chdir() {}
+ # from failing due to permissions when launched as a less privileged user.
+ end
+
+ # Reconfigure the application. You'll want to override and super this method.
+ def reconfigure
+ configure_chef
+ configure_logging
+ end
+
+ # Get this party started
+ def run
+ reconfigure
+ setup_application
+ run_application
+ end
+
+ # Parse the configuration file
+ def configure_chef
+ parse_options
+
+ begin
+ case config[:config_file]
+ when /^(http|https):\/\//
+ Chef::REST.new("", nil, nil).fetch(config[:config_file]) { |f| apply_config(f.path) }
+ else
+ ::File::open(config[:config_file]) { |f| apply_config(f.path) }
+ end
+ rescue Errno::ENOENT => error
+ Chef::Log.warn("*****************************************")
+ Chef::Log.warn("Did not find config file: #{config[:config_file]}, using command line options.")
+ Chef::Log.warn("*****************************************")
+
+ Chef::Config.merge!(config)
+ rescue SocketError => error
+ Chef::Application.fatal!("Error getting config file #{Chef::Config[:config_file]}", 2)
+ rescue Chef::Exceptions::ConfigurationError => error
+ Chef::Application.fatal!("Error processing config file #{Chef::Config[:config_file]} with error #{error.message}", 2)
+ rescue Exception => error
+ Chef::Application.fatal!("Unknown error processing config file #{Chef::Config[:config_file]} with error #{error.message}", 2)
+ end
+
+ end
+
+ # Initialize and configure the logger. If the configured log location is not
+ # STDOUT, but stdout is a TTY and we're not daemonizing, we set up a secondary
+ # logger with output to stdout. This way, we magically do the right thing when
+ # the user has configured logging to a file but they're running chef in the
+ # shell to debug something.
+ #
+ # If the user has configured a formatter, then we skip the magical logger to
+ # keep the output pretty.
+ def configure_logging
+ require 'pp'
+ Chef::Log.init(Chef::Config[:log_location])
+ if ( Chef::Config[:log_location] != STDOUT ) && STDOUT.tty? && (!Chef::Config[:daemonize]) && (Chef::Config.formatter == "null")
+ stdout_logger = Logger.new(STDOUT)
+ STDOUT.sync = true
+ stdout_logger.formatter = Chef::Log.logger.formatter
+ Chef::Log.loggers << stdout_logger
+ end
+ Chef::Log.level = Chef::Config[:log_level]
+ end
+
+ # Called prior to starting the application, by the run method
+ def setup_application
+ raise Chef::Exceptions::Application, "#{self.to_s}: you must override setup_application"
+ end
+
+ # Actually run the application
+ def run_application
+ raise Chef::Exceptions::Application, "#{self.to_s}: you must override run_application"
+ end
+
+ private
+
+ def apply_config(config_file_path)
+ Chef::Config.from_file(config_file_path)
+ Chef::Config.merge!(config)
+ end
+
+
+ class << self
+ def debug_stacktrace(e)
+ message = "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
+ chef_stacktrace_out = "Generated at #{Time.now.to_s}\n"
+ chef_stacktrace_out += message
+
+ Chef::FileCache.store("chef-stacktrace.out", chef_stacktrace_out)
+ Chef::Log.fatal("Stacktrace dumped to #{Chef::FileCache.load("chef-stacktrace.out", false)}")
+ Chef::Log.debug(message)
+ true
+ end
+
+ # Log a fatal error message to both STDERR and the Logger, exit the application
+ def fatal!(msg, err = -1)
+ Chef::Log.fatal(msg)
+ Process.exit err
+ end
+
+ def exit!(msg, err = -1)
+ Chef::Log.debug(msg)
+ Process.exit err
+ end
+ end
+
+end
diff --git a/lib/chef/application/agent.rb b/lib/chef/application/agent.rb
new file mode 100644
index 0000000000..353d45252e
--- /dev/null
+++ b/lib/chef/application/agent.rb
@@ -0,0 +1,18 @@
+#
+# Author:: AJ Christensen (<aj@opscode.comz>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/application'
diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb
new file mode 100644
index 0000000000..3a68a41c14
--- /dev/null
+++ b/lib/chef/application/client.rb
@@ -0,0 +1,327 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Mark Mzyk (mmzyk@opscode.com)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/application'
+require 'chef/client'
+require 'chef/config'
+require 'chef/daemon'
+require 'chef/log'
+require 'chef/rest'
+require 'chef/handler/error_report'
+
+
+class Chef::Application::Client < Chef::Application
+
+ # Mimic self_pipe sleep from Unicorn to capture signals safely
+ SELF_PIPE = []
+
+ option :config_file,
+ :short => "-c CONFIG",
+ :long => "--config CONFIG",
+ :default => Chef::Config.platform_specific_path("/etc/chef/client.rb"),
+ :description => "The configuration file to use"
+
+ option :formatter,
+ :short => "-F FORMATTER",
+ :long => "--format FORMATTER",
+ :description => "output format to use"
+
+ option :color,
+ :long => '--[no-]color',
+ :boolean => true,
+ :default => false,
+ :description => "Use colored output, defaults to enabled"
+
+ option :log_level,
+ :short => "-l LEVEL",
+ :long => "--log_level LEVEL",
+ :description => "Set the log level (debug, info, warn, error, fatal)",
+ :proc => lambda { |l| l.to_sym }
+
+ option :log_location,
+ :short => "-L LOGLOCATION",
+ :long => "--logfile LOGLOCATION",
+ :description => "Set the log file location, defaults to STDOUT - recommended for daemonizing",
+ :proc => nil
+
+ option :help,
+ :short => "-h",
+ :long => "--help",
+ :description => "Show this message",
+ :on => :tail,
+ :boolean => true,
+ :show_options => true,
+ :exit => 0
+
+ option :user,
+ :short => "-u USER",
+ :long => "--user USER",
+ :description => "User to set privilege to",
+ :proc => nil
+
+ option :group,
+ :short => "-g GROUP",
+ :long => "--group GROUP",
+ :description => "Group to set privilege to",
+ :proc => nil
+
+ option :daemonize,
+ :short => "-d",
+ :long => "--daemonize",
+ :description => "Daemonize the process",
+ :proc => lambda { |p| true }
+
+ option :pid_file,
+ :short => "-P PID_FILE",
+ :long => "--pid PIDFILE",
+ :description => "Set the PID file location, defaults to /tmp/chef-client.pid",
+ :proc => nil
+
+ option :interval,
+ :short => "-i SECONDS",
+ :long => "--interval SECONDS",
+ :description => "Run chef-client periodically, in seconds",
+ :proc => lambda { |s| s.to_i }
+
+ option :once,
+ :long => "--once",
+ :description => "Cancel any interval or splay options, run chef once and exit",
+ :boolean => true
+
+ option :json_attribs,
+ :short => "-j JSON_ATTRIBS",
+ :long => "--json-attributes JSON_ATTRIBS",
+ :description => "Load attributes from a JSON file or URL",
+ :proc => nil
+
+ option :node_name,
+ :short => "-N NODE_NAME",
+ :long => "--node-name NODE_NAME",
+ :description => "The node name for this client",
+ :proc => nil
+
+ option :splay,
+ :short => "-s SECONDS",
+ :long => "--splay SECONDS",
+ :description => "The splay time for running at intervals, in seconds",
+ :proc => lambda { |s| s.to_i }
+
+ option :chef_server_url,
+ :short => "-S CHEFSERVERURL",
+ :long => "--server CHEFSERVERURL",
+ :description => "The chef server URL",
+ :proc => nil
+
+ option :validation_key,
+ :short => "-K KEY_FILE",
+ :long => "--validation_key KEY_FILE",
+ :description => "Set the validation key file location, used for registering new clients",
+ :proc => nil
+
+ option :client_key,
+ :short => "-k KEY_FILE",
+ :long => "--client_key KEY_FILE",
+ :description => "Set the client key file location",
+ :proc => nil
+
+ option :environment,
+ :short => '-E ENVIRONMENT',
+ :long => '--environment ENVIRONMENT',
+ :description => 'Set the Chef Environment on the node'
+
+ option :version,
+ :short => "-v",
+ :long => "--version",
+ :description => "Show chef version",
+ :boolean => true,
+ :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"},
+ :exit => 0
+
+ option :override_runlist,
+ :short => "-o RunlistItem,RunlistItem...",
+ :long => "--override-runlist RunlistItem,RunlistItem...",
+ :description => "Replace current run list with specified items",
+ :proc => lambda{|items|
+ items = items.split(',')
+ items.compact.map{|item|
+ Chef::RunList::RunListItem.new(item)
+ }
+ }
+
+ option :why_run,
+ :short => '-W',
+ :long => '--why-run',
+ :description => 'Enable whyrun mode',
+ :boolean => true
+
+ option :client_fork,
+ :short => "-f",
+ :long => "--fork",
+ :description => "Fork client",
+ :boolean => true
+
+ option :enable_reporting,
+ :short => "-R",
+ :long => "--enable-reporting",
+ :description => "Enable reporting data collection for chef runs",
+ :boolean => true
+
+ attr_reader :chef_client_json
+
+ def initialize
+ super
+
+ @chef_client = nil
+ @chef_client_json = nil
+ end
+
+ # Reconfigure the chef client
+ # Re-open the JSON attributes and load them into the node
+ def reconfigure
+ super
+
+ Chef::Config[:chef_server_url] = config[:chef_server_url] if config.has_key? :chef_server_url
+ unless Chef::Config[:exception_handlers].any? {|h| Chef::Handler::ErrorReport === h}
+ Chef::Config[:exception_handlers] << Chef::Handler::ErrorReport.new
+ end
+
+ if Chef::Config[:daemonize]
+ Chef::Config[:interval] ||= 1800
+ end
+
+ if Chef::Config[:once]
+ Chef::Config[:interval] = nil
+ Chef::Config[:splay] = nil
+ end
+
+ if Chef::Config[:json_attribs]
+ begin
+ json_io = case Chef::Config[:json_attribs]
+ when /^(http|https):\/\//
+ @rest = Chef::REST.new(Chef::Config[:json_attribs], nil, nil)
+ @rest.get_rest(Chef::Config[:json_attribs], true).open
+ else
+ open(Chef::Config[:json_attribs])
+ end
+ rescue SocketError => error
+ Chef::Application.fatal!("I cannot connect to #{Chef::Config[:json_attribs]}", 2)
+ rescue Errno::ENOENT => error
+ Chef::Application.fatal!("I cannot find #{Chef::Config[:json_attribs]}", 2)
+ rescue Errno::EACCES => error
+ Chef::Application.fatal!("Permissions are incorrect on #{Chef::Config[:json_attribs]}. Please chmod a+r #{Chef::Config[:json_attribs]}", 2)
+ rescue Exception => error
+ Chef::Application.fatal!("Got an unexpected error reading #{Chef::Config[:json_attribs]}: #{error.message}", 2)
+ end
+
+ begin
+ @chef_client_json = Chef::JSONCompat.from_json(json_io.read)
+ json_io.close unless json_io.closed?
+ rescue JSON::ParserError => error
+ Chef::Application.fatal!("Could not parse the provided JSON file (#{Chef::Config[:json_attribs]})!: " + error.message, 2)
+ end
+ end
+ end
+
+ def configure_logging
+ super
+ Mixlib::Authentication::Log.use_log_devices( Chef::Log )
+ Ohai::Log.use_log_devices( Chef::Log )
+ end
+
+ def setup_application
+ Chef::Daemon.change_privilege
+ end
+
+ # Run the chef client, optionally daemonizing or looping at intervals.
+ def run_application
+ unless Chef::Platform.windows?
+ SELF_PIPE.replace IO.pipe
+
+ trap("USR1") do
+ Chef::Log.info("SIGUSR1 received, waking up")
+ SELF_PIPE[1].putc('.') # wakeup master process from select
+ end
+ end
+
+ if Chef::Config[:version]
+ puts "Chef version: #{::Chef::VERSION}"
+ end
+
+ if Chef::Config[:daemonize]
+ Chef::Daemon.daemonize("chef-client")
+ end
+
+ loop do
+ begin
+ if Chef::Config[:splay]
+ splay = rand Chef::Config[:splay]
+ Chef::Log.debug("Splay sleep #{splay} seconds")
+ sleep splay
+ end
+ @chef_client = Chef::Client.new(
+ @chef_client_json,
+ :override_runlist => config[:override_runlist]
+ )
+ @chef_client_json = nil
+
+ @chef_client.run
+ @chef_client = nil
+ if Chef::Config[:interval]
+ Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds")
+ unless SELF_PIPE.empty?
+ client_sleep Chef::Config[:interval]
+ else
+ # Windows
+ sleep Chef::Config[:interval]
+ end
+ else
+ Chef::Application.exit! "Exiting", 0
+ end
+ rescue Chef::Application::Wakeup => e
+ Chef::Log.debug("Received Wakeup signal. Starting run.")
+ next
+ rescue SystemExit => e
+ raise
+ rescue Exception => e
+ if Chef::Config[:interval]
+ Chef::Log.error("#{e.class}: #{e}")
+ Chef::Application.debug_stacktrace(e)
+ Chef::Log.error("Sleeping for #{Chef::Config[:interval]} seconds before trying again")
+ unless SELF_PIPE.empty?
+ client_sleep Chef::Config[:interval]
+ else
+ # Windows
+ sleep Chef::Config[:interval]
+ end
+ retry
+ else
+ Chef::Application.debug_stacktrace(e)
+ Chef::Application.fatal!("#{e.class}: #{e.message}", 1)
+ end
+ end
+ end
+ end
+
+ private
+
+ def client_sleep(sec)
+ IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return
+ SELF_PIPE[0].getc
+ end
+end
diff --git a/lib/chef/application/knife.rb b/lib/chef/application/knife.rb
new file mode 100644
index 0000000000..629cd9fc5f
--- /dev/null
+++ b/lib/chef/application/knife.rb
@@ -0,0 +1,183 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/knife'
+require 'chef/application'
+require 'mixlib/log'
+require 'ohai/config'
+
+class Chef::Application::Knife < Chef::Application
+
+ NO_COMMAND_GIVEN = "You need to pass a sub-command (e.g., knife SUB-COMMAND)\n"
+
+ banner "Usage: knife sub-command (options)"
+
+ option :config_file,
+ :short => "-c CONFIG",
+ :long => "--config CONFIG",
+ :description => "The configuration file to use",
+ :proc => lambda { |path| File.expand_path(path, Dir.pwd) }
+
+ verbosity_level = 0
+ option :verbosity,
+ :short => '-V',
+ :long => '--verbose',
+ :description => "More verbose output. Use twice for max verbosity",
+ :proc => Proc.new { verbosity_level += 1},
+ :default => 0
+
+ option :color,
+ :long => '--[no-]color',
+ :boolean => true,
+ :default => true,
+ :description => "Use colored output, defaults to enabled"
+
+ option :environment,
+ :short => "-E ENVIRONMENT",
+ :long => "--environment ENVIRONMENT",
+ :description => "Set the Chef environment"
+
+ option :editor,
+ :short => "-e EDITOR",
+ :long => "--editor EDITOR",
+ :description => "Set the editor to use for interactive commands",
+ :default => ENV['EDITOR']
+
+ option :disable_editing,
+ :short => "-d",
+ :long => "--disable-editing",
+ :description => "Do not open EDITOR, just accept the data as is",
+ :boolean => true,
+ :defaut => false
+
+ option :help,
+ :short => "-h",
+ :long => "--help",
+ :description => "Show this message",
+ :on => :tail,
+ :boolean => true
+
+ option :node_name,
+ :short => "-u USER",
+ :long => "--user USER",
+ :description => "API Client Username"
+
+ option :client_key,
+ :short => "-k KEY",
+ :long => "--key KEY",
+ :description => "API Client Key",
+ :proc => lambda { |path| File.expand_path(path, Dir.pwd) }
+
+ option :chef_server_url,
+ :short => "-s URL",
+ :long => "--server-url URL",
+ :description => "Chef Server URL"
+
+ option :yes,
+ :short => "-y",
+ :long => "--yes",
+ :description => "Say yes to all prompts for confirmation"
+
+ option :defaults,
+ :long => "--defaults",
+ :description => "Accept default values for all questions"
+
+ option :print_after,
+ :long => "--print-after",
+ :description => "Show the data after a destructive operation"
+
+ option :format,
+ :short => "-F FORMAT",
+ :long => "--format FORMAT",
+ :description => "Which format to use for output",
+ :default => "summary"
+
+ option :version,
+ :short => "-v",
+ :long => "--version",
+ :description => "Show chef version",
+ :boolean => true,
+ :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"},
+ :exit => 0
+
+
+ # Run knife
+ def run
+ Mixlib::Log::Formatter.show_time = false
+ validate_and_parse_options
+ quiet_traps
+ Chef::Knife.run(ARGV, options)
+ exit 0
+ end
+
+ private
+
+ def quiet_traps
+ trap("TERM") do
+ exit 1
+ end
+
+ trap("INT") do
+ exit 2
+ end
+ end
+
+ def validate_and_parse_options
+ # Checking ARGV validity *before* parse_options because parse_options
+ # mangles ARGV in some situations
+ if no_command_given?
+ print_help_and_exit(1, NO_COMMAND_GIVEN)
+ elsif no_subcommand_given?
+ if (want_help? || want_version?)
+ print_help_and_exit
+ else
+ print_help_and_exit(2, NO_COMMAND_GIVEN)
+ end
+ end
+ end
+
+ def no_subcommand_given?
+ ARGV[0] =~ /^-/
+ end
+
+ def no_command_given?
+ ARGV.empty?
+ end
+
+ def want_help?
+ ARGV[0] =~ /^(--help|-h)$/
+ end
+
+ def want_version?
+ ARGV[0] =~ /^(--version|-v)$/
+ end
+
+ def print_help_and_exit(exitcode=1, fatal_message=nil)
+ Chef::Log.error(fatal_message) if fatal_message
+
+ begin
+ self.parse_options
+ rescue OptionParser::InvalidOption => e
+ puts "#{e}\n"
+ end
+ puts self.opt_parser
+ puts
+ Chef::Knife.list_commands
+ exit exitcode
+ end
+
+end
diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb
new file mode 100644
index 0000000000..7ec6c36e76
--- /dev/null
+++ b/lib/chef/application/solo.rb
@@ -0,0 +1,254 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Author:: Mark Mzyk (mmzyk@opscode.com)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef'
+require 'chef/application'
+require 'chef/client'
+require 'chef/config'
+require 'chef/daemon'
+require 'chef/log'
+require 'chef/rest'
+require 'open-uri'
+require 'fileutils'
+
+class Chef::Application::Solo < Chef::Application
+
+ option :config_file,
+ :short => "-c CONFIG",
+ :long => "--config CONFIG",
+ :default => Chef::Config.platform_specfic_path('/etc/chef/solo.rb'),
+ :description => "The configuration file to use"
+
+ option :formatter,
+ :short => "-F FORMATTER",
+ :long => "--format FORMATTER",
+ :description => "output format to use"
+
+ option :color,
+ :long => '--[no-]color',
+ :boolean => true,
+ :default => false,
+ :description => "Use colored output, defaults to disabled"
+
+ option :log_level,
+ :short => "-l LEVEL",
+ :long => "--log_level LEVEL",
+ :description => "Set the log level (debug, info, warn, error, fatal)",
+ :proc => lambda { |l| l.to_sym }
+
+ option :log_location,
+ :short => "-L LOGLOCATION",
+ :long => "--logfile LOGLOCATION",
+ :description => "Set the log file location, defaults to STDOUT",
+ :proc => nil
+
+ option :help,
+ :short => "-h",
+ :long => "--help",
+ :description => "Show this message",
+ :on => :tail,
+ :boolean => true,
+ :show_options => true,
+ :exit => 0
+
+ option :user,
+ :short => "-u USER",
+ :long => "--user USER",
+ :description => "User to set privilege to",
+ :proc => nil
+
+ option :group,
+ :short => "-g GROUP",
+ :long => "--group GROUP",
+ :description => "Group to set privilege to",
+ :proc => nil
+
+ option :daemonize,
+ :short => "-d",
+ :long => "--daemonize",
+ :description => "Daemonize the process",
+ :proc => lambda { |p| true }
+
+ option :interval,
+ :short => "-i SECONDS",
+ :long => "--interval SECONDS",
+ :description => "Run chef-client periodically, in seconds",
+ :proc => lambda { |s| s.to_i }
+
+ option :json_attribs,
+ :short => "-j JSON_ATTRIBS",
+ :long => "--json-attributes JSON_ATTRIBS",
+ :description => "Load attributes from a JSON file or URL",
+ :proc => nil
+
+ option :node_name,
+ :short => "-N NODE_NAME",
+ :long => "--node-name NODE_NAME",
+ :description => "The node name for this client",
+ :proc => nil
+
+ option :splay,
+ :short => "-s SECONDS",
+ :long => "--splay SECONDS",
+ :description => "The splay time for running at intervals, in seconds",
+ :proc => lambda { |s| s.to_i }
+
+ option :recipe_url,
+ :short => "-r RECIPE_URL",
+ :long => "--recipe-url RECIPE_URL",
+ :description => "Pull down a remote gzipped tarball of recipes and untar it to the cookbook cache.",
+ :proc => nil
+
+ option :version,
+ :short => "-v",
+ :long => "--version",
+ :description => "Show chef version",
+ :boolean => true,
+ :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"},
+ :exit => 0
+
+ option :override_runlist,
+ :short => "-o RunlistItem,RunlistItem...",
+ :long => "--override-runlist RunlistItem,RunlistItem...",
+ :description => "Replace current run list with specified items",
+ :proc => lambda{|items|
+ items = items.split(',')
+ items.compact.map{|item|
+ Chef::RunList::RunListItem.new(item)
+ }
+ }
+
+ option :client_fork,
+ :short => "-f",
+ :long => "--fork",
+ :description => "Fork client",
+ :boolean => true
+
+ option :why_run,
+ :short => '-W',
+ :long => '--why-run',
+ :description => 'Enable whyrun mode',
+ :boolean => true
+
+ attr_reader :chef_solo_json
+
+ def initialize
+ super
+ @chef_solo = nil
+ @chef_solo_json = nil
+ end
+
+ def reconfigure
+ super
+
+ Chef::Config[:solo] = true
+
+ if Chef::Config[:daemonize]
+ Chef::Config[:interval] ||= 1800
+ end
+
+ if Chef::Config[:json_attribs]
+ begin
+ json_io = case Chef::Config[:json_attribs]
+ when /^(http|https):\/\//
+ @rest = Chef::REST.new(Chef::Config[:json_attribs], nil, nil)
+ @rest.get_rest(Chef::Config[:json_attribs], true).open
+ else
+ open(Chef::Config[:json_attribs])
+ end
+ rescue SocketError => error
+ Chef::Application.fatal!("I cannot connect to #{Chef::Config[:json_attribs]}", 2)
+ rescue Errno::ENOENT => error
+ Chef::Application.fatal!("I cannot find #{Chef::Config[:json_attribs]}", 2)
+ rescue Errno::EACCES => error
+ Chef::Application.fatal!("Permissions are incorrect on #{Chef::Config[:json_attribs]}. Please chmod a+r #{Chef::Config[:json_attribs]}", 2)
+ rescue Exception => error
+ Chef::Application.fatal!("Got an unexpected error reading #{Chef::Config[:json_attribs]}: #{error.message}", 2)
+ end
+
+ begin
+ @chef_solo_json = Chef::JSONCompat.from_json(json_io.read)
+ json_io.close unless json_io.closed?
+ rescue JSON::ParserError => error
+ Chef::Application.fatal!("Could not parse the provided JSON file (#{Chef::Config[:json_attribs]})!: " + error.message, 2)
+ end
+ end
+
+ if Chef::Config[:recipe_url]
+ cookbooks_path = Array(Chef::Config[:cookbook_path]).detect{|e| e =~ /\/cookbooks\/*$/ }
+ recipes_path = File.expand_path(File.join(cookbooks_path, '..'))
+ target_file = File.join(recipes_path, 'recipes.tgz')
+
+ Chef::Log.debug "Creating path #{recipes_path} to extract recipes into"
+ FileUtils.mkdir_p recipes_path
+ path = File.join(recipes_path, 'recipes.tgz')
+ File.open(path, 'wb') do |f|
+ open(Chef::Config[:recipe_url]) do |r|
+ f.write(r.read)
+ end
+ end
+ Chef::Mixin::Command.run_command(:command => "tar zxvfC #{path} #{recipes_path}")
+ end
+ end
+
+ def setup_application
+ Chef::Daemon.change_privilege
+ end
+
+ def run_application
+ if Chef::Config[:daemonize]
+ Chef::Daemon.daemonize("chef-client")
+ end
+
+ loop do
+ begin
+ if Chef::Config[:splay]
+ splay = rand Chef::Config[:splay]
+ Chef::Log.debug("Splay sleep #{splay} seconds")
+ sleep splay
+ end
+
+ @chef_solo = Chef::Client.new(
+ @chef_solo_json,
+ :override_runlist => config[:override_runlist]
+ )
+ @chef_solo.run
+ @chef_solo = nil
+ if Chef::Config[:interval]
+ Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds")
+ sleep Chef::Config[:interval]
+ else
+ Chef::Application.exit! "Exiting", 0
+ end
+ rescue SystemExit => e
+ raise
+ rescue Exception => e
+ if Chef::Config[:interval]
+ Chef::Log.error("#{e.class}: #{e}")
+ Chef::Log.debug("#{e.class}: #{e}\n#{e.backtrace.join("\n")}")
+ Chef::Log.fatal("Sleeping for #{Chef::Config[:interval]} seconds before trying again")
+ sleep Chef::Config[:interval]
+ retry
+ else
+ Chef::Application.debug_stacktrace(e)
+ Chef::Application.fatal!("#{e.class}: #{e.message}", 1)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/application/windows_service.rb b/lib/chef/application/windows_service.rb
new file mode 100644
index 0000000000..961ec8f92f
--- /dev/null
+++ b/lib/chef/application/windows_service.rb
@@ -0,0 +1,237 @@
+#
+# Author:: Christopher Maier (<maier@lambda.local>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef'
+require 'chef/application'
+require 'chef/client'
+require 'chef/config'
+require 'chef/handler/error_report'
+require 'chef/log'
+require 'chef/rest'
+require 'mixlib/cli'
+require 'socket'
+require 'win32/daemon'
+
+class Chef
+ class Application
+ class WindowsService < ::Win32::Daemon
+ include Mixlib::CLI
+
+ option :config_file,
+ :short => "-c CONFIG",
+ :long => "--config CONFIG",
+ :default => "#{ENV['SYSTEMDRIVE']}/chef/client.rb",
+ :description => ""
+
+ option :log_location,
+ :short => "-L LOGLOCATION",
+ :long => "--logfile LOGLOCATION",
+ :description => "Set the log file location",
+ :default => "#{ENV['SYSTEMDRIVE']}/chef/client.log"
+
+ option :splay,
+ :short => "-s SECONDS",
+ :long => "--splay SECONDS",
+ :description => "The splay time for running at intervals, in seconds",
+ :proc => lambda { |s| s.to_i }
+
+ option :interval,
+ :short => "-i SECONDS",
+ :long => "--interval SECONDS",
+ :description => "Set the number of seconds to wait between chef-client runs",
+ :proc => lambda { |s| s.to_i }
+
+ option :override_runlist,
+ :short => "-o RunlistItem,RunlistItem...",
+ :long => "--override-runlist RunlistItem,RunlistItem...",
+ :description => "Replace current run list with specified items",
+ :proc => lambda{|items|
+ items = items.split(',')
+ items.compact.map{|item|
+ Chef::RunList::RunListItem.new(item)
+ }
+ }
+
+ def service_init
+ reconfigure
+ Chef::Log.info("Chef Client Service initialized")
+ end
+
+ def service_main(*startup_parameters)
+
+ while running?
+ if state == RUNNING
+ begin
+ # Reconfigure each time through to pick up any changes in the client file
+ Chef::Log.info("Reconfiguring with startup parameters")
+ reconfigure(startup_parameters)
+
+ splay = rand Chef::Config[:splay]
+ Chef::Log.debug("Splay sleep #{splay} seconds")
+ sleep splay
+
+ # If we've stopped, then bail out now, instead of going on to run Chef
+ next if state != RUNNING
+
+ @chef_client = Chef::Client.new(
+ @chef_client_json,
+ :override_runlist => config[:override_runlist]
+ )
+ @chef_client_json = nil
+
+ @chef_client.run
+ @chef_client = nil
+
+ Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds")
+ client_sleep Chef::Config[:interval]
+ rescue Chef::Application::Wakeup => e
+ Chef::Log.debug("Received Wakeup signal. Starting run.")
+ next
+ rescue SystemExit => e
+ raise
+ rescue Exception => e
+ Chef::Log.error("#{e.class}: #{e}")
+ Chef::Application.debug_stacktrace(e)
+ Chef::Log.error("Sleeping for #{Chef::Config[:interval]} seconds before trying again")
+ client_sleep Chef::Config[:interval]
+ retry
+ end
+ else # PAUSED or IDLE
+ sleep 5
+ end
+ end
+ end
+
+ ################################################################################
+ # Control Signal Callback Methods
+ ################################################################################
+
+ def service_stop
+ Chef::Log.info("SERVICE_CONTROL_STOP received, stopping")
+ end
+
+ def service_pause
+ Chef::Log.info("SERVICE_CONTROL_PAUSE received, pausing")
+ end
+
+ def service_resume
+ Chef::Log.info("SERVICE_CONTROL_CONTINUE received, resuming")
+ end
+
+ def service_shutdown
+ Chef::Log.info("SERVICE_CONTROL_SHUTDOWN received, shutting down")
+ end
+
+ ################################################################################
+ # Internal Methods
+ ################################################################################
+
+ private
+
+ def apply_config(config_file_path)
+ Chef::Config.from_file(config_file_path)
+ Chef::Config.merge!(config)
+ end
+
+ # Lifted from Chef::Application, with addition of optional startup parameters
+ # for playing nicely with Windows Services
+ def reconfigure(startup_parameters=[])
+ configure_chef startup_parameters
+ configure_logging
+
+ Chef::Config[:chef_server_url] = config[:chef_server_url] if config.has_key? :chef_server_url
+ unless Chef::Config[:exception_handlers].any? {|h| Chef::Handler::ErrorReport === h}
+ Chef::Config[:exception_handlers] << Chef::Handler::ErrorReport.new
+ end
+
+ Chef::Config[:interval] ||= 1800
+ end
+
+ # Lifted from Chef::Application and Chef::Application::Client
+ # MUST BE RUN AFTER configuration has been parsed!
+ def configure_logging
+ # Implementation from Chef::Application
+ Chef::Log.init(Chef::Config[:log_location])
+ Chef::Log.level = Chef::Config[:log_level]
+
+ # Implementation from Chef::Application::Client
+ Mixlib::Authentication::Log.use_log_devices( Chef::Log )
+ Ohai::Log.use_log_devices( Chef::Log )
+ end
+
+ def configure_chef(startup_parameters)
+ # Bit of a hack ahead:
+ # It is possible to specify a service's binary_path_name with arguments, like "foo.exe -x argX".
+ # It is also possible to specify startup parameters separately, either via the the Services manager
+ # or by using the registry (I think).
+
+ # In order to accommodate all possible sources of parameterization, we first parse any command line
+ # arguments. We then parse any startup parameters. This works, because Mixlib::CLI reuses its internal
+ # 'config' hash; thus, anything in startup parameters will override any command line parameters that
+ # might be set via the service's binary_path_name
+ #
+ # All these parameters then get layered on top of those from Chef::Config
+
+ parse_options # Operates on ARGV by default
+ parse_options startup_parameters
+
+ begin
+ case config[:config_file]
+ when /^(http|https):\/\//
+ Chef::REST.new("", nil, nil).fetch(config[:config_file]) { |f| apply_config(f.path) }
+ else
+ ::File::open(config[:config_file]) { |f| apply_config(f.path) }
+ end
+ rescue Errno::ENOENT => error
+ Chef::Log.warn("*****************************************")
+ Chef::Log.warn("Did not find config file: #{config[:config_file]}, using command line options.")
+ Chef::Log.warn("*****************************************")
+
+ Chef::Config.merge!(config)
+ rescue SocketError => error
+ Chef::Application.fatal!("Error getting config file #{Chef::Config[:config_file]}", 2)
+ rescue Chef::Exceptions::ConfigurationError => error
+ Chef::Application.fatal!("Error processing config file #{Chef::Config[:config_file]} with error #{error.message}", 2)
+ rescue Exception => error
+ Chef::Application.fatal!("Unknown error processing config file #{Chef::Config[:config_file]} with error #{error.message}", 2)
+ end
+ end
+
+ # Since we need to be able to respond to signals between Chef runs, we need to periodically
+ # wake up to see if we're still in the running state. The method returns when it has slept
+ # for +sec+ seconds (but at least +10+ seconds), or when the service
+ # is no client_sleep in the +RUNNING+ state, whichever comes first.
+ def client_sleep(sec)
+ chunk_length = 10
+ chunks = sec / chunk_length
+ chunks = 1 if chunks < 1
+ (1..chunks).each do
+ return unless state == RUNNING
+ sleep chunk_length
+ end
+ end
+
+ end
+ end
+end
+
+# To run this file as a service, it must be called as a script from within
+# the Windows Service framework. In that case, kick off the main loop!
+if __FILE__ == $0
+ Chef::Application::WindowsService.mainloop
+end
diff --git a/lib/chef/applications.rb b/lib/chef/applications.rb
new file mode 100644
index 0000000000..48fd56acf4
--- /dev/null
+++ b/lib/chef/applications.rb
@@ -0,0 +1,4 @@
+require 'chef/application/agent'
+require 'chef/application/client'
+require 'chef/application/knife'
+require 'chef/application/solo'
diff --git a/lib/chef/checksum/storage.rb b/lib/chef/checksum/storage.rb
new file mode 100644
index 0000000000..90aef57081
--- /dev/null
+++ b/lib/chef/checksum/storage.rb
@@ -0,0 +1,18 @@
+#
+# Author:: Andrea Campi (<andrea.campi@zephirworks.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/checksum/storage/filesystem'
diff --git a/lib/chef/checksum/storage/filesystem.rb b/lib/chef/checksum/storage/filesystem.rb
new file mode 100644
index 0000000000..7500a5ad85
--- /dev/null
+++ b/lib/chef/checksum/storage/filesystem.rb
@@ -0,0 +1,56 @@
+#
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+ class Checksum
+ class Storage
+ class Filesystem
+ def initialize(base_dir, checksum)
+ @base_dir = base_dir
+ @checksum = checksum
+ end
+
+ def file_location
+ File.join(checksum_repo_directory, @checksum)
+ end
+ alias :to_s :file_location
+
+ def checksum_repo_directory
+ File.join(Chef::Config.checksum_path, @checksum[0..1])
+ end
+
+ def commit(sandbox_file)
+ FileUtils.mkdir_p(checksum_repo_directory)
+ File.rename(sandbox_file, file_location)
+ end
+
+ def revert(original_committed_file_location)
+ File.rename(file_location, original_committed_file_location)
+ end
+
+ # Deletes the file backing this checksum from the on-disk repo.
+ # Purging the checksums is how users can get back to a valid state if
+ # they've deleted files, so we silently swallow Errno::ENOENT here.
+ def purge
+ FileUtils.rm(file_location)
+ rescue Errno::ENOENT
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/checksum_cache.rb b/lib/chef/checksum_cache.rb
new file mode 100644
index 0000000000..6db7115a56
--- /dev/null
+++ b/lib/chef/checksum_cache.rb
@@ -0,0 +1,190 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'set'
+require 'fileutils'
+require 'chef/log'
+require 'chef/config'
+require 'chef/client'
+require 'chef/mixin/convert_to_class_name'
+require 'singleton'
+require 'moneta'
+
+class Chef
+ class ChecksumCache
+ include Chef::Mixin::ConvertToClassName
+ include ::Singleton
+
+ attr_reader :moneta
+
+ def initialize(*args)
+ self.reset!(*args)
+ end
+
+ def reset!(backend=nil, options=nil)
+ backend ||= Chef::Config[:cache_type]
+ options ||= Chef::Config[:cache_options]
+
+ begin
+ require "moneta/#{convert_to_snake_case(backend, 'Moneta')}"
+ require 'chef/monkey_patches/moneta'
+ rescue LoadError => e
+ Chef::Log.fatal("Could not load Moneta back end #{backend.inspect}")
+ raise e
+ end
+
+ @moneta = Moneta.const_get(backend).new(options)
+ end
+
+ def self.reset_cache_validity
+ @valid_cached_checksums = nil
+ end
+
+ Chef::Client.when_run_starts do |run_status|
+ reset_cache_validity
+ end
+
+ def self.valid_cached_checksums
+ @valid_cached_checksums ||= Set.new
+ end
+
+ def self.validate_checksum(checksum_key)
+ valid_cached_checksums << checksum_key
+ end
+
+ def self.all_cached_checksums
+ all_checksums_with_filenames = {}
+
+ Dir[File.join(Chef::Config[:cache_options][:path], '*')].each do |cksum_file|
+ all_checksums_with_filenames[File.basename(cksum_file)] = cksum_file
+ end
+ all_checksums_with_filenames
+ end
+
+ def self.cleanup_checksum_cache
+ Chef::Log.debug("Cleaning the checksum cache")
+ if (Chef::Config[:cache_type].to_s == "BasicFile")
+ all_cached_checksums.each do |cache_key, cksum_cache_file|
+ unless valid_cached_checksums.include?(cache_key)
+ remove_unused_checksum(cksum_cache_file)
+ end
+ end
+ end
+ end
+
+ Chef::Client.when_run_completes_successfully do |run_status|
+ cleanup_checksum_cache
+ end
+
+ def self.remove_unused_checksum(checksum_file)
+ Chef::Log.debug("Removing unused checksum cache file #{checksum_file}")
+ FileUtils.rm(checksum_file)
+ end
+
+ def self.checksum_for_file(*args)
+ instance.checksum_for_file(*args)
+ end
+
+ def validate_checksum(*args)
+ self.class.validate_checksum(*args)
+ end
+
+ def checksum_for_file(file, key=nil)
+ key ||= generate_key(file)
+ fstat = File.stat(file)
+ lookup_checksum(key, fstat) || generate_checksum(key, file, fstat)
+ end
+
+ def lookup_checksum(key, fstat)
+ cached = fetch(key)
+ if cached && file_unchanged?(cached, fstat)
+ validate_checksum(key)
+ cached["checksum"]
+ else
+ nil
+ end
+ end
+
+ def generate_checksum(key, file, fstat)
+ checksum = checksum_file(file, Digest::SHA256.new)
+ moneta.store(key, {"mtime" => fstat.mtime.to_f, "checksum" => checksum})
+ validate_checksum(key)
+ checksum
+ end
+
+ def generate_key(file, group="chef")
+ "#{group}-file-#{file.gsub(/(#{File::SEPARATOR}|\.)/, '-')}"
+ end
+
+ def self.generate_md5_checksum_for_file(*args)
+ instance.generate_md5_checksum_for_file(*args)
+ end
+
+ def generate_md5_checksum_for_file(file)
+ checksum_file(file, Digest::MD5.new)
+ end
+
+ def generate_md5_checksum(io)
+ checksum_io(io, Digest::MD5.new)
+ end
+
+ private
+
+ def fetch(key)
+ @moneta.fetch(key)
+ rescue ArgumentError => e
+ Log.warn "Error loading cached checksum for key #{key.inspect}"
+ Log.warn(e)
+ repair_checksum_cache
+ nil
+ end
+
+ def repair_checksum_cache
+ Chef::Log.info("Removing invalid checksum cache files")
+ Dir["#{Chef::Config[:cache_options][:path]}/*"].each do |file_path|
+ File.unlink(file_path) unless File.size?(file_path)
+ end
+ end
+
+ def file_unchanged?(cached, fstat)
+ cached["mtime"].to_f == fstat.mtime.to_f
+ end
+
+ def checksum_file(file, digest)
+ File.open(file, 'rb') { |f| checksum_io(f, digest) }
+ end
+
+ def checksum_io(io, digest)
+ while chunk = io.read(1024 * 8)
+ digest.update(chunk)
+ end
+ digest.hexdigest
+ end
+
+ end
+end
+
+module Moneta
+ module Defaults
+ def default
+ nil
+ end
+ end
+end
diff --git a/lib/chef/chef_fs.rb b/lib/chef/chef_fs.rb
new file mode 100644
index 0000000000..14ab8c0a6e
--- /dev/null
+++ b/lib/chef/chef_fs.rb
@@ -0,0 +1,11 @@
+require 'chef/chef_fs/file_system/chef_server_root_dir'
+require 'chef/config'
+require 'chef/rest'
+
+class Chef
+ module ChefFS
+ def self.windows?
+ false
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/command_line.rb b/lib/chef/chef_fs/command_line.rb
new file mode 100644
index 0000000000..a8362b962b
--- /dev/null
+++ b/lib/chef/chef_fs/command_line.rb
@@ -0,0 +1,232 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system'
+
+class Chef
+ module ChefFS
+ module CommandLine
+ def self.diff(pattern, a_root, b_root, recurse_depth, output_mode)
+ found_result = false
+ Chef::ChefFS::FileSystem.list_pairs(pattern, a_root, b_root) do |a, b|
+ existed = diff_entries(a, b, recurse_depth, output_mode) do |diff|
+ yield diff
+ end
+ found_result = true if existed
+ end
+ if !found_result && pattern.exact_path
+ yield "#{pattern}: No such file or directory on remote or local"
+ end
+ end
+
+ # Diff two known entries (could be files or dirs)
+ def self.diff_entries(old_entry, new_entry, recurse_depth, output_mode)
+ # If both are directories
+ if old_entry.dir?
+ if new_entry.dir?
+ if recurse_depth == 0
+ if output_mode != :name_only && output_mode != :name_status
+ yield "Common subdirectories: #{old_entry.path}\n"
+ end
+ else
+ Chef::ChefFS::FileSystem.child_pairs(old_entry, new_entry).each do |old_child,new_child|
+ diff_entries(old_child, new_child,
+ recurse_depth ? recurse_depth - 1 : nil, output_mode) do |diff|
+ yield diff
+ end
+ end
+ end
+
+ # If old is a directory and new is a file
+ elsif new_entry.exists?
+ if output_mode == :name_only
+ yield "#{new_entry.path_for_printing}\n"
+ elsif output_mode == :name_status
+ yield "T\t#{new_entry.path_for_printing}\n"
+ else
+ yield "File #{new_entry.path_for_printing} is a directory while file #{new_entry.path_for_printing} is a regular file\n"
+ end
+
+ # If old is a directory and new does not exist
+ elsif new_entry.parent.can_have_child?(old_entry.name, old_entry.dir?)
+ if output_mode == :name_only
+ yield "#{new_entry.path_for_printing}\n"
+ elsif output_mode == :name_status
+ yield "D\t#{new_entry.path_for_printing}\n"
+ else
+ yield "Only in #{old_entry.parent.path_for_printing}: #{old_entry.name}\n"
+ end
+ end
+
+ # If new is a directory and old is a file
+ elsif new_entry.dir?
+ if old_entry.exists?
+ if output_mode == :name_only
+ yield "#{new_entry.path_for_printing}\n"
+ elsif output_mode == :name_status
+ yield "T\t#{new_entry.path_for_printing}\n"
+ else
+ yield "File #{old_entry.path_for_printing} is a regular file while file #{old_entry.path_for_printing} is a directory\n"
+ end
+
+ # If new is a directory and old does not exist
+ elsif old_entry.parent.can_have_child?(new_entry.name, new_entry.dir?)
+ if output_mode == :name_only
+ yield "#{new_entry.path_for_printing}\n"
+ elsif output_mode == :name_status
+ yield "A\t#{new_entry.path_for_printing}\n"
+ else
+ yield "Only in #{new_entry.parent.path_for_printing}: #{new_entry.name}\n"
+ end
+ end
+
+ # Neither is a directory, so they are diffable with file diff
+ else
+ are_same, old_value, new_value = Chef::ChefFS::FileSystem.compare(old_entry, new_entry)
+ if are_same
+ return old_value != :none
+ else
+ if old_value == :none
+ old_exists = false
+ elsif old_value.nil?
+ old_exists = old_entry.exists?
+ else
+ old_exists = true
+ end
+ if new_value == :none
+ new_exists = false
+ elsif new_value.nil?
+ new_exists = new_entry.exists?
+ else
+ new_exists = true
+ end
+
+ # If one of the files doesn't exist, we only want to print the diff if the
+ # other file *could be uploaded/downloaded*.
+ if !old_exists && !old_entry.parent.can_have_child?(new_entry.name, new_entry.dir?)
+ return true
+ end
+ if !new_exists && !new_entry.parent.can_have_child?(old_entry.name, old_entry.dir?)
+ return true
+ end
+
+ if output_mode == :name_only
+ yield "#{new_entry.path_for_printing}\n"
+ elsif output_mode == :name_status
+ if old_value == :none || (old_value == nil && !old_entry.exists?)
+ yield "A\t#{new_entry.path_for_printing}\n"
+ elsif new_value == :none
+ yield "D\t#{new_entry.path_for_printing}\n"
+ else
+ yield "M\t#{new_entry.path_for_printing}\n"
+ end
+ else
+ # If we haven't read the values yet, get them now.
+ begin
+ old_value = old_entry.read if old_value.nil?
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ old_value = :none
+ end
+ begin
+ new_value = new_entry.read if new_value.nil?
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ new_value = :none
+ end
+
+ old_path = old_entry.path_for_printing
+ new_path = new_entry.path_for_printing
+ result = ''
+ result << "diff --knife #{old_path} #{new_path}\n"
+ if old_value == :none
+ result << "new file\n"
+ old_path = "/dev/null"
+ old_value = ''
+ end
+ if new_value == :none
+ result << "deleted file\n"
+ new_path = "/dev/null"
+ new_value = ''
+ end
+ result << diff_text(old_path, new_path, old_value, new_value)
+ yield result
+ end
+ end
+ end
+ return true
+ end
+
+ private
+
+ def self.sort_keys(json_object)
+ if json_object.is_a?(Array)
+ json_object.map { |o| sort_keys(o) }
+ elsif json_object.is_a?(Hash)
+ new_hash = {}
+ json_object.keys.sort.each { |key| new_hash[key] = sort_keys(json_object[key]) }
+ new_hash
+ else
+ json_object
+ end
+ end
+
+ def self.canonicalize_json(json_text)
+ parsed_json = JSON.parse(json_text, :create_additions => false)
+ sorted_json = sort_keys(parsed_json)
+ JSON.pretty_generate(sorted_json)
+ end
+
+ def self.diff_text(old_path, new_path, old_value, new_value)
+ # Reformat JSON for a nicer diff.
+ if old_path =~ /\.json$/
+ begin
+ reformatted_old_value = canonicalize_json(old_value)
+ reformatted_new_value = canonicalize_json(new_value)
+ old_value = reformatted_old_value
+ new_value = reformatted_new_value
+ rescue
+ # If JSON parsing fails, we just won't change any values and fall back
+ # to normal diff.
+ end
+ end
+
+ # Copy to tempfiles before diffing
+ # TODO don't copy things that are already in files! Or find an in-memory diff algorithm
+ begin
+ new_tempfile = Tempfile.new("new")
+ new_tempfile.write(new_value)
+ new_tempfile.close
+
+ begin
+ old_tempfile = Tempfile.new("old")
+ old_tempfile.write(old_value)
+ old_tempfile.close
+
+ result = `diff -u #{old_tempfile.path} #{new_tempfile.path}`
+ result = result.gsub(/^--- #{old_tempfile.path}/, "--- #{old_path}")
+ result = result.gsub(/^\+\+\+ #{new_tempfile.path}/, "+++ #{new_path}")
+ result
+ ensure
+ old_tempfile.close!
+ end
+ ensure
+ new_tempfile.close!
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_pattern.rb b/lib/chef/chef_fs/file_pattern.rb
new file mode 100644
index 0000000000..134d22cbd5
--- /dev/null
+++ b/lib/chef/chef_fs/file_pattern.rb
@@ -0,0 +1,312 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs'
+require 'chef/chef_fs/path_utils'
+
+class Chef
+ module ChefFS
+ #
+ # Represents a glob pattern. This class is designed so that it can
+ # match arbitrary strings, and tell you about partial matches.
+ #
+ # Examples:
+ # * <tt>a*z</tt>
+ # - Matches <tt>abcz</tt>
+ # - Does not match <tt>ab/cd/ez</tt>
+ # - Does not match <tt>xabcz</tt>
+ # * <tt>a**z</tt>
+ # - Matches <tt>abcz</tt>
+ # - Matches <tt>ab/cd/ez</tt>
+ #
+ # Special characters supported:
+ # * <tt>/</tt> (and <tt>\\</tt> on Windows) - directory separators
+ # * <tt>\*</tt> - match zero or more characters (but not directory separators)
+ # * <tt>\*\*</tt> - match zero or more characters, including directory separators
+ # * <tt>?</tt> - match exactly one character (not a directory separator)
+ # Only on Unix:
+ # * <tt>[abc0-9]</tt> - match one of the included characters
+ # * <tt>\\<character></tt> - escape character: match the given character
+ #
+ class FilePattern
+ # Initialize a new FilePattern with the pattern string.
+ #
+ # Raises +ArgumentError+ if empty file pattern is specified
+ def initialize(pattern)
+ @pattern = pattern
+ end
+
+ # The pattern string.
+ attr_reader :pattern
+
+ # Reports whether this pattern could match children of <tt>path</tt>.
+ # If the pattern doesn't match the path up to this point or
+ # if it matches and doesn't allow further children, this will
+ # return <tt>false</tt>.
+ #
+ # ==== Attributes
+ #
+ # * +path+ - a path to check
+ #
+ # ==== Examples
+ #
+ # abc/def.could_match_children?('abc') == true
+ # abc.could_match_children?('abc') == false
+ # abc/def.could_match_children?('x') == false
+ # a**z.could_match_children?('ab/cd') == true
+ def could_match_children?(path)
+ return false if path == '' # Empty string is not a path
+
+ argument_is_absolute = !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/)
+ return false if is_absolute != argument_is_absolute
+ path = path[1,path.length-1] if argument_is_absolute
+
+ path_parts = Chef::ChefFS::PathUtils::split(path)
+ # If the pattern is shorter than the path (or same size), children will be larger than the pattern, and will not match.
+ return false if regexp_parts.length <= path_parts.length && !has_double_star
+ # If the path doesn't match up to this point, children won't match either.
+ return false if path_parts.zip(regexp_parts).any? { |part,regexp| !regexp.nil? && !regexp.match(part) }
+ # Otherwise, it's possible we could match: the path matches to this point, and the pattern is longer than the path.
+ # TODO There is one edge case where the double star comes after some characters like abc**def--we could check whether the next
+ # bit of path starts with abc in that case.
+ return true
+ end
+
+ # Returns the immediate child of a path that would be matched
+ # if this FilePattern was applied. If more than one child
+ # could match, this method returns nil.
+ #
+ # ==== Attributes
+ #
+ # * +path+ - The path to look for an exact child name under.
+ #
+ # ==== Returns
+ #
+ # The next directory in the pattern under the given path.
+ # If the directory part could match more than one child, it
+ # returns +nil+.
+ #
+ # ==== Examples
+ #
+ # abc/def.exact_child_name_under('abc') == 'def'
+ # abc/def/ghi.exact_child_name_under('abc') == 'def'
+ # abc/*/ghi.exact_child_name_under('abc') == nil
+ # abc/*/ghi.exact_child_name_under('abc/def') == 'ghi'
+ # abc/**/ghi.exact_child_name_under('abc/def') == nil
+ #
+ # This method assumes +could_match_children?(path)+ is +true+.
+ def exact_child_name_under(path)
+ path = path[1,path.length-1] if !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/)
+ dirs_in_path = Chef::ChefFS::PathUtils::split(path).length
+ return nil if exact_parts.length <= dirs_in_path
+ return exact_parts[dirs_in_path]
+ end
+
+ # If this pattern represents an exact path, returns the exact path.
+ #
+ # abc/def.exact_path == 'abc/def'
+ # abc/*def.exact_path == 'abc/def'
+ # abc/x\\yz.exact_path == 'abc/xyz'
+ def exact_path
+ return nil if has_double_star || exact_parts.any? { |part| part.nil? }
+ result = Chef::ChefFS::PathUtils::join(*exact_parts)
+ is_absolute ? Chef::ChefFS::PathUtils::join('', result) : result
+ end
+
+ # Returns the normalized version of the pattern, with / as the directory
+ # separator, and "." and ".." removed.
+ #
+ # This does not presently change things like \b to b, but in the future
+ # it might.
+ def normalized_pattern
+ calculate
+ @normalized_pattern
+ end
+
+ # Tell whether this pattern matches absolute, or relative paths
+ def is_absolute
+ calculate
+ @is_absolute
+ end
+
+ # Returns <tt>true+ if this pattern matches the path, <tt>false+ otherwise.
+ #
+ # abc/*/def.match?('abc/foo/def') == true
+ # abc/*/def.match?('abc/foo') == false
+ def match?(path)
+ argument_is_absolute = !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/)
+ return false if is_absolute != argument_is_absolute
+ path = path[1,path.length-1] if argument_is_absolute
+ !!regexp.match(path)
+ end
+
+ # Returns the string pattern
+ def to_s
+ pattern
+ end
+
+ # Given a relative file pattern and a directory, makes a new file pattern
+ # starting with the directory.
+ #
+ # FilePattern.relative_to('/usr/local', 'bin/*grok') == FilePattern.new('/usr/local/bin/*grok')
+ #
+ # BUG: this does not support patterns starting with <tt>..</tt>
+ def self.relative_to(dir, pattern)
+ return FilePattern.new(pattern) if pattern =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/
+ FilePattern.new(Chef::ChefFS::PathUtils::join(dir, pattern))
+ end
+
+ private
+
+ def regexp
+ calculate
+ @regexp
+ end
+
+ def regexp_parts
+ calculate
+ @regexp_parts
+ end
+
+ def exact_parts
+ calculate
+ @exact_parts
+ end
+
+ def has_double_star
+ calculate
+ @has_double_star
+ end
+
+ def calculate
+ if !@regexp
+ @is_absolute = !!(@pattern =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/)
+
+ full_regexp_parts = []
+ normalized_parts = []
+ @regexp_parts = []
+ @exact_parts = []
+ @has_double_star = false
+
+ Chef::ChefFS::PathUtils::split(pattern).each do |part|
+ regexp, exact, has_double_star = FilePattern::pattern_to_regexp(part)
+ if has_double_star
+ @has_double_star = true
+ end
+
+ # Skip // and /./ (pretend it's not there)
+ if exact == '' || exact == '.'
+ next
+ end
+
+ # Back up when you see .. (unless the prior part has ** in it, in which case .. must be preserved)
+ if exact == '..'
+ if @is_absolute && normalized_parts.length == 0
+ # If we are at the root, just pretend the .. isn't there
+ next
+ elsif normalized_parts.length > 0
+ regexp_prev, exact_prev, has_double_star_prev = FilePattern.pattern_to_regexp(normalized_parts[-1])
+ if has_double_star_prev
+ raise ArgumentError, ".. overlapping a ** is unsupported"
+ end
+ full_regexp_parts.pop
+ normalized_parts.pop
+ if !@has_double_star
+ @regexp_parts.pop
+ @exact_parts.pop
+ end
+ next
+ end
+ end
+
+ # Build up the regexp
+ full_regexp_parts << regexp
+ normalized_parts << part
+ if !@has_double_star
+ @regexp_parts << Regexp.new("^#{regexp}$")
+ @exact_parts << exact
+ end
+ end
+
+ @regexp = Regexp.new("^#{full_regexp_parts.join(Chef::ChefFS::PathUtils::regexp_path_separator)}$")
+ @normalized_pattern = Chef::ChefFS::PathUtils.join(*normalized_parts)
+ @normalized_pattern = Chef::ChefFS::PathUtils.join('', @normalized_pattern) if @is_absolute
+ end
+ end
+
+ def self.pattern_special_characters
+ if Chef::ChefFS::windows?
+ @pattern_special_characters ||= /(\*\*|\*|\?|[\*\?\.\|\(\)\[\]\{\}\+\\\\\^\$])/
+ else
+ # Unix also supports character regexes and backslashes
+ @pattern_special_characters ||= /(\\.|\[[^\]]+\]|\*\*|\*|\?|[\*\?\.\|\(\)\[\]\{\}\+\\\\\^\$])/
+ end
+ @pattern_special_characters
+ end
+
+ def self.regexp_escape_characters
+ [ '[', '\\', '^', '$', '.', '|', '?', '*', '+', '(', ')', '{', '}' ]
+ end
+
+ def self.pattern_to_regexp(pattern)
+ regexp = ""
+ exact = ""
+ has_double_star = false
+ pattern.split(pattern_special_characters).each_with_index do |part, index|
+ # Odd indexes from the split are symbols. Even are normal bits.
+ if index % 2 == 0
+ exact << part if !exact.nil?
+ regexp << part
+ else
+ case part
+ # **, * and ? happen on both platforms.
+ when '**'
+ exact = nil
+ has_double_star = true
+ regexp << '.*'
+ when '*'
+ exact = nil
+ regexp << '[^\/]*'
+ when '?'
+ exact = nil
+ regexp << '.'
+ else
+ if part[0,1] == '\\' && part.length == 2
+ # backslash escapes are only supported on Unix, and are handled here by leaving the escape on (it means the same thing in a regex)
+ exact << part[1,1] if !exact.nil?
+ if regexp_escape_characters.include?(part[1,1])
+ regexp << part
+ else
+ regexp << part[1,1]
+ end
+ elsif part[0,1] == '[' && part.length > 1
+ # [...] happens only on Unix, and is handled here by *not* backslashing (it means the same thing in and out of regex)
+ exact = nil
+ regexp << part
+ else
+ exact += part if !exact.nil?
+ regexp << "\\#{part}"
+ end
+ end
+ end
+ end
+ [regexp, exact, has_double_star]
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system.rb b/lib/chef/chef_fs/file_system.rb
new file mode 100644
index 0000000000..1805869e32
--- /dev/null
+++ b/lib/chef/chef_fs/file_system.rb
@@ -0,0 +1,358 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/path_utils'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ # Yields a list of all things under (and including) this entry that match the
+ # given pattern.
+ #
+ # ==== Attributes
+ #
+ # * +entry+ - Entry to start listing under
+ # * +pattern+ - Chef::ChefFS::FilePattern to match children under
+ #
+ def self.list(entry, pattern, &block)
+ # Include self in results if it matches
+ if pattern.match?(entry.path)
+ block.call(entry)
+ end
+
+ if entry.dir? && pattern.could_match_children?(entry.path)
+ # If it's possible that our children could match, descend in and add matches.
+ exact_child_name = pattern.exact_child_name_under(entry.path)
+
+ # If we've got an exact name, don't bother listing children; just grab the
+ # child with the given name.
+ if exact_child_name
+ exact_child = entry.child(exact_child_name)
+ if exact_child
+ list(exact_child, pattern, &block)
+ end
+
+ # Otherwise, go through all children and find any matches
+ else
+ entry.children.each do |child|
+ list(child, pattern, &block)
+ end
+ end
+ end
+ end
+
+ # Resolve the given path against the entry, returning
+ # the entry at the end of the path.
+ #
+ # ==== Attributes
+ #
+ # * +entry+ - the entry to start looking under. Relative
+ # paths will be resolved from here.
+ # * +path+ - the path to resolve. If it starts with +/+,
+ # the path will be resolved starting from +entry.root+.
+ #
+ # ==== Examples
+ #
+ # Chef::ChefFS::FileSystem.resolve_path(root_path, 'cookbooks/java/recipes/default.rb')
+ #
+ def self.resolve_path(entry, path)
+ return entry if path.length == 0
+ return resolve_path(entry.root, path) if path[0,1] == "/" && entry.root != entry
+ if path[0,1] == "/"
+ path = path[1,path.length-1]
+ end
+
+ result = entry
+ Chef::ChefFS::PathUtils::split(path).each do |part|
+ result = result.child(part)
+ end
+ result
+ end
+
+ # Copy everything matching the given pattern from src to dest.
+ #
+ # After this method completes, everything in dest matching the
+ # given pattern will look identical to src.
+ #
+ # ==== Attributes
+ #
+ # * +pattern+ - Chef::ChefFS::FilePattern to match children under
+ # * +src_root+ - the root from which things will be copied
+ # * +dest_root+ - the root to which things will be copied
+ # * +recurse_depth+ - the maximum depth to copy things. +nil+
+ # means infinite depth. 0 means no recursion.
+ # * +options+ - hash of options:
+ # - +purge+ - if +true+, items in +dest+ that are not in +src+
+ # will be deleted from +dest+. If +false+, these items will
+ # be left alone.
+ # - +force+ - if +true+, matching files are always copied from
+ # +src+ to +dest+. If +false+, they will only be copied if
+ # actually different (which will take time to determine).
+ # - +dry_run+ - if +true+, action will not actually be taken;
+ # things will be printed out instead.
+ #
+ # ==== Examples
+ #
+ # Chef::ChefFS::FileSystem.copy_to(FilePattern.new('/cookbooks'),
+ # chef_fs, local_fs, nil, true) do |message|
+ # puts message
+ # end
+ #
+ def self.copy_to(pattern, src_root, dest_root, recurse_depth, options)
+ found_result = false
+ list_pairs(pattern, src_root, dest_root) do |src, dest|
+ found_result = true
+ new_dest_parent = get_or_create_parent(dest, options)
+ copy_entries(src, dest, new_dest_parent, recurse_depth, options)
+ end
+ if !found_result && pattern.exact_path
+ puts "#{pattern}: No such file or directory on remote or local"
+ end
+ end
+
+ # Yield entries for children that are in either +a_root+ or +b_root+, with
+ # matching pairs matched up.
+ #
+ # ==== Yields
+ #
+ # Yields matching entries in pairs:
+ #
+ # [ a_entry, b_entry ]
+ #
+ # ==== Example
+ #
+ # Chef::ChefFS::FileSystem.list_pairs(FilePattern.new('**x.txt', a_root, b_root)) do |a, b|
+ # ...
+ # end
+ #
+ def self.list_pairs(pattern, a_root, b_root)
+ # Make sure everything on the server is also on the filesystem, and diff
+ found_paths = Set.new
+ Chef::ChefFS::FileSystem.list(a_root, pattern) do |a|
+ found_paths << a.path
+ b = Chef::ChefFS::FileSystem.resolve_path(b_root, a.path)
+ yield [ a, b ]
+ end
+
+ # Check the outer regex pattern to see if it matches anything on the
+ # filesystem that isn't on the server
+ Chef::ChefFS::FileSystem.list(b_root, pattern) do |b|
+ if !found_paths.include?(b.path)
+ a = Chef::ChefFS::FileSystem.resolve_path(a_root, b.path)
+ yield [ a, b ]
+ end
+ end
+ end
+
+ # Get entries for children of either a or b, with matching pairs matched up.
+ #
+ # ==== Returns
+ #
+ # An array of child pairs.
+ #
+ # [ [ a_child, b_child ], ... ]
+ #
+ # If a child is only in a or only in b, the other child entry will be
+ # retrieved by name (and will most likely be a "nonexistent child").
+ #
+ # ==== Example
+ #
+ # Chef::ChefFS::FileSystem.child_pairs(a, b).length
+ #
+ def self.child_pairs(a, b)
+ # If both are directories, recurse into them and diff the children instead of returning ourselves.
+ result = []
+ a_children_names = Set.new
+ a.children.each do |a_child|
+ a_children_names << a_child.name
+ result << [ a_child, b.child(a_child.name) ]
+ end
+
+ # Check b for children that aren't in a
+ b.children.each do |b_child|
+ if !a_children_names.include?(b_child.name)
+ result << [ a.child(b_child.name), b_child ]
+ end
+ end
+ result
+ end
+
+ def self.compare(a, b)
+ are_same, a_value, b_value = a.compare_to(b)
+ if are_same.nil?
+ are_same, b_value, a_value = b.compare_to(a)
+ end
+ if are_same.nil?
+ begin
+ a_value = a.read if a_value.nil?
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ a_value = :none
+ end
+ begin
+ b_value = b.read if b_value.nil?
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ b_value = :none
+ end
+ are_same = (a_value == b_value)
+ end
+ [ are_same, a_value, b_value ]
+ end
+
+ private
+
+ # Copy two entries (could be files or dirs)
+ def self.copy_entries(src_entry, dest_entry, new_dest_parent, recurse_depth, options)
+ # A NOTE about this algorithm:
+ # There are cases where this algorithm does too many network requests.
+ # knife upload with a specific filename will first check if the file
+ # exists (a "dir" in the parent) before deciding whether to POST or
+ # PUT it. If we just tried PUT (or POST) and then tried the other if
+ # the conflict failed, we wouldn't need to check existence.
+ # On the other hand, we may already have DONE the request, in which
+ # case we shouldn't waste time trying PUT if we know the file doesn't
+ # exist.
+ # Will need to decide how that works with checksums, though.
+
+ if !src_entry.exists?
+ if options[:purge]
+ # If we would not have uploaded it, we will not purge it.
+ if src_entry.parent.can_have_child?(dest_entry.name, dest_entry.dir?)
+ if options[:dry_run]
+ puts "Would delete #{dest_entry.path_for_printing}"
+ else
+ dest_entry.delete(true)
+ puts "Deleted extra entry #{dest_entry.path_for_printing} (purge is on)"
+ end
+ else
+ Chef::Log.info("Not deleting extra entry #{dest_entry.path_for_printing} (purge is off)")
+ end
+ end
+
+ elsif !dest_entry.exists?
+ if new_dest_parent.can_have_child?(src_entry.name, src_entry.dir?)
+ # If the entry can do a copy directly from filesystem, do that.
+ if new_dest_parent.respond_to?(:create_child_from)
+ if options[:dry_run]
+ puts "Would create #{dest_entry.path_for_printing}"
+ else
+ new_dest_parent.create_child_from(src_entry)
+ puts "Created #{dest_entry.path_for_printing}"
+ end
+ return
+ end
+
+ if src_entry.dir?
+ if options[:dry_run]
+ puts "Would create #{dest_entry.path_for_printing}"
+ new_dest_dir = new_dest_parent.child(src_entry.name)
+ else
+ new_dest_dir = new_dest_parent.create_child(src_entry.name, nil)
+ puts "Created #{dest_entry.path_for_printing}/"
+ end
+ # Directory creation is recursive.
+ if recurse_depth != 0
+ src_entry.children.each do |src_child|
+ new_dest_child = new_dest_dir.child(src_child.name)
+ copy_entries(src_child, new_dest_child, new_dest_dir, recurse_depth ? recurse_depth - 1 : recurse_depth, options)
+ end
+ end
+ else
+ if options[:dry_run]
+ puts "Would create #{dest_entry.path_for_printing}"
+ else
+ new_dest_parent.create_child(src_entry.name, src_entry.read)
+ puts "Created #{dest_entry.path_for_printing}"
+ end
+ end
+ end
+
+ else
+ # Both exist.
+
+ # If the entry can do a copy directly, do that.
+ if dest_entry.respond_to?(:copy_from)
+ if options[:force] || compare(src_entry, dest_entry)[0] == false
+ if options[:dry_run]
+ puts "Would update #{dest_entry.path_for_printing}"
+ else
+ dest_entry.copy_from(src_entry)
+ puts "Updated #{dest_entry.path_for_printing}"
+ end
+ end
+ return
+ end
+
+ # If they are different types, log an error.
+ if src_entry.dir?
+ if dest_entry.dir?
+ # If both are directories, recurse into their children
+ if recurse_depth != 0
+ child_pairs(src_entry, dest_entry).each do |src_child, dest_child|
+ copy_entries(src_child, dest_child, dest_entry, recurse_depth ? recurse_depth - 1 : recurse_depth, options)
+ end
+ end
+ else
+ # If they are different types.
+ Chef::Log.error("File #{dest_entry.path_for_printing} is a directory while file #{dest_entry.path_for_printing} is a regular file\n")
+ return
+ end
+ else
+ if dest_entry.dir?
+ Chef::Log.error("File #{dest_entry.path_for_printing} is a directory while file #{dest_entry.path_for_printing} is a regular file\n")
+ return
+ else
+
+ # Both are files! Copy them unless we're sure they are the same.
+ if options[:force]
+ should_copy = true
+ src_value = nil
+ else
+ are_same, src_value, dest_value = compare(src_entry, dest_entry)
+ should_copy = !are_same
+ end
+ if should_copy
+ if options[:dry_run]
+ puts "Would update #{dest_entry.path_for_printing}"
+ else
+ src_value = src_entry.read if src_value.nil?
+ dest_entry.write(src_value)
+ puts "Updated #{dest_entry.path_for_printing}"
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def self.get_or_create_parent(entry, options)
+ parent = entry.parent
+ if parent && !parent.exists?
+ parent_parent = get_or_create_parent(entry.parent, options)
+ if options[:dry_run]
+ puts "Would create #{parent.path_for_printing}"
+ else
+ parent = parent_parent.create_child(parent.name, true)
+ puts "Created #{parent.path_for_printing}"
+ end
+ end
+ return parent
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/base_fs_dir.rb b/lib/chef/chef_fs/file_system/base_fs_dir.rb
new file mode 100644
index 0000000000..74038f481b
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/base_fs_dir.rb
@@ -0,0 +1,47 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/base_fs_object'
+require 'chef/chef_fs/file_system/nonexistent_fs_object'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class BaseFSDir < BaseFSObject
+ def initialize(name, parent)
+ super
+ end
+
+ def dir?
+ true
+ end
+
+ # Override child(name) to provide a child object by name without the network read
+ def child(name)
+ children.select { |child| child.name == name }.first || NonexistentFSObject.new(name, self)
+ end
+
+ def can_have_child?(name, is_dir)
+ true
+ end
+
+ # Abstract: children
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/base_fs_object.rb b/lib/chef/chef_fs/file_system/base_fs_object.rb
new file mode 100644
index 0000000000..855892fc89
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/base_fs_object.rb
@@ -0,0 +1,121 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/path_utils'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class BaseFSObject
+ def initialize(name, parent)
+ @parent = parent
+ @name = name
+ if parent
+ @path = Chef::ChefFS::PathUtils::join(parent.path, name)
+ else
+ if name != ''
+ raise ArgumentError, "Name of root object must be empty string: was '#{name}' instead"
+ end
+ @path = '/'
+ end
+ end
+
+ attr_reader :name
+ attr_reader :parent
+ attr_reader :path
+
+ def root
+ parent ? parent.root : self
+ end
+
+ def path_for_printing
+ if parent
+ parent_path = parent.path_for_printing
+ if parent_path == '.'
+ name
+ else
+ Chef::ChefFS::PathUtils::join(parent.path_for_printing, name)
+ end
+ else
+ name
+ end
+ end
+
+ def dir?
+ false
+ end
+
+ def exists?
+ true
+ end
+
+ def child(name)
+ NonexistentFSObject.new(name, self)
+ end
+
+ # Override can_have_child? to report whether a given file *could* be added
+ # to this directory. (Some directories can't have subdirs, some can only have .json
+ # files, etc.)
+ def can_have_child?(name, is_dir)
+ false
+ end
+
+ # Override this if you have a special comparison algorithm that can tell
+ # you whether this entry is the same as another--either a quicker or a
+ # more reliable one. Callers will use this to decide whether to upload,
+ # download or diff an object.
+ #
+ # You should not override this if you're going to do the standard
+ # +self.read == other.read+. If you return +nil+, the caller will call
+ # +other.compare_to(you)+ instead. Give them a chance :)
+ #
+ # ==== Parameters
+ #
+ # * +other+ - the entry to compare to
+ #
+ # ==== Returns
+ #
+ # * +[ are_same, value, other_value ]+
+ # +are_same+ may be +true+, +false+ or +nil+ (which means "don't know").
+ # +value+ and +other_value+ must either be the text of +self+ or +other+,
+ # +:none+ (if the entry does not exist or has no value) or +nil+ if the
+ # value was not retrieved.
+ # * +nil+ if a definitive answer cannot be had and nothing was retrieved.
+ #
+ # ==== Example
+ #
+ # are_same, value, other_value = entry.compare_to(other)
+ # if are_same.nil?
+ # are_same, other_value, value = other.compare_to(entry)
+ # end
+ # if are_same.nil?
+ # value = entry.read if value.nil?
+ # other_value = entry.read if other_value.nil?
+ # are_same = (value == other_value)
+ # end
+ def compare_to(other)
+ return nil
+ end
+
+ # Important directory attributes: name, parent, path, root
+ # Overridable attributes: dir?, child(name), path_for_printing
+ # Abstract: read, write, delete, children
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb
new file mode 100644
index 0000000000..87d904e830
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb
@@ -0,0 +1,109 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/file_system_entry'
+require 'chef/cookbook/chefignore'
+require 'chef/cookbook/cookbook_version_loader'
+require 'chef/node'
+require 'chef/role'
+require 'chef/environment'
+require 'chef/data_bag_item'
+require 'chef/client'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ # ChefRepositoryFileSystemEntry works just like FileSystemEntry,
+ # except it pretends files in /cookbooks/chefignore don't exist
+ # and it can inflate Chef objects
+ class ChefRepositoryFileSystemEntry < FileSystemEntry
+ def initialize(name, parent, file_path = nil)
+ super(name, parent, file_path)
+ # Load /cookbooks/chefignore
+ if name == "cookbooks" && path == "/cookbooks" # We check name first because it's a faster fail than path
+ @chefignore = Chef::Cookbook::Chefignore.new(self.file_path)
+ # If we are a cookbook or a cookbook subdirectory, empty directories
+ # underneath us are ignored (since they cannot be uploaded)
+ elsif parent && parent.name === "cookbooks" && parent.path == "/cookbooks"
+ @ignore_empty_directories = true
+ elsif parent && parent.ignore_empty_directories?
+ @ignore_empty_directories = true
+ end
+ end
+
+ attr_reader :chefignore
+
+ def ignore_empty_directories?
+ @ignore_empty_directories
+ end
+
+ def chef_object
+ begin
+ if parent.path == "/cookbooks"
+ loader = Chef::Cookbook::CookbookVersionLoader.new(file_path, parent.chefignore)
+ loader.load_cookbooks
+ return loader.cookbook_version
+ end
+
+ # Otherwise the information to inflate the object, is in the file (json_class).
+ return Chef::JSONCompat.from_json(read)
+ rescue
+ Chef::Log.error("Could not read #{path_for_printing} into a Chef object: #{$!}")
+ end
+ nil
+ end
+
+ def children
+ @children ||= Dir.entries(file_path).select { |entry| entry != '.' && entry != '..' && !ignored?(entry) }.
+ map { |entry| ChefRepositoryFileSystemEntry.new(entry, self) }
+ end
+
+ attr_reader :chefignore
+
+ private
+
+ def ignored?(child_name)
+ # empty directories inside a cookbook are ignored
+ if ignore_empty_directories?
+ child_path = PathUtils.join(file_path, child_name)
+ if File.directory?(child_path) && Dir.entries(child_path) == [ '.', '..' ]
+ return true
+ end
+ end
+
+ ignorer = self
+ begin
+ if ignorer.chefignore
+ # Grab the path from entry to child
+ path_to_child = child_name
+ child = self
+ while child != ignorer
+ path_to_child = PathUtils.join(child.name, path_to_child)
+ child = child.parent
+ end
+ # Check whether that relative path is ignored
+ return ignorer.chefignore.ignored?(path_to_child)
+ end
+ ignorer = ignorer.parent
+ end while ignorer
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb
new file mode 100644
index 0000000000..fdad68003c
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb
@@ -0,0 +1,31 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/chef_repository_file_system_entry'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class ChefRepositoryFileSystemRootDir < ChefRepositoryFileSystemEntry
+ def initialize(file_path)
+ super("", nil, file_path)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb
new file mode 100644
index 0000000000..d3c217d11c
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb
@@ -0,0 +1,84 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/base_fs_dir'
+require 'chef/chef_fs/file_system/rest_list_dir'
+require 'chef/chef_fs/file_system/cookbooks_dir'
+require 'chef/chef_fs/file_system/data_bags_dir'
+require 'chef/chef_fs/file_system/nodes_dir'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class ChefServerRootDir < BaseFSDir
+ def initialize(root_name, chef_config, repo_mode)
+ super("", nil)
+ @chef_server_url = chef_config[:chef_server_url]
+ @chef_username = chef_config[:node_name]
+ @chef_private_key = chef_config[:client_key]
+ @environment = chef_config[:environment]
+ @repo_mode = repo_mode
+ @root_name = root_name
+ end
+
+ attr_reader :chef_server_url
+ attr_reader :chef_username
+ attr_reader :chef_private_key
+ attr_reader :environment
+ attr_reader :repo_mode
+
+ def rest
+ Chef::REST.new(chef_server_url, chef_username, chef_private_key)
+ end
+
+ def api_path
+ ""
+ end
+
+ def path_for_printing
+ "#{@root_name}/"
+ end
+
+ def can_have_child?(name, is_dir)
+ is_dir && children.any? { |child| child.name == name }
+ end
+
+ def children
+ @children ||= begin
+ result = [
+ CookbooksDir.new(self),
+ DataBagsDir.new(self),
+ RestListDir.new("environments", self),
+ RestListDir.new("roles", self)
+ ]
+ if repo_mode == 'everything'
+ result += [
+ RestListDir.new("clients", self),
+ NodesDir.new(self),
+ RestListDir.new("users", self)
+ ]
+ end
+ result.sort_by { |child| child.name }
+ end
+ end
+
+ # Yeah, sorry, I'm not putting delete on this thing.
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/cookbook_dir.rb b/lib/chef/chef_fs/file_system/cookbook_dir.rb
new file mode 100644
index 0000000000..e87d5dd49d
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/cookbook_dir.rb
@@ -0,0 +1,188 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/rest_list_dir'
+require 'chef/chef_fs/file_system/cookbook_subdir'
+require 'chef/chef_fs/file_system/cookbook_file'
+require 'chef/chef_fs/file_system/not_found_error'
+require 'chef/cookbook_version'
+require 'chef/cookbook_uploader'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class CookbookDir < BaseFSDir
+ def initialize(name, parent, versions = nil)
+ super(name, parent)
+ @versions = versions
+ end
+
+ attr_reader :versions
+
+ COOKBOOK_SEGMENT_INFO = {
+ :attributes => { :ruby_only => true },
+ :definitions => { :ruby_only => true },
+ :recipes => { :ruby_only => true },
+ :libraries => { :ruby_only => true },
+ :templates => { :recursive => true },
+ :files => { :recursive => true },
+ :resources => { :ruby_only => true, :recursive => true },
+ :providers => { :ruby_only => true, :recursive => true },
+ :root_files => { }
+ }
+
+ def add_child(child)
+ @children << child
+ end
+
+ def api_path
+ "#{parent.api_path}/#{name}/_latest"
+ end
+
+ def child(name)
+ # Since we're ignoring the rules and doing a network request here,
+ # we need to make sure we don't rethrow the exception. (child(name)
+ # is not supposed to fail.)
+ begin
+ result = children.select { |child| child.name == name }.first
+ return result if result
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ end
+ return NonexistentFSObject.new(name, self)
+ end
+
+ def can_have_child?(name, is_dir)
+ # A cookbook's root may not have directories unless they are segment directories
+ if is_dir
+ return name != 'root_files' &&
+ COOKBOOK_SEGMENT_INFO.keys.any? { |segment| segment.to_s == name }
+ end
+ true
+ end
+
+ def children
+ if @children.nil?
+ @children = []
+ manifest = chef_object.manifest
+ COOKBOOK_SEGMENT_INFO.each do |segment, segment_info|
+ next unless manifest.has_key?(segment)
+
+ # Go through each file in the manifest for the segment, and
+ # add cookbook subdirs and files for it.
+ manifest[segment].each do |segment_file|
+ parts = segment_file[:path].split('/')
+ # Get or create the path to the file
+ container = self
+ parts[0,parts.length-1].each do |part|
+ old_container = container
+ container = old_container.children.select { |child| part == child.name }.first
+ if !container
+ container = CookbookSubdir.new(part, old_container, segment_info[:ruby_only], segment_info[:recursive])
+ old_container.add_child(container)
+ end
+ end
+ # Create the file itself
+ container.add_child(CookbookFile.new(parts[parts.length-1], container, segment_file))
+ end
+ end
+ end
+ @children
+ end
+
+ def dir?
+ exists?
+ end
+
+ def read
+ # This will only be called if dir? is false, which means exists? is false.
+ raise Chef::ChefFS::FileSystem::NotFoundError, path_for_printing
+ end
+
+ def exists?
+ if !@versions
+ child = parent.children.select { |child| child.name == name }.first
+ @versions = child.versions if child
+ end
+ !!@versions
+ end
+
+ def compare_to(other)
+ if !other.dir?
+ return [ !exists?, nil, nil ]
+ end
+ are_same = true
+ Chef::ChefFS::CommandLine::diff_entries(self, other, nil, :name_only) do
+ are_same = false
+ end
+ [ are_same, nil, nil ]
+ end
+
+ def copy_from(other)
+ parent.upload_cookbook_from(other)
+ end
+
+ def rest
+ parent.rest
+ end
+
+ def chef_object
+ # We cheat and cache here, because it seems like a good idea to keep
+ # the cookbook view consistent with the directory structure.
+ return @chef_object if @chef_object
+
+ # The negative (not found) response is cached
+ if @could_not_get_chef_object
+ raise Chef::ChefFS::FileSystem::NotFoundError.new(@could_not_get_chef_object), "#{path_for_printing} not found"
+ end
+
+ begin
+ # We want to fail fast, for now, because of the 500 issue :/
+ # This will make things worse for parallelism, a little, because
+ # Chef::Config is global and this could affect other requests while
+ # this request is going on. (We're not parallel yet, but we will be.)
+ # Chef bug http://tickets.opscode.com/browse/CHEF-3066
+ old_retry_count = Chef::Config[:http_retry_count]
+ begin
+ Chef::Config[:http_retry_count] = 0
+ @chef_object ||= rest.get_rest(api_path)
+ ensure
+ Chef::Config[:http_retry_count] = old_retry_count
+ end
+ rescue Net::HTTPServerException
+ if $!.response.code == "404"
+ @could_not_get_chef_object = $!
+ raise Chef::ChefFS::FileSystem::NotFoundError.new(@could_not_get_chef_object), "#{path_for_printing} not found"
+ else
+ raise
+ end
+
+ # Chef bug http://tickets.opscode.com/browse/CHEF-3066 ... instead of 404 we get 500 right now.
+ # Remove this when that bug is fixed.
+ rescue Net::HTTPFatalError
+ if $!.response.code == "500"
+ @could_not_get_chef_object = $!
+ raise Chef::ChefFS::FileSystem::NotFoundError.new(@could_not_get_chef_object), "#{path_for_printing} not found"
+ else
+ raise
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/cookbook_file.rb b/lib/chef/chef_fs/file_system/cookbook_file.rb
new file mode 100644
index 0000000000..baa71f5d9e
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/cookbook_file.rb
@@ -0,0 +1,78 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/base_fs_object'
+require 'digest/md5'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class CookbookFile < BaseFSObject
+ def initialize(name, parent, file)
+ super(name, parent)
+ @file = file
+ end
+
+ attr_reader :file
+
+ def checksum
+ file[:checksum]
+ end
+
+ def read
+ old_sign_on_redirect = rest.sign_on_redirect
+ rest.sign_on_redirect = false
+ begin
+ rest.get_rest(file[:url])
+ ensure
+ rest.sign_on_redirect = old_sign_on_redirect
+ end
+ end
+
+ def rest
+ parent.rest
+ end
+
+ def compare_to(other)
+ other_value = nil
+ if other.respond_to?(:checksum)
+ other_checksum = other.checksum
+ else
+ begin
+ other_value = other.read
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ return [ false, nil, :none ]
+ end
+ other_checksum = calc_checksum(other_value)
+ end
+ [ checksum == other_checksum, nil, other_value ]
+ end
+
+ private
+
+ def calc_checksum(value)
+ begin
+ Digest::MD5.hexdigest(value)
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/cookbook_subdir.rb b/lib/chef/chef_fs/file_system/cookbook_subdir.rb
new file mode 100644
index 0000000000..73c709e01e
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/cookbook_subdir.rb
@@ -0,0 +1,54 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/base_fs_dir'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class CookbookSubdir < BaseFSDir
+ def initialize(name, parent, ruby_only, recursive)
+ super(name, parent)
+ @children = []
+ @ruby_only = ruby_only
+ @recursive = recursive
+ end
+
+ attr_reader :versions
+ attr_reader :children
+
+ def add_child(child)
+ @children << child
+ end
+
+ def can_have_child?(name, is_dir)
+ if is_dir
+ return false if !@recursive
+ else
+ return false if @ruby_only && name !~ /\.rb$/
+ end
+ true
+ end
+
+ def rest
+ parent.rest
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/cookbooks_dir.rb b/lib/chef/chef_fs/file_system/cookbooks_dir.rb
new file mode 100644
index 0000000000..9249b42aaa
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/cookbooks_dir.rb
@@ -0,0 +1,68 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/rest_list_dir'
+require 'chef/chef_fs/file_system/cookbook_dir'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class CookbooksDir < RestListDir
+ def initialize(parent)
+ super("cookbooks", parent)
+ end
+
+ def child(name)
+ result = @children.select { |child| child.name == name }.first if @children
+ result || CookbookDir.new(name, self)
+ end
+
+ def children
+ @children ||= rest.get_rest(api_path).map { |key, value| CookbookDir.new(key, self, value) }
+ end
+
+ def create_child_from(other)
+ upload_cookbook_from(other)
+ end
+
+ def upload_cookbook_from(other)
+ other_cookbook_version = other.chef_object
+ # TODO this only works on the file system. And it can't be broken into
+ # pieces.
+ begin
+ uploader = Chef::CookbookUploader.new(other_cookbook_version, other.parent.file_path)
+ uploader.upload_cookbooks
+ rescue Net::HTTPServerException => e
+ case e.response.code
+ when "409"
+ ui.error "Version #{other_cookbook_version.version} of cookbook #{other_cookbook_version.name} is frozen. Use --force to override."
+ Chef::Log.debug(e)
+ raise Exceptions::CookbookFrozen
+ else
+ raise
+ end
+ end
+ end
+
+ def can_have_child?(name, is_dir)
+ is_dir
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/data_bag_dir.rb b/lib/chef/chef_fs/file_system/data_bag_dir.rb
new file mode 100644
index 0000000000..41fb5dfc63
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/data_bag_dir.rb
@@ -0,0 +1,78 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/rest_list_dir'
+require 'chef/chef_fs/file_system/data_bag_item'
+require 'chef/chef_fs/file_system/not_found_error'
+require 'chef/chef_fs/file_system/must_delete_recursively_error'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class DataBagDir < RestListDir
+ def initialize(name, parent, exists = nil)
+ super(name, parent)
+ @exists = nil
+ end
+
+ def dir?
+ exists?
+ end
+
+ def read
+ # This will only be called if dir? is false, which means exists? is false.
+ raise Chef::ChefFS::FileSystem::NotFoundError, "#{path_for_printing} not found"
+ end
+
+ def exists?
+ if @exists.nil?
+ @exists = parent.children.any? { |child| child.name == name }
+ end
+ @exists
+ end
+
+ def create_child(name, file_contents)
+ json = Chef::JSONCompat.from_json(file_contents).to_hash
+ id = name[0,name.length-5]
+ if json.include?('id') && json['id'] != id
+ raise "ID in #{path_for_printing}/#{name} must be '#{id}' (is '#{json['id']}')"
+ end
+ rest.post_rest(api_path, json)
+ _make_child_entry(name, true)
+ end
+
+ def _make_child_entry(name, exists = nil)
+ DataBagItem.new(name, self, exists)
+ end
+
+ def delete(recurse)
+ if !recurse
+ raise Chef::ChefFS::FileSystem::MustDeleteRecursivelyError.new, "#{path_for_printing} must be deleted recursively"
+ end
+ begin
+ rest.delete_rest(api_path)
+ rescue Net::HTTPServerException
+ if $!.response.code == "404"
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/data_bag_item.rb b/lib/chef/chef_fs/file_system/data_bag_item.rb
new file mode 100644
index 0000000000..2f6eb15232
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/data_bag_item.rb
@@ -0,0 +1,59 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/rest_list_entry'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class DataBagItem < RestListEntry
+ def initialize(name, parent, exists = nil)
+ super(name, parent, exists)
+ end
+
+ def write(file_contents)
+ # Write is just a little tiny bit different for data bags:
+ # you set raw_data in the JSON instead of putting the items
+ # in the top level.
+ json = Chef::JSONCompat.from_json(file_contents).to_hash
+ id = name[0,name.length-5] # Strip off the .json from the end
+ if json['id'] != id
+ raise "Id in #{path_for_printing}/#{name} must be '#{id}' (is '#{json['id']}')"
+ end
+ begin
+ data_bag = parent.name
+ json = {
+ "name" => "data_bag_item_#{data_bag}_#{id}",
+ "json_class" => "Chef::DataBagItem",
+ "chef_type" => "data_bag_item",
+ "data_bag" => data_bag,
+ "raw_data" => json
+ }
+ rest.put_rest(api_path, json)
+ rescue Net::HTTPServerException
+ if $!.response.code == "404"
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found"
+ else
+ raise
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/data_bags_dir.rb b/lib/chef/chef_fs/file_system/data_bags_dir.rb
new file mode 100644
index 0000000000..6eca990545
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/data_bags_dir.rb
@@ -0,0 +1,66 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/rest_list_dir'
+require 'chef/chef_fs/file_system/data_bag_dir'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class DataBagsDir < RestListDir
+ def initialize(parent)
+ super("data_bags", parent, "data")
+ end
+
+ def child(name)
+ result = @children.select { |child| child.name == name }.first if @children
+ result || DataBagDir.new(name, self)
+ end
+
+ def children
+ begin
+ @children ||= rest.get_rest(api_path).keys.map do |entry|
+ DataBagDir.new(entry, self, true)
+ end
+ rescue Net::HTTPServerException
+ if $!.response.code == "404"
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found"
+ else
+ raise
+ end
+ end
+ end
+
+ def can_have_child?(name, is_dir)
+ is_dir
+ end
+
+ def create_child(name, file_contents)
+ begin
+ rest.post_rest(api_path, { 'name' => name })
+ rescue Net::HTTPServerException
+ if $!.response.code != "409"
+ raise
+ end
+ end
+ DataBagDir.new(name, self, true)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/file_system_entry.rb b/lib/chef/chef_fs/file_system/file_system_entry.rb
new file mode 100644
index 0000000000..a86e0cb82a
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/file_system_entry.rb
@@ -0,0 +1,90 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/base_fs_dir'
+require 'chef/chef_fs/file_system/rest_list_dir'
+require 'chef/chef_fs/file_system/not_found_error'
+require 'chef/chef_fs/path_utils'
+require 'fileutils'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class FileSystemEntry < BaseFSDir
+ def initialize(name, parent, file_path = nil)
+ super(name, parent)
+ @file_path = file_path || "#{parent.file_path}/#{name}"
+ end
+
+ attr_reader :file_path
+
+ def path_for_printing
+ Chef::ChefFS::PathUtils::relative_to(file_path, File.expand_path(Dir.pwd))
+ end
+
+ def children
+ begin
+ @children ||= Dir.entries(file_path).select { |entry| entry != '.' && entry != '..' }.map { |entry| FileSystemEntry.new(entry, self) }
+ rescue Errno::ENOENT
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{file_path} not found"
+ end
+ end
+
+ def create_child(child_name, file_contents=nil)
+ result = FileSystemEntry.new(child_name, self)
+ if file_contents
+ result.write(file_contents)
+ else
+ Dir.mkdir(result.file_path)
+ end
+ result
+ end
+
+ def dir?
+ File.directory?(file_path)
+ end
+
+ def delete(recurse)
+ if dir?
+ if recurse
+ FileUtils.rm_rf(file_path)
+ else
+ File.rmdir(file_path)
+ end
+ else
+ File.delete(file_path)
+ end
+ end
+
+ def read
+ begin
+ File.open(file_path, "rb") {|f| f.read}
+ rescue Errno::ENOENT
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{file_path} not found"
+ end
+ end
+
+ def write(content)
+ File.open(file_path, 'wb') do |file|
+ file.write(content)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/file_system_error.rb b/lib/chef/chef_fs/file_system/file_system_error.rb
new file mode 100644
index 0000000000..a461221108
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/file_system_error.rb
@@ -0,0 +1,31 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class FileSystemError < StandardError
+ def initialize(cause = nil)
+ @cause = cause
+ end
+
+ attr_reader :cause
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/file_system_root_dir.rb b/lib/chef/chef_fs/file_system/file_system_root_dir.rb
new file mode 100644
index 0000000000..afbf7b1901
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/file_system_root_dir.rb
@@ -0,0 +1,31 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/file_system_entry'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class FileSystemRootDir < FileSystemEntry
+ def initialize(file_path)
+ super("", nil, file_path)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb b/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb
new file mode 100644
index 0000000000..d247a5b4ed
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb
@@ -0,0 +1,31 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/file_system_error'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class MustDeleteRecursivelyError < FileSystemError
+ def initialize(cause = nil)
+ super(cause)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/nodes_dir.rb b/lib/chef/chef_fs/file_system/nodes_dir.rb
new file mode 100644
index 0000000000..4dfbf6d850
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/nodes_dir.rb
@@ -0,0 +1,47 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/base_fs_dir'
+require 'chef/chef_fs/file_system/rest_list_entry'
+require 'chef/chef_fs/file_system/not_found_error'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class NodesDir < RestListDir
+ def initialize(parent)
+ super("nodes", parent)
+ end
+
+ # Override children to respond to environment
+ def children
+ @children ||= begin
+ env_api_path = environment ? "environments/#{environment}/#{api_path}" : api_path
+ rest.get_rest(env_api_path).keys.map { |key| RestListEntry.new("#{key}.json", self, true) }
+ rescue Net::HTTPServerException
+ if $!.response.code == "404"
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found"
+ else
+ raise
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/nonexistent_fs_object.rb b/lib/chef/chef_fs/file_system/nonexistent_fs_object.rb
new file mode 100644
index 0000000000..dc82e83b0d
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/nonexistent_fs_object.rb
@@ -0,0 +1,40 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/base_fs_object'
+require 'chef/chef_fs/file_system/not_found_error'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class NonexistentFSObject < BaseFSObject
+ def initialize(name, parent)
+ super
+ end
+
+ def exists?
+ false
+ end
+
+ def read
+ raise Chef::ChefFS::FileSystem::NotFoundError, "Nonexistent #{path_for_printing}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/not_found_error.rb b/lib/chef/chef_fs/file_system/not_found_error.rb
new file mode 100644
index 0000000000..0b608f1abf
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/not_found_error.rb
@@ -0,0 +1,31 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/file_system_error'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class NotFoundError < FileSystemError
+ def initialize(cause = nil)
+ super(cause)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/rest_list_dir.rb b/lib/chef/chef_fs/file_system/rest_list_dir.rb
new file mode 100644
index 0000000000..0e8db4d7b9
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/rest_list_dir.rb
@@ -0,0 +1,84 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/base_fs_dir'
+require 'chef/chef_fs/file_system/rest_list_entry'
+require 'chef/chef_fs/file_system/not_found_error'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class RestListDir < BaseFSDir
+ def initialize(name, parent, api_path = nil)
+ super(name, parent)
+ @api_path = api_path || (parent.api_path == "" ? name : "#{parent.api_path}/#{name}")
+ end
+
+ attr_reader :api_path
+
+ def child(name)
+ result = @children.select { |child| child.name == name }.first if @children
+ result ||= can_have_child?(name, false) ?
+ _make_child_entry(name) : NonexistentFSObject.new(name, self)
+ end
+
+ def can_have_child?(name, is_dir)
+ name =~ /\.json$/ && !is_dir
+ end
+
+ def children
+ begin
+ @children ||= rest.get_rest(api_path).keys.map do |key|
+ _make_child_entry("#{key}.json", true)
+ end
+ rescue Net::HTTPServerException
+ if $!.response.code == "404"
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found"
+ else
+ raise
+ end
+ end
+ end
+
+ # NOTE if you change this significantly, you will likely need to change
+ # DataBagDir.create_child as well.
+ def create_child(name, file_contents)
+ json = Chef::JSONCompat.from_json(file_contents).to_hash
+ base_name = name[0,name.length-5]
+ if json.include?('name') && json['name'] != base_name
+ raise "Name in #{path_for_printing}/#{name} must be '#{base_name}' (is '#{json['name']}')"
+ end
+ rest.post_rest(api_path, json)
+ _make_child_entry(name, true)
+ end
+
+ def environment
+ parent.environment
+ end
+
+ def rest
+ parent.rest
+ end
+
+ def _make_child_entry(name, exists = nil)
+ RestListEntry.new(name, self, exists)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/rest_list_entry.rb b/lib/chef/chef_fs/file_system/rest_list_entry.rb
new file mode 100644
index 0000000000..dd504ef341
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/rest_list_entry.rb
@@ -0,0 +1,123 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/base_fs_object'
+require 'chef/chef_fs/file_system/not_found_error'
+require 'chef/role'
+require 'chef/node'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ class RestListEntry < BaseFSObject
+ def initialize(name, parent, exists = nil)
+ super(name, parent)
+ @exists = exists
+ end
+
+ def api_path
+ if name.length < 5 || name[-5,5] != ".json"
+ raise "Invalid name #{path}: must end in .json"
+ end
+ api_child_name = name[0,name.length-5]
+ "#{parent.api_path}/#{api_child_name}"
+ end
+
+ def environment
+ parent.environment
+ end
+
+ def exists?
+ if @exists.nil?
+ begin
+ @exists = parent.children.any? { |child| child.name == name }
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ @exists = false
+ end
+ end
+ @exists
+ end
+
+ def delete(recurse)
+ begin
+ rest.delete_rest(api_path)
+ rescue Net::HTTPServerException
+ if $!.response.code == "404"
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found"
+ else
+ raise
+ end
+ end
+ end
+
+ def read
+ Chef::JSONCompat.to_json_pretty(chef_object.to_hash)
+ end
+
+ def chef_object
+ begin
+ # REST will inflate the Chef object using json_class
+ rest.get_rest(api_path)
+ rescue Net::HTTPServerException
+ if $!.response.code == "404"
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found"
+ else
+ raise
+ end
+ end
+ end
+
+ def compare_to(other)
+ begin
+ other_value = other.read
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ return [ nil, nil, :none ]
+ end
+ begin
+ value = chef_object.to_hash
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ return [ false, :none, other_value ]
+ end
+ are_same = (value == Chef::JSONCompat.from_json(other_value, :create_additions => false))
+ [ are_same, Chef::JSONCompat.to_json_pretty(value), other_value ]
+ end
+
+ def rest
+ parent.rest
+ end
+
+ def write(file_contents)
+ json = Chef::JSONCompat.from_json(file_contents).to_hash
+ base_name = name[0,name.length-5]
+ if json['name'] != base_name
+ raise "Name in #{path_for_printing}/#{name} must be '#{base_name}' (is '#{json['name']}')"
+ end
+ begin
+ rest.put_rest(api_path, json)
+ rescue Net::HTTPServerException
+ if $!.response.code == "404"
+ raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found"
+ else
+ raise
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/knife.rb b/lib/chef/chef_fs/knife.rb
new file mode 100644
index 0000000000..8a116d980e
--- /dev/null
+++ b/lib/chef/chef_fs/knife.rb
@@ -0,0 +1,77 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs/file_system/chef_server_root_dir'
+require 'chef/chef_fs/file_system/chef_repository_file_system_root_dir'
+require 'chef/chef_fs/file_pattern'
+require 'chef/chef_fs/path_utils'
+require 'chef/config'
+
+class Chef
+ module ChefFS
+ class Knife < Chef::Knife
+ def self.common_options
+ option :repo_mode,
+ :long => '--repo-mode MODE',
+ :default => "default",
+ :description => "Specifies the local repository layout. Values: default or full"
+ end
+
+ def base_path
+ @base_path ||= begin
+ relative_to_base = Chef::ChefFS::PathUtils::relative_to(File.expand_path(Dir.pwd), chef_repo)
+ relative_to_base == '.' ? '/' : "/#{relative_to_base}"
+ end
+ end
+
+ def chef_fs
+ @chef_fs ||= Chef::ChefFS::FileSystem::ChefServerRootDir.new("remote", Chef::Config, config[:repo_mode])
+ end
+
+ def chef_repo
+ @chef_repo ||= File.expand_path(File.join(Chef::Config.cookbook_path, ".."))
+ end
+
+ def format_path(path)
+ if path[0,base_path.length] == base_path
+ if path == base_path
+ return "."
+ elsif path[base_path.length] == "/"
+ return path[base_path.length + 1, path.length - base_path.length - 1]
+ elsif base_path == "/" && path[0] == "/"
+ return path[1, path.length - 1]
+ end
+ end
+ path
+ end
+
+ def local_fs
+ @local_fs ||= Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(chef_repo)
+ end
+
+ def pattern_args
+ @pattern_args ||= pattern_args_from(name_args)
+ end
+
+ def pattern_args_from(args)
+ args.map { |arg| Chef::ChefFS::FilePattern::relative_to(base_path, arg) }.to_a
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/path_utils.rb b/lib/chef/chef_fs/path_utils.rb
new file mode 100644
index 0000000000..67c62a7545
--- /dev/null
+++ b/lib/chef/chef_fs/path_utils.rb
@@ -0,0 +1,64 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/chef_fs'
+
+class Chef
+ module ChefFS
+ class PathUtils
+
+ # If you are in 'source', this is what you would have to type to reach 'dest'
+ # relative_to('/a/b/c/d/e', '/a/b/x/y') == '../../c/d/e'
+ # relative_to('/a/b', '/a/b') == ''
+ def self.relative_to(dest, source)
+ # Skip past the common parts
+ source_parts = Chef::ChefFS::PathUtils.split(source)
+ dest_parts = Chef::ChefFS::PathUtils.split(dest)
+ i = 0
+ until i >= source_parts.length || i >= dest_parts.length || source_parts[i] != source_parts[i]
+ i+=1
+ end
+ # dot-dot up from 'source' to the common ancestor, then
+ # descend to 'dest' from the common ancestor
+ result = Chef::ChefFS::PathUtils.join(*(['..']*(source_parts.length-i) + dest_parts[i,dest.length-i]))
+ result == '' ? '.' : result
+ end
+
+ def self.join(*parts)
+ return "" if parts.length == 0
+ # Determine if it started with a slash
+ absolute = parts[0].length == 0 || parts[0].length > 0 && parts[0] =~ /^#{regexp_path_separator}/
+ # Remove leading and trailing slashes from each part so that the join will work (and the slash at the end will go away)
+ parts = parts.map { |part| part.gsub(/^\/|\/$/, "") }
+ # Don't join empty bits
+ result = parts.select { |part| part != "" }.join("/")
+ # Put the / back on
+ absolute ? "/#{result}" : result
+ end
+
+ def self.split(path)
+ path.split(Regexp.new(regexp_path_separator))
+ end
+
+ def self.regexp_path_separator
+ Chef::ChefFS::windows? ? '[/\\]' : '/'
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/client.rb b/lib/chef/client.rb
new file mode 100644
index 0000000000..ea74f5f50a
--- /dev/null
+++ b/lib/chef/client.rb
@@ -0,0 +1,505 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2008-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/config'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/path_sanity'
+require 'chef/log'
+require 'chef/rest'
+require 'chef/api_client'
+require 'chef/platform'
+require 'chef/node'
+require 'chef/role'
+require 'chef/file_cache'
+require 'chef/run_context'
+require 'chef/runner'
+require 'chef/run_status'
+require 'chef/cookbook/cookbook_collection'
+require 'chef/cookbook/file_vendor'
+require 'chef/cookbook/file_system_file_vendor'
+require 'chef/cookbook/remote_file_vendor'
+require 'chef/event_dispatch/dispatcher'
+require 'chef/formatters/base'
+require 'chef/formatters/doc'
+require 'chef/formatters/minimal'
+require 'chef/version'
+require 'chef/resource_reporter'
+require 'chef/run_lock'
+require 'ohai'
+require 'rbconfig'
+
+class Chef
+ # == Chef::Client
+ # The main object in a Chef run. Preps a Chef::Node and Chef::RunContext,
+ # syncs cookbooks if necessary, and triggers convergence.
+ class Client
+ include Chef::Mixin::PathSanity
+
+ # Clears all notifications for client run status events.
+ # Primarily for testing purposes.
+ def self.clear_notifications
+ @run_start_notifications = nil
+ @run_completed_successfully_notifications = nil
+ @run_failed_notifications = nil
+ end
+
+ # The list of notifications to be run when the client run starts.
+ def self.run_start_notifications
+ @run_start_notifications ||= []
+ end
+
+ # The list of notifications to be run when the client run completes
+ # successfully.
+ def self.run_completed_successfully_notifications
+ @run_completed_successfully_notifications ||= []
+ end
+
+ # The list of notifications to be run when the client run fails.
+ def self.run_failed_notifications
+ @run_failed_notifications ||= []
+ end
+
+ # Add a notification for the 'client run started' event. The notification
+ # is provided as a block. The current Chef::RunStatus object will be passed
+ # to the notification_block when the event is triggered.
+ def self.when_run_starts(&notification_block)
+ run_start_notifications << notification_block
+ end
+
+ # Add a notification for the 'client run success' event. The notification
+ # is provided as a block. The current Chef::RunStatus object will be passed
+ # to the notification_block when the event is triggered.
+ def self.when_run_completes_successfully(&notification_block)
+ run_completed_successfully_notifications << notification_block
+ end
+
+ # Add a notification for the 'client run failed' event. The notification
+ # is provided as a block. The current Chef::RunStatus is passed to the
+ # notification_block when the event is triggered.
+ def self.when_run_fails(&notification_block)
+ run_failed_notifications << notification_block
+ end
+
+ # Callback to fire notifications that the Chef run is starting
+ def run_started
+ self.class.run_start_notifications.each do |notification|
+ notification.call(run_status)
+ end
+ end
+
+ # Callback to fire notifications that the run completed successfully
+ def run_completed_successfully
+ success_handlers = self.class.run_completed_successfully_notifications
+ success_handlers.each do |notification|
+ notification.call(run_status)
+ end
+ end
+
+ # Callback to fire notifications that the Chef run failed
+ def run_failed
+ failure_handlers = self.class.run_failed_notifications
+ failure_handlers.each do |notification|
+ notification.call(run_status)
+ end
+ end
+
+ attr_accessor :node
+ attr_accessor :ohai
+ attr_accessor :rest
+ attr_accessor :runner
+
+ #--
+ # TODO: timh/cw: 5-19-2010: json_attribs should be moved to RunContext?
+ attr_reader :json_attribs
+
+ attr_reader :run_status
+
+ # Creates a new Chef::Client.
+ def initialize(json_attribs=nil, args={})
+ @json_attribs = json_attribs
+ @node = nil
+ @run_status = nil
+ @runner = nil
+ @ohai = Ohai::System.new
+
+ # If we want why-run output and user hasn't explicitly specified a format
+ # we need to use a formatter that will render whyrun output.
+ if Chef::Config.why_run
+ if Chef::Config.formatter == "null"
+ Chef::Log.warn("Forcing formatter of 'doc' to capture whyrun output.")
+ Chef::Config[:formatter] = 'doc'
+ end
+ end
+ formatter = Chef::Formatters.new(Chef::Config.formatter, STDOUT, STDERR)
+ @events = EventDispatch::Dispatcher.new(formatter)
+ @override_runlist = args.delete(:override_runlist)
+ runlist_override_sanity_check!
+ end
+
+ # Do a full run for this Chef::Client. Calls:
+ # * do_run
+ #
+ # This provides a wrapper around #do_run allowing the
+ # run to be optionally forked.
+ # === Returns
+ # boolean:: Return value from #do_run. Should always returns true.
+ def run
+ if(Chef::Config[:client_fork] && Process.respond_to?(:fork))
+ Chef::Log.info "Forking chef instance to converge..."
+ pid = fork do
+ Chef::Log.info "Forked instance now converging"
+ do_run
+ exit
+ end
+ Chef::Log.info "Fork successful. Waiting for new chef pid: #{pid}"
+ result = Process.waitpid2(pid)
+ raise "Forked convergence run failed" unless result.last.success?
+ Chef::Log.info "Forked child successfully reaped (pid: #{pid})"
+ true
+ else
+ do_run
+ end
+ end
+
+ # Configures the Chef::Cookbook::FileVendor class to fetch file from the
+ # server or disk as appropriate, creates the run context for this run, and
+ # sanity checks the cookbook collection.
+ #===Returns
+ # Chef::RunContext:: the run context for this run.
+ def setup_run_context
+ if Chef::Config[:solo]
+ Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, Chef::Config[:cookbook_path]) }
+ cl = Chef::CookbookLoader.new(Chef::Config[:cookbook_path])
+ cl.load_cookbooks
+ cookbook_collection = Chef::CookbookCollection.new(cl)
+ run_context = Chef::RunContext.new(node, cookbook_collection, @events)
+ else
+ Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, rest) }
+ cookbook_hash = sync_cookbooks
+ cookbook_collection = Chef::CookbookCollection.new(cookbook_hash)
+ run_context = Chef::RunContext.new(node, cookbook_collection, @events)
+ end
+ run_status.run_context = run_context
+
+ run_context.load(@run_list_expansion)
+ assert_cookbook_path_not_empty(run_context)
+ run_context
+ end
+
+ def save_updated_node
+ unless Chef::Config[:solo]
+ Chef::Log.debug("Saving the current state of node #{node_name}")
+ if(@original_runlist)
+ @node.run_list(*@original_runlist)
+ @node.automatic_attrs[:runlist_override_history] = {Time.now.to_i => @override_runlist.inspect}
+ end
+ @node.save
+ end
+ end
+
+ def run_ohai
+ ohai.all_plugins
+ end
+
+ def node_name
+ name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:hostname]
+ Chef::Config[:node_name] = name
+
+ unless name
+ msg = "Unable to determine node name: configure node_name or configure the system's hostname and fqdn"
+ raise Chef::Exceptions::CannotDetermineNodeName, msg
+ end
+
+ # node names > 90 bytes only work with authentication protocol >= 1.1
+ # see discussion in config.rb.
+ if name.bytesize > 90
+ Chef::Config[:authentication_protocol_version] = "1.1"
+ end
+
+ name
+ end
+
+ # Applies environment, external JSON attributes, and override run list to
+ # the node, Then expands the run_list.
+ #
+ # === Returns
+ # node<Chef::Node>:: The modified @node object. @node is modified in place.
+ def build_node
+ # Allow user to override the environment of a node by specifying
+ # a config parameter.
+ if Chef::Config[:environment] && !Chef::Config[:environment].chop.empty?
+ @node.chef_environment(Chef::Config[:environment])
+ end
+
+ # consume_external_attrs may add items to the run_list. Save the
+ # expanded run_list, which we will pass to the server later to
+ # determine which versions of cookbooks to use.
+ @node.reset_defaults_and_overrides
+ @node.consume_external_attrs(ohai.data, @json_attribs)
+
+ unless(@override_runlist.empty?)
+ @original_runlist = @node.run_list.run_list_items.dup
+ runlist_override_sanity_check!
+ @node.run_list(*@override_runlist)
+ Chef::Log.warn "Run List override has been provided."
+ Chef::Log.warn "Original Run List: [#{@original_runlist.join(', ')}]"
+ Chef::Log.warn "Overridden Run List: [#{@node.run_list}]"
+ end
+
+ @run_list_expansion = expand_run_list
+
+ # @run_list_expansion is a RunListExpansion.
+ #
+ # Convert @expanded_run_list, which is an
+ # Array of Hashes of the form
+ # {:name => NAME, :version_constraint => Chef::VersionConstraint },
+ # into @expanded_run_list_with_versions, an
+ # Array of Strings of the form
+ # "#{NAME}@#{VERSION}"
+ @expanded_run_list_with_versions = @run_list_expansion.recipes.with_version_constraints_strings
+
+ Chef::Log.info("Run List is [#{@node.run_list}]")
+ Chef::Log.info("Run List expands to [#{@expanded_run_list_with_versions.join(', ')}]")
+
+ @run_status = Chef::RunStatus.new(@node, @events)
+
+ @events.node_load_completed(node, @expanded_run_list_with_versions, Chef::Config)
+
+ @node
+ end
+
+ # In client-server operation, loads the node state from the server. In
+ # chef-solo operation, builds a new node object.
+ def load_node
+ @events.node_load_start(node_name, Chef::Config)
+ Chef::Log.debug("Building node object for #{node_name}")
+
+ if Chef::Config[:solo]
+ @node = Chef::Node.build(node_name)
+ else
+ @node = Chef::Node.find_or_create(node_name)
+ end
+ rescue Exception => e
+ # TODO: wrap this exception so useful error info can be given to the
+ # user.
+ @events.node_load_failed(node_name, e, Chef::Config)
+ raise
+ end
+
+ def expand_run_list
+ if Chef::Config[:solo]
+ @node.expand!('disk')
+ else
+ @node.expand!('server')
+ end
+ rescue Exception => e
+ # TODO: wrap/munge exception with useful error output.
+ @events.run_list_expand_failed(node, e)
+ raise
+ end
+
+ #
+ # === Returns
+ # rest<Chef::REST>:: returns Chef::REST connection object
+ def register(client_name=node_name, config=Chef::Config)
+ if File.exists?(config[:client_key])
+ @events.skipping_registration(client_name, config)
+ Chef::Log.debug("Client key #{config[:client_key]} is present - skipping registration")
+ else
+ @events.registration_start(node_name, config)
+ Chef::Log.info("Client key #{config[:client_key]} is not present - registering")
+ Chef::REST.new(config[:client_url], config[:validation_client_name], config[:validation_key]).register(client_name, config[:client_key])
+ @events.registration_completed
+ end
+ # We now have the client key, and should use it from now on.
+ @rest = Chef::REST.new(config[:chef_server_url], client_name, config[:client_key])
+ @resource_reporter = Chef::ResourceReporter.new(@rest)
+ @events.register(@resource_reporter)
+ rescue Exception => e
+ # TODO: munge exception so a semantic failure message can be given to the
+ # user
+ @events.registration_failed(node_name, e, config)
+ raise
+ end
+
+ # Sync_cookbooks eagerly loads all files except files and
+ # templates. It returns the cookbook_hash -- the return result
+ # from /environments/#{node.chef_environment}/cookbook_versions,
+ # which we will use for our run_context.
+ #
+ # === Returns
+ # Hash:: The hash of cookbooks with download URLs as given by the server
+ def sync_cookbooks
+ Chef::Log.debug("Synchronizing cookbooks")
+
+ begin
+ @events.cookbook_resolution_start(@expanded_run_list_with_versions)
+ cookbook_hash = rest.post_rest("environments/#{@node.chef_environment}/cookbook_versions",
+ {:run_list => @expanded_run_list_with_versions})
+ rescue Exception => e
+ # TODO: wrap/munge exception to provide helpful error output
+ @events.cookbook_resolution_failed(@expanded_run_list_with_versions, e)
+ raise
+ else
+ @events.cookbook_resolution_complete(cookbook_hash)
+ end
+
+ synchronizer = Chef::CookbookSynchronizer.new(cookbook_hash, @events)
+ synchronizer.sync_cookbooks
+
+ # register the file cache path in the cookbook path so that CookbookLoader actually picks up the synced cookbooks
+ Chef::Config[:cookbook_path] = File.join(Chef::Config[:file_cache_path], "cookbooks")
+
+ cookbook_hash
+ end
+
+ # Converges the node.
+ #
+ # === Returns
+ # true:: Always returns true
+ def converge(run_context)
+ @events.converge_start(run_context)
+ Chef::Log.debug("Converging node #{node_name}")
+ @runner = Chef::Runner.new(run_context)
+ runner.converge
+ @events.converge_complete
+ true
+ rescue Exception
+ # TODO: should this be a separate #converge_failed(exception) method?
+ @events.converge_complete
+ raise
+ end
+
+ private
+
+ # Do a full run for this Chef::Client. Calls:
+ #
+ # * run_ohai - Collect information about the system
+ # * build_node - Get the last known state, merge with local changes
+ # * register - If not in solo mode, make sure the server knows about this client
+ # * sync_cookbooks - If not in solo mode, populate the local cache with the node's cookbooks
+ # * converge - Bring this system up to date
+ #
+ # === Returns
+ # true:: Always returns true.
+ def do_run
+ runlock = RunLock.new(Chef::Config)
+ runlock.acquire
+
+ run_context = nil
+ @events.run_start(Chef::VERSION)
+ Chef::Log.info("*** Chef #{Chef::VERSION} ***")
+ enforce_path_sanity
+ run_ohai
+ @events.ohai_completed(node)
+ register unless Chef::Config[:solo]
+
+ load_node
+
+ begin
+ build_node
+
+ run_status.start_clock
+ Chef::Log.info("Starting Chef Run for #{node.name}")
+ run_started
+
+ run_context = setup_run_context
+
+ converge(run_context)
+
+ save_updated_node
+
+ run_status.stop_clock
+ Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds")
+ run_completed_successfully
+ @events.run_completed(node)
+ true
+ rescue Exception => e
+ # CHEF-3336: Send the error first in case something goes wrong below and we don't know why
+ Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}")
+ # If we failed really early, we may not have a run_status yet. Too early for these to be of much use.
+ if run_status
+ run_status.stop_clock
+ run_status.exception = e
+ run_failed
+ end
+ @events.run_failed(e)
+ raise
+ ensure
+ @run_status = nil
+ run_context = nil
+ runlock.release
+ GC.start
+ end
+ true
+ end
+
+ # Ensures runlist override contains RunListItem instances
+ def runlist_override_sanity_check!
+ # Convert to array and remove whitespace
+ if @override_runlist.is_a?(String)
+ @override_runlist = @override_runlist.split(',').map { |e| e.strip }
+ end
+ @override_runlist = [@override_runlist].flatten.compact
+ @override_runlist.map! do |item|
+ if(item.is_a?(Chef::RunList::RunListItem))
+ item
+ else
+ Chef::RunList::RunListItem.new(item)
+ end
+ end
+ end
+
+ def directory_not_empty?(path)
+ File.exists?(path) && (Dir.entries(path).size > 2)
+ end
+
+ def is_last_element?(index, object)
+ object.kind_of?(Array) ? index == object.size - 1 : true
+ end
+
+ def assert_cookbook_path_not_empty(run_context)
+ if Chef::Config[:solo]
+ # Check for cookbooks in the path given
+ # Chef::Config[:cookbook_path] can be a string or an array
+ # if it's an array, go through it and check each one, raise error at the last one if no files are found
+ Chef::Log.debug "Loading from cookbook_path: #{Array(Chef::Config[:cookbook_path]).map { |path| File.expand_path(path) }.join(', ')}"
+ Array(Chef::Config[:cookbook_path]).each_with_index do |cookbook_path, index|
+ if directory_not_empty?(cookbook_path)
+ break
+ else
+ msg = "No cookbook found in #{Chef::Config[:cookbook_path].inspect}, make sure cookbook_path is set correctly."
+ Chef::Log.fatal(msg)
+ raise Chef::Exceptions::CookbookNotFound, msg if is_last_element?(index, Chef::Config[:cookbook_path])
+ end
+ end
+ else
+ Chef::Log.warn("Node #{node_name} has an empty run list.") if run_context.node.run_list.empty?
+ end
+
+ end
+ end
+end
+
+# HACK cannot load this first, but it must be loaded.
+require 'chef/cookbook_loader'
+require 'chef/cookbook_version'
+require 'chef/cookbook/synchronizer'
+
diff --git a/lib/chef/config.rb b/lib/chef/config.rb
new file mode 100644
index 0000000000..61c8806a66
--- /dev/null
+++ b/lib/chef/config.rb
@@ -0,0 +1,310 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Author:: Mark Mzyk (mmzyk@opscode.com)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/log'
+require 'mixlib/config'
+
+class Chef
+ class Config
+
+ extend Mixlib::Config
+
+ # Manages the chef secret session key
+ # === Returns
+ # <newkey>:: A new or retrieved session key
+ #
+ def self.manage_secret_key
+ newkey = nil
+ if Chef::FileCache.has_key?("chef_server_cookie_id")
+ newkey = Chef::FileCache.load("chef_server_cookie_id")
+ else
+ chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
+ newkey = ""
+ 40.times { |i| newkey << chars[rand(chars.size-1)] }
+ Chef::FileCache.store("chef_server_cookie_id", newkey)
+ end
+ newkey
+ end
+
+ def self.inspect
+ configuration.inspect
+ end
+
+ def self.platform_specific_path(path)
+ #10.times { puts "* " * 40}
+ #pp caller
+
+ if RUBY_PLATFORM =~ /mswin|mingw|windows/
+ # turns /etc/chef/client.rb into C:/chef/client.rb
+ system_drive = ENV['SYSTEMDRIVE'] ? ENV['SYSTEMDRIVE'] : ""
+ path = File.join(system_drive, path.split('/')[2..-1])
+ # ensure all forward slashes are backslashes
+ path.gsub!(File::SEPARATOR, (File::ALT_SEPARATOR || '\\'))
+ end
+ path
+ end
+
+ # Override the config dispatch to set the value of multiple server options simultaneously
+ #
+ # === Parameters
+ # url<String>:: String to be set for all of the chef-server-api URL's
+ #
+ config_attr_writer :chef_server_url do |url|
+ url.strip!
+ configure do |c|
+ [ :registration_url,
+ :template_url,
+ :remotefile_url,
+ :search_url,
+ :chef_server_url,
+ :role_url ].each do |u|
+ c[u] = url
+ end
+ end
+ url
+ end
+
+ # When you are using ActiveSupport, they monkey-patch 'daemonize' into Kernel.
+ # So while this is basically identical to what method_missing would do, we pull
+ # it up here and get a real method written so that things get dispatched
+ # properly.
+ config_attr_writer :daemonize do |v|
+ configure do |c|
+ c[:daemonize] = v
+ end
+ end
+
+ # Override the config dispatch to set the value of log_location configuration option
+ #
+ # === Parameters
+ # location<IO||String>:: Logging location as either an IO stream or string representing log file path
+ #
+ config_attr_writer :log_location do |location|
+ if location.respond_to? :sync=
+ location.sync = true
+ location
+ elsif location.respond_to? :to_str
+ begin
+ f = File.new(location.to_str, "a")
+ f.sync = true
+ rescue Errno::ENOENT => error
+ raise Chef::Exceptions::ConfigurationError("Failed to open or create log file at #{location.to_str}")
+ end
+ f
+ end
+ end
+
+ # Override the config dispatch to set the value of authorized_openid_providers when openid_providers (deprecated) is used
+ #
+ # === Parameters
+ # providers<Array>:: An array of openid providers that are authorized to login to the chef server
+ #
+ config_attr_writer :openid_providers do |providers|
+ configure { |c| c[:authorized_openid_providers] = providers }
+ providers
+ end
+
+ # Turn on "path sanity" by default. See also: http://wiki.opscode.com/display/chef/User+Environment+PATH+Sanity
+ enforce_path_sanity(true)
+
+ # Formatted Chef Client output is a beta feature, disabled by default:
+ formatter "null"
+
+ # Used when OpenID authentication is enabled in the Web UI
+ authorized_openid_identifiers nil
+ authorized_openid_providers nil
+
+ # The number of times the client should retry when registering with the server
+ client_registration_retries 5
+
+ # Where the cookbooks are located. Meaning is somewhat context dependent between
+ # knife, chef-client, and chef-solo.
+ cookbook_path [ platform_specific_path("/var/chef/cookbooks"),
+ platform_specific_path("/var/chef/site-cookbooks") ]
+
+ # An array of paths to search for knife exec scripts if they aren't in the current directory
+ script_path []
+
+ # Where files are stored temporarily during uploads
+ sandbox_path "/var/chef/sandboxes"
+
+ # Where cookbook files are stored on the server (by content checksum)
+ checksum_path "/var/chef/checksums"
+
+ # Where chef's cache files should be stored
+ file_cache_path platform_specific_path("/var/chef/cache")
+
+ # By default, chef-client (or solo) creates a lockfile in
+ # `file_cache_path`/chef-client-running.pid
+ # If `lockfile` is explicitly set, this path will be used instead.
+ #
+ # If your `file_cache_path` resides on a NFS (or non-flock()-supporting
+ # fs), it's recommended to set this to something like
+ # '/tmp/chef-client-running.pid'
+ lockfile nil
+
+ # Where backups of chef-managed files should go
+ file_backup_path platform_specific_path("/var/chef/backup")
+
+ ## Daemonization Settings ##
+ # What user should Chef run as?
+ user nil
+ group nil
+ umask 0022
+
+ http_retry_count 5
+ http_retry_delay 5
+ interval nil
+ json_attribs nil
+ log_level :info
+ log_location STDOUT
+ # toggle info level log items that can create a lot of output
+ verbose_logging true
+ node_name nil
+ node_path "/var/chef/node"
+ diff_disable false
+ diff_filesize_threshold 10000000
+ diff_output_threshold 1000000
+
+ pid_file nil
+
+ chef_server_url "http://localhost:4000"
+ registration_url "http://localhost:4000"
+ template_url "http://localhost:4000"
+ role_url "http://localhost:4000"
+ remotefile_url "http://localhost:4000"
+ search_url "http://localhost:4000"
+
+ client_url "http://localhost:4042"
+
+ rest_timeout 300
+ run_command_stderr_timeout 120
+ run_command_stdout_timeout 120
+ solo false
+ splay nil
+ why_run false
+ color false
+ client_fork false
+ enable_reporting true
+ enable_reporting_url_fatals false
+
+ # Set these to enable SSL authentication / mutual-authentication
+ # with the server
+ ssl_client_cert nil
+ ssl_client_key nil
+ ssl_verify_mode :verify_none
+ ssl_ca_path nil
+ ssl_ca_file nil
+
+
+ # Where should chef-solo look for role files?
+ role_path platform_specific_path("/var/chef/roles")
+
+ data_bag_path platform_specific_path("/var/chef/data_bags")
+
+ # Where should chef-solo download recipes from?
+ recipe_url nil
+
+ # Sets the version of the signed header authentication protocol to use (see
+ # the 'mixlib-authorization' project for more detail). Currently, versions
+ # 1.0 and 1.1 are available; however, the chef-server must first be
+ # upgraded to support version 1.1 before clients can begin using it.
+ #
+ # Version 1.1 of the protocol is required when using a `node_name` greater
+ # than ~90 bytes (~90 ascii characters), so chef-client will automatically
+ # switch to using version 1.1 when `node_name` is too large for the 1.0
+ # protocol. If you intend to use large node names, ensure that your server
+ # supports version 1.1. Automatic detection of large node names means that
+ # users will generally not need to manually configure this.
+ #
+ # In the future, this configuration option may be replaced with an
+ # automatic negotiation scheme.
+ authentication_protocol_version "1.0"
+
+ # This key will be used to sign requests to the Chef server. This location
+ # must be writable by Chef during initial setup when generating a client
+ # identity on the server.
+ #
+ # The chef-server will look up the public key for the client using the
+ # `node_name` of the client.
+ client_key platform_specific_path("/etc/chef/client.pem")
+
+ # If there is no file in the location given by `client_key`, chef-client
+ # will temporarily use the "validator" identity to generate one. If the
+ # `client_key` is not present and the `validation_key` is also not present,
+ # chef-client will not be able to authenticate to the server.
+ #
+ # The `validation_key` is never used if the `client_key` exists.
+ validation_key platform_specific_path("/etc/chef/validation.pem")
+ validation_client_name "chef-validator"
+ web_ui_client_name "chef-webui"
+ web_ui_key "/etc/chef/webui.pem"
+ web_ui_admin_user_name "admin"
+ web_ui_admin_default_password "p@ssw0rd1"
+
+ # Server Signing CA
+ #
+ # In truth, these don't even have to change
+ signing_ca_cert "/var/chef/ca/cert.pem"
+ signing_ca_key "/var/chef/ca/key.pem"
+ signing_ca_user nil
+ signing_ca_group nil
+ signing_ca_country "US"
+ signing_ca_state "Washington"
+ signing_ca_location "Seattle"
+ signing_ca_org "Chef User"
+ signing_ca_domain "opensource.opscode.com"
+ signing_ca_email "opensource-cert@opscode.com"
+
+ # Report Handlers
+ report_handlers []
+
+ # Exception Handlers
+ exception_handlers []
+
+ # Start handlers
+ start_handlers []
+
+ # Checksum Cache
+ # Uses Moneta on the back-end
+ cache_type "BasicFile"
+ cache_options({ :path => platform_specific_path("/var/chef/cache/checksums"), :skip_expires => true })
+
+ # Arbitrary knife configuration data
+ knife Hash.new
+
+ # Those lists of regular expressions define what chef considers a
+ # valid user and group name
+ if RUBY_PLATFORM =~ /mswin|mingw|windows/
+ # From http://technet.microsoft.com/en-us/library/cc776019(WS.10).aspx
+
+ principal_valid_regex_part = '[^"\/\\\\\[\]\:;|=,+*?<>]+'
+ user_valid_regex [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ]
+ group_valid_regex [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ]
+ else
+ user_valid_regex [ /^([-a-zA-Z0-9_.]+)$/, /^\d+$/ ]
+ group_valid_regex [ /^([-a-zA-Z0-9_.\\ ]+)$/, /^\d+$/ ]
+ end
+
+ # returns a platform specific path to the user home dir
+ windows_home_path = ENV['SYSTEMDRIVE'] + ENV['HOMEPATH'] if ENV['SYSTEMDRIVE'] && ENV['HOMEPATH']
+ user_home (ENV['HOME'] || windows_home_path || ENV['USERPROFILE'])
+ end
+end
diff --git a/lib/chef/cookbook/chefignore.rb b/lib/chef/cookbook/chefignore.rb
new file mode 100644
index 0000000000..e9d54639e4
--- /dev/null
+++ b/lib/chef/cookbook/chefignore.rb
@@ -0,0 +1,66 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Cookbook
+ class Chefignore
+
+ COMMENTS_AND_WHITESPACE = /^\w*(?:#.*)?$/
+
+ attr_reader :ignores
+
+ def initialize(ignore_file_or_repo)
+ @ignore_file = find_ignore_file(ignore_file_or_repo)
+ @ignores = parse_ignore_file
+ end
+
+ def remove_ignores_from(file_list)
+ Array(file_list).inject([]) do |unignored, file|
+ ignored?(file) ? unignored : unignored << file
+ end
+ end
+
+ def ignored?(file_name)
+ @ignores.any? {|glob| File.fnmatch?(glob, file_name)}
+ end
+
+ private
+
+ def parse_ignore_file
+ ignore_globs = []
+ if File.exist?(@ignore_file) && File.readable?(@ignore_file)
+ File.foreach(@ignore_file) do |line|
+ ignore_globs << line.strip unless line =~ COMMENTS_AND_WHITESPACE
+ end
+ else
+ Chef::Log.debug("No chefignore file found at #@ignore_file no files will be ignored")
+ end
+ ignore_globs
+ end
+
+ def find_ignore_file(path)
+ if File.basename(path) =~ /chefignore/
+ path
+ else
+ File.join(path, 'chefignore')
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/cookbook/cookbook_collection.rb b/lib/chef/cookbook/cookbook_collection.rb
new file mode 100644
index 0000000000..ae63abfc93
--- /dev/null
+++ b/lib/chef/cookbook/cookbook_collection.rb
@@ -0,0 +1,45 @@
+#--
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mash'
+
+class Chef
+ # == Chef::CookbookCollection
+ # This class is the consistent interface for a node to obtain its
+ # cookbooks by name.
+ #
+ # This class is basically a glorified Hash, but since there are
+ # several ways this cookbook information is collected,
+ # (e.g. CookbookLoader for solo, hash of auto-vivified Cookbook
+ # objects for lazily-loaded remote cookbooks), it gets transformed
+ # into this.
+ class CookbookCollection < Mash
+
+ # The input is a mapping of cookbook name to CookbookVersion objects. We
+ # simply extract them
+ def initialize(cookbook_versions={})
+ super() do |hash, key|
+ raise Chef::Exceptions::CookbookNotFound, "Cookbook #{key} not found. " <<
+ "If you're loading #{key} from another cookbook, make sure you configure the dependency in your metadata"
+ end
+ cookbook_versions.each{ |cookbook_name, cookbook_version| self[cookbook_name] = cookbook_version }
+ end
+
+ end
+end
diff --git a/lib/chef/cookbook/cookbook_version_loader.rb b/lib/chef/cookbook/cookbook_version_loader.rb
new file mode 100644
index 0000000000..48de17cc5a
--- /dev/null
+++ b/lib/chef/cookbook/cookbook_version_loader.rb
@@ -0,0 +1,173 @@
+
+require 'chef/config'
+require 'chef/cookbook_version'
+require 'chef/cookbook/chefignore'
+require 'chef/cookbook/metadata'
+
+class Chef
+ class Cookbook
+ class CookbookVersionLoader
+
+ FILETYPES_SUBJECT_TO_IGNORE = [ :attribute_filenames,
+ :definition_filenames,
+ :recipe_filenames,
+ :template_filenames,
+ :file_filenames,
+ :library_filenames,
+ :resource_filenames,
+ :provider_filenames]
+
+
+ attr_reader :cookbook_name
+ attr_reader :cookbook_settings
+ attr_reader :metadata_filenames
+
+ def initialize(path, chefignore=nil)
+ @cookbook_path = File.expand_path( path )
+ @cookbook_name = File.basename( path )
+ @chefignore = chefignore
+ @metadata = Hash.new
+ @relative_path = /#{Regexp.escape(@cookbook_path)}\/(.+)$/
+ @cookbook_settings = {
+ :attribute_filenames => {},
+ :definition_filenames => {},
+ :recipe_filenames => {},
+ :template_filenames => {},
+ :file_filenames => {},
+ :library_filenames => {},
+ :resource_filenames => {},
+ :provider_filenames => {},
+ :root_filenames => {}
+ }
+
+ @metadata_filenames = []
+ end
+
+ def load_cookbooks
+ load_as(:attribute_filenames, 'attributes', '*.rb')
+ load_as(:definition_filenames, 'definitions', '*.rb')
+ load_as(:recipe_filenames, 'recipes', '*.rb')
+ load_as(:library_filenames, 'libraries', '*.rb')
+ load_recursively_as(:template_filenames, "templates", "*")
+ load_recursively_as(:file_filenames, "files", "*")
+ load_recursively_as(:resource_filenames, "resources", "*.rb")
+ load_recursively_as(:provider_filenames, "providers", "*.rb")
+ load_root_files
+
+ remove_ignored_files
+
+ if File.exists?(File.join(@cookbook_path, "metadata.rb"))
+ @metadata_filenames << File.join(@cookbook_path, "metadata.rb")
+ elsif File.exists?(File.join(@cookbook_path, "metadata.json"))
+ @metadata_filenames << File.join(@cookbook_path, "metadata.json")
+ end
+
+ if empty?
+ Chef::Log.warn "found a directory #{cookbook_name} in the cookbook path, but it contains no cookbook files. skipping."
+ end
+ @cookbook_settings
+ end
+
+ def cookbook_version
+ return nil if empty?
+
+ Chef::CookbookVersion.new(@cookbook_name.to_sym).tap do |c|
+ c.root_dir = @cookbook_path
+ c.attribute_filenames = cookbook_settings[:attribute_filenames].values
+ c.definition_filenames = cookbook_settings[:definition_filenames].values
+ c.recipe_filenames = cookbook_settings[:recipe_filenames].values
+ c.template_filenames = cookbook_settings[:template_filenames].values
+ c.file_filenames = cookbook_settings[:file_filenames].values
+ c.library_filenames = cookbook_settings[:library_filenames].values
+ c.resource_filenames = cookbook_settings[:resource_filenames].values
+ c.provider_filenames = cookbook_settings[:provider_filenames].values
+ c.root_filenames = cookbook_settings[:root_filenames].values
+ c.metadata_filenames = @metadata_filenames
+ c.metadata = metadata(c)
+ end
+ end
+
+ # Generates the Cookbook::Metadata object
+ def metadata(cookbook_version)
+ @metadata = Chef::Cookbook::Metadata.new(cookbook_version)
+ @metadata_filenames.each do |metadata_file|
+ case metadata_file
+ when /\.rb$/
+ apply_ruby_metadata(metadata_file)
+ when /\.json$/
+ apply_json_metadata(metadata_file)
+ else
+ raise RuntimeError, "Invalid metadata file: #{metadata_file} for cookbook: #{cookbook_version}"
+ end
+ end
+ @metadata
+ end
+
+ def empty?
+ cookbook_settings.inject(true) do |all_empty, files|
+ all_empty && files.last.empty?
+ end
+ end
+
+ def merge!(other_cookbook_loader)
+ other_cookbook_settings = other_cookbook_loader.cookbook_settings
+ @cookbook_settings.each do |file_type, file_list|
+ file_list.merge!(other_cookbook_settings[file_type])
+ end
+ @metadata_filenames.concat(other_cookbook_loader.metadata_filenames)
+ end
+
+ def chefignore
+ @chefignore ||= Chefignore.new(File.basename(@cookbook_path))
+ end
+
+ def load_root_files
+ Dir.glob(File.join(@cookbook_path, '*'), File::FNM_DOTMATCH).each do |file|
+ next if File.directory?(file)
+ @cookbook_settings[:root_filenames][file[@relative_path, 1]] = file
+ end
+ end
+
+ def load_recursively_as(category, category_dir, glob)
+ file_spec = File.join(@cookbook_path, category_dir, '**', glob)
+ Dir.glob(file_spec, File::FNM_DOTMATCH).each do |file|
+ next if File.directory?(file)
+ @cookbook_settings[category][file[@relative_path, 1]] = file
+ end
+ end
+
+ def load_as(category, *path_glob)
+ Dir[File.join(@cookbook_path, *path_glob)].each do |file|
+ @cookbook_settings[category][file[@relative_path, 1]] = file
+ end
+ end
+
+ def remove_ignored_files
+ @cookbook_settings.each_value do |file_list|
+ file_list.reject! do |relative_path, full_path|
+ chefignore.ignored?(relative_path)
+ end
+ end
+ end
+
+ def apply_ruby_metadata(file)
+ begin
+ @metadata.from_file(file)
+ rescue JSON::ParserError
+ Chef::Log.error("Error evaluating metadata.rb for #@cookbook_name in " + file)
+ raise
+ end
+ end
+
+ def apply_json_metadata(file)
+ begin
+ @metadata.from_json(IO.read(file))
+ rescue JSON::ParserError
+ Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in " + file)
+ raise
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/cookbook/file_system_file_vendor.rb b/lib/chef/cookbook/file_system_file_vendor.rb
new file mode 100644
index 0000000000..8896e3ed30
--- /dev/null
+++ b/lib/chef/cookbook/file_system_file_vendor.rb
@@ -0,0 +1,56 @@
+#--
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/cookbook/file_vendor'
+
+class Chef
+ class Cookbook
+ # == Chef::Cookbook::FileSystemFileVendor
+ # This FileVendor loads files from Chef::Config.cookbook_path. The
+ # thing that's sort of janky about this FileVendor implementation is
+ # that it basically takes only the cookbook's name from the manifest
+ # and throws the rest away then re-builds the list of files on the
+ # disk. This is due to the manifest not having the on-disk file
+ # locations, since in the chef-client case, that information is
+ # non-sensical.
+ class FileSystemFileVendor < FileVendor
+
+ def initialize(manifest, *repo_paths)
+ @cookbook_name = manifest[:cookbook_name]
+ @repo_paths = repo_paths.flatten
+ raise ArgumentError, "You must specify at least one repo path" if @repo_paths.empty?
+ end
+
+ # Implements abstract base's requirement. It looks in the
+ # Chef::Config.cookbook_path file hierarchy for the requested
+ # file.
+ def get_filename(filename)
+ location = @repo_paths.inject(nil) do |memo, basepath|
+ candidate_location = File.join(basepath, @cookbook_name, filename)
+ memo = candidate_location if File.exist?(candidate_location)
+ memo
+ end
+ raise "File #{filename} does not exist for cookbook #{@cookbook_name}" unless location
+
+ location
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/cookbook/file_vendor.rb b/lib/chef/cookbook/file_vendor.rb
new file mode 100644
index 0000000000..38eab185ca
--- /dev/null
+++ b/lib/chef/cookbook/file_vendor.rb
@@ -0,0 +1,48 @@
+#
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+class Chef
+ class Cookbook
+ # == Chef::Cookbook::FileVendor
+ # This class handles fetching of cookbook files based on specificity.
+ class FileVendor
+
+ def self.on_create(&block)
+ @instance_creator = block
+ end
+
+ # Factory method that creates the appropriate kind of
+ # Cookbook::FileVendor to serve the contents of the manifest
+ def self.create_from_manifest(manifest)
+ raise "Must call Chef::Cookbook::FileVendor.on_create before calling create_from_manifest factory" unless defined?(@instance_creator)
+ @instance_creator.call(manifest)
+ end
+
+ # Gets the on-disk location for the given cookbook file.
+ #
+ # Subclasses are responsible for determining exactly how the
+ # files are obtained and where they are stored.
+ def get_filename(filename)
+ raise NotImplemented, "Subclasses must implement this method"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb
new file mode 100644
index 0000000000..8398de442c
--- /dev/null
+++ b/lib/chef/cookbook/metadata.rb
@@ -0,0 +1,629 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright 2008-2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mash'
+require 'chef/mixin/from_file'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/check_helper'
+require 'chef/log'
+require 'chef/version_class'
+require 'chef/version_constraint'
+
+class Chef
+ class Cookbook
+
+ # == Chef::Cookbook::Metadata
+ # Chef::Cookbook::Metadata provides a convenient DSL for declaring metadata
+ # about Chef Cookbooks.
+ class Metadata
+
+ NAME = 'name'.freeze
+ DESCRIPTION = 'description'.freeze
+ LONG_DESCRIPTION = 'long_description'.freeze
+ MAINTAINER = 'maintainer'.freeze
+ MAINTAINER_EMAIL = 'maintainer_email'.freeze
+ LICENSE = 'license'.freeze
+ PLATFORMS = 'platforms'.freeze
+ DEPENDENCIES = 'dependencies'.freeze
+ RECOMMENDATIONS = 'recommendations'.freeze
+ SUGGESTIONS = 'suggestions'.freeze
+ CONFLICTING = 'conflicting'.freeze
+ PROVIDING = 'providing'.freeze
+ REPLACING = 'replacing'.freeze
+ ATTRIBUTES = 'attributes'.freeze
+ GROUPINGS = 'groupings'.freeze
+ RECIPES = 'recipes'.freeze
+ VERSION = 'version'.freeze
+
+ COMPARISON_FIELDS = [ :name, :description, :long_description, :maintainer,
+ :maintainer_email, :license, :platforms, :dependencies,
+ :recommendations, :suggestions, :conflicting, :providing,
+ :replacing, :attributes, :groupings, :recipes, :version]
+
+ VERSION_CONSTRAINTS = {:depends => DEPENDENCIES,
+ :recommends => RECOMMENDATIONS,
+ :suggests => SUGGESTIONS,
+ :conflicts => CONFLICTING,
+ :provides => PROVIDING,
+ :replaces => REPLACING }
+
+ include Chef::Mixin::CheckHelper
+ include Chef::Mixin::ParamsValidate
+ include Chef::Mixin::FromFile
+
+ attr_reader :cookbook,
+ :platforms,
+ :dependencies,
+ :recommendations,
+ :suggestions,
+ :conflicting,
+ :providing,
+ :replacing,
+ :attributes,
+ :groupings,
+ :recipes,
+ :version
+
+ # Builds a new Chef::Cookbook::Metadata object.
+ #
+ # === Parameters
+ # cookbook<String>:: An optional cookbook object
+ # maintainer<String>:: An optional maintainer
+ # maintainer_email<String>:: An optional maintainer email
+ # license<String>::An optional license. Default is Apache v2.0
+ #
+ # === Returns
+ # metadata<Chef::Cookbook::Metadata>
+ def initialize(cookbook=nil, maintainer='YOUR_COMPANY_NAME', maintainer_email='YOUR_EMAIL', license='none')
+ @cookbook = cookbook
+ @name = cookbook ? cookbook.name : ""
+ @long_description = ""
+ self.maintainer(maintainer)
+ self.maintainer_email(maintainer_email)
+ self.license(license)
+ self.description('A fabulous new cookbook')
+ @platforms = Mash.new
+ @dependencies = Mash.new
+ @recommendations = Mash.new
+ @suggestions = Mash.new
+ @conflicting = Mash.new
+ @providing = Mash.new
+ @replacing = Mash.new
+ @attributes = Mash.new
+ @groupings = Mash.new
+ @recipes = Mash.new
+ @version = Version.new "0.0.0"
+ if cookbook
+ @recipes = cookbook.fully_qualified_recipe_names.inject({}) do |r, e|
+ e = self.name if e =~ /::default$/
+ r[e] = ""
+ self.provides e
+ r
+ end
+ end
+ end
+
+ def ==(other)
+ COMPARISON_FIELDS.inject(true) do |equal_so_far, field|
+ equal_so_far && other.respond_to?(field) && (other.send(field) == send(field))
+ end
+ end
+
+ # Sets the cookbooks maintainer, or returns it.
+ #
+ # === Parameters
+ # maintainer<String>:: The maintainers name
+ #
+ # === Returns
+ # maintainer<String>:: Returns the current maintainer.
+ def maintainer(arg=nil)
+ set_or_return(
+ :maintainer,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the maintainers email address, or returns it.
+ #
+ # === Parameters
+ # maintainer_email<String>:: The maintainers email address
+ #
+ # === Returns
+ # maintainer_email<String>:: Returns the current maintainer email.
+ def maintainer_email(arg=nil)
+ set_or_return(
+ :maintainer_email,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the current license, or returns it.
+ #
+ # === Parameters
+ # license<String>:: The current license.
+ #
+ # === Returns
+ # license<String>:: Returns the current license
+ def license(arg=nil)
+ set_or_return(
+ :license,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the current description, or returns it. Should be short - one line only!
+ #
+ # === Parameters
+ # description<String>:: The new description
+ #
+ # === Returns
+ # description<String>:: Returns the description
+ def description(arg=nil)
+ set_or_return(
+ :description,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the current long description, or returns it. Might come from a README, say.
+ #
+ # === Parameters
+ # long_description<String>:: The new long description
+ #
+ # === Returns
+ # long_description<String>:: Returns the long description
+ def long_description(arg=nil)
+ set_or_return(
+ :long_description,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the current cookbook version, or returns it. Can be two or three digits, seperated
+ # by dots. ie: '2.1', '1.5.4' or '0.9'.
+ #
+ # === Parameters
+ # version<String>:: The curent version, as a string
+ #
+ # === Returns
+ # version<String>:: Returns the current version
+ def version(arg=nil)
+ if arg
+ @version = Chef::Version.new(arg)
+ end
+
+ @version.to_s
+ end
+
+ # Sets the name of the cookbook, or returns it.
+ #
+ # === Parameters
+ # name<String>:: The curent cookbook name.
+ #
+ # === Returns
+ # name<String>:: Returns the current cookbook name.
+ def name(arg=nil)
+ set_or_return(
+ :name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Adds a supported platform, with version checking strings.
+ #
+ # === Parameters
+ # platform<String>,<Symbol>:: The platform (like :ubuntu or :mac_os_x)
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def supports(platform, *version_args)
+ version = new_args_format(:supports, platform, version_args)
+ validate_version_constraint(:supports, platform, version)
+ @platforms[platform] = version
+ @platforms[platform]
+ end
+
+ # Adds a dependency on another cookbook, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def depends(cookbook, *version_args)
+ version = new_args_format(:depends, cookbook, version_args)
+ validate_version_constraint(:depends, cookbook, version)
+ @dependencies[cookbook] = version
+ @dependencies[cookbook]
+ end
+
+ # Adds a recommendation for another cookbook, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def recommends(cookbook, *version_args)
+ version = new_args_format(:recommends, cookbook, version_args)
+ validate_version_constraint(:recommends, cookbook, version)
+ @recommendations[cookbook] = version
+ @recommendations[cookbook]
+ end
+
+ # Adds a suggestion for another cookbook, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has the
+ # formx.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def suggests(cookbook, *version_args)
+ version = new_args_format(:suggests, cookbook, version_args)
+ validate_version_constraint(:suggests, cookbook, version)
+ @suggestions[cookbook] = version
+ @suggestions[cookbook]
+ end
+
+ # Adds a conflict for another cookbook, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def conflicts(cookbook, *version_args)
+ version = new_args_format(:conflicts, cookbook, version_args)
+ validate_version_constraint(:conflicts, cookbook, version)
+ @conflicting[cookbook] = version
+ @conflicting[cookbook]
+ end
+
+ # Adds a recipe, definition, or resource provided by this cookbook.
+ #
+ # Recipes are specified as normal
+ # Definitions are followed by (), and can include :params for prototyping
+ # Resources are the stringified version (service[apache2])
+ #
+ # === Parameters
+ # recipe, definition, resource<String>:: The thing we provide
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def provides(cookbook, *version_args)
+ version = new_args_format(:provides, cookbook, version_args)
+ validate_version_constraint(:provides, cookbook, version)
+ @providing[cookbook] = version
+ @providing[cookbook]
+ end
+
+ # Adds a cookbook that is replaced by this one, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook we replace
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def replaces(cookbook, *version_args)
+ version = new_args_format(:replaces, cookbook, version_args)
+ validate_version_constraint(:replaces, cookbook, version)
+ @replacing[cookbook] = version
+ @replacing[cookbook]
+ end
+
+ # Adds a description for a recipe.
+ #
+ # === Parameters
+ # recipe<String>:: The recipe
+ # description<String>:: The description of the recipe
+ #
+ # === Returns
+ # description<String>:: Returns the current description
+ def recipe(name, description)
+ @recipes[name] = description
+ end
+
+ # Adds an attribute )hat a user needs to configure for this cookbook. Takes
+ # a name (with the / notation for a nested attribute), followed by any of
+ # these options
+ #
+ # display_name<String>:: What a UI should show for this attribute
+ # description<String>:: A hint as to what this attr is for
+ # choice<Array>:: An array of choices to present to the user.
+ # calculated<Boolean>:: If true, the default value is calculated by the recipe and cannot be displayed.
+ # type<String>:: "string" or "array" - default is "string" ("hash" is supported for backwards compatibility)
+ # required<String>:: Whether this attr is 'required', 'recommended' or 'optional' - default 'optional' (true/false values also supported for backwards compatibility)
+ # recipes<Array>:: An array of recipes which need this attr set.
+ # default<String>,<Array>,<Hash>:: The default value
+ #
+ # === Parameters
+ # name<String>:: The name of the attribute ('foo', or 'apache2/log_dir')
+ # options<Hash>:: The description of the options
+ #
+ # === Returns
+ # options<Hash>:: Returns the current options hash
+ def attribute(name, options)
+ validate(
+ options,
+ {
+ :display_name => { :kind_of => String },
+ :description => { :kind_of => String },
+ :choice => { :kind_of => [ Array ], :default => [] },
+ :calculated => { :equal_to => [ true, false ], :default => false },
+ :type => { :equal_to => [ "string", "array", "hash", "symbol" ], :default => "string" },
+ :required => { :equal_to => [ "required", "recommended", "optional", true, false ], :default => "optional" },
+ :recipes => { :kind_of => [ Array ], :default => [] },
+ :default => { :kind_of => [ String, Array, Hash ] }
+ }
+ )
+ options[:required] = remap_required_attribute(options[:required]) unless options[:required].nil?
+ validate_string_array(options[:choice])
+ validate_calculated_default_rule(options)
+ validate_choice_default_rule(options)
+
+ @attributes[name] = options
+ @attributes[name]
+ end
+
+ def grouping(name, options)
+ validate(
+ options,
+ {
+ :title => { :kind_of => String },
+ :description => { :kind_of => String }
+ }
+ )
+ @groupings[name] = options
+ @groupings[name]
+ end
+
+ def to_hash
+ {
+ NAME => self.name,
+ DESCRIPTION => self.description,
+ LONG_DESCRIPTION => self.long_description,
+ MAINTAINER => self.maintainer,
+ MAINTAINER_EMAIL => self.maintainer_email,
+ LICENSE => self.license,
+ PLATFORMS => self.platforms,
+ DEPENDENCIES => self.dependencies,
+ RECOMMENDATIONS => self.recommendations,
+ SUGGESTIONS => self.suggestions,
+ CONFLICTING => self.conflicting,
+ PROVIDING => self.providing,
+ REPLACING => self.replacing,
+ ATTRIBUTES => self.attributes,
+ GROUPINGS => self.groupings,
+ RECIPES => self.recipes,
+ VERSION => self.version
+ }
+ end
+
+ def to_json(*a)
+ self.to_hash.to_json(*a)
+ end
+
+ def self.from_hash(o)
+ cm = self.new()
+ cm.from_hash(o)
+ cm
+ end
+
+ def from_hash(o)
+ @name = o[NAME] if o.has_key?(NAME)
+ @description = o[DESCRIPTION] if o.has_key?(DESCRIPTION)
+ @long_description = o[LONG_DESCRIPTION] if o.has_key?(LONG_DESCRIPTION)
+ @maintainer = o[MAINTAINER] if o.has_key?(MAINTAINER)
+ @maintainer_email = o[MAINTAINER_EMAIL] if o.has_key?(MAINTAINER_EMAIL)
+ @license = o[LICENSE] if o.has_key?(LICENSE)
+ @platforms = o[PLATFORMS] if o.has_key?(PLATFORMS)
+ @dependencies = handle_deprecated_constraints(o[DEPENDENCIES]) if o.has_key?(DEPENDENCIES)
+ @recommendations = handle_deprecated_constraints(o[RECOMMENDATIONS]) if o.has_key?(RECOMMENDATIONS)
+ @suggestions = handle_deprecated_constraints(o[SUGGESTIONS]) if o.has_key?(SUGGESTIONS)
+ @conflicting = handle_deprecated_constraints(o[CONFLICTING]) if o.has_key?(CONFLICTING)
+ @providing = o[PROVIDING] if o.has_key?(PROVIDING)
+ @replacing = handle_deprecated_constraints(o[REPLACING]) if o.has_key?(REPLACING)
+ @attributes = o[ATTRIBUTES] if o.has_key?(ATTRIBUTES)
+ @groupings = o[GROUPINGS] if o.has_key?(GROUPINGS)
+ @recipes = o[RECIPES] if o.has_key?(RECIPES)
+ @version = o[VERSION] if o.has_key?(VERSION)
+ self
+ end
+
+ def self.from_json(string)
+ o = Chef::JSONCompat.from_json(string)
+ self.from_hash(o)
+ end
+
+ def self.validate_json(json_str)
+ o = Chef::JSONCompat.from_json(json_str)
+ metadata = new()
+ VERSION_CONSTRAINTS.each do |method_name, hash_key|
+ if constraints = o[hash_key]
+ constraints.each do |cb_name, constraints|
+ metadata.send(method_name, cb_name, *Array(constraints))
+ end
+ end
+ end
+ true
+ end
+
+ def from_json(string)
+ o = Chef::JSONCompat.from_json(string)
+ from_hash(o)
+ end
+
+ private
+
+ def new_args_format(caller_name, dep_name, version_constraints)
+ if version_constraints.empty?
+ ">= 0.0.0"
+ elsif version_constraints.size == 1
+ version_constraints.first
+ else
+ msg=<<-OBSOLETED
+The dependency specification syntax you are using is no longer valid. You may not
+specify more than one version constraint for a particular cookbook.
+Consult http://wiki.opscode.com/display/chef/Metadata for the updated syntax.
+
+Called by: #{caller_name} '#{dep_name}', #{version_constraints.map {|vc| vc.inspect}.join(", ")}
+Called from:
+#{caller[0...5].map {|line| " " + line}.join("\n")}
+OBSOLETED
+ raise Exceptions::ObsoleteDependencySyntax, msg
+ end
+ end
+
+ def validate_version_constraint(caller_name, dep_name, constraint_str)
+ Chef::VersionConstraint.new(constraint_str)
+ rescue Chef::Exceptions::InvalidVersionConstraint => e
+ Log.debug(e)
+
+ msg=<<-INVALID
+The version constraint syntax you are using is not valid. If you recently
+upgraded to Chef 0.10.0, be aware that you no may longer use "<<" and ">>" for
+'less than' and 'greater than'; use '<' and '>' instead.
+Consult http://wiki.opscode.com/display/chef/Metadata for more information.
+
+Called by: #{caller_name} '#{dep_name}', '#{constraint_str}'
+Called from:
+#{caller[0...5].map {|line| " " + line}.join("\n")}
+INVALID
+ raise Exceptions::InvalidVersionConstraint, msg
+ end
+ # Verify that the given array is an array of strings
+ #
+ # Raise an exception if the members of the array are not Strings
+ #
+ # === Parameters
+ # arry<Array>:: An array to be validated
+ def validate_string_array(arry)
+ if arry.kind_of?(Array)
+ arry.each do |choice|
+ validate( {:choice => choice}, {:choice => {:kind_of => String}} )
+ end
+ end
+ end
+
+ # For backwards compatibility, remap Boolean values to String
+ # true is mapped to "required"
+ # false is mapped to "optional"
+ #
+ # === Parameters
+ # required_attr<String><Boolean>:: The value of options[:required]
+ #
+ # === Returns
+ # required_attr<String>:: "required", "recommended", or "optional"
+ def remap_required_attribute(value)
+ case value
+ when true
+ value = "required"
+ when false
+ value = "optional"
+ end
+ value
+ end
+
+ def validate_calculated_default_rule(options)
+ calculated_conflict = ((options[:default].is_a?(Array) && !options[:default].empty?) ||
+ (options[:default].is_a?(String) && !options[:default] != "")) &&
+ options[:calculated] == true
+ raise ArgumentError, "Default cannot be specified if calculated is true!" if calculated_conflict
+ end
+
+ def validate_choice_default_rule(options)
+ return if !options[:choice].is_a?(Array) || options[:choice].empty?
+
+ if options[:default].is_a?(String) && options[:default] != ""
+ raise ArgumentError, "Default must be one of your choice values!" if options[:choice].index(options[:default]) == nil
+ end
+
+ if options[:default].is_a?(Array) && !options[:default].empty?
+ options[:default].each do |val|
+ raise ArgumentError, "Default values must be a subset of your choice values!" if options[:choice].index(val) == nil
+ end
+ end
+ end
+
+ # This method translates version constraint strings from
+ # cookbooks with the old format.
+ #
+ # Before we began respecting version constraints, we allowed
+ # multiple constraints to be placed on cookbooks, as well as the
+ # << and >> operators, which are now just < and >. For
+ # specifications with more than one constraint, we return an
+ # empty array (otherwise, we're silently abiding only part of
+ # the contract they have specified to us). If there is only one
+ # constraint, we are replacing the old << and >> with the new <
+ # and >.
+ def handle_deprecated_constraints(specification)
+ specification.inject(Mash.new) do |acc, (cb, constraints)|
+ constraints = Array(constraints)
+ acc[cb] = (constraints.empty? || constraints.size > 1) ? [] : constraints.first.gsub(/>>/, '>').gsub(/<</, '<')
+ acc
+ end
+ end
+
+ end
+
+ #== Chef::Cookbook::MinimalMetadata
+ # MinimalMetadata is a duck type of Cookbook::Metadata, used
+ # internally by Chef Server when determining the optimal set of
+ # cookbooks for a node.
+ #
+ # MinimalMetadata objects typically contain only enough information
+ # to solve the cookbook collection for a run list, but not enough to
+ # generate the proper response
+ class MinimalMetadata < Metadata
+ def initialize(name, params)
+ @name = name
+ from_hash(params)
+ end
+ end
+
+
+ end
+end
diff --git a/lib/chef/cookbook/remote_file_vendor.rb b/lib/chef/cookbook/remote_file_vendor.rb
new file mode 100644
index 0000000000..49de62cf65
--- /dev/null
+++ b/lib/chef/cookbook/remote_file_vendor.rb
@@ -0,0 +1,84 @@
+#
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/cookbook/file_vendor'
+
+class Chef
+ class Cookbook
+ # == Chef::Cookbook::RemoteFileVendor
+ # This FileVendor loads files by either fetching them from the local cache, or
+ # if not available, loading them from the remote server.
+ class RemoteFileVendor < FileVendor
+
+ def initialize(manifest, rest)
+ @manifest = manifest
+ @cookbook_name = @manifest[:cookbook_name]
+ @rest = rest
+ end
+
+ # Implements abstract base's requirement. It looks in the
+ # Chef::Config.cookbook_path file hierarchy for the requested
+ # file.
+ def get_filename(filename)
+ if filename =~ /([^\/]+)\/(.+)$/
+ segment = $1
+ else
+ raise "get_filename: Cannot determine segment/filename for incoming filename #{filename}"
+ end
+
+ raise "No such segment #{segment} in cookbook #{@cookbook_name}" unless @manifest[segment]
+ found_manifest_record = @manifest[segment].find {|manifest_record| manifest_record[:path] == filename }
+ raise "No such file #{filename} in #{@cookbook_name}" unless found_manifest_record
+
+ cache_filename = File.join("cookbooks", @cookbook_name, found_manifest_record['path'])
+
+ # update valid_cache_entries so the upstream cache cleaner knows what
+ # we've used.
+ validate_cached_copy(cache_filename)
+
+ current_checksum = nil
+ if Chef::FileCache.has_key?(cache_filename)
+ current_checksum = Chef::CookbookVersion.checksum_cookbook_file(Chef::FileCache.load(cache_filename, false))
+ end
+
+ # If the checksums are different between on-disk (current) and on-server
+ # (remote, per manifest), do the update. This will also execute if there
+ # is no current checksum.
+ if current_checksum != found_manifest_record['checksum']
+ raw_file = @rest.get_rest(found_manifest_record[:url], true)
+
+ Chef::Log.debug("Storing updated #{cache_filename} in the cache.")
+ Chef::FileCache.move_to(raw_file.path, cache_filename)
+ else
+ Chef::Log.debug("Not fetching #{cache_filename}, as the cache is up to date.")
+ Chef::Log.debug("current checksum: #{current_checksum}; manifest checksum: #{found_manifest_record['checksum']})")
+ end
+
+ full_path_cache_filename = Chef::FileCache.load(cache_filename, false)
+
+ # return the filename, not the contents (second argument= false)
+ full_path_cache_filename
+ end
+
+ def validate_cached_copy(cache_filename)
+ CookbookCacheCleaner.instance.mark_file_as_valid(cache_filename)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/cookbook/synchronizer.rb b/lib/chef/cookbook/synchronizer.rb
new file mode 100644
index 0000000000..54cadb941c
--- /dev/null
+++ b/lib/chef/cookbook/synchronizer.rb
@@ -0,0 +1,216 @@
+require 'chef/client'
+require 'singleton'
+
+class Chef
+
+ # Keep track of the filenames that we use in both eager cookbook
+ # downloading (during sync_cookbooks) and lazy (during the run
+ # itself, through FileVendor). After the run is over, clean up the
+ # cache.
+ class CookbookCacheCleaner
+
+ # Setup a notification to clear the valid_cache_entries when a Chef client
+ # run starts
+ Chef::Client.when_run_starts do |run_status|
+ instance.reset!
+ end
+
+ # Register a notification to cleanup unused files from cookbooks
+ Chef::Client.when_run_completes_successfully do |run_status|
+ instance.cleanup_file_cache
+ end
+
+ include Singleton
+
+ def initialize
+ reset!
+ end
+
+ def reset!
+ @valid_cache_entries = {}
+ end
+
+ def mark_file_as_valid(cache_path)
+ @valid_cache_entries[cache_path] = true
+ end
+
+ def cache
+ Chef::FileCache
+ end
+
+ def cleanup_file_cache
+ unless Chef::Config[:solo]
+ # Delete each file in the cache that we didn't encounter in the
+ # manifest.
+ cache.find(File.join(%w{cookbooks ** *})).each do |cache_filename|
+ unless @valid_cache_entries[cache_filename]
+ Chef::Log.info("Removing #{cache_filename} from the cache; it is no longer needed by chef-client.")
+ cache.delete(cache_filename)
+ end
+ end
+ end
+ end
+
+ end
+
+ # Synchronizes the locally cached copies of cookbooks with the files on the
+ # server.
+ class CookbookSynchronizer
+ EAGER_SEGMENTS = Chef::CookbookVersion::COOKBOOK_SEGMENTS.dup
+ EAGER_SEGMENTS.delete(:files)
+ EAGER_SEGMENTS.delete(:templates)
+ EAGER_SEGMENTS.freeze
+
+ def initialize(cookbooks_by_name, events)
+ @cookbooks_by_name, @events = cookbooks_by_name, events
+ end
+
+ def cache
+ Chef::FileCache
+ end
+
+ def cookbook_names
+ @cookbooks_by_name.keys
+ end
+
+ def cookbooks
+ @cookbooks_by_name.values
+ end
+
+ def cookbook_count
+ @cookbooks_by_name.size
+ end
+
+ def have_cookbook?(cookbook_name)
+ @cookbooks_by_name.key?(cookbook_name)
+ end
+
+ # Synchronizes all the cookbooks from the chef-server.
+ #)
+ # === Returns
+ # true:: Always returns true
+ def sync_cookbooks
+ Chef::Log.info("Loading cookbooks [#{cookbook_names.sort.join(', ')}]")
+ Chef::Log.debug("Cookbooks detail: #{cookbooks.inspect}")
+
+ clear_obsoleted_cookbooks
+
+ @events.cookbook_sync_start(cookbook_count)
+
+ # Synchronize each of the node's cookbooks, and add to the
+ # valid_cache_entries hash.
+ cookbooks.each do |cookbook|
+ sync_cookbook(cookbook)
+ end
+
+ rescue Exception => e
+ @events.cookbook_sync_failed(cookbooks, e)
+ raise
+ else
+ @events.cookbook_sync_complete
+ true
+ end
+
+ # Iterates over cached cookbooks' files, removing files belonging to
+ # cookbooks that don't appear in +cookbook_hash+
+ def clear_obsoleted_cookbooks
+ @events.cookbook_clean_start
+ # Remove all cookbooks no longer relevant to this node
+ cache.find(File.join(%w{cookbooks ** *})).each do |cache_file|
+ cache_file =~ /^cookbooks\/([^\/]+)\//
+ unless have_cookbook?($1)
+ Chef::Log.info("Removing #{cache_file} from the cache; its cookbook is no longer needed on this client.")
+ cache.delete(cache_file)
+ @events.removed_cookbook_file(cache_file)
+ end
+ end
+ @events.cookbook_clean_complete
+ end
+
+ # Sync the eagerly loaded files contained by +cookbook+
+ #
+ # === Arguments
+ # cookbook<Chef::Cookbook>:: The cookbook to update
+ # valid_cache_entries<Hash>:: Out-param; Added to this hash are the files that
+ # were referred to by this cookbook
+ def sync_cookbook(cookbook)
+ Chef::Log.debug("Synchronizing cookbook #{cookbook.name}")
+
+ # files and templates are lazily loaded, and will be done later.
+
+ EAGER_SEGMENTS.each do |segment|
+ segment_filenames = Array.new
+ cookbook.manifest[segment].each do |manifest_record|
+
+ cache_filename = sync_file_in_cookbook(cookbook, manifest_record)
+ # make the segment filenames a full path.
+ full_path_cache_filename = cache.load(cache_filename, false)
+ segment_filenames << full_path_cache_filename
+ end
+
+ # replace segment filenames with a full-path one.
+ if segment.to_sym == :recipes
+ cookbook.recipe_filenames = segment_filenames
+ elsif segment.to_sym == :attributes
+ cookbook.attribute_filenames = segment_filenames
+ else
+ cookbook.segment_filenames(segment).replace(segment_filenames)
+ end
+ end
+ @events.synchronized_cookbook(cookbook.name)
+ end
+
+ # Sync an individual file if needed. If there is an up to date copy
+ # locally, nothing is done.
+ #
+ # === Arguments
+ # file_manifest::: A Hash of the form {"path" => 'relative/path', "url" => "location to fetch the file"}
+ # === Returns
+ # Path to the cached file as a String
+ def sync_file_in_cookbook(cookbook, file_manifest)
+ cache_filename = File.join("cookbooks", cookbook.name, file_manifest['path'])
+ mark_cached_file_valid(cache_filename)
+
+ # If the checksums are different between on-disk (current) and on-server
+ # (remote, per manifest), do the update. This will also execute if there
+ # is no current checksum.
+ if !cached_copy_up_to_date?(cache_filename, file_manifest['checksum'])
+ download_file(file_manifest['url'], cache_filename)
+ @events.updated_cookbook_file(cookbook.name, cache_filename)
+ else
+ Chef::Log.debug("Not storing #{cache_filename}, as the cache is up to date.")
+ end
+
+ cache_filename
+ end
+
+ def cached_copy_up_to_date?(local_path, expected_checksum)
+ if cache.has_key?(local_path)
+ current_checksum = CookbookVersion.checksum_cookbook_file(cache.load(local_path, false))
+ expected_checksum == current_checksum
+ else
+ false
+ end
+ end
+
+ # Unconditionally download the file from the given URL. File will be
+ # downloaded to the path +destination+ which is relative to the Chef file
+ # cache root.
+ def download_file(url, destination)
+ raw_file = server_api.get_rest(url, true)
+
+ Chef::Log.info("Storing updated #{destination} in the cache.")
+ cache.move_to(raw_file.path, destination)
+ end
+
+ # Marks the given file as valid (non-stale).
+ def mark_cached_file_valid(cache_filename)
+ CookbookCacheCleaner.instance.mark_file_as_valid(cache_filename)
+ end
+
+ def server_api
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ end
+end
diff --git a/lib/chef/cookbook/syntax_check.rb b/lib/chef/cookbook/syntax_check.rb
new file mode 100644
index 0000000000..bf7c45e252
--- /dev/null
+++ b/lib/chef/cookbook/syntax_check.rb
@@ -0,0 +1,136 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/checksum_cache'
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Cookbook
+ # == Chef::Cookbook::SyntaxCheck
+ # Encapsulates the process of validating the ruby syntax of files in Chef
+ # cookbooks.
+ class SyntaxCheck
+ include Chef::Mixin::ShellOut
+
+ attr_reader :cookbook_path
+
+ # Creates a new SyntaxCheck given the +cookbook_name+ and a +cookbook_path+.
+ # If no +cookbook_path+ is given, +Chef::Config.cookbook_path+ is used.
+ def self.for_cookbook(cookbook_name, cookbook_path=nil)
+ cookbook_path ||= Chef::Config.cookbook_path
+ unless cookbook_path
+ raise ArgumentError, "Cannot find cookbook #{cookbook_name} unless Chef::Config.cookbook_path is set or an explicit cookbook path is given"
+ end
+ new(File.join(cookbook_path, cookbook_name.to_s))
+ end
+
+ # Create a new SyntaxCheck object
+ # === Arguments
+ # cookbook_path::: the (on disk) path to the cookbook
+ def initialize(cookbook_path)
+ @cookbook_path = cookbook_path
+ end
+
+ def cache
+ Chef::ChecksumCache.instance
+ end
+
+ def ruby_files
+ Dir[File.join(cookbook_path, '**', '*.rb')]
+ end
+
+ def untested_ruby_files
+ ruby_files.reject do |file|
+ if validated?(file)
+ Chef::Log.debug("Ruby file #{file} is unchanged, skipping syntax check")
+ true
+ else
+ false
+ end
+ end
+ end
+
+ def template_files
+ Dir[File.join(cookbook_path, '**', '*.erb')]
+ end
+
+ def untested_template_files
+ template_files.reject do |file|
+ if validated?(file)
+ Chef::Log.debug("Template #{file} is unchanged, skipping syntax check")
+ true
+ else
+ false
+ end
+ end
+ end
+
+ def validated?(file)
+ !!cache.lookup_checksum(cache_key(file), File.stat(file))
+ end
+
+ def validated(file)
+ cache.generate_checksum(cache_key(file), file, File.stat(file))
+ end
+
+ def cache_key(file)
+ @cache_keys ||= {}
+ @cache_keys[file] ||= cache.generate_key(file, "chef-test")
+ end
+
+ def validate_ruby_files
+ untested_ruby_files.each do |ruby_file|
+ return false unless validate_ruby_file(ruby_file)
+ validated(ruby_file)
+ end
+ end
+
+ def validate_templates
+ untested_template_files.each do |template|
+ return false unless validate_template(template)
+ validated(template)
+ end
+ end
+
+ def validate_template(erb_file)
+ Chef::Log.debug("Testing template #{erb_file} for syntax errors...")
+ result = shell_out("erubis -x #{erb_file} | ruby -c")
+ result.error!
+ true
+ rescue Mixlib::ShellOut::ShellCommandFailed
+ file_relative_path = erb_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1]
+ Chef::Log.fatal("Erb template #{file_relative_path} has a syntax error:")
+ result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) }
+ false
+ end
+
+ def validate_ruby_file(ruby_file)
+ Chef::Log.debug("Testing #{ruby_file} for syntax errors...")
+ result = shell_out("ruby -c #{ruby_file}")
+ result.error!
+ true
+ rescue Mixlib::ShellOut::ShellCommandFailed
+ file_relative_path = ruby_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1]
+ Chef::Log.fatal("Cookbook file #{file_relative_path} has a ruby syntax error:")
+ result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) }
+ false
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/cookbook_loader.rb b/lib/chef/cookbook_loader.rb
new file mode 100644
index 0000000000..27cf978acb
--- /dev/null
+++ b/lib/chef/cookbook_loader.rb
@@ -0,0 +1,134 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/config'
+require 'chef/exceptions'
+require 'chef/cookbook/cookbook_version_loader'
+require 'chef/cookbook_version'
+require 'chef/cookbook/chefignore'
+require 'chef/cookbook/metadata'
+
+#
+# CookbookLoader class loads the cookbooks lazily as read
+#
+class Chef
+ class CookbookLoader
+
+ attr_reader :cookbooks_by_name
+ attr_reader :merged_cookbooks
+ attr_reader :cookbook_paths
+ attr_reader :metadata
+
+ include Enumerable
+
+ def initialize(*repo_paths)
+ repo_paths = repo_paths.flatten
+ raise ArgumentError, "You must specify at least one cookbook repo path" if repo_paths.empty?
+ @cookbooks_by_name = Mash.new
+ @loaded_cookbooks = {}
+ @metadata = Mash.new
+ @cookbooks_paths = Hash.new {|h,k| h[k] = []} # for deprecation warnings
+ @chefignores = {}
+ @repo_paths = repo_paths.map do |repo_path|
+ repo_path = File.expand_path(repo_path)
+ end
+
+ # Used to track which cookbooks appear in multiple places in the cookbook repos
+ # and are merged in to a single cookbook by file shadowing. This behavior is
+ # deprecated, so users of this class may issue warnings to the user by checking
+ # this variable
+ @merged_cookbooks = []
+ end
+
+ def merged_cookbook_paths # for deprecation warnings
+ merged_cookbook_paths = {}
+ @merged_cookbooks.each {|c| merged_cookbook_paths[c] = @cookbooks_paths[c]}
+ merged_cookbook_paths
+ end
+
+ def load_cookbooks
+ @repo_paths.each do |repo_path|
+ Dir[File.join(repo_path, "*")].each do |cookbook_path|
+ load_cookbook(File.basename(cookbook_path), [repo_path])
+ end
+ end
+ @cookbooks_by_name
+ end
+
+ def load_cookbook(cookbook_name, repo_paths=nil)
+ repo_paths ||= @repo_paths
+ repo_paths.each do |repo_path|
+ @chefignores[repo_path] ||= Cookbook::Chefignore.new(repo_path)
+ cookbook_path = File.join(repo_path, cookbook_name.to_s)
+ next unless File.directory?(cookbook_path) and Dir[File.join(repo_path, "*")].include?(cookbook_path)
+ loader = Cookbook::CookbookVersionLoader.new(cookbook_path, @chefignores[repo_path])
+ loader.load_cookbooks
+ next if loader.empty?
+ cookbook_name = loader.cookbook_name
+ @cookbooks_paths[cookbook_name] << cookbook_path # for deprecation warnings
+ if @loaded_cookbooks.key?(cookbook_name)
+ @merged_cookbooks << cookbook_name # for deprecation warnings
+ @loaded_cookbooks[cookbook_name].merge!(loader)
+ else
+ @loaded_cookbooks[cookbook_name] = loader
+ end
+ end
+
+ if @loaded_cookbooks.has_key?(cookbook_name)
+ cookbook_version = @loaded_cookbooks[cookbook_name].cookbook_version
+ @cookbooks_by_name[cookbook_name] = cookbook_version
+ @metadata[cookbook_name] = cookbook_version.metadata
+ end
+ @cookbooks_by_name[cookbook_name]
+ end
+
+ def [](cookbook)
+ if @cookbooks_by_name.has_key?(cookbook.to_sym) or load_cookbook(cookbook.to_sym)
+ @cookbooks_by_name[cookbook.to_sym]
+ else
+ raise Exceptions::CookbookNotFoundInRepo, "Cannot find a cookbook named #{cookbook.to_s}; did you forget to add metadata to a cookbook? (http://wiki.opscode.com/display/chef/Metadata)"
+ end
+ end
+
+ alias :fetch :[]
+
+ def has_key?(cookbook_name)
+ not self[cookbook_name.to_sym].nil?
+ end
+ alias :cookbook_exists? :has_key?
+ alias :key? :has_key?
+
+ def each
+ @cookbooks_by_name.keys.sort { |a,b| a.to_s <=> b.to_s }.each do |cname|
+ yield(cname, @cookbooks_by_name[cname])
+ end
+ end
+
+ def cookbook_names
+ @cookbooks_by_name.keys.sort
+ end
+
+ def values
+ @cookbooks_by_name.values
+ end
+ alias :cookbooks :values
+
+ end
+end
diff --git a/lib/chef/cookbook_site_streaming_uploader.rb b/lib/chef/cookbook_site_streaming_uploader.rb
new file mode 100644
index 0000000000..abb5499042
--- /dev/null
+++ b/lib/chef/cookbook_site_streaming_uploader.rb
@@ -0,0 +1,244 @@
+#
+# Author:: Stanislav Vitvitskiy
+# Author:: Nuo Yan (nuo@opscode.com)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'net/http'
+require 'mixlib/authentication/signedheaderauth'
+require 'openssl'
+
+class Chef
+ # == Chef::CookbookSiteStreamingUploader
+ # A streaming multipart HTTP upload implementation. Used to upload cookbooks
+ # (in tarball form) to http://cookbooks.opscode.com
+ #
+ # inspired by http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html
+ class CookbookSiteStreamingUploader
+
+ DefaultHeaders = { 'accept' => 'application/json', 'x-chef-version' => ::Chef::VERSION }
+
+ class << self
+
+ def create_build_dir(cookbook)
+ tmp_cookbook_path = Tempfile.new("chef-#{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.debug("Staging at #{tmp_cookbook_dir}")
+ checksums_to_on_disk_paths = cookbook.checksums
+ Chef::CookbookVersion::COOKBOOK_SEGMENTS.each do |segment|
+ cookbook.manifest[segment].each 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.debug("Staging #{on_disk_path} to #{dest}")
+ FileUtils.cp(on_disk_path, dest)
+ end
+ end
+
+ # First, generate metadata
+ Chef::Log.debug("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
+
+ timestamp = Time.now.utc.iso8601
+ secret_key = OpenSSL::PKey::RSA.new(File.read(secret_key_filename))
+
+ unless params.nil? || params.empty?
+ params.each do |key, value|
+ if value.kind_of?(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}, path: #{url.path}, 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 = Net::HTTP.new(url.host, url.port)
+ if url.scheme == "https"
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+ res = http.request(req)
+ #res = http.start {|http_proc| http_proc.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
+
+ # BUGBUG this makes the response compatible with what respsonse_steps expects to test headers (response.headers[] -> response[])
+ def headers
+ self
+ end
+
+ def status
+ 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 startiung 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)
+ return nil if @part_no >= @parts.size
+
+ 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)
+ current_part + if next_part
+ next_part
+ else
+ ''
+ end
+ else
+ @part_offset += how_much_current_part
+ current_part
+ end
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/cookbook_uploader.rb b/lib/chef/cookbook_uploader.rb
new file mode 100644
index 0000000000..8dd50ac043
--- /dev/null
+++ b/lib/chef/cookbook_uploader.rb
@@ -0,0 +1,163 @@
+
+require 'set'
+require 'rest_client'
+require 'chef/exceptions'
+require 'chef/knife/cookbook_metadata'
+require 'chef/checksum_cache'
+require 'chef/cookbook_version'
+require 'chef/cookbook/syntax_check'
+require 'chef/cookbook/file_system_file_vendor'
+
+class Chef
+ class CookbookUploader
+
+ def self.work_queue
+ @work_queue ||= Queue.new
+ end
+
+ def self.setup_worker_threads
+ @worker_threads ||= begin
+ work_queue
+ (1...10).map do
+ Thread.new do
+ loop do
+ work_queue.pop.call
+ end
+ end
+ end
+ end
+ end
+
+ attr_reader :cookbooks
+ attr_reader :path
+ attr_reader :opts
+ attr_reader :rest
+
+ # Creates a new CookbookUploader.
+ # ===Arguments:
+ # * cookbooks::: A Chef::CookbookVersion or array of them describing the
+ # cookbook(s) to be uploaded
+ # * path::: A String or Array of Strings representing the base paths to the
+ # cookbook repositories.
+ # * opts::: (optional) An options Hash
+ # ===Options:
+ # * :force indicates that the uploader should set the force option when
+ # uploading the cookbook. This allows frozen CookbookVersion
+ # documents on the server to be overwritten (otherwise a 409 is
+ # returned by the server)
+ # * :rest A Chef::REST object that you have configured the way you like it.
+ # If you don't provide this, one will be created using the values
+ # in Chef::Config.
+ def initialize(cookbooks, path, opts={})
+ @path, @opts = path, opts
+ @cookbooks = Array(cookbooks)
+ @rest = opts[:rest] || Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def upload_cookbooks
+ Thread.abort_on_exception = true
+
+ # Syntax Check
+ validate_cookbooks
+ # generate checksums of cookbook files and create a sandbox
+ checksum_files = {}
+ cookbooks.each do |cb|
+ Chef::Log.info("Saving #{cb.name}")
+ checksum_files.merge!(cb.checksums)
+ end
+
+ checksums = checksum_files.inject({}){|memo,elt| memo[elt.first]=nil ; memo}
+ new_sandbox = rest.post_rest("sandboxes", { :checksums => checksums })
+
+ Chef::Log.info("Uploading files")
+
+ self.class.setup_worker_threads
+
+ checksums_to_upload = Set.new
+
+ # upload the new checksums and commit the sandbox
+ new_sandbox['checksums'].each do |checksum, info|
+ if info['needs_upload'] == true
+ checksums_to_upload << checksum
+ Chef::Log.info("Uploading #{checksum_files[checksum]} (checksum hex = #{checksum}) to #{info['url']}")
+ self.class.work_queue << uploader_function_for(checksum_files[checksum], checksum, info['url'], checksums_to_upload)
+ else
+ Chef::Log.debug("#{checksum_files[checksum]} has not changed")
+ end
+ end
+
+ until checksums_to_upload.empty?
+ sleep 0.1
+ end
+
+ sandbox_url = new_sandbox['uri']
+ Chef::Log.debug("Committing sandbox")
+ # Retry if S3 is claims a checksum doesn't exist (the eventual
+ # in eventual consistency)
+ retries = 0
+ begin
+ rest.put_rest(sandbox_url, {:is_completed => true})
+ rescue Net::HTTPServerException => e
+ if e.message =~ /^400/ && (retries += 1) <= 5
+ sleep 2
+ retry
+ else
+ raise
+ end
+ end
+
+ # files are uploaded, so save the manifest
+ cookbooks.each do |cb|
+ save_url = opts[:force] ? cb.force_save_url : cb.save_url
+ rest.put_rest(save_url, cb)
+ end
+
+ Chef::Log.info("Upload complete!")
+ end
+
+ def worker_thread(work_queue)
+ end
+
+ def uploader_function_for(file, checksum, url, checksums_to_upload)
+ lambda do
+ # Checksum is the hexadecimal representation of the md5,
+ # but we need the base64 encoding for the content-md5
+ # header
+ checksum64 = Base64.encode64([checksum].pack("H*")).strip
+ timestamp = Time.now.utc.iso8601
+ file_contents = File.open(file, "rb") {|f| f.read}
+ # TODO - 5/28/2010, cw: make signing and sending the request streaming
+ sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(
+ :http_method => :put,
+ :path => URI.parse(url).path,
+ :body => file_contents,
+ :timestamp => timestamp,
+ :user_id => rest.client_name
+ )
+ headers = { 'content-type' => 'application/x-binary', 'content-md5' => checksum64, :accept => 'application/json' }
+ headers.merge!(sign_obj.sign(OpenSSL::PKey::RSA.new(rest.signing_key)))
+
+ begin
+ RestClient::Resource.new(url, :headers=>headers, :timeout=>1800, :open_timeout=>1800).put(file_contents)
+ checksums_to_upload.delete(checksum)
+ rescue RestClient::Exception => e
+ Chef::Knife.ui.error("Failed to upload #@cookbook : #{e.message}\n#{e.response.body}")
+ raise
+ end
+ end
+ end
+
+ def validate_cookbooks
+ cookbooks.each do |cb|
+ syntax_checker = Chef::Cookbook::SyntaxCheck.for_cookbook(cb.name, @user_cookbook_path)
+ Chef::Log.info("Validating ruby files")
+ exit(1) unless syntax_checker.validate_ruby_files
+ Chef::Log.info("Validating templates")
+ exit(1) unless syntax_checker.validate_templates
+ Chef::Log.info("Syntax OK")
+ true
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/cookbook_version.rb b/lib/chef/cookbook_version.rb
new file mode 100644
index 0000000000..0e11174a07
--- /dev/null
+++ b/lib/chef/cookbook_version.rb
@@ -0,0 +1,782 @@
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright 2008-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/log'
+require 'chef/node'
+require 'chef/resource_definition_list'
+require 'chef/recipe'
+require 'chef/cookbook/file_vendor'
+require 'chef/cookbook/metadata'
+require 'chef/version_class'
+
+class Chef
+
+ # == Chef::CookbookVersion
+ # CookbookVersion is a model object encapsulating the data about a Chef
+ # cookbook. Chef supports maintaining multiple versions of a cookbook on a
+ # single server; each version is represented by a distinct instance of this
+ # class.
+ #--
+ # TODO: timh/cw: 5-24-2010: mutators for files (e.g., recipe_filenames=,
+ # recipe_filenames.insert) should dirty the manifest so it gets regenerated.
+ class CookbookVersion
+ include Comparable
+
+ COOKBOOK_SEGMENTS = [ :resources, :providers, :recipes, :definitions, :libraries, :attributes, :files, :templates, :root_files ]
+
+ attr_accessor :root_dir
+ attr_accessor :definition_filenames
+ attr_accessor :template_filenames
+ attr_accessor :file_filenames
+ attr_accessor :library_filenames
+ attr_accessor :resource_filenames
+ attr_accessor :provider_filenames
+ attr_accessor :root_filenames
+ attr_accessor :name
+ attr_accessor :metadata
+ attr_accessor :metadata_filenames
+ attr_accessor :status
+
+ # attribute_filenames also has a setter that has non-default
+ # functionality.
+ attr_reader :attribute_filenames
+
+ # recipe_filenames also has a setter that has non-default
+ # functionality.
+ attr_reader :recipe_filenames
+
+ attr_reader :recipe_filenames_by_name
+ attr_reader :attribute_filenames_by_short_filename
+
+ # This is the one and only method that knows how cookbook files'
+ # checksums are generated.
+ def self.checksum_cookbook_file(filepath)
+ Chef::ChecksumCache.generate_md5_checksum_for_file(filepath)
+ rescue Errno::ENOENT
+ Chef::Log.debug("File #{filepath} does not exist, so there is no checksum to generate")
+ nil
+ end
+
+ def self.cache
+ Chef::FileCache
+ end
+
+ # Synchronizes all the cookbooks from the chef-server.
+ #
+ # === Returns
+ # true:: Always returns true
+ def self.sync_cookbooks(cookbook_hash)
+ Chef::Log.info("Loading cookbooks [#{cookbook_hash.keys.sort.join(', ')}]")
+ Chef::Log.debug("Cookbooks detail: #{cookbook_hash.inspect}")
+
+ clear_obsoleted_cookbooks(cookbook_hash)
+
+ # Synchronize each of the node's cookbooks, and add to the
+ # valid_cache_entries hash.
+ cookbook_hash.values.each do |cookbook|
+ sync_cookbook_file_cache(cookbook)
+ end
+
+ true
+ end
+
+ # Iterates over cached cookbooks' files, removing files belonging to
+ # cookbooks that don't appear in +cookbook_hash+
+ def self.clear_obsoleted_cookbooks(cookbook_hash)
+ # Remove all cookbooks no longer relevant to this node
+ cache.find(File.join(%w{cookbooks ** *})).each do |cache_file|
+ cache_file =~ /^cookbooks\/([^\/]+)\//
+ unless cookbook_hash.has_key?($1)
+ Chef::Log.info("Removing #{cache_file} from the cache; its cookbook is no longer needed on this client.")
+ cache.delete(cache_file)
+ end
+ end
+ end
+
+ # Update the file caches for a given cache segment. Takes a segment name
+ # and a hash that matches one of the cookbooks/_attribute_files style
+ # remote file listings.
+ #
+ # === Parameters
+ # cookbook<Chef::Cookbook>:: The cookbook to update
+ # valid_cache_entries<Hash>:: Out-param; Added to this hash are the files that
+ # were referred to by this cookbook
+ def self.sync_cookbook_file_cache(cookbook)
+ Chef::Log.debug("Synchronizing cookbook #{cookbook.name}")
+
+ # files and templates are lazily loaded, and will be done later.
+ eager_segments = COOKBOOK_SEGMENTS.dup
+
+ unless Chef::Config[:no_lazy_load] then
+ eager_segments.delete(:files)
+ eager_segments.delete(:templates)
+ end
+
+ eager_segments.each do |segment|
+ segment_filenames = Array.new
+ cookbook.manifest[segment].each do |manifest_record|
+ # segment = cookbook segment
+ # remote_list = list of file hashes
+ #
+ # We need the list of known good attribute files, so we can delete any that are
+ # just laying about.
+
+ cache_filename = File.join("cookbooks", cookbook.name, manifest_record['path'])
+ valid_cache_entries[cache_filename] = true
+
+ current_checksum = nil
+ if cache.has_key?(cache_filename)
+ current_checksum = checksum_cookbook_file(cache.load(cache_filename, false))
+ end
+
+ # If the checksums are different between on-disk (current) and on-server
+ # (remote, per manifest), do the update. This will also execute if there
+ # is no current checksum.
+ if current_checksum != manifest_record['checksum']
+ raw_file = chef_server_rest.get_rest(manifest_record[:url], true)
+
+ Chef::Log.info("Storing updated #{cache_filename} in the cache.")
+ cache.move_to(raw_file.path, cache_filename)
+ else
+ Chef::Log.debug("Not storing #{cache_filename}, as the cache is up to date.")
+ end
+
+ # make the segment filenames a full path.
+ full_path_cache_filename = cache.load(cache_filename, false)
+ segment_filenames << full_path_cache_filename
+ end
+
+ # replace segment filenames with a full-path one.
+ if segment.to_sym == :recipes
+ cookbook.recipe_filenames = segment_filenames
+ elsif segment.to_sym == :attributes
+ cookbook.attribute_filenames = segment_filenames
+ else
+ cookbook.segment_filenames(segment).replace(segment_filenames)
+ end
+ end
+ end
+
+ def self.cleanup_file_cache
+ unless Chef::Config[:solo]
+ # Delete each file in the cache that we didn't encounter in the
+ # manifest.
+ cache.find(File.join(%w{cookbooks ** *})).each do |cache_filename|
+ unless valid_cache_entries[cache_filename]
+ Chef::Log.info("Removing #{cache_filename} from the cache; it is no longer needed by chef-client.")
+ cache.delete(cache_filename)
+ end
+ end
+ end
+ end
+
+ # Creates a new Chef::CookbookVersion object.
+ #
+ # === Returns
+ # object<Chef::CookbookVersion>:: Duh. :)
+ def initialize(name)
+ @name = name
+ @frozen = false
+ @attribute_filenames = Array.new
+ @definition_filenames = Array.new
+ @template_filenames = Array.new
+ @file_filenames = Array.new
+ @recipe_filenames = Array.new
+ @recipe_filenames_by_name = Hash.new
+ @library_filenames = Array.new
+ @resource_filenames = Array.new
+ @provider_filenames = Array.new
+ @metadata_filenames = Array.new
+ @root_dir = nil
+ @root_filenames = Array.new
+ @status = :ready
+ @manifest = nil
+ @file_vendor = nil
+ @metadata = Chef::Cookbook::Metadata.new
+ end
+
+ def version
+ metadata.version
+ end
+
+ # Indicates if this version is frozen or not. Freezing a coobkook version
+ # indicates that a new cookbook with the same name and version number
+ # shoule
+ def frozen_version?
+ @frozen
+ end
+
+ def freeze_version
+ @frozen = true
+ end
+
+ def version=(new_version)
+ manifest["version"] = new_version
+ metadata.version(new_version)
+ end
+
+ # A manifest is a Mash that maps segment names to arrays of manifest
+ # records (see #preferred_manifest_record for format of manifest records),
+ # as well as describing cookbook metadata. The manifest follows a form
+ # like the following:
+ #
+ # {
+ # :cookbook_name = "apache2",
+ # :version = "1.0",
+ # :name = "Apache 2"
+ # :metadata = ???TODO: timh/cw: 5-24-2010: describe this format,
+ #
+ # :files => [
+ # {
+ # :name => "afile.rb",
+ # :path => "files/ubuntu-9.10/afile.rb",
+ # :checksum => "2222",
+ # :specificity => "ubuntu-9.10"
+ # },
+ # ],
+ # :templates => [ manifest_record1, ... ],
+ # ...
+ # }
+ def manifest
+ unless @manifest
+ generate_manifest
+ end
+ @manifest
+ end
+
+ def manifest=(new_manifest)
+ @manifest = Mash.new new_manifest
+ @checksums = extract_checksums_from_manifest(@manifest)
+ @manifest_records_by_path = extract_manifest_records_by_path(@manifest)
+
+ COOKBOOK_SEGMENTS.each do |segment|
+ next unless @manifest.has_key?(segment)
+ filenames = @manifest[segment].map{|manifest_record| manifest_record['name']}
+
+ if segment == :recipes
+ self.recipe_filenames = filenames
+ elsif segment == :attributes
+ self.attribute_filenames = filenames
+ else
+ segment_filenames(segment).clear
+ filenames.each { |filename| segment_filenames(segment) << filename }
+ end
+ end
+ end
+
+ # Returns a hash of checksums to either nil or the on disk path (which is
+ # done by generate_manifest).
+ def checksums
+ unless @checksums
+ generate_manifest
+ end
+ @checksums
+ end
+
+ def manifest_records_by_path
+ @manifest_records_by_path || generate_manifest
+ @manifest_records_by_path
+ end
+
+ def full_name
+ "#{name}-#{version}"
+ end
+
+ def attribute_filenames=(*filenames)
+ @attribute_filenames = filenames.flatten
+ @attribute_filenames_by_short_filename = filenames_by_name(attribute_filenames)
+ attribute_filenames
+ end
+
+ ## BACKCOMPAT/DEPRECATED - Remove these and fix breakage before release [DAN - 5/20/2010]##
+ alias :attribute_files :attribute_filenames
+ alias :attribute_files= :attribute_filenames=
+
+ # Return recipe names in the form of cookbook_name::recipe_name
+ def fully_qualified_recipe_names
+ results = Array.new
+ recipe_filenames_by_name.each_key do |rname|
+ results << "#{name}::#{rname}"
+ end
+ results
+ end
+
+ def recipe_filenames=(*filenames)
+ @recipe_filenames = filenames.flatten
+ @recipe_filenames_by_name = filenames_by_name(recipe_filenames)
+ recipe_filenames
+ end
+
+ ## BACKCOMPAT/DEPRECATED - Remove these and fix breakage before release [DAN - 5/20/2010]##
+ alias :recipe_files :recipe_filenames
+ alias :recipe_files= :recipe_filenames=
+
+ # called from DSL
+ def load_recipe(recipe_name, run_context)
+ unless recipe_filenames_by_name.has_key?(recipe_name)
+ raise Chef::Exceptions::RecipeNotFound, "could not find recipe #{recipe_name} for cookbook #{name}"
+ end
+
+ Chef::Log.debug("Found recipe #{recipe_name} in cookbook #{name}")
+ recipe = Chef::Recipe.new(name, recipe_name, run_context)
+ recipe_filename = recipe_filenames_by_name[recipe_name]
+
+ unless recipe_filename
+ raise Chef::Exceptions::RecipeNotFound, "could not find #{recipe_name} files for cookbook #{name}"
+ end
+
+ recipe.from_file(recipe_filename)
+ recipe
+ end
+
+ def segment_filenames(segment)
+ unless COOKBOOK_SEGMENTS.include?(segment)
+ raise ArgumentError, "invalid segment #{segment}: must be one of #{COOKBOOK_SEGMENTS.join(', ')}"
+ end
+
+ case segment.to_sym
+ when :resources
+ @resource_filenames
+ when :providers
+ @provider_filenames
+ when :recipes
+ @recipe_filenames
+ when :libraries
+ @library_filenames
+ when :definitions
+ @definition_filenames
+ when :attributes
+ @attribute_filenames
+ when :files
+ @file_filenames
+ when :templates
+ @template_filenames
+ when :root_files
+ @root_filenames
+ end
+ end
+
+ # Determine the most specific manifest record for the given
+ # segment/filename, given information in the node. Throws
+ # FileNotFound if there is no such segment and filename in the
+ # manifest.
+ #
+ # A manifest record is a Mash that follows the following form:
+ # {
+ # :name => "example.rb",
+ # :path => "files/default/example.rb",
+ # :specificity => "default",
+ # :checksum => "1234"
+ # }
+ def preferred_manifest_record(node, segment, filename)
+ preferences = preferences_for_path(node, segment, filename)
+
+ # ensure that we generate the manifest, which will also generate
+ # @manifest_records_by_path
+ manifest
+
+ # in order of prefernce, look for the filename in the manifest
+ found_pref = preferences.find {|preferred_filename| @manifest_records_by_path[preferred_filename] }
+ if found_pref
+ @manifest_records_by_path[found_pref]
+ else
+ if segment == :files || segment == :templates
+ error_message = "Cookbook '#{name}' (#{version}) does not contain a file at any of these locations:\n"
+ error_locations = [
+ " #{segment}/#{node[:platform]}-#{node[:platform_version]}/#{filename}",
+ " #{segment}/#{node[:platform]}/#{filename}",
+ " #{segment}/default/#{filename}",
+ ]
+ error_message << error_locations.join("\n")
+ existing_files = segment_filenames(segment)
+ # Show the files that the cookbook does have. If the user made a typo,
+ # hopefully they'll see it here.
+ unless existing_files.empty?
+ error_message << "\n\nThis cookbook _does_ contain: ['#{existing_files.join("','")}']"
+ end
+ raise Chef::Exceptions::FileNotFound, error_message
+ else
+ raise Chef::Exceptions::FileNotFound, "cookbook #{name} does not contain file #{segment}/#{filename}"
+ end
+ end
+ end
+
+ def preferred_filename_on_disk_location(node, segment, filename, current_filepath=nil)
+ manifest_record = preferred_manifest_record(node, segment, filename)
+ if current_filepath && (manifest_record['checksum'] == self.class.checksum_cookbook_file(current_filepath))
+ nil
+ else
+ file_vendor.get_filename(manifest_record['path'])
+ end
+ end
+
+ def relative_filenames_in_preferred_directory(node, segment, dirname)
+ preferences = preferences_for_path(node, segment, dirname)
+ filenames_by_pref = Hash.new
+ preferences.each { |pref| filenames_by_pref[pref] = Array.new }
+
+ manifest[segment].each do |manifest_record|
+ manifest_record_path = manifest_record[:path]
+
+ # find the NON SPECIFIC filenames, but prefer them by filespecificity.
+ # For example, if we have a file:
+ # 'files/default/somedir/somefile.conf' we only keep
+ # 'somedir/somefile.conf'. If there is also
+ # 'files/$hostspecific/somedir/otherfiles' that matches the requested
+ # hostname specificity, that directory will win, as it is more specific.
+ #
+ # This is clearly ugly b/c the use case is for remote directory, where
+ # we're just going to make cookbook_files out of these and make the
+ # cookbook find them by filespecificity again. but it's the shortest
+ # path to "success" for now.
+ if manifest_record_path =~ /(#{Regexp.escape(segment.to_s)}\/[^\/]+\/#{Regexp.escape(dirname)})\/.+$/
+ specificity_dirname = $1
+ non_specific_path = manifest_record_path[/#{Regexp.escape(segment.to_s)}\/[^\/]+\/#{Regexp.escape(dirname)}\/(.+)$/, 1]
+ # Record the specificity_dirname only if it's in the list of
+ # valid preferences
+ if filenames_by_pref[specificity_dirname]
+ filenames_by_pref[specificity_dirname] << non_specific_path
+ end
+ end
+ end
+
+ best_pref = preferences.find { |pref| !filenames_by_pref[pref].empty? }
+
+ raise Chef::Exceptions::FileNotFound, "cookbook #{name} has no directory #{segment}/default/#{dirname}" unless best_pref
+
+ filenames_by_pref[best_pref]
+
+ end
+
+ # Determine the manifest records from the most specific directory
+ # for the given node. See #preferred_manifest_record for a
+ # description of entries of the returned Array.
+ def preferred_manifest_records_for_directory(node, segment, dirname)
+ preferences = preferences_for_path(node, segment, dirname)
+ records_by_pref = Hash.new
+ preferences.each { |pref| records_by_pref[pref] = Array.new }
+
+ manifest[segment].each do |manifest_record|
+ manifest_record_path = manifest_record[:path]
+
+ # extract the preference part from the path.
+ if manifest_record_path =~ /(#{Regexp.escape(segment.to_s)}\/[^\/]+\/#{Regexp.escape(dirname)})\/.+$/
+ # Note the specificy_dirname includes the segment and
+ # dirname argument as above, which is what
+ # preferences_for_path returns. It could be
+ # "files/ubuntu-9.10/dirname", for example.
+ specificity_dirname = $1
+
+ # Record the specificity_dirname only if it's in the list of
+ # valid preferences
+ if records_by_pref[specificity_dirname]
+ records_by_pref[specificity_dirname] << manifest_record
+ end
+ end
+ end
+
+ best_pref = preferences.find { |pref| !records_by_pref[pref].empty? }
+
+ raise Chef::Exceptions::FileNotFound, "cookbook #{name} (#{version}) has no directory #{segment}/default/#{dirname}" unless best_pref
+
+ records_by_pref[best_pref]
+ end
+
+
+ # Given a node, segment and path (filename or directory name),
+ # return the priority-ordered list of preference locations to
+ # look.
+ def preferences_for_path(node, segment, path)
+ # only files and templates can be platform-specific
+ if segment.to_sym == :files || segment.to_sym == :templates
+ begin
+ platform, version = Chef::Platform.find_platform_and_version(node)
+ rescue ArgumentError => e
+ # Skip platform/version if they were not found by find_platform_and_version
+ if e.message =~ /Cannot find a (?:platform|version)/
+ platform = "/unknown_platform/"
+ version = "/unknown_platform_version/"
+ else
+ raise
+ end
+ end
+
+ fqdn = node[:fqdn]
+
+ # Break version into components, eg: "5.7.1" => [ "5.7.1", "5.7", "5" ]
+ search_versions = []
+ parts = version.to_s.split('.')
+
+ parts.size.times do
+ search_versions << parts.join('.')
+ parts.pop
+ end
+
+ # Most specific to least specific places to find the path
+ search_path = [ File.join(segment.to_s, "host-#{fqdn}", path) ]
+ search_versions.each do |v|
+ search_path << File.join(segment.to_s, "#{platform}-#{v}", path)
+ end
+ search_path << File.join(segment.to_s, platform.to_s, path)
+ search_path << File.join(segment.to_s, "default", path)
+
+ search_path
+ else
+ [File.join(segment, path)]
+ end
+ end
+ private :preferences_for_path
+
+ def to_hash
+ result = manifest.dup
+ result['frozen?'] = frozen_version?
+ result['chef_type'] = 'cookbook_version'
+ result.to_hash
+ end
+
+ def to_json(*a)
+ result = self.to_hash
+ result['json_class'] = self.class.name
+ result.to_json(*a)
+ end
+
+ def self.json_create(o)
+ cookbook_version = new(o["cookbook_name"])
+ # We want the Chef::Cookbook::Metadata object to always be inflated
+ cookbook_version.metadata = Chef::Cookbook::Metadata.from_hash(o["metadata"])
+ cookbook_version.manifest = o
+
+ # We don't need the following step when we decide to stop supporting deprecated operators in the metadata (e.g. <<, >>)
+ cookbook_version.manifest["metadata"] = JSON.parse(cookbook_version.metadata.to_json)
+
+ cookbook_version.freeze_version if o["frozen?"]
+ cookbook_version
+ end
+
+ def generate_manifest_with_urls(&url_generator)
+ rendered_manifest = manifest.dup
+ COOKBOOK_SEGMENTS.each do |segment|
+ if rendered_manifest.has_key?(segment)
+ rendered_manifest[segment].each do |manifest_record|
+ url_options = { :cookbook_name => name.to_s, :cookbook_version => version, :checksum => manifest_record["checksum"] }
+ manifest_record["url"] = url_generator.call(url_options)
+ end
+ end
+ end
+ rendered_manifest
+ end
+
+ def metadata_json_file
+ File.join(root_dir, "metadata.json")
+ end
+
+ def metadata_rb_file
+ File.join(root_dir, "metadata.rb")
+ end
+
+ def reload_metadata!
+ if File.exists?(metadata_json_file)
+ metadata.from_json(IO.read(metadata_json_file))
+ end
+ end
+
+ ##
+ # REST API
+ ##
+ def self.chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def chef_server_rest
+ self.class.chef_server_rest
+ end
+
+ # Return the URL to save (PUT) this object to the server via the
+ # REST api. If there is an existing document on the server and it
+ # is marked frozen, a PUT will result in a 409 Conflict.
+ def save_url
+ "cookbooks/#{name}/#{version}"
+ end
+
+ # Adds the `force=true` parameter to the upload URL. This allows
+ # the user to overwrite a frozen cookbook (a PUT against the
+ # normal #save_url raises a 409 Conflict in this case).
+ def force_save_url
+ "cookbooks/#{name}/#{version}?force=true"
+ end
+
+ def destroy
+ chef_server_rest.delete_rest("cookbooks/#{name}/#{version}")
+ self
+ end
+
+ def self.load(name, version="_latest")
+ version = "_latest" if version == "latest"
+ chef_server_rest.get_rest("cookbooks/#{name}/#{version}")
+ end
+
+ # The API returns only a single version of each cookbook in the result from the cookbooks method
+ def self.list
+ chef_server_rest.get_rest('cookbooks')
+ end
+
+ def self.list_all_versions
+ chef_server_rest.get_rest('cookbooks?num_versions=all')
+ end
+
+ ##
+ # Given a +cookbook_name+, get a list of all versions that exist on the
+ # server.
+ # ===Returns
+ # [String]:: Array of cookbook versions, which are strings like 'x.y.z'
+ # nil:: if the cookbook doesn't exist. an error will also be logged.
+ def self.available_versions(cookbook_name)
+ chef_server_rest.get_rest("cookbooks/#{cookbook_name}")[cookbook_name]["versions"].map do |cb|
+ cb["version"]
+ end
+ rescue Net::HTTPServerException => e
+ if e.to_s =~ /^404/
+ Chef::Log.error("Cannot find a cookbook named #{cookbook_name}")
+ nil
+ else
+ raise
+ end
+ end
+
+ # Get the newest version of all cookbooks
+ def self.latest_cookbooks
+ chef_server_rest.get_rest('cookbooks/_latest')
+ end
+
+ def <=>(o)
+ raise Chef::Exceptions::CookbookVersionNameMismatch if self.name != o.name
+ # FIXME: can we change the interface to the Metadata class such
+ # that metadata.version returns a Chef::Version instance instead
+ # of a string?
+ Chef::Version.new(self.version) <=> Chef::Version.new(o.version)
+ end
+
+ private
+
+ # For each filename, produce a mapping of base filename (i.e. recipe name
+ # or attribute file) to on disk location
+ def filenames_by_name(filenames)
+ filenames.select{|filename| filename =~ /\.rb$/}.inject({}){|memo, filename| memo[File.basename(filename, '.rb')] = filename ; memo }
+ end
+
+ # See #manifest for a description of the manifest return value.
+ # See #preferred_manifest_record for a description an individual manifest record.
+ def generate_manifest
+ manifest = Mash.new({
+ :recipes => Array.new,
+ :definitions => Array.new,
+ :libraries => Array.new,
+ :attributes => Array.new,
+ :files => Array.new,
+ :templates => Array.new,
+ :resources => Array.new,
+ :providers => Array.new,
+ :root_files => Array.new
+ })
+ checksums_to_on_disk_paths = {}
+
+ COOKBOOK_SEGMENTS.each do |segment|
+ segment_filenames(segment).each do |segment_file|
+ next if File.directory?(segment_file)
+
+ file_name = nil
+ path = nil
+ specificity = "default"
+
+ if segment == :root_files
+ matcher = segment_file.match(".+/#{Regexp.escape(name.to_s)}/(.+)")
+ file_name = matcher[1]
+ path = file_name
+ elsif segment == :templates || segment == :files
+ matcher = segment_file.match("/#{Regexp.escape(name.to_s)}/(#{Regexp.escape(segment.to_s)}/(.+?)/(.+))")
+ unless matcher
+ Chef::Log.debug("Skipping file #{segment_file}, as it isn't in any of the proper directories (platform-version, platform or default)")
+ Chef::Log.debug("You probably need to move #{segment_file} into the 'default' sub-directory")
+ next
+ end
+ path = matcher[1]
+ specificity = matcher[2]
+ file_name = matcher[3]
+ else
+ matcher = segment_file.match("/#{Regexp.escape(name.to_s)}/(#{Regexp.escape(segment.to_s)}/(.+))")
+ path = matcher[1]
+ file_name = matcher[2]
+ end
+
+ csum = self.class.checksum_cookbook_file(segment_file)
+ checksums_to_on_disk_paths[csum] = segment_file
+ rs = Mash.new({
+ :name => file_name,
+ :path => path,
+ :checksum => csum
+ })
+ rs[:specificity] = specificity
+
+ manifest[segment] << rs
+ end
+ end
+
+ manifest[:cookbook_name] = name.to_s
+ manifest[:metadata] = metadata
+ manifest[:version] = metadata.version
+ manifest[:name] = full_name
+
+ @checksums = checksums_to_on_disk_paths
+ @manifest = manifest
+ @manifest_records_by_path = extract_manifest_records_by_path(manifest)
+ end
+
+ def file_vendor
+ unless @file_vendor
+ @file_vendor = Chef::Cookbook::FileVendor.create_from_manifest(manifest)
+ end
+ @file_vendor
+ end
+
+ def extract_checksums_from_manifest(manifest)
+ checksums = {}
+ COOKBOOK_SEGMENTS.each do |segment|
+ next unless manifest.has_key?(segment)
+ manifest[segment].each do |manifest_record|
+ checksums[manifest_record[:checksum]] = nil
+ end
+ end
+ checksums
+ end
+
+ def extract_manifest_records_by_path(manifest)
+ manifest_records_by_path = {}
+ COOKBOOK_SEGMENTS.each do |segment|
+ next unless manifest.has_key?(segment)
+ manifest[segment].each do |manifest_record|
+ manifest_records_by_path[manifest_record[:path]] = manifest_record
+ end
+ end
+ manifest_records_by_path
+ end
+
+ end
+end
diff --git a/lib/chef/daemon.rb b/lib/chef/daemon.rb
new file mode 100644
index 0000000000..bb5ccf753a
--- /dev/null
+++ b/lib/chef/daemon.rb
@@ -0,0 +1,172 @@
+#
+# Author:: AJ Christensen (<aj@junglist.gen.nz>)
+# Copyright:: Copyright (c) 2008 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.
+
+# I love you Merb (lib/merb-core/server.rb)
+
+require 'chef/config'
+require 'etc'
+
+class Chef
+ class Daemon
+ class << self
+ attr_accessor :name
+
+ # Daemonize the current process, managing pidfiles and process uid/gid
+ #
+ # === Parameters
+ # name<String>:: The name to be used for the pid file
+ #
+ def daemonize(name)
+ @name = name
+ pid = pid_from_file
+ unless running?
+ remove_pid_file()
+ Chef::Log.info("Daemonizing..")
+ begin
+ exit if fork
+ Process.setsid
+ exit if fork
+ Chef::Log.info("Forked, in #{Process.pid}. Privileges: #{Process.euid} #{Process.egid}")
+ File.umask Chef::Config[:umask]
+ $stdin.reopen("/dev/null")
+ $stdout.reopen("/dev/null", "a")
+ $stderr.reopen($stdout)
+ save_pid_file
+ at_exit { remove_pid_file }
+ rescue NotImplementedError => e
+ Chef::Application.fatal!("There is no fork: #{e.message}")
+ end
+ else
+ Chef::Application.fatal!("Chef is already running pid #{pid}")
+ end
+ end
+
+ # Check if Chef is running based on the pid_file
+ # ==== Returns
+ # Boolean::
+ # True if Chef is running
+ # False if Chef is not running
+ #
+ def running?
+ if pid_from_file.nil?
+ false
+ else
+ Process.kill(0, pid_from_file)
+ true
+ end
+ rescue Errno::ESRCH, Errno::ENOENT
+ false
+ rescue Errno::EACCES => e
+ Chef::Application.fatal!("You don't have access to the PID file at #{pid_file}: #{e.message}")
+ end
+
+ # Gets the pid file for @name
+ # ==== Returns
+ # String::
+ # Location of the pid file for @name
+ def pid_file
+ Chef::Config[:pid_file] or "/tmp/#{@name}.pid"
+ end
+
+ # Suck the pid out of pid_file
+ # ==== Returns
+ # Integer::
+ # The PID from pid_file
+ # nil::
+ # Returned if the pid_file does not exist.
+ #
+ def pid_from_file
+ File.read(pid_file).chomp.to_i
+ rescue Errno::ENOENT, Errno::EACCES
+ nil
+ end
+
+ # Store the PID on the filesystem
+ # This uses the Chef::Config[:pid_file] option, or "/tmp/name.pid" otherwise
+ #
+ def save_pid_file
+ file = pid_file
+ begin
+ FileUtils.mkdir_p(File.dirname(file))
+ rescue Errno::EACCES => e
+ Chef::Application.fatal!("Failed store pid in #{File.dirname(file)}, permission denied: #{e.message}")
+ end
+
+ begin
+ File.open(file, "w") { |f| f.write(Process.pid.to_s) }
+ rescue Errno::EACCES => e
+ Chef::Application.fatal!("Couldn't write to pidfile #{file}, permission denied: #{e.message}")
+ end
+ end
+
+ # Delete the PID from the filesystem
+ def remove_pid_file
+ FileUtils.rm(pid_file) if File.exists?(pid_file)
+ end
+
+ # Change process user/group to those specified in Chef::Config
+ #
+ def change_privilege
+ Dir.chdir("/")
+
+ if Chef::Config[:user] and Chef::Config[:group]
+ Chef::Log.info("About to change privilege to #{Chef::Config[:user]}:#{Chef::Config[:group]}")
+ _change_privilege(Chef::Config[:user], Chef::Config[:group])
+ elsif Chef::Config[:user]
+ Chef::Log.info("About to change privilege to #{Chef::Config[:user]}")
+ _change_privilege(Chef::Config[:user])
+ end
+ end
+
+ # Change privileges of the process to be the specified user and group
+ #
+ # ==== Parameters
+ # user<String>:: The user to change the process to.
+ # group<String>:: The group to change the process to.
+ #
+ # ==== Alternatives
+ # If group is left out, the user will be used (changing to user:user)
+ #
+ def _change_privilege(user, group=user)
+ uid, gid = Process.euid, Process.egid
+
+ begin
+ target_uid = Etc.getpwnam(user).uid
+ rescue ArgumentError => e
+ Chef::Application.fatal!("Failed to get UID for user #{user}, does it exist? #{e.message}")
+ return false
+ end
+
+ begin
+ target_gid = Etc.getgrnam(group).gid
+ rescue ArgumentError => e
+ Chef::Application.fatal!("Failed to get GID for group #{group}, does it exist? #{e.message}")
+ return false
+ end
+
+ if (uid != target_uid) or (gid != target_gid)
+ Process.initgroups(user, target_gid)
+ Process::GID.change_privilege(target_gid)
+ Process::UID.change_privilege(target_uid)
+ end
+ true
+ rescue Errno::EPERM => e
+ Chef::Application.fatal!("Permission denied when trying to change #{uid}:#{gid} to #{target_uid}:#{target_gid}. #{e.message}")
+ end
+ end
+ end
+end
diff --git a/lib/chef/data_bag.rb b/lib/chef/data_bag.rb
new file mode 100644
index 0000000000..9ce6215b20
--- /dev/null
+++ b/lib/chef/data_bag.rb
@@ -0,0 +1,145 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/from_file'
+require 'chef/data_bag_item'
+require 'chef/mash'
+require 'chef/json_compat'
+
+class Chef
+ class DataBag
+
+ include Chef::Mixin::FromFile
+ include Chef::Mixin::ParamsValidate
+
+ VALID_NAME = /^[\-[:alnum:]_]+$/
+
+ def self.validate_name!(name)
+ unless name =~ VALID_NAME
+ raise Exceptions::InvalidDataBagName, "DataBags must have a name matching #{VALID_NAME.inspect}, you gave #{name.inspect}"
+ end
+ end
+
+ # Create a new Chef::DataBag
+ def initialize
+ @name = ''
+ end
+
+ def name(arg=nil)
+ set_or_return(
+ :name,
+ arg,
+ :regex => VALID_NAME
+ )
+ end
+
+ def to_hash
+ result = {
+ "name" => @name,
+ 'json_class' => self.class.name,
+ "chef_type" => "data_bag",
+ }
+ result
+ end
+
+ # Serialize this object as a hash
+ def to_json(*a)
+ to_hash.to_json(*a)
+ end
+
+ def chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def self.chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ # Create a Chef::Role from JSON
+ def self.json_create(o)
+ bag = new
+ bag.name(o["name"])
+ bag
+ end
+
+ def self.list(inflate=false)
+ if inflate
+ # Can't search for all data bags like other objects, fall back to N+1 :(
+ list(false).inject({}) do |response, bag_and_uri|
+ response[bag_and_uri.first] = load(bag_and_uri.first)
+ response
+ end
+ else
+ Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("data")
+ end
+ end
+
+ # Load a Data Bag by name via either the RESTful API or local data_bag_path if run in solo mode
+ def self.load(name)
+ if Chef::Config[:solo]
+ unless File.directory?(Chef::Config[:data_bag_path])
+ raise Chef::Exceptions::InvalidDataBagPath, "Data bag path '#{Chef::Config[:data_bag_path]}' is invalid"
+ end
+
+ Dir.glob(File.join(Chef::Config[:data_bag_path], "#{name}", "*.json")).inject({}) do |bag, f|
+ item = JSON.parse(IO.read(f))
+ bag[item['id']] = item
+ bag
+ end
+ else
+ Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("data/#{name}")
+ end
+ end
+
+ def destroy
+ chef_server_rest.delete_rest("data/#{@name}")
+ end
+
+ # Save the Data Bag via RESTful API
+ def save
+ begin
+ if Chef::Config[:why_run]
+ Chef::Log.warn("In whyrun mode, so NOT performing data bag save.")
+ else
+ chef_server_rest.put_rest("data/#{@name}", self)
+ end
+ rescue Net::HTTPServerException => e
+ raise e unless e.response.code == "404"
+ chef_server_rest.post_rest("data", self)
+ end
+ self
+ end
+
+ #create a data bag via RESTful API
+ def create
+ chef_server_rest.post_rest("data", self)
+ self
+ end
+
+ # As a string
+ def to_s
+ "data_bag[#{@name}]"
+ end
+
+ end
+end
+
diff --git a/lib/chef/data_bag_item.rb b/lib/chef/data_bag_item.rb
new file mode 100644
index 0000000000..3528ba724a
--- /dev/null
+++ b/lib/chef/data_bag_item.rb
@@ -0,0 +1,214 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'forwardable'
+
+require 'chef/config'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/from_file'
+require 'chef/data_bag'
+require 'chef/mash'
+require 'chef/json_compat'
+
+class Chef
+ class DataBagItem
+
+ extend Forwardable
+
+ include Chef::Mixin::FromFile
+ include Chef::Mixin::ParamsValidate
+
+ VALID_ID = /^[\-[:alnum:]_]+$/
+
+ def self.validate_id!(id_str)
+ if id_str.nil? || ( id_str !~ VALID_ID )
+ raise Exceptions::InvalidDataBagItemID, "Data Bag items must have an id matching #{VALID_ID.inspect}, you gave: #{id_str.inspect}"
+ end
+ end
+
+ # Define all Hash's instance methods as delegating to @raw_data
+ def_delegators(:@raw_data, *(Hash.instance_methods - Object.instance_methods))
+
+ attr_reader :raw_data
+
+ # Create a new Chef::DataBagItem
+ def initialize
+ @data_bag = nil
+ @raw_data = Mash.new
+ end
+
+ def chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def self.chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def raw_data
+ @raw_data
+ end
+
+ def validate_id!(id_str)
+ self.class.validate_id!(id_str)
+ end
+
+ def raw_data=(new_data)
+ unless new_data.respond_to?(:[]) && new_data.respond_to?(:keys)
+ raise Exceptions::ValidationFailed, "Data Bag Items must contain a Hash or Mash!"
+ end
+ validate_id!(new_data["id"])
+ @raw_data = new_data
+ end
+
+ def data_bag(arg=nil)
+ set_or_return(
+ :data_bag,
+ arg,
+ :regex => /^[\-[:alnum:]_]+$/
+ )
+ end
+
+ def name
+ object_name
+ end
+
+ def object_name
+ raise Exceptions::ValidationFailed, "You must have an 'id' or :id key in the raw data" unless raw_data.has_key?('id')
+ raise Exceptions::ValidationFailed, "You must have declared what bag this item belongs to!" unless data_bag
+
+ id = raw_data['id']
+ "data_bag_item_#{data_bag}_#{id}"
+ end
+
+ def self.object_name(data_bag_name, id)
+ "data_bag_item_#{data_bag_name}_#{id}"
+ end
+
+ def to_hash
+ result = self.raw_data
+ result["chef_type"] = "data_bag_item"
+ result["data_bag"] = self.data_bag
+ result
+ end
+
+ # Serialize this object as a hash
+ def to_json(*a)
+ result = {
+ "name" => self.object_name,
+ "json_class" => self.class.name,
+ "chef_type" => "data_bag_item",
+ "data_bag" => self.data_bag,
+ "raw_data" => self.raw_data
+ }
+ result.to_json(*a)
+ end
+
+ def self.from_hash(h)
+ item = new
+ item.raw_data = h
+ item
+ end
+
+ # Create a Chef::DataBagItem from JSON
+ def self.json_create(o)
+ bag_item = new
+ bag_item.data_bag(o["data_bag"])
+ o.delete("data_bag")
+ o.delete("chef_type")
+ o.delete("json_class")
+ o.delete("name")
+
+ bag_item.raw_data = Mash.new(o["raw_data"])
+ bag_item
+ end
+
+ # Load a Data Bag Item by name via either the RESTful API or local data_bag_path if run in solo mode
+ def self.load(data_bag, name)
+ if Chef::Config[:solo]
+ bag = Chef::DataBag.load(data_bag)
+ item = bag[name]
+ else
+ item = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("data/#{data_bag}/#{name}")
+ end
+
+ if item.kind_of?(DataBagItem)
+ item
+ else
+ item = from_hash(item)
+ item.data_bag(data_bag)
+ item
+ end
+ end
+
+ def destroy(data_bag=data_bag, databag_item=name)
+ chef_server_rest.delete_rest("data/#{data_bag}/#{databag_item}")
+ end
+
+ # Save this Data Bag Item via RESTful API
+ def save(item_id=@raw_data['id'])
+ r = chef_server_rest
+ begin
+ if Chef::Config[:why_run]
+ Chef::Log.warn("In whyrun mode, so NOT performing data bag item save.")
+ else
+ r.put_rest("data/#{data_bag}/#{item_id}", self)
+ end
+ rescue Net::HTTPServerException => e
+ raise e unless e.response.code == "404"
+ r.post_rest("data/#{data_bag}", self)
+ end
+ self
+ end
+
+ # Create this Data Bag Item via RESTful API
+ def create
+ chef_server_rest.post_rest("data/#{data_bag}", self)
+ self
+ end
+
+ def ==(other)
+ other.respond_to?(:to_hash) &&
+ other.respond_to?(:data_bag) &&
+ (other.to_hash == to_hash) &&
+ (other.data_bag.to_s == data_bag.to_s)
+ end
+
+ # As a string
+ def to_s
+ "data_bag_item[#{id}]"
+ end
+
+ def inspect
+ "data_bag_item[#{data_bag.inspect}, #{raw_data['id'].inspect}, #{raw_data.inspect}]"
+ end
+
+ def pretty_print(pretty_printer)
+ pretty_printer.pp({"data_bag_item('#{data_bag}', '#{id}')" => self.to_hash})
+ end
+
+ def id
+ @raw_data['id']
+ end
+
+ end
+end
+
+
diff --git a/lib/chef/dsl.rb b/lib/chef/dsl.rb
new file mode 100644
index 0000000000..74244fafbb
--- /dev/null
+++ b/lib/chef/dsl.rb
@@ -0,0 +1,5 @@
+require 'chef/dsl/recipe'
+require 'chef/dsl/platform_introspection'
+require 'chef/dsl/data_query'
+require 'chef/dsl/include_recipe'
+require 'chef/dsl/include_attribute'
diff --git a/lib/chef/dsl/data_query.rb b/lib/chef/dsl/data_query.rb
new file mode 100644
index 0000000000..ef5b490020
--- /dev/null
+++ b/lib/chef/dsl/data_query.rb
@@ -0,0 +1,66 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/search/query'
+require 'chef/data_bag'
+require 'chef/data_bag_item'
+require 'chef/encrypted_data_bag_item'
+
+class Chef
+ module DSL
+
+ # ==Chef::DSL::DataQuery
+ # Provides DSL for querying data from the chef-server via search or data
+ # bag.
+ module DataQuery
+
+ def search(*args, &block)
+ # If you pass a block, or have at least the start argument, do raw result parsing
+ #
+ # Otherwise, do the iteration for the end user
+ if Kernel.block_given? || args.length >= 4
+ Chef::Search::Query.new.search(*args, &block)
+ else
+ results = Array.new
+ Chef::Search::Query.new.search(*args) do |o|
+ results << o
+ end
+ results
+ end
+ end
+
+ def data_bag(bag)
+ DataBag.validate_name!(bag.to_s)
+ rbag = DataBag.load(bag)
+ rbag.keys
+ rescue Exception
+ Log.error("Failed to list data bag items in data bag: #{bag.inspect}")
+ raise
+ end
+
+ def data_bag_item(bag, item)
+ DataBag.validate_name!(bag.to_s)
+ DataBagItem.validate_id!(item)
+ DataBagItem.load(bag, item)
+ rescue Exception
+ Log.error("Failed to load data bag item: #{bag.inspect} #{item.inspect}")
+ raise
+ end
+ end
+ end
+end
diff --git a/lib/chef/dsl/include_attribute.rb b/lib/chef/dsl/include_attribute.rb
new file mode 100644
index 0000000000..d8342af6a7
--- /dev/null
+++ b/lib/chef/dsl/include_attribute.rb
@@ -0,0 +1,60 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/log'
+
+class Chef
+ module DSL
+ module IncludeAttribute
+
+ # Loads the attribute file specified by the short name of the
+ # file, e.g., loads specified cookbook's
+ # "attributes/mailservers.rb"
+ # if passed
+ # "mailservers"
+ def include_attribute(*attr_file_specs)
+ attr_file_specs.flatten.each do |attr_file_spec|
+ cookbook_name, attr_file = parse_attribute_file_spec(attr_file_spec)
+ if run_context.loaded_fully_qualified_attribute?(cookbook_name, attr_file)
+ Chef::Log.debug("I am not loading attribute file #{cookbook_name}::#{attr_file}, because I have already seen it.")
+ else
+ Chef::Log.debug("Loading Attribute #{cookbook_name}::#{attr_file}")
+ run_context.loaded_attribute(cookbook_name, attr_file)
+ attr_file_path = run_context.resolve_attribute(cookbook_name, attr_file)
+ node.from_file(attr_file_path)
+ end
+ end
+ true
+ end
+
+ # Takes a attribute file specification, like "apache2" or "mysql::server"
+ # and converts it to a 2 element array of [cookbook_name, attribute_file_name]
+ def parse_attribute_file_spec(file_spec)
+ if match = file_spec.match(/(.+?)::(.+)/)
+ [match[1], match[2]]
+ else
+ [file_spec, "default"]
+ end
+ end
+
+ end
+ end
+end
+
+
+
diff --git a/lib/chef/dsl/include_recipe.rb b/lib/chef/dsl/include_recipe.rb
new file mode 100644
index 0000000000..8cbee7a733
--- /dev/null
+++ b/lib/chef/dsl/include_recipe.rb
@@ -0,0 +1,42 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/log'
+
+class Chef
+ module DSL
+ module IncludeRecipe
+
+ def include_recipe(*recipe_names)
+ run_context.include_recipe(*recipe_names)
+ end
+
+ def load_recipe(recipe_name)
+ run_context.load_recipe(recipe_name)
+ end
+
+ def require_recipe(*args)
+ Chef::Log.warn("require_recipe is deprecated and will be removed in a future release, please use include_recipe")
+ include_recipe(*args)
+ end
+
+ end
+ end
+end
+
+
diff --git a/lib/chef/dsl/platform_introspection.rb b/lib/chef/dsl/platform_introspection.rb
new file mode 100644
index 0000000000..211def2797
--- /dev/null
+++ b/lib/chef/dsl/platform_introspection.rb
@@ -0,0 +1,215 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module DSL
+
+ # == Chef::DSL::PlatformIntrospection
+ # Provides the DSL for platform-dependent switch logic, such as
+ # #value_for_platform.
+ module PlatformIntrospection
+
+ # Implementation class for determining platform dependent values
+ class PlatformDependentValue
+
+ # Create a platform dependent value object.
+ # === Arguments
+ # platform_hash (Hash) a hash of the same structure as Chef::Platform,
+ # like this:
+ # {
+ # :debian => {:default => 'the value for all debian'}
+ # [:centos, :redhat, :fedora] => {:default => "value for all EL variants"}
+ # :ubuntu => { :default => "default for ubuntu", '10.04' => "value for 10.04 only"},
+ # :default => "the default when nothing else matches"
+ # }
+ # * platforms can be specified as Symbols or Strings
+ # * multiple platforms can be grouped by using an Array as the key
+ # * values for platforms need to be Hashes of the form:
+ # {platform_version => value_for_that_version}
+ # * the exception to the above is the default value, which is given as
+ # :default => default_value
+ def initialize(platform_hash)
+ @values = {}
+ platform_hash.each { |platforms, value| set(platforms, value)}
+ end
+
+ def value_for_node(node)
+ platform, version = node[:platform].to_s, node[:platform_version].to_s
+ if @values.key?(platform) && @values[platform].key?(version)
+ @values[platform][version]
+ elsif @values.key?(platform) && @values[platform].key?("default")
+ @values[platform]["default"]
+ elsif @values.key?("default")
+ @values["default"]
+ else
+ nil
+ end
+ end
+
+ private
+
+ def set(platforms, value)
+ if platforms.to_s == 'default'
+ @values["default"] = value
+ else
+ assert_valid_platform_values!(platforms, value)
+ Array(platforms).each { |platform| @values[platform.to_s] = normalize_keys(value)}
+ value
+ end
+ end
+
+ def normalize_keys(hash)
+ hash.inject({}) do |h, key_value|
+ keys, value = *key_value
+ Array(keys).each do |key|
+ h[key.to_s] = value
+ end
+ h
+ end
+ end
+
+ def assert_valid_platform_values!(platforms, value)
+ unless value.kind_of?(Hash)
+ msg = "platform dependent values must be specified in the format :platform => {:version => value} "
+ msg << "you gave a value #{value.inspect} for platform(s) #{platforms}"
+ raise ArgumentError, msg
+ end
+ end
+ end
+
+
+
+ # Given a hash similar to the one we use for Platforms, select a value from the hash. Supports
+ # per platform defaults, along with a single base default. Arrays may be passed as hash keys and
+ # will be expanded.
+ #
+ # === Parameters
+ # platform_hash:: A platform-style hash.
+ #
+ # === Returns
+ # value:: Whatever the most specific value of the hash is.
+ def value_for_platform(platform_hash)
+ PlatformDependentValue.new(platform_hash).value_for_node(node)
+ end
+
+ # Given a list of platforms, returns true if the current recipe is being run on a node with
+ # that platform, false otherwise.
+ #
+ # === Parameters
+ # args:: A list of platforms. Each platform can be in string or symbol format.
+ #
+ # === Returns
+ # true:: If the current platform is in the list
+ # false:: If the current platform is not in the list
+ def platform?(*args)
+ has_platform = false
+
+ args.flatten.each do |platform|
+ has_platform = true if platform.to_s == node[:platform]
+ end
+
+ has_platform
+ end
+
+
+
+ # Implementation class for determining platform family dependent values
+ class PlatformFamilyDependentValue
+
+ # Create a platform family dependent value object.
+ # === Arguments
+ # platform_family_hash (Hash) a map of platform families to values.
+ # like this:
+ # {
+ # :rhel => "value for all EL variants"
+ # :fedora => "value for fedora variants fedora and amazon" ,
+ # [:fedora, :rhel] => "value for all known redhat variants"
+ # :debian => "value for debian variants including debian, ubuntu, mint" ,
+ # :default => "the default when nothing else matches"
+ # }
+ # * platform families can be specified as Symbols or Strings
+ # * multiple platform families can be grouped by using an Array as the key
+ # * values for platform families can be any object, with no restrictions. Some examples:
+ # - [:stop, :start]
+ # - "mysql-devel"
+ # - { :key => "value" }
+ def initialize(platform_family_hash)
+ @values = {}
+ @values["default"] = nil
+ platform_family_hash.each { |platform_families, value| set(platform_families, value)}
+ end
+
+ def value_for_node(node)
+ if node.key?(:platform_family)
+ platform_family = node[:platform_family].to_s
+ if @values.key?(platform_family)
+ @values[platform_family]
+ else
+ @values["default"]
+ end
+ else
+ @values["default"]
+ end
+ end
+
+ private
+
+ def set(platform_family, value)
+ if platform_family.to_s == 'default'
+ @values["default"] = value
+ else
+ Array(platform_family).each { |family| @values[family.to_s] = value }
+ value
+ end
+ end
+ end
+
+
+ # Given a hash mapping platform families to values, select a value from the hash. Supports a single
+ # base default if platform family is not in the map. Arrays may be passed as hash keys and will be
+ # expanded
+ #
+ # === Parameters
+ # platform_family_hash:: A hash in the form { platform_family_name => value }
+ #
+ # === Returns
+ # value:: Whatever the most specific value of the hash is.
+ def value_for_platform_family(platform_family_hash)
+ PlatformFamilyDependentValue.new(platform_family_hash).value_for_node(node)
+ end
+
+ # Given a list of platform families, returns true if the current recipe is being run on a
+ # node within that platform family, false otherwise.
+ #
+ # === Parameters
+ # args:: A list of platform families. Each platform family can be in string or symbol format.
+ #
+ # === Returns
+ # true:: if the current node platform family is in the list.
+ # false:: if the current node platform family is not in the list.
+ def platform_family?(*args)
+ has_pf = false
+ args.flatten.each do |platform_family|
+ has_pf = true if platform_family.to_s == node[:platform_family]
+ end
+ has_pf
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb
new file mode 100644
index 0000000000..1bfe8da0e0
--- /dev/null
+++ b/lib/chef/dsl/recipe.rb
@@ -0,0 +1,84 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+require 'chef/resource_platform_map'
+require 'chef/mixin/convert_to_class_name'
+require 'chef/mixin/language'
+
+class Chef
+ module DSL
+
+ # == Chef::DSL::Recipe
+ # Provides the primary recipe DSL functionality for defining Chef resource
+ # objects via method calls.
+ module Recipe
+
+ include Chef::Mixin::ConvertToClassName
+
+ def method_missing(method_symbol, *args, &block)
+ # If we have a definition that matches, we want to use that instead. This should
+ # let you do some really crazy over-riding of "native" types, if you really want
+ # to.
+ if run_context.definitions.has_key?(method_symbol)
+ # This dupes the high level object, but we still need to dup the params
+ new_def = run_context.definitions[method_symbol].dup
+ new_def.params = new_def.params.dup
+ new_def.node = run_context.node
+ # This sets up the parameter overrides
+ new_def.instance_eval(&block) if block
+ new_recipe = Chef::Recipe.new(cookbook_name, @recipe_name, run_context)
+ new_recipe.params = new_def.params
+ new_recipe.params[:name] = args[0]
+ new_recipe.instance_eval(&new_def.recipe)
+ else
+ # Otherwise, we're rocking the regular resource call route.
+
+ # Checks the new platform => short_name => resource mapping initially
+ # then fall back to the older approach (Chef::Resource.const_get) for
+ # backward compatibility
+ resource_class = Chef::Resource.resource_for_node(method_symbol, run_context.node)
+
+ super unless resource_class
+ raise ArgumentError, "You must supply a name when declaring a #{method_symbol} resource" unless args.size > 0
+
+ # If we have a resource like this one, we want to steal its state
+ args << run_context
+ resource = resource_class.new(*args)
+ resource.load_prior_resource
+ resource.cookbook_name = cookbook_name
+ resource.recipe_name = @recipe_name
+ resource.params = @params
+ resource.source_line = caller[0]
+ # Determine whether this resource is being created in the context of an enclosing Provider
+ resource.enclosing_provider = self.is_a?(Chef::Provider) ? self : nil
+ # Evaluate resource attribute DSL
+ resource.instance_eval(&block) if block
+
+ # Run optional resource hook
+ resource.after_created
+
+ run_context.resource_collection.insert(resource)
+ resource
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/encrypted_data_bag_item.rb b/lib/chef/encrypted_data_bag_item.rb
new file mode 100644
index 0000000000..048ab8d57e
--- /dev/null
+++ b/lib/chef/encrypted_data_bag_item.rb
@@ -0,0 +1,139 @@
+#
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'base64'
+require 'openssl'
+require 'chef/data_bag_item'
+require 'yaml'
+require 'open-uri'
+
+# An EncryptedDataBagItem represents a read-only data bag item where
+# all values, except for the value associated with the id key, have
+# been encrypted.
+#
+# EncrypedDataBagItem can be used in recipes to decrypt data bag item
+# members.
+#
+# Data bag item values are assumed to have been encrypted using the
+# default symmetric encryption provided by Encryptor.encrypt where
+# values are converted to YAML prior to encryption.
+#
+# If the shared secret is not specified at initialization or load,
+# then the contents of the file referred to in
+# Chef::Config[:encrypted_data_bag_secret] will be used as the
+# secret. The default path is /etc/chef/encrypted_data_bag_secret
+#
+# EncryptedDataBagItem is intended to provide a means to avoid storing
+# data bag items in the clear on the Chef server. This provides some
+# protection against a breach of the Chef server or of Chef server
+# backup data. Because the secret must be stored in the clear on any
+# node needing access to an EncryptedDataBagItem, this approach
+# provides no protection of data bag items from actors with access to
+# such nodes in the infrastructure.
+#
+class Chef::EncryptedDataBagItem
+ DEFAULT_SECRET_FILE = "/etc/chef/encrypted_data_bag_secret"
+ ALGORITHM = 'aes-256-cbc'
+
+ def initialize(enc_hash, secret)
+ @enc_hash = enc_hash
+ @secret = secret
+ end
+
+ def [](key)
+ value = @enc_hash[key]
+ if key == "id" || value.nil?
+ value
+ else
+ self.class.decrypt_value(value, @secret)
+ end
+ end
+
+ def []=(key, value)
+ raise ArgumentError, "assignment not supported for #{self.class}"
+ end
+
+ def to_hash
+ @enc_hash.keys.inject({}) { |hash, key| hash[key] = self[key]; hash }
+ end
+
+ def self.from_plain_hash(plain_hash, secret)
+ self.new(self.encrypt_data_bag_item(plain_hash, secret), secret)
+ end
+
+ def self.encrypt_data_bag_item(plain_hash, secret)
+ plain_hash.inject({}) do |h, (key, val)|
+ h[key] = if key != "id"
+ self.encrypt_value(val, secret)
+ else
+ val
+ end
+ h
+ end
+ end
+
+ def self.load(data_bag, name, secret = nil)
+ path = "data/#{data_bag}/#{name}"
+ raw_hash = Chef::DataBagItem.load(data_bag, name)
+ secret = secret || self.load_secret
+ self.new(raw_hash, secret)
+ end
+
+ def self.encrypt_value(value, key)
+ Base64.encode64(self.cipher(:encrypt, value.to_yaml, key))
+ end
+
+ def self.decrypt_value(value, key)
+ YAML.load(self.cipher(:decrypt, Base64.decode64(value), key))
+ end
+
+ def self.load_secret(path=nil)
+ path = path || Chef::Config[:encrypted_data_bag_secret] || DEFAULT_SECRET_FILE
+ secret = case path
+ when /^\w+:\/\//
+ # We have a remote key
+ begin
+ Kernel.open(path).read.strip
+ rescue Errno::ECONNREFUSED
+ raise ArgumentError, "Remote key not available from '#{path}'"
+ rescue OpenURI::HTTPError
+ raise ArgumentError, "Remote key not found at '#{path}'"
+ end
+ else
+ if !File.exists?(path)
+ raise Errno::ENOENT, "file not found '#{path}'"
+ end
+ IO.read(path).strip
+ end
+ if secret.size < 1
+ raise ArgumentError, "invalid zero length secret in '#{path}'"
+ end
+ secret
+ end
+
+ protected
+
+ def self.cipher(direction, data, key)
+ cipher = OpenSSL::Cipher::Cipher.new(ALGORITHM)
+ cipher.send(direction)
+ cipher.pkcs5_keyivgen(key)
+ ans = cipher.update(data)
+ ans << cipher.final
+ ans
+ end
+end
diff --git a/lib/chef/environment.rb b/lib/chef/environment.rb
new file mode 100644
index 0000000000..00cc253083
--- /dev/null
+++ b/lib/chef/environment.rb
@@ -0,0 +1,287 @@
+#
+# Author:: Stephen Delano (<stephen@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Author:: John Keiser (<jkeiser@ospcode.com>)
+# Copyright:: Copyright 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'chef/mash'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/from_file'
+require 'chef/version_constraint'
+
+class Chef
+ class Environment
+
+ DEFAULT = "default"
+
+ include Chef::Mixin::ParamsValidate
+ include Chef::Mixin::FromFile
+
+ COMBINED_COOKBOOK_CONSTRAINT = /(.+)(?:[\s]+)((?:#{Chef::VersionConstraint::OPS.join('|')})(?:[\s]+).+)$/.freeze
+
+ def initialize
+ @name = ''
+ @description = ''
+ @default_attributes = Mash.new
+ @override_attributes = Mash.new
+ @cookbook_versions = Hash.new
+ end
+
+ def chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def self.chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def name(arg=nil)
+ set_or_return(
+ :name,
+ arg,
+ { :regex => /^[\-[:alnum:]_]+$/, :kind_of => String }
+ )
+ end
+
+ def description(arg=nil)
+ set_or_return(
+ :description,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def default_attributes(arg=nil)
+ set_or_return(
+ :default_attributes,
+ arg,
+ :kind_of => Hash
+ )
+ end
+
+ def default_attributes=(attrs)
+ default_attributes(attrs)
+ end
+
+ def override_attributes(arg=nil)
+ set_or_return(
+ :override_attributes,
+ arg,
+ :kind_of => Hash
+ )
+ end
+
+ def override_attributes=(attrs)
+ override_attributes(attrs)
+ end
+
+ def cookbook_versions(arg=nil)
+ set_or_return(
+ :cookbook_versions,
+ arg,
+ {
+ :kind_of => Hash,
+ :callbacks => {
+ "should be a valid set of cookbook version requirements" => lambda { |cv| Chef::Environment.validate_cookbook_versions(cv) }
+ }
+ }
+ )
+ end
+
+ def cookbook(cookbook, version)
+ validate({
+ :version => version
+ },{
+ :version => {
+ :callbacks => { "should be a valid version requirement" => lambda { |v| Chef::Environment.validate_cookbook_version(v) } }
+ }
+ })
+ @cookbook_versions[cookbook] = version
+ end
+
+ def to_hash
+ result = {
+ "name" => @name,
+ "description" => @description,
+ "cookbook_versions" => @cookbook_versions,
+ "json_class" => self.class.name,
+ "chef_type" => "environment",
+ "default_attributes" => @default_attributes,
+ "override_attributes" => @override_attributes
+ }
+ result
+ end
+
+ def to_json(*a)
+ to_hash.to_json(*a)
+ end
+
+ def update_from!(o)
+ description(o.description)
+ cookbook_versions(o.cookbook_versions)
+ default_attributes(o.default_attributes)
+ override_attributes(o.override_attributes)
+ self
+ end
+
+
+ def update_attributes_from_params(params)
+ unless params[:default_attributes].nil? || params[:default_attributes].size == 0
+ default_attributes(Chef::JSONCompat.from_json(params[:default_attributes]))
+ end
+ unless params[:override_attributes].nil? || params[:override_attributes].size == 0
+ override_attributes(Chef::JSONCompat.from_json(params[:override_attributes]))
+ end
+ end
+
+ def update_from_params(params)
+ # reset because everything we need will be in the params, this is necessary because certain constraints
+ # may have been removed in the params and need to be removed from cookbook_versions as well.
+ bkup_cb_versions = cookbook_versions
+ cookbook_versions(Hash.new)
+ valid = true
+
+ begin
+ name(params[:name])
+ rescue Chef::Exceptions::ValidationFailed => e
+ invalid_fields[:name] = e.message
+ valid = false
+ end
+ description(params[:description])
+
+ unless params[:cookbook_version].nil?
+ params[:cookbook_version].each do |index, cookbook_constraint_spec|
+ unless (cookbook_constraint_spec.nil? || cookbook_constraint_spec.size == 0)
+ valid = valid && update_cookbook_constraint_from_param(index, cookbook_constraint_spec)
+ end
+ end
+ end
+
+ update_attributes_from_params(params)
+
+ valid = validate_required_attrs_present && valid
+ cookbook_versions(bkup_cb_versions) unless valid # restore the old cookbook_versions if valid is false
+ valid
+ end
+
+ def update_cookbook_constraint_from_param(index, cookbook_constraint_spec)
+ valid = true
+ md = cookbook_constraint_spec.match(COMBINED_COOKBOOK_CONSTRAINT)
+ if md.nil? || md[2].nil?
+ valid = false
+ add_cookbook_constraint_error(index, cookbook_constraint_spec)
+ elsif self.class.validate_cookbook_version(md[2])
+ cookbook_versions[md[1]] = md[2]
+ else
+ valid = false
+ add_cookbook_constraint_error(index, cookbook_constraint_spec)
+ end
+ valid
+ end
+
+ def add_cookbook_constraint_error(index, cookbook_constraint_spec)
+ invalid_fields[:cookbook_version] ||= {}
+ invalid_fields[:cookbook_version][index] = "#{cookbook_constraint_spec} is not a valid cookbook constraint"
+ end
+
+ def invalid_fields
+ @invalid_fields ||= {}
+ end
+
+ def validate_required_attrs_present
+ if name.nil? || name.size == 0
+ invalid_fields[:name] ||= "name cannot be empty"
+ false
+ else
+ true
+ end
+ end
+
+
+ def self.json_create(o)
+ environment = new
+ environment.name(o["name"])
+ environment.description(o["description"])
+ environment.cookbook_versions(o["cookbook_versions"])
+ environment.default_attributes(o["default_attributes"])
+ environment.override_attributes(o["override_attributes"])
+ environment
+ end
+
+ def self.list(inflate=false)
+ if inflate
+ response = Hash.new
+ Chef::Search::Query.new.search(:environment) do |e|
+ response[e.name] = e unless e.nil?
+ end
+ response
+ else
+ chef_server_rest.get_rest("environments")
+ end
+ end
+
+ def self.load(name)
+ chef_server_rest.get_rest("environments/#{name}")
+ end
+
+ def destroy
+ chef_server_rest.delete_rest("environments/#{@name}")
+ end
+
+ def save
+ begin
+ chef_server_rest.put_rest("environments/#{@name}", self)
+ rescue Net::HTTPServerException => e
+ raise e unless e.response.code == "404"
+ chef_server_rest.post_rest("environments", self)
+ end
+ self
+ end
+
+ def create
+ chef_server_rest.post_rest("environments", self)
+ self
+ end
+
+ def self.load_filtered_recipe_list(environment)
+ chef_server_rest.get_rest("environments/#{environment}/recipes")
+ end
+
+ def to_s
+ @name
+ end
+
+ def self.validate_cookbook_versions(cv)
+ return false unless cv.kind_of?(Hash)
+ cv.each do |cookbook, version|
+ return false unless Chef::Environment.validate_cookbook_version(version)
+ end
+ true
+ end
+
+ def self.validate_cookbook_version(version)
+ begin
+ Chef::VersionConstraint.new version
+ true
+ rescue ArgumentError
+ false
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb
new file mode 100644
index 0000000000..232bf7f1b4
--- /dev/null
+++ b/lib/chef/event_dispatch/base.rb
@@ -0,0 +1,311 @@
+class Chef
+
+ # ==EventDispatch
+ # Classes in EventDispatch deal with collecting, distributing, and handling
+ # information in response to events that occur during a chef-client run.
+ #
+ # EventDispatch uses a simple publishing system where data from all events
+ # are forwarded to all subscribers unconditionally.
+ #
+ # EventDispatch is used to implement custom console output formatters so that
+ # users may have more control over the formatting and verbosity of Chef
+ # client output and client-side data collection for server-side client
+ # history storage and reporting.
+ #
+ # === API Stability Status
+ # The EventDispatch API is intended to become a stable, public API upon which
+ # end-users can implement their own custom output formatters, reporting
+ # integration libraries, and more. This is a new feature, however, so
+ # breaking changes may be required as it "bakes" in order to provide a clean,
+ # coherent and supportable API in the long term. Therefore, developers should
+ # consider the feature "beta" for now and be prepared for possible breaking
+ # changes in point releases.
+ module EventDispatch
+
+ # == EventDispatch::Base
+ # EventDispatch::Base is a completely abstract base class that defines the
+ # API used by both the classes that collect event information and those
+ # that process them.
+ class Base
+
+ # Called at the very start of a Chef Run
+ def run_start(version)
+ end
+
+ # Called at the end a successful Chef run.
+ def run_completed(node)
+ end
+
+ # Called at the end of a failed Chef run.
+ def run_failed(exception)
+ end
+
+ # Called right after ohai runs.
+ def ohai_completed(node)
+ end
+
+ # Already have a client key, assuming this node has registered.
+ def skipping_registration(node_name, config)
+ end
+
+ # About to attempt to register as +node_name+
+ def registration_start(node_name, config)
+ end
+
+ def registration_completed
+ end
+
+ # Failed to register this client with the server.
+ def registration_failed(node_name, exception, config)
+ end
+
+ # Called before Chef client loads the node data from the server
+ def node_load_start(node_name, config)
+ end
+
+ # TODO: def node_run_list_overridden(*args)
+
+ # Failed to load node data from the server
+ def node_load_failed(node_name, exception, config)
+ end
+
+ # Error expanding the run list
+ def run_list_expand_failed(node, exception)
+ end
+
+ # Called after Chef client has loaded the node data.
+ # Default and override attrs from roles have been computed, but not yet applied.
+ # Normal attrs from JSON have been added to the node.
+ def node_load_completed(node, expanded_run_list, config)
+ end
+
+ # Called before the cookbook collection is fetched from the server.
+ def cookbook_resolution_start(expanded_run_list)
+ end
+
+ # Called when there is an error getting the cookbook collection from the
+ # server.
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ end
+
+ # Called when the cookbook collection is returned from the server.
+ def cookbook_resolution_complete(cookbook_collection)
+ end
+
+ # Called before unneeded cookbooks are removed
+ def cookbook_clean_start
+ end
+
+ # Called after the file at +path+ is removed. It may be removed if the
+ # cookbook containing it was removed from the run list, or if the file was
+ # removed from the cookbook.
+ def removed_cookbook_file(path)
+ end
+
+ # Called when cookbook cleaning is finished.
+ def cookbook_clean_complete
+ end
+
+ # Called before cookbook sync starts
+ def cookbook_sync_start(cookbook_count)
+ end
+
+ # Called when cookbook +cookbook_name+ has been sync'd
+ def synchronized_cookbook(cookbook_name)
+ end
+
+ # Called when an individual file in a cookbook has been updated
+ def updated_cookbook_file(cookbook_name, path)
+ end
+
+ # Called when an error occurs during cookbook sync
+ def cookbook_sync_failed(cookbooks, exception)
+ end
+
+ # Called after all cookbooks have been sync'd.
+ def cookbook_sync_complete
+ end
+
+ ## TODO: add cookbook name to the API for file load callbacks
+
+ ## TODO: add callbacks for overall cookbook eval start and complete.
+
+ # Called when library file loading starts
+ def library_load_start(file_count)
+ end
+
+ # Called when library file has been loaded
+ def library_file_loaded(path)
+ end
+
+ # Called when a library file has an error on load.
+ def library_file_load_failed(path, exception)
+ end
+
+ # Called when library file loading has finished
+ def library_load_complete
+ end
+
+ # Called when LWRP loading starts
+ def lwrp_load_start(lwrp_file_count)
+ end
+
+ # Called after a LWR or LWP has been loaded
+ def lwrp_file_loaded(path)
+ end
+
+ # Called after a LWR or LWP file errors on load
+ def lwrp_file_load_failed(path, exception)
+ end
+
+ # Called when LWRPs are finished loading
+ def lwrp_load_complete
+ end
+
+ # Called before attribute files are loaded
+ def attribute_load_start(attribute_file_count)
+ end
+
+ # Called after the attribute file is loaded
+ def attribute_file_loaded(path)
+ end
+
+ # Called when an attribute file fails to load.
+ def attribute_file_load_failed(path, exception)
+ end
+
+ # Called when attribute file loading is finished
+ def attribute_load_complete
+ end
+
+ # Called before resource definitions are loaded
+ def definition_load_start(definition_file_count)
+ end
+
+ # Called when a resource definition has been loaded
+ def definition_file_loaded(path)
+ end
+
+ # Called when a resource definition file fails to load
+ def definition_file_load_failed(path, exception)
+ end
+
+ # Called when resource defintions are done loading
+ def definition_load_complete
+ end
+
+ # Called before recipes are loaded
+ def recipe_load_start(recipe_count)
+ end
+
+ # Called after the recipe has been loaded
+ def recipe_file_loaded(path)
+ end
+
+ # Called after a recipe file fails to load
+ def recipe_file_load_failed(path, exception)
+ end
+
+ # Called when a recipe cannot be resolved
+ def recipe_not_found(exception)
+ end
+
+ # Called when recipes have been loaded.
+ def recipe_load_complete
+ end
+
+ # Called before convergence starts
+ def converge_start(run_context)
+ end
+
+ # Called when the converge phase is finished.
+ def converge_complete
+ end
+
+ # TODO: need events for notification resolve?
+ # def notifications_resolved
+ # end
+
+ # Called before action is executed on a resource.
+ def resource_action_start(resource, action, notification_type=nil, notifier=nil)
+ end
+
+ # Called when a resource fails, but will retry.
+ def resource_failed_retriable(resource, action, retry_count, exception)
+ end
+
+ # Called when a resource fails and will not be retried.
+ def resource_failed(resource, action, exception)
+ end
+
+ # Called when a resource action has been skipped b/c of a conditional
+ def resource_skipped(resource, action, conditional)
+ end
+
+ # Called when a resource action has been completed
+ def resource_completed(resource)
+ end
+
+ # Called after #load_current_resource has run.
+ def resource_current_state_loaded(resource, action, current_resource)
+ end
+
+ # Called when resource current state load is skipped due to the provider
+ # not supporting whyrun mode.
+ def resource_current_state_load_bypassed(resource, action, current_resource)
+ end
+
+ # Called when evaluating a resource that does not support whyrun in whyrun mode
+ def resource_bypassed(resource, action, current_resource)
+ end
+
+ # Called when a resource has no converge actions, e.g., it was already correct.
+ def resource_up_to_date(resource, action)
+ end
+
+ # Called when a change has been made to a resource. May be called multiple
+ # times per resource, e.g., a file may have its content updated, and then
+ # its permissions updated.
+ def resource_update_applied(resource, action, update)
+ end
+
+ # Called after a resource has been completely converged, but only if
+ # modifications were made.
+ def resource_updated(resource, action)
+ end
+
+ # Called before handlers run
+ def handlers_start(handler_count)
+ end
+
+ # Called after an individual handler has run
+ def handler_executed(handler)
+ end
+
+ # Called after all handlers have executed
+ def handlers_completed
+ end
+
+ # Called when an assertion declared by a provider fails
+ def provider_requirement_failed(action, resource, exception, message)
+ end
+
+ # Called when a provider makes an assumption after a failed assertion
+ # in whyrun mode, in order to allow execution to continue
+ def whyrun_assumption(action, resource, message)
+ end
+
+ ## TODO: deprecation warning. this way we can queue them up and present
+ # them all at once.
+
+ # An uncategorized message. This supports the case that a user needs to
+ # pass output that doesn't fit into one of the callbacks above. Note that
+ # there's no semantic information about the content or importance of the
+ # message. That means that if you're using this too often, you should add a
+ # callback for it.
+ def msg(message)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/event_dispatch/dispatcher.rb b/lib/chef/event_dispatch/dispatcher.rb
new file mode 100644
index 0000000000..82da69a60c
--- /dev/null
+++ b/lib/chef/event_dispatch/dispatcher.rb
@@ -0,0 +1,42 @@
+require 'chef/event_dispatch/base'
+
+class Chef
+ module EventDispatch
+
+ # == EventDispatch::Dispatcher
+ # The Dispatcher handles receiving event data from the sources
+ # (Chef::Client, Resources and Providers, etc.) and publishing the data to
+ # the registered subscribers.
+ class Dispatcher < Base
+
+ def initialize(*subscribers)
+ @subscribers = subscribers
+ end
+
+ # Add a new subscriber to the list of registered subscribers
+ def register(subscriber)
+ @subscribers << subscriber
+ end
+
+ ####
+ # All messages are unconditionally forwarded to all subscribers, so just
+ # define the forwarding in one go:
+ #
+
+ # Define a method that will be forwarded to all
+ def self.def_forwarding_method(method_name)
+ class_eval(<<-END_OF_METHOD, __FILE__, __LINE__)
+ def #{method_name}(*args)
+ @subscribers.each {|s| s.#{method_name}(*args)}
+ end
+ END_OF_METHOD
+ end
+
+ (Base.instance_methods - Object.instance_methods).each do |method_name|
+ def_forwarding_method(method_name)
+ end
+
+ end
+ end
+end
+
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
new file mode 100644
index 0000000000..87802639d3
--- /dev/null
+++ b/lib/chef/exceptions.rb
@@ -0,0 +1,260 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright 2008-2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+ # == Chef::Exceptions
+ # Chef's custom exceptions are all contained within the Chef::Exceptions
+ # namespace.
+ class Exceptions
+
+ # Backcompat with Chef::ShellOut code:
+ require 'mixlib/shellout/exceptions'
+
+ def self.const_missing(const_name)
+ if const_name == :ShellCommandFailed
+ Chef::Log.warn("Chef::Exceptions::ShellCommandFailed is deprecated, use Mixlib::ShellOut::ShellCommandFailed")
+ called_from = caller[0..3].inject("Called from:\n") {|msg, trace_line| msg << " #{trace_line}\n" }
+ Chef::Log.warn(called_from)
+ Mixlib::ShellOut::ShellCommandFailed
+ else
+ super
+ end
+ end
+
+ class Application < RuntimeError; end
+ class Cron < RuntimeError; end
+ class Env < RuntimeError; end
+ class Exec < RuntimeError; end
+ class ErlCall < RuntimeError; end
+ class FileNotFound < RuntimeError; end
+ class Package < RuntimeError; end
+ class Service < RuntimeError; end
+ class Route < RuntimeError; end
+ class SearchIndex < RuntimeError; end
+ class Override < RuntimeError; end
+ class UnsupportedAction < RuntimeError; end
+ class MissingLibrary < RuntimeError; end
+ class CannotDetermineNodeName < RuntimeError; end
+ class User < RuntimeError; end
+ class Group < RuntimeError; end
+ class Link < RuntimeError; end
+ class Mount < RuntimeError; end
+ class PrivateKeyMissing < RuntimeError; end
+ class CannotWritePrivateKey < RuntimeError; end
+ class RoleNotFound < RuntimeError; end
+ class ValidationFailed < ArgumentError; end
+ class InvalidPrivateKey < ArgumentError; end
+ class ConfigurationError < ArgumentError; end
+ class RedirectLimitExceeded < RuntimeError; end
+ class AmbiguousRunlistSpecification < ArgumentError; end
+ class CookbookFrozen < ArgumentError; end
+ class CookbookNotFound < RuntimeError; end
+ # Cookbook loader used to raise an argument error when cookbook not found.
+ # for back compat, need to raise an error that inherits from ArgumentError
+ class CookbookNotFoundInRepo < ArgumentError; end
+ class RecipeNotFound < ArgumentError; end
+ class AttributeNotFound < RuntimeError; end
+ class InvalidCommandOption < RuntimeError; end
+ class CommandTimeout < RuntimeError; end
+ class RequestedUIDUnavailable < RuntimeError; end
+ class InvalidHomeDirectory < ArgumentError; end
+ class DsclCommandFailed < RuntimeError; end
+ class UserIDNotFound < ArgumentError; end
+ class GroupIDNotFound < ArgumentError; end
+ class InvalidResourceReference < RuntimeError; end
+ class ResourceNotFound < RuntimeError; end
+ class InvalidResourceSpecification < ArgumentError; end
+ class SolrConnectionError < RuntimeError; end
+ class IllegalChecksumRevert < RuntimeError; end
+ class CookbookVersionNameMismatch < ArgumentError; end
+ class MissingParentDirectory < RuntimeError; end
+ class UnresolvableGitReference < RuntimeError; end
+ class InvalidRemoteGitReference < RuntimeError; end
+ class InvalidEnvironmentRunListSpecification < ArgumentError; end
+ class InvalidDataBagItemID < ArgumentError; end
+ class InvalidDataBagName < ArgumentError; end
+ class EnclosingDirectoryDoesNotExist < ArgumentError; end
+ # Errors originating from calls to the Win32 API
+ class Win32APIError < RuntimeError; end
+ # Thrown when Win32 API layer binds to non-existent Win32 function. Occurs
+ # when older versions of Windows don't support newer Win32 API functions.
+ class Win32APIFunctionNotImplemented < NotImplementedError; end
+
+ class ObsoleteDependencySyntax < ArgumentError; end
+ class InvalidDataBagPath < ArgumentError; end
+
+ # A different version of a cookbook was added to a
+ # VersionedRecipeList than the one already there.
+ class CookbookVersionConflict < ArgumentError ; end
+
+ # does not follow X.Y.Z format. ArgumentError?
+ class InvalidCookbookVersion < ArgumentError; end
+
+ # version constraint should be a string or array, or it doesn't
+ # match OP VERSION. ArgumentError?
+ class InvalidVersionConstraint < ArgumentError; end
+
+ # File operation attempted but no permissions to perform it
+ class InsufficientPermissions < RuntimeError; end
+
+ # Ifconfig failed
+ class Ifconfig < RuntimeError; end
+
+ # Invalid "source" parameter to a remote_file resource
+ class InvalidRemoteFileURI < ArgumentError; end
+
+ # Node::Attribute computes the merged version of of attributes
+ # and makes it read-only. Attempting to modify a read-only
+ # attribute will cause this error.
+ class ImmutableAttributeModification < NoMethodError; end
+
+ # Merged node attributes are invalidated when the component
+ # attributes are updated. Attempting to read from a stale copy
+ # of merged attributes will trigger this error.
+ class StaleAttributeRead < StandardError; end
+
+ class MissingRole < RuntimeError
+ NULL = Object.new
+
+ attr_reader :expansion
+
+ def initialize(message_or_expansion=NULL)
+ @expansion = nil
+ case message_or_expansion
+ when NULL
+ super()
+ when String
+ super
+ when RunList::RunListExpansion
+ @expansion = message_or_expansion
+ missing_roles = @expansion.errors.join(', ')
+ super("The expanded run list includes nonexistent roles: #{missing_roles}")
+ end
+ end
+
+
+ end
+ # Exception class for collecting multiple failures. Used when running
+ # delayed notifications so that chef can process each delayed
+ # notification even if chef client or other notifications fail.
+ class MultipleFailures < StandardError
+ def initialize(*args)
+ super
+ @all_failures = []
+ end
+
+ def message
+ base = "Multiple failures occurred:\n"
+ @all_failures.inject(base) do |message, (location, error)|
+ message << "* #{error.class} occurred in #{location}: #{error.message}\n"
+ end
+ end
+
+ def client_run_failure(exception)
+ set_backtrace(exception.backtrace)
+ @all_failures << [ "chef run", exception ]
+ end
+
+ def notification_failure(exception)
+ @all_failures << [ "delayed notification", exception ]
+ end
+
+ def raise!
+ unless empty?
+ raise self.for_raise
+ end
+ end
+
+ def empty?
+ @all_failures.empty?
+ end
+
+ def for_raise
+ if @all_failures.size == 1
+ @all_failures[0][1]
+ else
+ self
+ end
+ end
+ end
+
+ class CookbookVersionSelection
+
+ # Compound exception: In run_list expansion and resolution,
+ # run_list items referred to cookbooks that don't exist and/or
+ # have no versions available.
+ class InvalidRunListItems < StandardError
+ attr_reader :non_existent_cookbooks
+ attr_reader :cookbooks_with_no_matching_versions
+
+ def initialize(message, non_existent_cookbooks, cookbooks_with_no_matching_versions)
+ super(message)
+
+ @non_existent_cookbooks = non_existent_cookbooks
+ @cookbooks_with_no_matching_versions = cookbooks_with_no_matching_versions
+ end
+
+ def to_json(*a)
+ result = {
+ "message" => message,
+ "non_existent_cookbooks" => non_existent_cookbooks,
+ "cookbooks_with_no_versions" => cookbooks_with_no_matching_versions
+ }
+ result.to_json(*a)
+ end
+ end
+
+ # In run_list expansion and resolution, a constraint was
+ # unsatisfiable.
+ #
+ # This exception may not be the complete error report. If you
+ # resolve the misconfiguration represented by this exception and
+ # re-solve, you may get another exception
+ class UnsatisfiableRunListItem < StandardError
+ attr_reader :run_list_item
+ attr_reader :non_existent_cookbooks, :most_constrained_cookbooks
+
+ # most_constrained_cookbooks: if I were to remove constraints
+ # regarding these cookbooks, I would get a solution or move on
+ # to the next error (deeper in the graph). An item in this list
+ # may be unsatisfiable, but when resolved may also reveal
+ # further unsatisfiable constraints; this condition would not be
+ # reported.
+ def initialize(message, run_list_item, non_existent_cookbooks, most_constrained_cookbooks)
+ super(message)
+
+ @run_list_item = run_list_item
+ @non_existent_cookbooks = non_existent_cookbooks
+ @most_constrained_cookbooks = most_constrained_cookbooks
+ end
+
+ def to_json(*a)
+ result = {
+ "message" => message,
+ "unsatisfiable_run_list_item" => run_list_item,
+ "non_existent_cookbooks" => non_existent_cookbooks,
+ "most_constrained_cookbooks" => most_constrained_cookbooks
+ }
+ result.to_json(*a)
+ end
+ end
+
+ end # CookbookVersionSelection
+
+ end
+end
diff --git a/lib/chef/file_access_control.rb b/lib/chef/file_access_control.rb
new file mode 100644
index 0000000000..cc7fa8fc1a
--- /dev/null
+++ b/lib/chef/file_access_control.rb
@@ -0,0 +1,75 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/log'
+
+class Chef
+
+ # == Chef::FileAccessControl
+ # FileAccessControl objects set the owner, group and mode of +file+ to
+ # the values specified by a value object, usually a Chef::Resource.
+ class FileAccessControl
+
+ if RUBY_PLATFORM =~ /mswin|mingw|windows/
+ require 'chef/file_access_control/windows'
+ include FileAccessControl::Windows
+ else
+ require 'chef/file_access_control/unix'
+ include FileAccessControl::Unix
+ end
+
+ attr_reader :current_resource
+ attr_reader :resource
+ attr_reader :provider
+ attr_reader :file
+
+ # FileAccessControl objects set the owner, group and mode of +file+ to
+ # the values specified by +resource+. +file+ is completely independent
+ # of any file or path attribute on +resource+, so it is possible to set
+ # access control settings on a tempfile (for example).
+ # === Arguments:
+ # resource: probably a Chef::Resource::File object (or subclass), but
+ # this is not required. Must respond to +owner+, +group+,
+ # and +mode+
+ # file: The file whose access control settings you wish to modify,
+ # given as a String.
+ #
+ # TODO requiring current_resource will break cookbook_file template_file
+ def initialize(current_resource, new_resource, provider)
+ @current_resource, @resource, @provider = current_resource, new_resource, provider
+ @file = @current_resource.path
+ @modified = false
+ end
+
+ def modified?
+ @modified
+ end
+
+ private
+
+ def modified
+ @modified = true
+ end
+
+ def log_string
+ @resource || @file
+ end
+
+ end
+end
diff --git a/lib/chef/file_access_control/unix.rb b/lib/chef/file_access_control/unix.rb
new file mode 100644
index 0000000000..1dbfe40f2f
--- /dev/null
+++ b/lib/chef/file_access_control/unix.rb
@@ -0,0 +1,216 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright (c) 2008-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/log'
+
+class Chef
+ class FileAccessControl
+ module Unix
+ UINT = (1 << 32)
+ UID_MAX = (1 << 32) - 10
+
+ def set_all!
+ set_owner!
+ set_group!
+ set_mode!
+ end
+
+ def set_all
+ set_owner
+ set_group
+ set_mode
+ end
+
+ # TODO factor this up
+ def requires_changes?
+ should_update_mode? || should_update_owner? || should_update_group?
+ end
+
+ def describe_changes
+ changes = []
+ changes << "change mode from '#{mode_to_s(current_mode)}' to '#{mode_to_s(target_mode)}'" if should_update_mode?
+ changes << "change owner from '#{current_resource.owner}' to '#{resource.owner}'" if should_update_owner?
+ changes << "change group from '#{current_resource.group}' to '#{resource.group}'" if should_update_group?
+ changes
+ end
+
+ def target_uid
+ uid_from_resource(resource)
+ end
+
+ def current_uid
+ uid_from_resource(current_resource)
+ end
+
+ def should_update_owner?
+ !target_uid.nil? && target_uid != current_uid
+ end
+
+ def set_owner!
+ unless target_uid.nil?
+ chown(target_uid, nil, file)
+ Chef::Log.info("#{log_string} owner changed to #{target_uid}")
+ modified
+ end
+ end
+
+ def set_owner
+ set_owner! if should_update_owner?
+ end
+
+ def target_gid
+ gid_from_resource(resource)
+ end
+
+ def current_gid
+ gid_from_resource(current_resource)
+ end
+
+ def gid_from_resource(resource)
+ return nil if resource == nil or resource.group.nil?
+ if resource.group.kind_of?(String)
+ diminished_radix_complement( Etc.getgrnam(resource.group).gid )
+ elsif resource.group.kind_of?(Integer)
+ resource.group
+ else
+ Chef::Log.error("The `group` parameter of the #@resource resource is set to an invalid value (#{resource.owner.inspect})")
+ raise ArgumentError, "cannot resolve #{resource.group.inspect} to gid, group must be a string or integer"
+ end
+ rescue ArgumentError
+ provider.requirements.assert(:create, :create_if_missing, :touch) do |a|
+ a.assertion { false }
+ a.failure_message(Chef::Exceptions::GroupIDNotFound, "cannot determine group id for '#{resource.group}', does the group exist on this system?")
+ a.whyrun("Assuming group #{resource.group} would have been created")
+ end
+ return nil
+ end
+
+ def should_update_group?
+ !target_gid.nil? && target_gid != current_gid
+ end
+
+ def set_group!
+ unless target_gid.nil?
+ chown(nil, target_gid, file)
+ Chef::Log.info("#{log_string} group changed to #{target_gid}")
+ modified
+ end
+ end
+
+ def set_group
+ set_group! if should_update_group?
+ end
+
+ def mode_from_resource(res)
+ return nil if res == nil or res.mode.nil?
+ (res.mode.respond_to?(:oct) ? res.mode.oct : res.mode.to_i) & 007777
+ end
+
+ def target_mode
+ mode_from_resource(resource)
+ end
+
+ def mode_to_s(mode)
+ mode.nil? ? "" : "0#{mode.to_s(8)}"
+ end
+
+ def current_mode
+ mode_from_resource(current_resource)
+ end
+
+ def should_update_mode?
+ !target_mode.nil? && current_mode != target_mode
+ end
+
+ def set_mode!
+ unless target_mode.nil?
+ chmod(target_mode, file)
+ Chef::Log.info("#{log_string} mode changed to #{target_mode.to_s(8)}")
+ modified
+ end
+ end
+
+ def set_mode
+ set_mode! if should_update_mode?
+ end
+
+ def stat
+ if File.symlink?(file)
+ @stat ||= File.lstat(file)
+ else
+ @stat ||= File.stat(file)
+ end
+ end
+
+ private
+
+ def chmod(mode, file)
+ if File.symlink?(file)
+ begin
+ File.lchmod(mode, file)
+ rescue NotImplementedError
+ Chef::Log.warn("#{file} mode not changed: File.lchmod is unimplemented on this OS and Ruby version")
+ end
+ else
+ File.chmod(mode, file)
+ end
+ end
+
+ def chown(uid, gid, file)
+ if ::File.symlink?(file)
+ File.lchown(uid, gid, file)
+ else
+ File.chown(uid, gid, file)
+ end
+ end
+
+ # Workaround the fact that Ruby's Etc module doesn't believe in negative
+ # uids, so negative uids show up as the diminished radix complement of
+ # a uint. For example, a uid of -2 is reported as 4294967294
+ def diminished_radix_complement(int)
+ if int > UID_MAX
+ int - UINT
+ else
+ int
+ end
+ end
+
+ def uid_from_resource(resource)
+ return nil if resource == nil or resource.owner.nil?
+ if resource.owner.kind_of?(String)
+ diminished_radix_complement( Etc.getpwnam(resource.owner).uid )
+ elsif resource.owner.kind_of?(Integer)
+ resource.owner
+ else
+ Chef::Log.error("The `owner` parameter of the #@resource resource is set to an invalid value (#{resource.owner.inspect})")
+ raise ArgumentError, "cannot resolve #{resource.owner.inspect} to uid, owner must be a string or integer"
+ end
+ rescue ArgumentError
+ provider.requirements.assert(:create, :create_if_missing, :touch) do |a|
+ a.assertion { false }
+ a.failure_message(Chef::Exceptions::UserIDNotFound, "cannot determine user id for '#{resource.owner}', does the user exist on this system?")
+ a.whyrun("Assuming user #{resource.owner} would have been created")
+ end
+ return nil
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/file_access_control/windows.rb b/lib/chef/file_access_control/windows.rb
new file mode 100644
index 0000000000..c08752c87d
--- /dev/null
+++ b/lib/chef/file_access_control/windows.rb
@@ -0,0 +1,310 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/security'
+
+class Chef
+ class FileAccessControl
+ module Windows
+ include Chef::ReservedNames::Win32::API::Security
+
+ Security = Chef::ReservedNames::Win32::Security
+ ACL = Security::ACL
+ ACE = Security::ACE
+ SID = Security::SID
+
+ def set_all!
+ set_owner!
+ set_group!
+ set_dacl
+ end
+
+ def set_all
+ set_owner
+ set_group
+ set_dacl
+ end
+
+ def requires_changes?
+ should_update_dacl? || should_update_owner? || should_update_group?
+ end
+
+ def describe_changes
+ # FIXME: describe what these are changing from and to
+ changes = []
+ changes << "change dacl" if should_update_dacl?
+ changes << "change owner" if should_update_owner?
+ changes << "change group" if should_update_group?
+ changes
+ end
+
+ private
+
+ # Compare the actual ACL on a resource with the ACL we want. This
+ # ignores explicit ACLs on the target, and does mask prediction (if you
+ # set GENERIC_WRITE, Windows will flip on a whole bunch of other rights
+ # on the file when you save the ACL)
+ def acls_equal(target_acl, actual_acl)
+ if actual_acl.nil?
+ return target_acl.nil?
+ end
+
+ actual_acl = actual_acl.select { |ace| !ace.inherited? }
+ # When ACLs apply to children, Windows splits them on the file system into two ACLs:
+ # one specific applying to this container, and one generic applying to children.
+ new_target_acl = []
+ target_acl.each do |target_ace|
+ if target_ace.flags & INHERIT_ONLY_ACE == 0
+ self_ace = target_ace.dup
+ self_ace.flags = 0
+ self_ace.mask = securable_object.predict_rights_mask(target_ace.mask)
+ new_target_acl << self_ace
+ end
+ if target_ace.flags & (CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE) != 0
+ children_ace = target_ace.dup
+ children_ace.flags |= INHERIT_ONLY_ACE
+ new_target_acl << children_ace
+ end
+ end
+ return actual_acl == new_target_acl
+ end
+
+ def existing_descriptor
+ securable_object.security_descriptor
+ end
+
+ def get_sid(value)
+ if value.kind_of?(String)
+ SID.from_account(value)
+ elsif value.kind_of?(SID)
+ value
+ else
+ raise "Must specify username, group or SID: #{value}"
+ end
+ end
+
+ def securable_object
+ @securable_object ||= begin
+ if file.kind_of?(String)
+ so = Chef::ReservedNames::Win32::Security::SecurableObject.new(file.dup)
+ end
+ raise ArgumentError, "'file' must be a valid path or object of type 'Chef::ReservedNames::Win32::Security::SecurableObject'" unless so.kind_of? Chef::ReservedNames::Win32::Security::SecurableObject
+ so
+ end
+ end
+
+ def should_update_dacl?
+ return true unless ::File.exists?(file)
+ dacl = target_dacl
+ existing_dacl = existing_descriptor.dacl
+ inherits = target_inherits
+ ( ! inherits.nil? && inherits != existing_descriptor.dacl_inherits? ) || ( dacl && !acls_equal(dacl, existing_dacl) )
+ end
+
+ def set_dacl!
+ set_dacl
+ end
+
+ def set_dacl
+ dacl = target_dacl
+ existing_dacl = existing_descriptor.dacl
+ inherits = target_inherits
+ if ! inherits.nil? && inherits != existing_descriptor.dacl_inherits?
+ # We have to set DACL along with inherits. If rights were not
+ # specified, we need to change only inherited ACLs and leave
+ # explicit ACLs alone.
+ if dacl.nil? && !existing_dacl.nil?
+ dacl = ACL.create(existing_dacl.select { |ace| !ace.inherited? })
+ end
+ securable_object.set_dacl(dacl, inherits)
+ Chef::Log.info("#{log_string} permissions changed to #{dacl} with inherits of #{inherits}")
+ modified
+ elsif dacl && !acls_equal(dacl, existing_dacl)
+ securable_object.dacl = dacl
+ Chef::Log.info("#{log_string} permissions changed to #{dacl}")
+ modified
+ end
+ end
+
+ def should_update_group?
+ return true unless ::File.exists?(file)
+ (group = target_group) && (group != existing_descriptor.group)
+ end
+
+ def set_group!
+ if (group = target_group)
+ Chef::Log.info("#{log_string} group changed to #{group}")
+ securable_object.group = group
+ modified
+ end
+ end
+
+ def set_group
+ if (group = target_group) && (group != existing_descriptor.group)
+ set_group!
+ end
+ end
+
+ def should_update_owner?
+ return true unless ::File.exists?(file)
+ (owner = target_owner) && (owner != existing_descriptor.owner)
+ end
+
+ def set_owner!
+ if owner = target_owner
+ Chef::Log.info("#{log_string} owner changed to #{owner}")
+ securable_object.owner = owner
+ modified
+ end
+ end
+
+ def set_owner
+ if (owner = target_owner) && (owner != existing_descriptor.owner)
+ set_owner!
+ end
+ end
+
+ def mode_ace(sid, mode)
+ mask = 0
+ mask |= GENERIC_READ if mode & 4 != 0
+ mask |= (GENERIC_WRITE | DELETE) if mode & 2 != 0
+ mask |= GENERIC_EXECUTE if mode & 1 != 0
+ return [] if mask == 0
+ [ ACE.access_allowed(sid, mask) ]
+ end
+
+ def calculate_mask(permissions)
+ mask = 0
+ [ permissions ].flatten.each do |permission|
+ case permission
+ when :full_control
+ mask |= GENERIC_ALL
+ when :modify
+ mask |= GENERIC_WRITE | GENERIC_READ | GENERIC_EXECUTE | DELETE
+ when :read
+ mask |= GENERIC_READ
+ when :read_execute
+ mask |= GENERIC_READ | GENERIC_EXECUTE
+ when :write
+ mask |= GENERIC_WRITE
+ else
+ # Otherwise, assume it's an integer specifying the actual flags
+ mask |= permission
+ end
+ end
+ mask
+ end
+
+ def calculate_flags(rights)
+ # Handle inheritance flags
+ flags = 0
+ case rights[:applies_to_children]
+ when :containers_only
+ flags |= CONTAINER_INHERIT_ACE
+ when :objects_only
+ flags |= OBJECT_INHERIT_ACE
+ when true
+ flags |= CONTAINER_INHERIT_ACE
+ flags |= OBJECT_INHERIT_ACE
+ when nil
+ flags |= CONTAINER_INHERIT_ACE
+ flags |= OBJECT_INHERIT_ACE
+ end
+
+ if rights[:applies_to_self] == false
+ flags |= INHERIT_ONLY_ACE
+ end
+
+ if rights[:one_level_deep]
+ flags |= NO_PROPAGATE_INHERIT_ACE
+ end
+ flags
+ end
+
+ def target_dacl
+ return nil if resource.rights.nil? && resource.deny_rights.nil? && resource.mode.nil?
+ acls = nil
+
+ if !resource.deny_rights.nil?
+ acls = [] if acls.nil?
+
+ resource.deny_rights.each do |rights|
+ mask = calculate_mask(rights[:permissions])
+ [ rights[:principals] ].flatten.each do |principal|
+ sid = get_sid(principal)
+ flags = calculate_flags(rights)
+ acls.push ACE.access_denied(sid, mask, flags)
+ end
+ end
+ end
+
+ if !resource.rights.nil?
+ acls = [] if acls.nil?
+
+ resource.rights.each do |rights|
+ mask = calculate_mask(rights[:permissions])
+ [ rights[:principals] ].flatten.each do |principal|
+ sid = get_sid(principal)
+ flags = calculate_flags(rights)
+ acls.push ACE.access_allowed(sid, mask, flags)
+ end
+ end
+ end
+
+ if !resource.mode.nil?
+ acls = [] if acls.nil?
+
+ mode = (resource.mode.respond_to?(:oct) ? resource.mode.oct : resource.mode.to_i) & 0777
+
+ owner = target_owner
+ if owner
+ acls += mode_ace(owner, (mode & 0700) >> 6)
+ elsif mode & 0700 != 0
+ Chef::Log.warn("Mode #{sprintf("%03o", mode)} includes bits for the owner, but owner is not specified")
+ end
+
+ group = target_group
+ if group
+ acls += mode_ace(group, (mode & 070) >> 3)
+ elsif mode & 070 != 0
+ Chef::Log.warn("Mode #{sprintf("%03o", mode)} includes bits for the group, but group is not specified")
+ end
+
+ acls += mode_ace(SID.Everyone, (mode & 07))
+ end
+
+ acls.nil? ? nil : Chef::ReservedNames::Win32::Security::ACL.create(acls)
+ end
+
+ def target_group
+ return nil if resource.group.nil?
+ sid = get_sid(resource.group)
+ end
+
+ def target_inherits
+ resource.inherits
+ end
+
+ def target_owner
+ return nil if resource.owner.nil?
+ sid = get_sid(resource.owner)
+ end
+ end
+ end
+end
diff --git a/lib/chef/file_cache.rb b/lib/chef/file_cache.rb
new file mode 100644
index 0000000000..89e934ea05
--- /dev/null
+++ b/lib/chef/file_cache.rb
@@ -0,0 +1,220 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/mixin/params_validate'
+require 'chef/mixin/create_path'
+require 'chef/exceptions'
+require 'chef/json_compat'
+require 'fileutils'
+
+class Chef
+ class FileCache
+ class << self
+ include Chef::Mixin::ParamsValidate
+ include Chef::Mixin::CreatePath
+
+ # Write a file to the File Cache.
+ #
+ # === Parameters
+ # path<String>:: The path to the file you want to put in the cache - should
+ # be relative to file_cache_path
+ # contents<String>:: A string with the contents you want written to the file
+ # perm<String>:: Sets file permission bits. Permission bits are platform
+ # dependent; on Unix systems, see open(2) for details.
+ #
+ # === Returns
+ # true
+ def store(path, contents, perm=0640)
+ validate(
+ {
+ :path => path,
+ :contents => contents
+ },
+ {
+ :path => { :kind_of => String },
+ :contents => { :kind_of => String },
+ }
+ )
+
+ file_path_array = File.split(path)
+ file_name = file_path_array.pop
+ cache_path = create_cache_path(File.join(file_path_array))
+ File.open(File.join(cache_path, file_name), "w", perm) do |io|
+ io.print(contents)
+ end
+ true
+ end
+
+ # Move a file into the cache. Useful with the REST raw file output.
+ #
+ # === Parameters
+ # file<String>:: The path to the file you want in the cache
+ # path<String>:: The relative name you want the new file to use
+ def move_to(file, path)
+ validate(
+ {
+ :file => file,
+ :path => path
+ },
+ {
+ :file => { :kind_of => String },
+ :path => { :kind_of => String },
+ }
+ )
+
+ file_path_array = File.split(path)
+ file_name = file_path_array.pop
+ if File.exists?(file) && File.writable?(file)
+ FileUtils.mv(
+ file,
+ File.join(create_cache_path(File.join(file_path_array), true), file_name)
+ )
+ else
+ raise RuntimeError, "Cannot move #{file} to #{path}!"
+ end
+ end
+
+ # Read a file from the File Cache
+ #
+ # === Parameters
+ # path<String>:: The path to the file you want to load - should
+ # be relative to file_cache_path
+ # read<True/False>:: Whether to return the file contents, or the path.
+ # Defaults to true.
+ #
+ # === Returns
+ # String:: A string with the file contents, or the path to the file.
+ #
+ # === Raises
+ # Chef::Exceptions::FileNotFound:: If it cannot find the file in the cache
+ def load(path, read=true)
+ validate(
+ {
+ :path => path
+ },
+ {
+ :path => { :kind_of => String }
+ }
+ )
+ cache_path = create_cache_path(path, false)
+ raise Chef::Exceptions::FileNotFound, "Cannot find #{cache_path} for #{path}!" unless File.exists?(cache_path)
+ if read
+ File.read(cache_path)
+ else
+ cache_path
+ end
+ end
+
+ # Delete a file from the File Cache
+ #
+ # === Parameters
+ # path<String>:: The path to the file you want to delete - should
+ # be relative to file_cache_path
+ #
+ # === Returns
+ # true
+ def delete(path)
+ validate(
+ {
+ :path => path
+ },
+ {
+ :path => { :kind_of => String },
+ }
+ )
+ cache_path = create_cache_path(path, false)
+ if File.exists?(cache_path)
+ File.unlink(cache_path)
+ end
+ true
+ end
+
+ # List all the files in the Cache
+ #
+ # === Returns
+ # Array:: An array of files in the cache, suitable for use with load, delete and store
+ def list
+ find("**#{File::Separator}*")
+ end
+
+ ##
+ # Find files in the cache by +glob_pattern+
+ # === Returns
+ # [String] - An array of file cache keys matching the glob
+ def find(glob_pattern)
+ keys = Array.new
+ Dir[File.join(file_cache_path, glob_pattern)].each do |f|
+ if File.file?(f)
+ keys << f[/^#{Regexp.escape(Dir[file_cache_path].first) + File::Separator}(.+)/, 1]
+ end
+ end
+ keys
+ end
+
+ # Whether or not this file exists in the Cache
+ #
+ # === Parameters
+ # path:: The path to the file you want to check - is relative
+ # to file_cache_path
+ #
+ # === Returns
+ # True:: If the file exists
+ # False:: If it does not
+ def has_key?(path)
+ validate(
+ {
+ :path => path
+ },
+ {
+ :path => { :kind_of => String },
+ }
+ )
+ full_path = create_cache_path(path, false)
+ if File.exists?(full_path)
+ true
+ else
+ false
+ end
+ end
+
+ # Create a full path to a given file in the cache. By default,
+ # also creates the path if it does not exist.
+ #
+ # === Parameters
+ # path:: The path to create, relative to file_cache_path
+ # create_if_missing:: True by default - whether to create the path if it does not exist
+ #
+ # === Returns
+ # String:: The fully expanded path
+ def create_cache_path(path, create_if_missing=true)
+ cache_dir = File.expand_path(File.join(file_cache_path, path))
+ if create_if_missing
+ create_path(cache_dir)
+ else
+ cache_dir
+ end
+ end
+
+ private
+
+ def file_cache_path
+ Chef::Config[:file_cache_path]
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/formatters/base.rb b/lib/chef/formatters/base.rb
new file mode 100644
index 0000000000..d8b2e49d8e
--- /dev/null
+++ b/lib/chef/formatters/base.rb
@@ -0,0 +1,247 @@
+#
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+#
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/event_dispatch/base'
+require 'chef/formatters/error_inspectors'
+require 'chef/formatters/error_descriptor'
+require 'chef/formatters/error_mapper'
+
+class Chef
+
+ # == Chef::Formatters
+ # Formatters handle printing output about the progress/status of a chef
+ # client run to the user's screen.
+ module Formatters
+
+ class UnknownFormatter < StandardError; end
+
+ def self.formatters_by_name
+ @formatters_by_name ||= {}
+ end
+
+ def self.register(name, formatter)
+ formatters_by_name[name.to_s] = formatter
+ end
+
+ def self.by_name(name)
+ formatters_by_name[name]
+ end
+
+ def self.available_formatters
+ formatters_by_name.keys
+ end
+
+ #--
+ # TODO: is it too clever to be defining new() on a module like this?
+ def self.new(name, out, err)
+ formatter_class = by_name(name) or
+ raise UnknownFormatter, "No output formatter found for #{name} (available: #{available_formatters.join(', ')})"
+
+ formatter_class.new(out, err)
+ end
+
+ # == Outputter
+ # Handles basic printing tasks like colorizing.
+ # --
+ # TODO: Duplicates functionality from knife, upfactor.
+ class Outputter
+
+ def initialize(out, err)
+ @out, @err = out, err
+ end
+
+ def highline
+ @highline ||= begin
+ require 'highline'
+ HighLine.new
+ end
+ end
+
+ def color(string, *colors)
+ if Chef::Config[:color]
+ @out.print highline.color(string, *colors)
+ else
+ @out.print string
+ end
+ end
+
+ alias :print :color
+
+ def puts(string, *colors)
+ if Chef::Config[:color]
+ @out.puts highline.color(string, *colors)
+ else
+ @out.puts string
+ end
+ end
+
+ end
+
+
+ # == Formatters::Base
+ # Base class that all formatters should inherit from.
+ class Base < EventDispatch::Base
+
+ include ErrorMapper
+
+ def self.cli_name(name)
+ Chef::Formatters.register(name, self)
+ end
+
+ attr_reader :out
+ attr_reader :err
+ attr_reader :output
+
+ def initialize(out, err)
+ @output = Outputter.new(out, err)
+ end
+
+ def puts(*args)
+ @output.puts(*args)
+ end
+
+ def print(*args)
+ @output.print(*args)
+ end
+
+ # Input: a Formatters::ErrorDescription object.
+ # Outputs error to SDOUT.
+ def display_error(description)
+ puts("")
+ description.display(output)
+ end
+
+ def registration_failed(node_name, exception, config)
+ #A Formatters::ErrorDescription object
+ description = ErrorMapper.registration_failed(node_name, exception, config)
+ display_error(description)
+ end
+
+ def node_load_failed(node_name, exception, config)
+ description = ErrorMapper.node_load_failed(node_name, exception, config)
+ display_error(description)
+ end
+
+ def run_list_expand_failed(node, exception)
+ description = ErrorMapper.run_list_expand_failed(node, exception)
+ display_error(description)
+ end
+
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ description = ErrorMapper.cookbook_resolution_failed(expanded_run_list, exception)
+ display_error(description)
+ end
+
+ def cookbook_sync_failed(cookbooks, exception)
+ description = ErrorMapper.cookbook_sync_failed(cookbooks, exception)
+ display_error(description)
+ end
+
+ def resource_failed(resource, action, exception)
+ description = ErrorMapper.resource_failed(resource, action, exception)
+ display_error(description)
+ end
+
+ # Generic callback for any attribute/library/lwrp/recipe file in a
+ # cookbook getting loaded. The per-filetype callbacks for file load are
+ # overriden so that they call this instead. This means that a subclass of
+ # Formatters::Base can implement #file_loaded to do the same thing for
+ # every kind of file that Chef loads from a recipe instead of
+ # implementing all the per-filetype callbacks.
+ def file_loaded(path)
+ end
+
+ # Generic callback for any attribute/library/lwrp/recipe file throwing an
+ # exception when loaded. Default behavior is to use CompileErrorInspector
+ # to print contextual info about the failure.
+ def file_load_failed(path, exception)
+ description = ErrorMapper.file_load_failed(path, exception)
+ display_error(description)
+ end
+
+ def recipe_not_found(exception)
+ description = ErrorMapper.file_load_failed(nil, exception)
+ display_error(description)
+ end
+
+ # Delegates to #file_loaded
+ def library_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def library_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ # Delegates to #file_loaded
+ def lwrp_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def lwrp_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ # Delegates to #file_loaded
+ def attribute_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def attribute_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ # Delegates to #file_loaded
+ def definition_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def definition_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ # Delegates to #file_loaded
+ def recipe_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def recipe_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ end
+
+
+ # == NullFormatter
+ # Formatter that doesn't actually produce any ouput. You can use this to
+ # disable the use of output formatters.
+ class NullFormatter < Base
+
+ cli_name(:null)
+
+ end
+
+ end
+end
+
diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb
new file mode 100644
index 0000000000..e5b2ab17d7
--- /dev/null
+++ b/lib/chef/formatters/doc.rb
@@ -0,0 +1,236 @@
+require 'chef/formatters/base'
+require 'chef/config'
+
+class Chef
+ module Formatters
+ #--
+ # TODO: not sold on the name, but the output is similar to what rspec calls
+ # "specdoc"
+ class Doc < Formatters::Base
+
+ cli_name(:doc)
+
+ def initialize(out, err)
+ super
+
+ @updated_resources = 0
+ end
+
+ def run_start(version)
+ puts "Starting Chef Client, version #{version}"
+ end
+
+ def run_completed(node)
+ if Chef::Config[:whyrun]
+ puts "Chef Client finished, #{@updated_resources} resources would have been updated"
+ else
+ puts "Chef Client finished, #{@updated_resources} resources updated"
+ end
+ end
+
+ def run_failed(exception)
+ if Chef::Config[:whyrun]
+ puts "Chef Client failed. #{@updated_resources} resources would have been updated"
+ else
+ puts "Chef Client failed. #{@updated_resources} resources updated"
+ end
+ end
+
+ # Called right after ohai runs.
+ def ohai_completed(node)
+ end
+
+ # Already have a client key, assuming this node has registered.
+ def skipping_registration(node_name, config)
+ end
+
+ # About to attempt to register as +node_name+
+ def registration_start(node_name, config)
+ puts "Creating a new client identity for #{node_name} using the validator key."
+ end
+
+ def registration_completed
+ end
+
+ def node_load_start(node_name, config)
+ end
+
+ # Failed to load node data from the server
+ def node_load_failed(node_name, exception, config)
+ super
+ end
+
+ # Default and override attrs from roles have been computed, but not yet applied.
+ # Normal attrs from JSON have been added to the node.
+ def node_load_completed(node, expanded_run_list, config)
+ end
+
+ # Called before the cookbook collection is fetched from the server.
+ def cookbook_resolution_start(expanded_run_list)
+ puts "resolving cookbooks for run list: #{expanded_run_list.inspect}"
+ end
+
+ # Called when there is an error getting the cookbook collection from the
+ # server.
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ super
+ end
+
+ # Called when the cookbook collection is returned from the server.
+ def cookbook_resolution_complete(cookbook_collection)
+ end
+
+ # Called before unneeded cookbooks are removed
+ def cookbook_clean_start
+ end
+
+ # Called after the file at +path+ is removed. It may be removed if the
+ # cookbook containing it was removed from the run list, or if the file was
+ # removed from the cookbook.
+ def removed_cookbook_file(path)
+ end
+
+ # Called when cookbook cleaning is finished.
+ def cookbook_clean_complete
+ end
+
+ # Called before cookbook sync starts
+ def cookbook_sync_start(cookbook_count)
+ puts "Synchronizing Cookbooks:"
+ end
+
+ # Called when cookbook +cookbook_name+ has been sync'd
+ def synchronized_cookbook(cookbook_name)
+ puts " - #{cookbook_name}"
+ end
+
+ # Called when an individual file in a cookbook has been updated
+ def updated_cookbook_file(cookbook_name, path)
+ end
+
+ # Called after all cookbooks have been sync'd.
+ def cookbook_sync_complete
+ end
+
+ # Called when cookbook loading starts.
+ def library_load_start(file_count)
+ puts "Compiling Cookbooks..."
+ end
+
+ # Called after a file in a cookbook is loaded.
+ def file_loaded(path)
+ end
+
+ # Called when recipes have been loaded.
+ def recipe_load_complete
+ end
+
+ # Called before convergence starts
+ def converge_start(run_context)
+ puts "Converging #{run_context.resource_collection.all_resources.size} resources"
+ end
+
+ # Called when the converge phase is finished.
+ def converge_complete
+ end
+
+ # Called before action is executed on a resource.
+ def resource_action_start(resource, action, notification_type=nil, notifier=nil)
+ if resource.cookbook_name && resource.recipe_name
+ resource_recipe = "#{resource.cookbook_name}::#{resource.recipe_name}"
+ else
+ resource_recipe = "<Dynamically Defined Resource>"
+ end
+
+ if resource_recipe != @current_recipe
+ puts "Recipe: #{resource_recipe}"
+ @current_recipe = resource_recipe
+ end
+ # TODO: info about notifies
+ print " * #{resource} action #{action}"
+ end
+
+ # Called when a resource fails, but will retry.
+ def resource_failed_retriable(resource, action, retry_count, exception)
+ end
+
+ # Called when a resource fails and will not be retried.
+ def resource_failed(resource, action, exception)
+ super
+ end
+
+ # Called when a resource action has been skipped b/c of a conditional
+ def resource_skipped(resource, action, conditional)
+ # TODO: more info about conditional
+ puts " (skipped due to #{conditional.positivity})"
+ end
+
+ # Called after #load_current_resource has run.
+ def resource_current_state_loaded(resource, action, current_resource)
+ end
+
+ # Called when a resource has no converge actions, e.g., it was already correct.
+ def resource_up_to_date(resource, action)
+ puts " (up to date)"
+ end
+
+ def resource_bypassed(resource, action, provider)
+ puts " (Skipped: whyrun not supported by provider #{provider.class.name})"
+ end
+
+ def output_record(line)
+
+ end
+
+ # Called when a change has been made to a resource. May be called multiple
+ # times per resource, e.g., a file may have its content updated, and then
+ # its permissions updated.
+ def resource_update_applied(resource, action, update)
+ prefix = Chef::Config[:why_run] ? "Would " : ""
+ Array(update).each do |line|
+ next if line.nil?
+ output_record line
+ if line.kind_of? String
+ @output.color "\n - #{prefix}#{line}", :green
+ elsif line.kind_of? Array
+ # Expanded output - delta
+ # @todo should we have a resource_update_delta callback?
+ line.each do |detail|
+ @output.color "\n #{detail}", :white
+ end
+ end
+ end
+ end
+
+ # Called after a resource has been completely converged.
+ def resource_updated(resource, action)
+ @updated_resources += 1
+ puts "\n"
+ end
+
+ # Called when resource current state load is skipped due to the provider
+ # not supporting whyrun mode.
+ def resource_current_state_load_bypassed(resource, action, current_resource)
+ @output.color("\n * Whyrun not supported for #{resource}, bypassing load.", :yellow)
+ end
+
+ # Called when a provider makes an assumption after a failed assertion
+ # in whyrun mode, in order to allow execution to continue
+ def whyrun_assumption(action, resource, message)
+ return unless message
+ [ message ].flatten.each do |line|
+ @output.color("\n * #{line}", :yellow)
+ end
+ end
+
+ # Called when an assertion declared by a provider fails
+ def provider_requirement_failed(action, resource, exception, message)
+ return unless message
+ color = Chef::Config[:why_run] ? :yellow : :red
+ [ message ].flatten.each do |line|
+ @output.color("\n * #{line}", color)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_descriptor.rb b/lib/chef/formatters/error_descriptor.rb
new file mode 100644
index 0000000000..abf10076be
--- /dev/null
+++ b/lib/chef/formatters/error_descriptor.rb
@@ -0,0 +1,66 @@
+#
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+#
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Formatters
+ # == Formatters::ErrorDescription
+ # Class for displaying errors on STDOUT.
+ class ErrorDescription
+
+ attr_reader :sections
+
+ def initialize(title)
+ @title = title
+ @sections = []
+ end
+
+ def section(heading, text)
+ @sections << [heading, text]
+ end
+
+ def display(out)
+ out.puts "=" * 80
+ out.puts @title, :red
+ out.puts "=" * 80
+ out.puts "\n"
+ sections.each do |section|
+ display_section(section, out)
+ end
+ end
+
+ def for_json()
+ {
+ 'title' => @title,
+ 'sections' => @sections
+ }
+ end
+
+ private
+
+ def display_section(section, out)
+ heading, text = section
+ out.puts heading
+ out.puts "-" * heading.size
+ out.puts text
+ out.puts "\n"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors.rb b/lib/chef/formatters/error_inspectors.rb
new file mode 100644
index 0000000000..418457322d
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors.rb
@@ -0,0 +1,19 @@
+require 'chef/formatters/error_inspectors/node_load_error_inspector'
+require "chef/formatters/error_inspectors/registration_error_inspector"
+require 'chef/formatters/error_inspectors/compile_error_inspector'
+require 'chef/formatters/error_inspectors/resource_failure_inspector'
+require 'chef/formatters/error_inspectors/run_list_expansion_error_inspector'
+require 'chef/formatters/error_inspectors/cookbook_resolve_error_inspector'
+require "chef/formatters/error_inspectors/cookbook_sync_error_inspector"
+
+class Chef
+ module Formatters
+
+ # == ErrorInspectors
+ # Error inspectors wrap exceptions and contextual information. They
+ # generate diagnostic messages about possible causes of the error for user
+ # consumption.
+ module ErrorInspectors
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/api_error_formatting.rb b/lib/chef/formatters/error_inspectors/api_error_formatting.rb
new file mode 100644
index 0000000000..bb5379ed3f
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/api_error_formatting.rb
@@ -0,0 +1,111 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Formatters
+
+ module APIErrorFormatting
+
+ NETWORK_ERROR_CLASSES = [Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError]
+
+ def describe_network_errors(error_description)
+ error_description.section("Networking Error:",<<-E)
+#{exception.message}
+
+Your chef_server_url may be misconfigured, or the network could be down.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+E
+ end
+
+ def describe_401_error(error_description)
+ if clock_skew?
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+The request failed because your clock has drifted by more than 15 minutes.
+Syncing your clock to an NTP Time source should resolve the issue.
+E
+ else
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+E
+
+ error_description.section("Server Response:", format_rest_error)
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+node_name "#{username}"
+client_key "#{api_key}"
+
+If these settings are correct, your client_key may be invalid.
+E
+ end
+ end
+
+ def describe_400_error(error_description)
+ error_description.section("Invalid Request Data:",<<-E)
+The data in your request was invalid (HTTP 400).
+E
+ error_description.section("Server Response:",format_rest_error)
+ end
+
+ def describe_500_error(error_description)
+ error_description.section("Unknown Server Error:",<<-E)
+The server had a fatal error attempting to load the node data.
+E
+ error_description.section("Server Response:", format_rest_error)
+ end
+
+ def describe_503_error(error_description)
+ error_description.section("Server Unavailable","The Chef Server is temporarily unavailable")
+ error_description.section("Server Response:", format_rest_error)
+ end
+
+
+ # Fallback for unexpected/uncommon http errors
+ def describe_http_error(error_description)
+ error_description.section("Unexpected API Request Failure:", format_rest_error)
+ end
+
+ # Parses JSON from the error response sent by Chef Server and returns the
+ # error message
+ def format_rest_error
+ Array(Chef::JSONCompat.from_json(exception.response.body)["error"]).join('; ')
+ rescue Exception
+ exception.response.body
+ end
+
+ def username
+ config[:node_name]
+ end
+
+ def api_key
+ config[:client_key]
+ end
+
+ def server_url
+ config[:chef_server_url]
+ end
+
+ def clock_skew?
+ exception.response.body =~ /synchronize the clock/i
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb
new file mode 100644
index 0000000000..1fa8a70b52
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb
@@ -0,0 +1,106 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+
+ # == CompileErrorInspector
+ # Wraps exceptions that occur during the compile phase of a Chef run and
+ # tries to find the code responsible for the error.
+ class CompileErrorInspector
+
+ attr_reader :path
+ attr_reader :exception
+
+ def initialize(path, exception)
+ @path, @exception = path, exception
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Chef::Exceptions::RecipeNotFound
+ error_description.section(exception.class.name, exception.message)
+ else
+ error_description.section(exception.class.name, exception.message)
+
+ traceback = filtered_bt.map {|line| " #{line}"}.join("\n")
+ error_description.section("Cookbook Trace:", traceback)
+ error_description.section("Relevant File Content:", context)
+ end
+ end
+
+ def context
+ context_lines = []
+ context_lines << "#{culprit_file}:\n\n"
+ Range.new(display_lower_bound, display_upper_bound).each do |i|
+ line_nr = (i + 1).to_s.rjust(3)
+ indicator = (i + 1) == culprit_line ? ">> " : ": "
+ context_lines << "#{line_nr}#{indicator}#{file_lines[i]}"
+ end
+ context_lines.join("")
+ end
+
+ def display_lower_bound
+ lower = (culprit_line - 8)
+ lower = 0 if lower < 0
+ lower
+ end
+
+ def display_upper_bound
+ upper = (culprit_line + 8)
+ upper = file_lines.size if upper > file_lines.size
+ upper
+ end
+
+ def file_lines
+ @file_lines ||= IO.readlines(culprit_file)
+ end
+
+ def culprit_backtrace_entry
+ @culprit_backtrace_entry ||= begin
+ bt_entry = filtered_bt.first
+ Chef::Log.debug("backtrace entry for compile error: '#{bt_entry}'")
+ bt_entry
+ end
+ end
+
+ def culprit_line
+ @culprit_line ||= begin
+ line_number = culprit_backtrace_entry[/^(?:.\:)?[^:]+:([\d]+)/,1].to_i
+ Chef::Log.debug("Line number of compile error: '#{line_number}'")
+ line_number
+ end
+ end
+
+ def culprit_file
+ @culprit_file ||= culprit_backtrace_entry[/^((?:.\:)?[^:]+):([\d]+)/,1]
+ end
+
+ def filtered_bt
+ filters = Array(Chef::Config.cookbook_path).map {|p| /^#{Regexp.escape(p)}/ }
+ r = exception.backtrace.select {|line| filters.any? {|filter| line =~ filter }}
+ Chef::Log.debug("filtered backtrace of compile error: #{r.join(",")}")
+ return r.count > 0 ? r : exception.backtrace
+ end
+
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb
new file mode 100644
index 0000000000..5642070336
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb
@@ -0,0 +1,146 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/formatters/error_inspectors/api_error_formatting'
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+ class CookbookResolveErrorInspector
+
+ attr_reader :exception
+ attr_reader :expanded_run_list
+
+ include APIErrorFormatting
+
+ def initialize(expanded_run_list, exception)
+ @expanded_run_list = expanded_run_list
+ @exception = exception
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ when *NETWORK_ERROR_CLASSES
+ describe_network_errors(error_description)
+ else
+ error_description.section("Unexpected Error:","#{exception.class.name}: #{exception.message}")
+ end
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ # TODO: this is where you'd see conflicts b/c of username/clientname stuff
+ describe_401_error(error_description)
+ when Net::HTTPForbidden
+ # TODO: we're rescuing errors from Node.find_or_create
+ # * could be no write on nodes container
+ # * could be no read on the node
+ error_description.section("Authorization Error",<<-E)
+This client is not authorized to read some of the information required to
+access its coobooks (HTTP 403).
+
+To access its cookbooks, a client needs to be able to read its environment and
+all of the cookbooks in its expanded run list.
+E
+ error_description.section("Expanded Run List:", expanded_run_list_ul)
+ error_description.section("Server Response:", format_rest_error)
+ when Net::HTTPPreconditionFailed
+ describe_412_error(error_description)
+ when Net::HTTPBadRequest
+ describe_400_error(error_description)
+ when Net::HTTPNotFound
+ when Net::HTTPInternalServerError
+ describe_500_error(error_description)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ describe_503_error(error_description)
+ else
+ describe_http_error(error_description)
+ end
+ end
+
+ def describe_412_error(error_description)
+ explanation = ""
+ error_reasons = extract_412_error_message
+ if !error_reasons.respond_to?(:key?)
+ explanation << error_reasons.to_s
+ else
+ if error_reasons.key?("non_existent_cookbooks") && !Array(error_reasons["non_existent_cookbooks"]).empty?
+ explanation << "The following cookbooks are required by the client but don't exist on the server:\n"
+ Array(error_reasons["non_existent_cookbooks"]).each do |cookbook|
+ explanation << "* #{cookbook}\n"
+ end
+ explanation << "\n"
+ end
+ if error_reasons.key?("cookbooks_with_no_versions") && !Array(error_reasons["cookbooks_with_no_versions"]).empty?
+ explanation << "The following cookbooks exist on the server, but there is no version that meets\nthe version constraints in this environment:\n"
+ Array(error_reasons["cookbooks_with_no_versions"]).each do |cookbook|
+ explanation << "* #{cookbook}\n"
+ end
+ explanation << "\n"
+ end
+ end
+
+ error_description.section("Missing Cookbooks:", explanation)
+ error_description.section("Expanded Run List:", expanded_run_list_ul)
+ end
+
+ def expanded_run_list_ul
+ @expanded_run_list.map {|i| "* #{i}"}.join("\n")
+ end
+
+ # In my tests, the error from the server is double JSON encoded, but we
+ # should not rely on this not getting fixed.
+ #
+ # Return *should* be a Hash like this:
+ # { "non_existent_cookbooks" => ["nope"],
+ # "cookbooks_with_no_versions" => [],
+ # "message" => "Run list contains invalid items: no such cookbook nope."}
+ def extract_412_error_message
+ # Example:
+ # "{\"error\":[\"{\\\"non_existent_cookbooks\\\":[\\\"nope\\\"],\\\"cookbooks_with_no_versions\\\":[],\\\"message\\\":\\\"Run list contains invalid items: no such cookbook nope.\\\"}\"]}"
+
+ wrapped_error_message = attempt_json_parse(exception.response.body)
+ unless wrapped_error_message.kind_of?(Hash) && wrapped_error_message.key?("error")
+ return wrapped_error_message.to_s
+ end
+
+ error_description = Array(wrapped_error_message["error"]).first
+ if error_description.kind_of?(Hash)
+ return error_description
+ end
+ attempt_json_parse(error_description)
+ end
+
+ private
+
+ def attempt_json_parse(maybe_json_string)
+ Chef::JSONCompat.from_json(maybe_json_string)
+ rescue Exception
+ maybe_json_string
+ end
+
+
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb b/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb
new file mode 100644
index 0000000000..054984a50e
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb
@@ -0,0 +1,80 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/formatters/error_inspectors/api_error_formatting'
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+
+ # == CookbookSyncErrorInspector
+ # Generates human-friendly explanations for errors encountered during
+ # cookbook sync.
+ #--
+ # TODO: Not sure what errors are commonly seen during cookbook sync, so
+ # the messaging is kinda generic.
+ class CookbookSyncErrorInspector
+
+ include APIErrorFormatting
+
+ attr_reader :exception
+ attr_reader :cookbooks
+
+ def initialize(cookbooks, exception)
+ @cookbooks, @exception = cookbooks, exception
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when *NETWORK_ERROR_CLASSES
+ describe_network_errors(error_description)
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ else
+ error_description.section("Unexpected Error:","#{exception.class.name}: #{exception.message}")
+ end
+ end
+
+ def config
+ Chef::Config
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ # TODO: this is where you'd see conflicts b/c of username/clientname stuff
+ describe_401_error(error_description)
+ when Net::HTTPBadRequest
+ describe_400_error(error_description)
+ when Net::HTTPNotFound
+ when Net::HTTPInternalServerError
+ describe_500_error(error_description)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ describe_503_error(error_description)
+ else
+ describe_http_error(error_description)
+ end
+ end
+
+ end
+ end
+ end
+end
+
+
diff --git a/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb b/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb
new file mode 100644
index 0000000000..7168ac0edb
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb
@@ -0,0 +1,125 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/formatters/error_inspectors/api_error_formatting'
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+
+
+ # == APIErrorInspector
+ # Wraps exceptions caused by API calls to the server.
+ class NodeLoadErrorInspector
+
+ include APIErrorFormatting
+
+ attr_reader :exception
+ attr_reader :node_name
+ attr_reader :config
+
+ def initialize(node_name, exception, config)
+ @node_name = node_name
+ @exception = exception
+ @config = config
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ when *NETWORK_ERROR_CLASSES
+ describe_network_errors(error_description)
+ when Chef::Exceptions::PrivateKeyMissing
+ error_description.section("Private Key Not Found:",<<-E)
+Your private key could not be loaded. If the key file exists, ensure that it is
+readable by chef-client.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+client_key "#{api_key}"
+E
+ else
+ error_description.section("Unexpected Error:","#{exception.class.name}: #{exception.message}")
+ end
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ # TODO: this is where you'd see conflicts b/c of username/clientname stuff
+ describe_401_error(error_description)
+ when Net::HTTPForbidden
+ # TODO: we're rescuing errors from Node.find_or_create
+ # * could be no write on nodes container
+ # * could be no read on the node
+ error_description.section("Authorization Error",<<-E)
+Your client is not authorized to load the node data (HTTP 403).
+E
+ error_description.section("Server Response:", format_rest_error)
+
+ error_description.section("Possible Causes:",<<-E)
+* Your client (#{username}) may have misconfigured authorization permissions.
+E
+ when Net::HTTPBadRequest
+ describe_400_error(error_description)
+ when Net::HTTPNotFound
+ describe_404_error(error_description)
+ when Net::HTTPInternalServerError
+ describe_500_error(error_description)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ describe_503_error(error_description)
+ else
+ describe_http_error(error_description)
+ end
+ end
+
+ # Custom 404 error messaging. Users sometimes see 404s when they have
+ # misconfigured server URLs, and the wrong one redirects to the new
+ # one, e.g., PUT http://wrong.url/nodes/node-name becomes a GET after a
+ # redirect.
+ def describe_404_error(error_description)
+ error_description.section("Resource Not Found:",<<-E)
+The server returned a HTTP 404. This usually indicates that your chef_server_url is incorrect.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+E
+ end
+
+ def username
+ config[:node_name]
+ end
+
+ def api_key
+ config[:client_key]
+ end
+
+ def server_url
+ config[:chef_server_url]
+ end
+
+ def clock_skew?
+ exception.response.body =~ /synchronize the clock/i
+ end
+
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/registration_error_inspector.rb b/lib/chef/formatters/error_inspectors/registration_error_inspector.rb
new file mode 100644
index 0000000000..5389f9f7d0
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/registration_error_inspector.rb
@@ -0,0 +1,137 @@
+class Chef
+ module Formatters
+ module ErrorInspectors
+
+ # == RegistrationErrorInspector
+ # Wraps exceptions that occur during the client registration process and
+ # suggests possible causes.
+ #--
+ # TODO: Lots of duplication with the node_load_error_inspector, just
+ # slightly tweaked to talk about validation keys instead of other keys.
+ class RegistrationErrorInspector
+ attr_reader :exception
+ attr_reader :node_name
+ attr_reader :config
+
+ def initialize(node_name, exception, config)
+ @node_name = node_name
+ @exception = exception
+ @config = config
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ when Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError
+ error_description.section("Network Error:",<<-E)
+There was a network error connecting to the Chef Server:
+#{exception.message}
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+
+If your chef_server_url is correct, your network could be down.
+E
+ when Chef::Exceptions::PrivateKeyMissing
+ error_description.section("Private Key Not Found:",<<-E)
+Your private key could not be loaded. If the key file exists, ensure that it is
+readable by chef-client.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+validation_key "#{api_key}"
+E
+ else
+ "#{exception.class.name}: #{exception.message}"
+ end
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ if clock_skew?
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+The request failed because your clock has drifted by more than 15 minutes.
+Syncing your clock to an NTP Time source should resolve the issue.
+E
+ else
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+E
+
+ error_description.section("Server Response:", format_rest_error)
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+validation_client_name "#{username}"
+validation_key "#{api_key}"
+
+If these settings are correct, your validation_key may be invalid.
+E
+ end
+ when Net::HTTPForbidden
+ error_description.section("Authorization Error:",<<-E)
+Your validation client is not authorized to create the client for this node (HTTP 403).
+E
+ error_description.section("Possible Causes:",<<-E)
+* There may already be a client named "#{config[:node_name]}"
+* Your validation client (#{username}) may have misconfigured authorization permissions.
+E
+ when Net::HTTPBadRequest
+ error_description.section("Invalid Request Data:",<<-E)
+The data in your request was invalid (HTTP 400).
+E
+ error_description.section("Server Response:",format_rest_error)
+ when Net::HTTPNotFound
+ error_description.section("Resource Not Found:",<<-E)
+The server returned a HTTP 404. This usually indicates that your chef_server_url is incorrect.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+E
+ when Net::HTTPInternalServerError
+ error_description.section("Unknown Server Error:",<<-E)
+The server had a fatal error attempting to load the node data.
+E
+ error_description.section("Server Response:", format_rest_error)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ error_description.section("Server Unavailable","The Chef Server is temporarily unavailable")
+ error_description.section("Server Response:", format_rest_error)
+ else
+ error_description.section("Unexpected API Request Failure:", format_rest_error)
+ end
+ end
+
+ def username
+ #config[:node_name]
+ config[:validation_client_name]
+ end
+
+ def api_key
+ config[:validation_key]
+ #config[:client_key]
+ end
+
+ def server_url
+ config[:chef_server_url]
+ end
+
+ def clock_skew?
+ exception.response.body =~ /synchronize the clock/i
+ 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
+ Array(Chef::JSONCompat.from_json(exception.response.body)["error"]).join('; ')
+ rescue Exception
+ exception.response.body
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb b/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb
new file mode 100644
index 0000000000..57d8de0ef9
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb
@@ -0,0 +1,108 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+ class ResourceFailureInspector
+
+ attr_reader :resource
+ attr_reader :action
+ attr_reader :exception
+
+ def initialize(resource, action, exception)
+ @resource = resource
+ @action = action
+ @exception = exception
+ end
+
+ def add_explanation(error_description)
+ error_description.section(exception.class.name, exception.message)
+
+ unless filtered_bt.empty?
+ error_description.section("Cookbook Trace:", filtered_bt.join("\n"))
+ end
+
+ unless dynamic_resource?
+ error_description.section("Resource Declaration:", recipe_snippet)
+ end
+
+ error_description.section("Compiled Resource:", "#{resource.to_text}")
+
+ # Template errors get wrapped in an exception class that can show the relevant template code,
+ # so add them to the error output.
+ if exception.respond_to?(:source_listing)
+ error_description.section("Template Context:", "#{exception.source_location}\n#{exception.source_listing}")
+ end
+ end
+
+ def recipe_snippet
+ return nil if dynamic_resource?
+ @snippet ||= begin
+ if file = resource.source_line[/^(([\w]:)?[^:]+):([\d]+)/,1] and line = resource.source_line[/^#{file}:([\d]+)/,1].to_i
+ lines = IO.readlines(file)
+
+ relevant_lines = ["# In #{file}\n\n"]
+
+
+ current_line = line - 1
+ current_line = 0 if current_line < 0
+ nesting = 0
+
+ loop do
+
+ # low rent parser. try to gracefully handle nested blocks in resources
+ nesting += 1 if lines[current_line] =~ /[\s]+do[\s]*/
+ nesting -= 1 if lines[current_line] =~ /end[\s]*$/
+
+ relevant_lines << format_line(current_line, lines[current_line])
+
+ break if lines[current_line + 1].nil?
+ break if current_line >= (line + 50)
+ break if nesting <= 0
+
+ current_line += 1
+ end
+ relevant_lines << format_line(current_line + 1, lines[current_line + 1]) if lines[current_line + 1]
+ relevant_lines.join("")
+ end
+ end
+ end
+
+ def dynamic_resource?
+ !resource.source_line
+ end
+
+ def filtered_bt
+ filters = Array(Chef::Config.cookbook_path).map {|p| /^#{Regexp.escape(p)}/ }
+ exception.backtrace.select {|line| filters.any? {|filter| line =~ filter }}
+ end
+
+ private
+
+ def format_line(line_nr, line)
+ # Print line number as 1-indexed not zero
+ line_nr_string = (line_nr + 1).to_s.rjust(3) + ": "
+ line_nr_string + line
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb b/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb
new file mode 100644
index 0000000000..ac19a983af
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb
@@ -0,0 +1,118 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/formatters/error_inspectors/api_error_formatting'
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+ class RunListExpansionErrorInspector
+
+ include APIErrorFormatting
+
+ attr_reader :exception
+ attr_reader :node
+
+ def initialize(node, exception)
+ @node, @exception = node, exception
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError
+ error_description.section("Networking Error:",<<-E)
+#{exception.message}
+
+Your chef_server_url may be misconfigured, or the network could be down.
+E
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ when Chef::Exceptions::MissingRole
+ describe_missing_role(error_description)
+ else
+ error_description.section("Unexpected Error:","#{exception.class.name}: #{exception.message}")
+ end
+ end
+
+ def describe_missing_role(error_description)
+ error_description.section("Missing Role(s) in Run List:", missing_roles_explained)
+ original_run_list = node.run_list.map {|item| "* #{item}"}.join("\n")
+ error_description.section("Original Run List", original_run_list)
+ end
+
+ def missing_roles_explained
+ run_list_expansion.missing_roles_with_including_role.map do |role, includer|
+ "* #{role} included by '#{includer}'"
+ end.join("\n")
+ end
+
+ def run_list_expansion
+ exception.expansion
+ end
+
+ def config
+ Chef::Config
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+E
+
+ error_description.section("Server Response:", format_rest_error)
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+node_name "#{username}"
+client_key "#{api_key}"
+
+If these settings are correct, your client_key may be invalid.
+E
+ when Net::HTTPForbidden
+ # TODO: we're rescuing errors from Node.find_or_create
+ # * could be no write on nodes container
+ # * could be no read on the node
+ error_description.section("Authorization Error",<<-E)
+Your client is not authorized to load one or more of your roles (HTTP 403).
+E
+ error_description.section("Server Response:", format_rest_error)
+
+ error_description.section("Possible Causes:",<<-E)
+* Your client (#{username}) may have misconfigured authorization permissions.
+E
+ when Net::HTTPInternalServerError
+ error_description.section("Unknown Server Error:",<<-E)
+The server had a fatal error attempting to load a role.
+E
+ error_description.section("Server Response:", format_rest_error)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ error_description.section("Server Unavailable","The Chef Server is temporarily unavailable")
+ error_description.section("Server Response:", format_rest_error)
+ else
+ error_description.section("Unexpected API Request Failure:", format_rest_error)
+ end
+ end
+
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/formatters/error_mapper.rb b/lib/chef/formatters/error_mapper.rb
new file mode 100644
index 0000000000..2140c638bc
--- /dev/null
+++ b/lib/chef/formatters/error_mapper.rb
@@ -0,0 +1,85 @@
+#--
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Formatters
+ # == Formatters::ErrorMapper
+ # Collection of methods for creating and returning
+ # Formatters::ErrorDescription objects based on node,
+ # exception, and configuration information.
+ module ErrorMapper
+
+ # Failed to register this client with the server.
+ def self.registration_failed(node_name, exception, config)
+ error_inspector = ErrorInspectors::RegistrationErrorInspector.new(node_name, exception, config)
+ headline = "Chef encountered an error attempting to create the client \"#{node_name}\""
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.node_load_failed(node_name, exception, config)
+ error_inspector = ErrorInspectors::NodeLoadErrorInspector.new(node_name, exception, config)
+ headline = "Chef encountered an error attempting to load the node data for \"#{node_name}\""
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.run_list_expand_failed(node, exception)
+ error_inspector = ErrorInspectors::RunListExpansionErrorInspector.new(node, exception)
+ headline = "Error expanding the run_list:"
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.cookbook_resolution_failed(expanded_run_list, exception)
+ error_inspector = ErrorInspectors::CookbookResolveErrorInspector.new(expanded_run_list, exception)
+ headline = "Error Resolving Cookbooks for Run List:"
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.cookbook_sync_failed(cookbooks, exception)
+ error_inspector = ErrorInspectors::CookbookSyncErrorInspector.new(cookbooks, exception)
+ headline = "Error Syncing Cookbooks:"
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.resource_failed(resource, action, exception)
+ error_inspector = ErrorInspectors::ResourceFailureInspector.new(resource, action, exception)
+ headline = "Error executing action `#{action}` on resource '#{resource}'"
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.file_load_failed(path, exception)
+ error_inspector = ErrorInspectors::CompileErrorInspector.new(path, exception)
+ headline = "Recipe Compile Error" + ( path ? " in #{path}" : "" )
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ description
+ end
+ end
+ end
+end
diff --git a/lib/chef/formatters/minimal.rb b/lib/chef/formatters/minimal.rb
new file mode 100644
index 0000000000..a189cc67eb
--- /dev/null
+++ b/lib/chef/formatters/minimal.rb
@@ -0,0 +1,235 @@
+require 'chef/formatters/base'
+
+class Chef
+
+ module Formatters
+
+
+ # == Formatters::Minimal
+ # Shows the progress of the chef run by printing single characters, and
+ # displays a summary of updates at the conclusion of the run. For events
+ # that don't have meaningful status information (loading a file, syncing a
+ # cookbook) a dot is printed. For resources, a dot, 'S' or 'U' is printed
+ # if the resource is up to date, skipped by not_if/only_if, or updated,
+ # respectively.
+ class Minimal < Formatters::Base
+
+ cli_name(:minimal)
+ cli_name(:min)
+
+ attr_reader :updated_resources
+ attr_reader :updates_by_resource
+
+
+ def initialize(out, err)
+ super
+ @updated_resources = []
+ @updates_by_resource = Hash.new {|h, k| h[k] = []}
+ end
+
+ # Called at the very start of a Chef Run
+ def run_start(version)
+ puts "Starting Chef Client, version #{version}"
+ end
+
+ # Called at the end of the Chef run.
+ def run_completed(node)
+ puts "chef client finished, #{@updated_resources.size} resources updated"
+ end
+
+ # called at the end of a failed run
+ def run_failed(exception)
+ puts "chef client failed. #{@updated_resources.size} resources updated"
+ end
+
+ # Called right after ohai runs.
+ def ohai_completed(node)
+ end
+
+ # Already have a client key, assuming this node has registered.
+ def skipping_registration(node_name, config)
+ end
+
+ # About to attempt to register as +node_name+
+ def registration_start(node_name, config)
+ end
+
+ def registration_completed
+ end
+
+ # Failed to register this client with the server.
+ def registration_failed(node_name, exception, config)
+ super
+ end
+
+ def node_load_start(node_name, config)
+ end
+
+ # Failed to load node data from the server
+ def node_load_failed(node_name, exception, config)
+ end
+
+ # Default and override attrs from roles have been computed, but not yet applied.
+ # Normal attrs from JSON have been added to the node.
+ def node_load_completed(node, expanded_run_list, config)
+ end
+
+ # Called before the cookbook collection is fetched from the server.
+ def cookbook_resolution_start(expanded_run_list)
+ puts "resolving cookbooks for run list: #{expanded_run_list.inspect}"
+ end
+
+ # Called when there is an error getting the cookbook collection from the
+ # server.
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ end
+
+ # Called when the cookbook collection is returned from the server.
+ def cookbook_resolution_complete(cookbook_collection)
+ end
+
+ # Called before unneeded cookbooks are removed
+ #--
+ # TODO: Should be called in CookbookVersion.sync_cookbooks
+ def cookbook_clean_start
+ end
+
+ # Called after the file at +path+ is removed. It may be removed if the
+ # cookbook containing it was removed from the run list, or if the file was
+ # removed from the cookbook.
+ def removed_cookbook_file(path)
+ end
+
+ # Called when cookbook cleaning is finished.
+ def cookbook_clean_complete
+ end
+
+ # Called before cookbook sync starts
+ def cookbook_sync_start(cookbook_count)
+ puts "Synchronizing cookbooks"
+ end
+
+ # Called when cookbook +cookbook_name+ has been sync'd
+ def synchronized_cookbook(cookbook_name)
+ print "."
+ end
+
+ # Called when an individual file in a cookbook has been updated
+ def updated_cookbook_file(cookbook_name, path)
+ end
+
+ # Called after all cookbooks have been sync'd.
+ def cookbook_sync_complete
+ puts "done."
+ end
+
+ # Called when cookbook loading starts.
+ def library_load_start(file_count)
+ puts "Compiling cookbooks"
+ end
+
+ # Called after a file in a cookbook is loaded.
+ def file_loaded(path)
+ print '.'
+ end
+
+ def file_load_failed(path, exception)
+ super
+ end
+
+ # Called when recipes have been loaded.
+ def recipe_load_complete
+ puts "done."
+ end
+
+ # Called before convergence starts
+ def converge_start(run_context)
+ puts "Converging #{run_context.resource_collection.all_resources.size} resources"
+ end
+
+ # Called when the converge phase is finished.
+ def converge_complete
+ puts "\n"
+ puts "System converged."
+ if updated_resources.empty?
+ puts "no resources updated"
+ else
+ puts "\n"
+ puts "resources updated this run:"
+ updated_resources.each do |resource|
+ puts "* #{resource.to_s}"
+ updates_by_resource[resource.name].flatten.each do |update|
+ puts " - #{update}"
+ end
+ puts "\n"
+ end
+ end
+ end
+
+ # Called before action is executed on a resource.
+ def resource_action_start(resource, action, notification_type=nil, notifier=nil)
+ end
+
+ # Called when a resource fails, but will retry.
+ def resource_failed_retriable(resource, action, retry_count, exception)
+ end
+
+ # Called when a resource fails and will not be retried.
+ def resource_failed(resource, action, exception)
+ end
+
+ # Called when a resource action has been skipped b/c of a conditional
+ def resource_skipped(resource, action, conditional)
+ print "S"
+ end
+
+ # Called after #load_current_resource has run.
+ def resource_current_state_loaded(resource, action, current_resource)
+ end
+
+ # Called when a resource has no converge actions, e.g., it was already correct.
+ def resource_up_to_date(resource, action)
+ print "."
+ end
+
+ ## TODO: callback for assertion failures
+
+ ## TODO: callback for assertion fallback in why run
+
+ # Called when a change has been made to a resource. May be called multiple
+ # times per resource, e.g., a file may have its content updated, and then
+ # its permissions updated.
+ def resource_update_applied(resource, action, update)
+ @updates_by_resource[resource.name] << Array(update)[0]
+ end
+
+ # Called after a resource has been completely converged.
+ def resource_updated(resource, action)
+ updated_resources << resource
+ print "U"
+ end
+
+ # Called before handlers run
+ def handlers_start(handler_count)
+ end
+
+ # Called after an individual handler has run
+ def handler_executed(handler)
+ end
+
+ # Called after all handlers have executed
+ def handlers_completed
+ end
+
+ # An uncategorized message. This supports the case that a user needs to
+ # pass output that doesn't fit into one of the callbacks above. Note that
+ # there's no semantic information about the content or importance of the
+ # message. That means that if you're using this too often, you should add a
+ # callback for it.
+ def msg(message)
+ end
+
+ end
+ end
+end
+
diff --git a/lib/chef/handler.rb b/lib/chef/handler.rb
new file mode 100644
index 0000000000..c4b729eeca
--- /dev/null
+++ b/lib/chef/handler.rb
@@ -0,0 +1,235 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+require 'chef/client'
+require 'forwardable'
+
+class Chef
+ # == Chef::Handler
+ # The base class for an Exception or Notification Handler. Create your own
+ # handler by subclassing Chef::Handler. When a Chef run fails with an
+ # uncaught Exception, Chef will set the +run_status+ on your handler and call
+ # +report+
+ #
+ # ===Example:
+ #
+ # require 'net/smtp'
+ #
+ # module MyOrg
+ # class OhNoes < Chef::Handler
+ #
+ # def report
+ # # Create the email message
+ # message = "From: Your Name <your@mail.address>\n"
+ # message << "To: Destination Address <someone@example.com>\n"
+ # message << "Subject: Chef Run Failure\n"
+ # message << "Date: #{Time.now.rfc2822}\n\n"
+ #
+ # # The Node is available as +node+
+ # message << "Chef run failed on #{node.name}\n"
+ # # +run_status+ is a value object with all of the run status data
+ # message << "#{run_status.formatted_exception}\n"
+ # # Join the backtrace lines. Coerce to an array just in case.
+ # message << Array(backtrace).join("\n")
+ #
+ # # Send the email
+ # Net::SMTP.start('your.smtp.server', 25) do |smtp|
+ # smtp.send_message message, 'from@address', 'to@address'
+ # end
+ # end
+ #
+ # end
+ # end
+ #
+ class Handler
+
+ # The list of currently configured start handlers
+ def self.start_handlers
+ Array(Chef::Config[:start_handlers])
+ end
+
+ # Run the start handlers. This will usually be called by a notification
+ # from Chef::Client
+ def self.run_start_handlers(run_status)
+ Chef::Log.info("Running start handlers")
+ start_handlers.each do |handler|
+ handler.run_report_safely(run_status)
+ end
+ Chef::Log.info("Start handlers complete.")
+ end
+
+ # Wire up a notification to run the start handlers when the chef run
+ # starts.
+ Chef::Client.when_run_starts do |run_status|
+ run_start_handlers(run_status)
+ end
+
+ # The list of currently configured report handlers
+ def self.report_handlers
+ Array(Chef::Config[:report_handlers])
+ end
+
+ # Run the report handlers. This will usually be called by a notification
+ # from Chef::Client
+ def self.run_report_handlers(run_status)
+ events = run_status.events
+ events.handlers_start(report_handlers.size)
+ Chef::Log.info("Running report handlers")
+ report_handlers.each do |handler|
+ handler.run_report_safely(run_status)
+ events.handler_executed(handler)
+ end
+ events.handlers_completed
+ Chef::Log.info("Report handlers complete")
+ end
+
+ # Wire up a notification to run the report handlers if the chef run
+ # succeeds.
+ Chef::Client.when_run_completes_successfully do |run_status|
+ run_report_handlers(run_status)
+ end
+
+ # The list of currently configured exception handlers
+ def self.exception_handlers
+ Array(Chef::Config[:exception_handlers])
+ end
+
+ # Run the exception handlers. Usually will be called by a notification
+ # from Chef::Client when the run fails.
+ def self.run_exception_handlers(run_status)
+ events = run_status.events
+ events.handlers_start(exception_handlers.size)
+ Chef::Log.error("Running exception handlers")
+ exception_handlers.each do |handler|
+ handler.run_report_safely(run_status)
+ events.handler_executed(handler)
+ end
+ events.handlers_completed
+ Chef::Log.error("Exception handlers complete")
+ end
+
+ # Wire up a notification to run the exception handlers if the chef run fails.
+ Chef::Client.when_run_fails do |run_status|
+ run_exception_handlers(run_status)
+ end
+
+ extend Forwardable
+
+ # The Chef::RunStatus object containing data about the Chef run.
+ attr_reader :run_status
+
+ ##
+ # :method: start_time
+ #
+ # The time the chef run started
+ def_delegator :@run_status, :start_time
+
+ ##
+ # :method: end_time
+ #
+ # The time the chef run ended
+ def_delegator :@run_status, :end_time
+
+ ##
+ # :method: elapsed_time
+ #
+ # The time elapsed between the start and finish of the chef run
+ def_delegator :@run_status, :elapsed_time
+
+ ##
+ # :method: run_context
+ #
+ # The Chef::RunContext object used by the chef run
+ def_delegator :@run_status, :run_context
+
+ ##
+ # :method: exception
+ #
+ # The uncaught Exception that terminated the chef run, or nil if the run
+ # completed successfully
+ def_delegator :@run_status, :exception
+
+ ##
+ # :method: backtrace
+ #
+ # The backtrace captured by the uncaught exception that terminated the chef
+ # run, or nil if the run completed successfully
+ def_delegator :@run_status, :backtrace
+
+ ##
+ # :method: node
+ #
+ # The Chef::Node for this client run
+ def_delegator :@run_status, :node
+
+ ##
+ # :method: all_resources
+ #
+ # An Array containing all resources in the chef run's resource_collection
+ def_delegator :@run_status, :all_resources
+
+ ##
+ # :method: updated_resources
+ #
+ # An Array containing all resources that were updated during the chef run
+ def_delegator :@run_status, :updated_resources
+
+ ##
+ # :method: success?
+ #
+ # Was the chef run successful? True if the chef run did not raise an
+ # uncaught exception
+ def_delegator :@run_status, :success?
+
+ ##
+ # :method: failed?
+ #
+ # Did the chef run fail? True if the chef run raised an uncaught exception
+ def_delegator :@run_status, :failed?
+
+ # The main entry point for report handling. Subclasses should override this
+ # method with their own report handling logic.
+ def report
+ end
+
+ # Runs the report handler, rescuing and logging any errors it may cause.
+ # This ensures that all handlers get a chance to run even if one fails.
+ # This method should not be overridden by subclasses unless you know what
+ # you're doing.
+ def run_report_safely(run_status)
+ run_report_unsafe(run_status)
+ rescue Exception => e
+ Chef::Log.error("Report handler #{self.class.name} raised #{e.inspect}")
+ Array(e.backtrace).each { |line| Chef::Log.error(line) }
+ ensure
+ @run_status = nil
+ end
+
+ # Runs the report handler without any error handling. This method should
+ # not be used directly except in testing.
+ def run_report_unsafe(run_status)
+ @run_status = run_status
+ report
+ end
+
+ # Return the Hash representation of the run_status
+ def data
+ @run_status.to_hash
+ end
+
+ end
+end
diff --git a/lib/chef/handler/error_report.rb b/lib/chef/handler/error_report.rb
new file mode 100644
index 0000000000..8bf676418d
--- /dev/null
+++ b/lib/chef/handler/error_report.rb
@@ -0,0 +1,33 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/handler'
+require 'chef/resource/directory'
+
+class Chef
+ class Handler
+ class ErrorReport < ::Chef::Handler
+
+ def report
+ Chef::FileCache.store("failed-run-data.json", Chef::JSONCompat.to_json_pretty(data), 0640)
+ Chef::Log.fatal("Saving node information to #{Chef::FileCache.load("failed-run-data.json", false)}")
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/handler/json_file.rb b/lib/chef/handler/json_file.rb
new file mode 100644
index 0000000000..977c5a2c92
--- /dev/null
+++ b/lib/chef/handler/json_file.rb
@@ -0,0 +1,64 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/handler'
+require 'chef/resource/directory'
+
+class Chef
+ class Handler
+ class JsonFile < ::Chef::Handler
+
+ attr_reader :config
+
+ def initialize(config={})
+ @config = config
+ @config[:path] ||= "/var/chef/reports"
+ @config
+ end
+
+ def report
+ if exception
+ Chef::Log.error("Creating JSON exception report")
+ else
+ Chef::Log.info("Creating JSON run report")
+ end
+
+ build_report_dir
+ savetime = Time.now.strftime("%Y%m%d%H%M%S")
+ File.open(File.join(config[:path], "chef-run-report-#{savetime}.json"), "w") do |file|
+
+ #ensure start time and end time are output in the json properly in the event activesupport happens to be on the system
+ run_data = data
+ run_data[:start_time] = run_data[:start_time].to_s
+ run_data[:end_time] = run_data[:end_time].to_s
+
+ file.puts Chef::JSONCompat.to_json_pretty(run_data)
+ end
+ end
+
+ def build_report_dir
+ unless File.exists?(config[:path])
+ FileUtils.mkdir_p(config[:path])
+ File.chmod(00700, config[:path])
+ end
+ end
+
+
+ end
+ end
+end
diff --git a/lib/chef/json_compat.rb b/lib/chef/json_compat.rb
new file mode 100644
index 0000000000..9f59a41839
--- /dev/null
+++ b/lib/chef/json_compat.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Wrapper class for interacting with JSON.
+
+require 'json'
+require 'yajl'
+
+class Chef
+ class JSONCompat
+ JSON_MAX_NESTING = 1000
+
+ class <<self
+ # See CHEF-1292/PL-538. Increase the max nesting for JSON, which defaults
+ # to 19, and isn't enough for some (for example, a Node within a Node)
+ # structures.
+ def opts_add_max_nesting(opts)
+ if opts.nil? || !opts.has_key?(:max_nesting)
+ opts = opts.nil? ? Hash.new : opts.clone
+ opts[:max_nesting] = JSON_MAX_NESTING
+ end
+ opts
+ end
+
+ # Just call the JSON gem's parse method with a modified :max_nesting field
+ def from_json(source, opts = {})
+ ::JSON.parse(source, opts_add_max_nesting(opts))
+ end
+
+ def to_json(obj, opts = nil)
+ obj.to_json(opts_add_max_nesting(opts))
+ end
+
+ def to_json_pretty(obj, opts = nil)
+ ::JSON.pretty_generate(obj, opts_add_max_nesting(opts))
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb
new file mode 100644
index 0000000000..35aedd2cb3
--- /dev/null
+++ b/lib/chef/knife.rb
@@ -0,0 +1,537 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'forwardable'
+require 'chef/version'
+require 'mixlib/cli'
+require 'chef/mixin/convert_to_class_name'
+require 'chef/mixin/path_sanity'
+require 'chef/knife/core/subcommand_loader'
+require 'chef/knife/core/ui'
+require 'chef/rest'
+require 'pp'
+
+class Chef
+ class Knife
+
+ Chef::REST::RESTRequest.user_agent = "Chef Knife#{Chef::REST::RESTRequest::UA_COMMON}"
+
+ include Mixlib::CLI
+ include Chef::Mixin::PathSanity
+ extend Chef::Mixin::ConvertToClassName
+ extend Forwardable
+
+ # 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_object
+ def_delegator :@ui, :confirm
+
+ attr_accessor :name_args
+ attr_accessor :ui
+
+ def self.ui
+ @ui ||= Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
+ end
+
+ def self.msg(msg="")
+ ui.msg(msg)
+ end
+
+ def self.reset_subcommands!
+ @@subcommands = {}
+ @subcommands_by_category = nil
+ end
+
+ def self.inherited(subclass)
+ unless subclass.unnamed?
+ subcommands[subclass.snake_case_name] = subclass
+ 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
+ # ===Arguments
+ # new_category::: A String to set the category to (see examples)
+ # ===Examples:
+ # 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 ||= Knife::SubcommandLoader.new(chef_config_dir)
+ end
+
+ def self.load_commands
+ @commands_loaded ||= subcommand_loader.load_commands
+ end
+
+ def self.subcommands
+ @@subcommands ||= {}
+ 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
+
+ # Print the list of subcommands knife knows about. If +preferred_category+
+ # is given, only subcommands in that category are shown
+ def self.list_commands(preferred_category=nil)
+ load_commands
+
+ category_desc = preferred_category ? preferred_category + " " : ''
+ msg "Available #{category_desc}subcommands: (for details, knife SUB-COMMAND --help)\n\n"
+
+ if preferred_category && subcommands_by_category.key?(preferred_category)
+ commands_to_show = {preferred_category => subcommands_by_category[preferred_category]}
+ else
+ commands_to_show = subcommands_by_category
+ end
+
+ commands_to_show.sort.each do |category, commands|
+ next if category =~ /deprecated/i
+ msg "** #{category.upcase} COMMANDS **"
+ commands.each do |command|
+ msg subcommands[command].banner if subcommands[command]
+ end
+ msg
+ end
+ end
+
+ # Run knife for the given +args+ (ARGV), adding +options+ to the list of
+ # CLI options that the subcommand knows how to handle.
+ # ===Arguments
+ # args::: usually ARGV
+ # options::: A Mixlib::CLI option parser hash. These +options+ are how
+ # subcommands know about global knife CLI options
+ def self.run(args, options={})
+ load_commands
+ 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.guess_category(args)
+ category_words = args.select {|arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ }
+ category_words.map! {|w| w.split('-')}.flatten!
+ matching_category = nil
+ while (!matching_category) && (!category_words.empty?)
+ candidate_category = category_words.join(' ')
+ matching_category = candidate_category if subcommands_by_category.key?(candidate_category)
+ matching_category || category_words.pop
+ end
+ matching_category
+ end
+
+ def self.subcommand_class_from(args)
+ command_words = args.select {|arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ }
+
+ subcommand_class = nil
+
+ while ( !subcommand_class ) && ( !command_words.empty? )
+ snake_case_class_name = command_words.join("_")
+ unless subcommand_class = subcommands[snake_case_class_name]
+ command_words.pop
+ end
+ end
+ # see if we got the command as e.g., knife node-list
+ subcommand_class ||= subcommands[args.first.gsub('-', '_')]
+ subcommand_class || subcommand_not_found!(args)
+ end
+
+ def self.deps(&block)
+ @dependency_loader = block
+ end
+
+ def self.load_deps
+ @dependency_loader && @dependency_loader.call
+ end
+
+ private
+
+ OFFICIAL_PLUGINS = %w[ec2 rackspace windows openstack terremark bluebox]
+
+ # :nodoc:
+ # Error out and print usage. probably becuase the arguments given by the
+ # user could not be resolved to a subcommand.
+ def self.subcommand_not_found!(args)
+ ui.fatal("Cannot find sub command for: '#{args.join(' ')}'")
+
+ if category_commands = guess_category(args)
+ list_commands(category_commands)
+ elsif missing_plugin = ( OFFICIAL_PLUGINS.find {|plugin| plugin == args[0]} )
+ ui.info("The #{missing_plugin} commands were moved to plugins in Chef 0.10")
+ ui.info("You can install the plugin with `(sudo) gem install knife-#{missing_plugin}")
+ else
+ list_commands
+ end
+
+ exit 10
+ end
+
+ @@chef_config_dir = nil
+
+ # search upward from current_dir until .chef directory is found
+ def self.chef_config_dir
+ if @@chef_config_dir.nil? # share this with subclasses
+ @@chef_config_dir = false
+ full_path = Dir.pwd.split(File::SEPARATOR)
+ (full_path.length - 1).downto(0) do |i|
+ candidate_directory = File.join(full_path[0..i] + [".chef" ])
+ if File.exist?(candidate_directory) && File.directory?(candidate_directory)
+ @@chef_config_dir = candidate_directory
+ break
+ end
+ end
+ end
+ @@chef_config_dir
+ end
+
+
+ public
+
+ # 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_words = command_name_words.join('_')
+ @name_args.reject! { |name_arg| command_name_words == name_arg }
+
+ if config[:help]
+ msg opt_parser
+ exit 1
+ end
+
+ # copy Mixlib::CLI over so that it cab be configured in knife.rb
+ # config file
+ Chef::Config[:verbosity] = config[:verbosity]
+ end
+
+ def parse_options(args)
+ super
+ rescue OptionParser::InvalidOption => e
+ puts "Error: " + e.to_s
+ show_usage
+ exit(1)
+ end
+
+ def configure_chef
+ unless config[:config_file]
+ candidate_configs = []
+
+ # Look for $KNIFE_HOME/knife.rb (allow multiple knives config on same machine)
+ if ENV['KNIFE_HOME']
+ candidate_configs << File.join(ENV['KNIFE_HOME'], 'knife.rb')
+ end
+ # Look for $PWD/knife.rb
+ if Dir.pwd
+ candidate_configs << File.join(Dir.pwd, 'knife.rb')
+ end
+ # Look for $UPWARD/.chef/knife.rb
+ if self.class.chef_config_dir
+ candidate_configs << File.join(self.class.chef_config_dir, 'knife.rb')
+ end
+ # Look for $HOME/.chef/knife.rb
+ if ENV['HOME']
+ candidate_configs << File.join(ENV['HOME'], '.chef', 'knife.rb')
+ end
+
+ candidate_configs.each do | candidate_config |
+ candidate_config = File.expand_path(candidate_config)
+ if File.exist?(candidate_config)
+ config[:config_file] = candidate_config
+ break
+ end
+ end
+ end
+
+ # Don't try to load a knife.rb if it doesn't exist.
+ if config[:config_file]
+ read_config_file(config[:config_file])
+ else
+ # ...but do log a message if no config was found.
+ Chef::Config[:color] = config[:color]
+ ui.warn("No knife configuration file found")
+ end
+
+ Chef::Config[:color] = config[:color]
+
+ case Chef::Config[:verbosity]
+ when 0
+ Chef::Config[:log_level] = :error
+ when 1
+ Chef::Config[:log_level] = :info
+ else
+ Chef::Config[:log_level] = :debug
+ end
+
+ 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]
+
+ # 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)
+
+ Chef::Log.debug("Using configuration from #{config[:config_file]}")
+
+ if Chef::Config[:node_name] && Chef::Config[:node_name].bytesize > 90
+ # node names > 90 bytes only work with authentication protocol >= 1.1
+ # see discussion in config.rb.
+ Chef::Config[:authentication_protocol_version] = "1.1"
+ end
+ end
+
+ def read_config_file(file)
+ Chef::Config.from_file(file)
+ rescue SyntaxError => e
+ ui.error "You have invalid ruby syntax in your config file #{file}"
+ ui.info(ui.color(e.message, :red))
+ if file_line = e.message[/#{Regexp.escape(file)}:[\d]+/]
+ line = file_line[/:([\d]+)$/, 1].to_i
+ highlight_config_error(file, line)
+ end
+ exit 1
+ rescue Exception => e
+ ui.error "You have an error in your config file #{file}"
+ ui.info "#{e.class.name}: #{e.message}"
+ filtered_trace = e.backtrace.grep(/#{Regexp.escape(file)}/)
+ filtered_trace.each {|line| ui.msg(" " + ui.color(line, :red))}
+ if !filtered_trace.empty?
+ line_nr = filtered_trace.first[/#{Regexp.escape(file)}:([\d]+)/, 1]
+ highlight_config_error(file, line_nr.to_i)
+ end
+
+ exit 1
+ end
+
+ def highlight_config_error(file, line)
+ config_file_lines = []
+ IO.readlines(file).each_with_index {|l, i| config_file_lines << "#{(i + 1).to_s.rjust(3)}: #{l.chomp}"}
+ if line == 1
+ lines = config_file_lines[0..3]
+ lines[0] = ui.color(lines[0], :red)
+ else
+ lines = config_file_lines[Range.new(line - 2, line)]
+ lines[1] = ui.color(lines[1], :red)
+ end
+ ui.msg ""
+ ui.msg ui.color(" # #{file}", :white)
+ lines.each {|l| ui.msg(l)}
+ ui.msg ""
+ end
+
+ def show_usage
+ stdout.puts("USAGE: " + self.opt_parser.to_s)
+ end
+
+ def run_with_pretty_exceptions
+ unless self.respond_to?(:run)
+ ui.error "You need to add a #run method to your knife command before you can use it"
+ end
+ enforce_path_sanity
+ run
+ rescue Exception => e
+ raise if 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::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(e)
+ 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 `-VV` 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"
+ 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"
+ 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)}"
+ 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
+
+ def create_object(object, pretty_name=nil, &block)
+ output = edit_data(object)
+
+ if Kernel.block_given?
+ output = block.call(output)
+ else
+ output.save
+ end
+
+ pretty_name ||= output
+
+ self.msg("Created #{pretty_name}")
+
+ output(output) if config[:print_after]
+ end
+
+ def delete_object(klass, name, delete_name=nil, &block)
+ confirm("Do you really want to delete #{name}")
+
+ if Kernel.block_given?
+ object = block.call
+ 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
+ self.msg("Deleted #{obj_name}")
+ end
+
+ def rest
+ @rest ||= begin
+ require 'chef/rest'
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+ end
+
+ def noauth_rest
+ @rest ||= begin
+ require 'chef/rest'
+ Chef::REST.new(Chef::Config[:chef_server_url], false, false)
+ end
+ end
+
+ def server_url
+ Chef::Config[:chef_server_url]
+ end
+
+ end
+end
+
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb
new file mode 100644
index 0000000000..a8e9201c26
--- /dev/null
+++ b/lib/chef/knife/bootstrap.rb
@@ -0,0 +1,234 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+require 'erubis'
+
+class Chef
+ class Knife
+ class Bootstrap < Knife
+
+ deps do
+ require 'chef/knife/core/bootstrap_context'
+ require 'chef/json_compat'
+ require 'tempfile'
+ require 'highline'
+ require 'net/ssh'
+ require 'net/ssh/multi'
+ require 'chef/knife/ssh'
+ Chef::Knife::Ssh.load_deps
+ end
+
+ banner "knife bootstrap FQDN (options)"
+
+ option :ssh_user,
+ :short => "-x USERNAME",
+ :long => "--ssh-user USERNAME",
+ :description => "The ssh username",
+ :default => "root"
+
+ option :ssh_password,
+ :short => "-P PASSWORD",
+ :long => "--ssh-password PASSWORD",
+ :description => "The ssh password"
+
+ option :ssh_port,
+ :short => "-p PORT",
+ :long => "--ssh-port PORT",
+ :description => "The ssh port",
+ :default => "22",
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
+
+ option :ssh_gateway,
+ :short => "-G GATEWAY",
+ :long => "--ssh-gateway GATEWAY",
+ :description => "The ssh gateway",
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
+
+ option :identity_file,
+ :short => "-i IDENTITY_FILE",
+ :long => "--identity-file IDENTITY_FILE",
+ :description => "The SSH identity file used for authentication"
+
+ option :chef_node_name,
+ :short => "-N NAME",
+ :long => "--node-name NAME",
+ :description => "The Chef node name for your new node"
+
+ option :prerelease,
+ :long => "--prerelease",
+ :description => "Install the pre-release chef gems"
+
+ option :bootstrap_version,
+ :long => "--bootstrap-version VERSION",
+ :description => "The version of Chef to install",
+ :proc => lambda { |v| Chef::Config[:knife][:bootstrap_version] = v }
+
+ option :bootstrap_proxy,
+ :long => "--bootstrap-proxy PROXY_URL",
+ :description => "The proxy server for the node being bootstrapped",
+ :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p }
+
+ option :distro,
+ :short => "-d DISTRO",
+ :long => "--distro DISTRO",
+ :description => "Bootstrap a distro using a template",
+ :default => "chef-full"
+
+ option :use_sudo,
+ :long => "--sudo",
+ :description => "Execute the bootstrap via sudo",
+ :boolean => true
+
+ option :template_file,
+ :long => "--template-file TEMPLATE",
+ :description => "Full path to location of template to use",
+ :default => false
+
+ option :run_list,
+ :short => "-r RUN_LIST",
+ :long => "--run-list RUN_LIST",
+ :description => "Comma separated list of roles/recipes to apply",
+ :proc => lambda { |o| o.split(/[\s,]+/) },
+ :default => []
+
+ option :first_boot_attributes,
+ :short => "-j JSON_ATTRIBS",
+ :long => "--json-attributes",
+ :description => "A JSON string to be added to the first run of chef-client",
+ :proc => lambda { |o| JSON.parse(o) },
+ :default => {}
+
+ option :host_key_verify,
+ :long => "--[no-]host-key-verify",
+ :description => "Verify host key, enabled by default.",
+ :boolean => true,
+ :default => true
+
+ option :hint,
+ :long => "--hint HINT_NAME[=HINT_FILE]",
+ :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
+ :proc => Proc.new { |h|
+ Chef::Config[:knife][:hints] ||= Hash.new
+ name, path = h.split("=")
+ Chef::Config[:knife][:hints][name] = path ? JSON.parse(::File.read(path)) : Hash.new }
+
+ def find_template(template=nil)
+ # Are we bootstrapping using an already shipped template?
+ if config[:template_file]
+ bootstrap_files = config[:template_file]
+ else
+ bootstrap_files = []
+ bootstrap_files << File.join(File.dirname(__FILE__), 'bootstrap', "#{config[:distro]}.erb")
+ bootstrap_files << File.join(Knife.chef_config_dir, "bootstrap", "#{config[:distro]}.erb") if Knife.chef_config_dir
+ bootstrap_files << File.join(ENV['HOME'], '.chef', 'bootstrap', "#{config[:distro]}.erb") if ENV['HOME']
+ bootstrap_files << Gem.find_files(File.join("chef","knife","bootstrap","#{config[:distro]}.erb"))
+ bootstrap_files.flatten!
+ end
+
+ template = Array(bootstrap_files).find do |bootstrap_template|
+ Chef::Log.debug("Looking for bootstrap template in #{File.dirname(bootstrap_template)}")
+ File.exists?(bootstrap_template)
+ end
+
+ unless template
+ ui.info("Can not find bootstrap definition for #{config[:distro]}")
+ raise Errno::ENOENT
+ end
+
+ Chef::Log.debug("Found bootstrap template in #{File.dirname(template)}")
+
+ template
+ end
+
+ def render_template(template=nil)
+ context = Knife::Core::BootstrapContext.new(config, config[:run_list], Chef::Config)
+ Erubis::Eruby.new(template).evaluate(context)
+ end
+
+ def read_template
+ IO.read(@template_file).chomp
+ end
+
+ def run
+ validate_name_args!
+ @template_file = find_template(config[:bootstrap_template])
+ @node_name = Array(@name_args).first
+ # back compat--templates may use this setting:
+ config[:server_name] = @node_name
+
+ $stdout.sync = true
+
+ ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}")
+
+ begin
+ knife_ssh.run
+ rescue Net::SSH::AuthenticationFailed
+ unless config[:ssh_password]
+ ui.info("Failed to authenticate #{config[:ssh_user]} - trying password auth")
+ knife_ssh_with_password_auth.run
+ end
+ end
+ end
+
+ def validate_name_args!
+ if Array(@name_args).first.nil?
+ ui.error("Must pass an FQDN or ip to bootstrap")
+ exit 1
+ end
+ end
+
+ def server_name
+ Array(@name_args).first
+ end
+
+ def knife_ssh
+ ssh = Chef::Knife::Ssh.new
+ ssh.ui = ui
+ ssh.name_args = [ server_name, ssh_command ]
+ ssh.config[:ssh_user] = config[:ssh_user]
+ ssh.config[:ssh_password] = config[:ssh_password]
+ ssh.config[:ssh_port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
+ ssh.config[:ssh_gateway] = Chef::Config[:knife][:ssh_gateway] || config[:ssh_gateway]
+ ssh.config[:identity_file] = config[:identity_file]
+ ssh.config[:manual] = true
+ ssh.config[:host_key_verify] = config[:host_key_verify]
+ ssh.config[:on_error] = :raise
+ ssh
+ end
+
+ def knife_ssh_with_password_auth
+ ssh = knife_ssh
+ ssh.config[:identity_file] = nil
+ ssh.config[:ssh_password] = ssh.get_password
+ ssh
+ end
+
+ def ssh_command
+ command = render_template(read_template)
+
+ if config[:use_sudo]
+ command = "sudo #{command}"
+ end
+
+ command
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/bootstrap/archlinux-gems.erb b/lib/chef/knife/bootstrap/archlinux-gems.erb
new file mode 100644
index 0000000000..85d6236197
--- /dev/null
+++ b/lib/chef/knife/bootstrap/archlinux-gems.erb
@@ -0,0 +1,75 @@
+bash -c '
+<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%>
+
+if [ ! -f /usr/bin/chef-client ]; then
+ pacman -Syy
+ pacman -S --noconfirm ruby ntp base-devel
+ ntpdate -u pool.ntp.org
+ gem install ohai --no-rdoc --no-ri --verbose
+ gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %>
+fi
+
+mkdir -p /etc/chef
+(
+cat <<'EOP'
+<%= validation_key %>
+EOP
+) > /tmp/validation.pem
+awk NF /tmp/validation.pem > /etc/chef/validation.pem
+rm /tmp/validation.pem
+chmod 0600 /etc/chef/validation.pem
+
+<% if @chef_config[:encrypted_data_bag_secret] -%>
+(
+cat <<'EOP'
+<%= encrypted_data_bag_secret %>
+EOP
+) > /tmp/encrypted_data_bag_secret
+awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret
+rm /tmp/encrypted_data_bag_secret
+chmod 0600 /etc/chef/encrypted_data_bag_secret
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%>
+mkdir -p /etc/chef/ohai/hints
+
+<% @chef_config[:knife][:hints].each do |name, hash| -%>
+(
+cat <<'EOP'
+<%= hash.to_json %>
+EOP
+) > /etc/chef/ohai/hints/<%= name %>.json
+<% end -%>
+<% end -%>
+
+(
+cat <<'EOP'
+log_level :info
+log_location STDOUT
+chef_server_url "<%= @chef_config[:chef_server_url] %>"
+validation_client_name "<%= @chef_config[:validation_client_name] %>"
+<% if @config[:chef_node_name] -%>
+node_name "<%= @config[:chef_node_name] %>"
+<% else -%>
+# Using default node name (fqdn)
+<% end -%>
+# ArchLinux follows the Filesystem Hierarchy Standard
+file_cache_path "/var/cache/chef"
+file_backup_path "/var/lib/chef/backup"
+pid_file "/var/run/chef/client.pid"
+cache_options({ :path => "/var/cache/chef/checksums", :skip_expires => true})
+<% if knife_config[:bootstrap_proxy] %>
+http_proxy "<%= knife_config[:bootstrap_proxy] %>"
+https_proxy "<%= knife_config[:bootstrap_proxy] %>"
+<% end -%>
+EOP
+) > /etc/chef/client.rb
+
+(
+cat <<'EOP'
+<%= first_boot.to_json %>
+EOP
+) > /etc/chef/first-boot.json
+
+<%= start_chef %>'
diff --git a/lib/chef/knife/bootstrap/centos5-gems.erb b/lib/chef/knife/bootstrap/centos5-gems.erb
new file mode 100644
index 0000000000..f9626c3c2b
--- /dev/null
+++ b/lib/chef/knife/bootstrap/centos5-gems.erb
@@ -0,0 +1,71 @@
+bash -c '
+<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%>
+
+if [ ! -f /usr/bin/chef-client ]; then
+ wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm
+ rpm -Uvh epel-release-5-4.noarch.rpm
+ wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://rpm.aegisco.com/aegisco/rhel/aegisco-rhel.rpm
+ rpm -Uvh aegisco-rhel.rpm
+
+ yum install -y ruby ruby-devel gcc gcc-c++ automake autoconf make
+
+ cd /tmp
+ wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://production.cf.rubygems.org/rubygems/rubygems-1.6.2.tgz
+ tar zxf rubygems-1.6.2.tgz
+ cd rubygems-1.6.2
+ ruby setup.rb --no-format-executable
+fi
+
+gem update --system
+gem update
+gem install ohai --no-rdoc --no-ri --verbose
+gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %>
+
+mkdir -p /etc/chef
+
+(
+cat <<'EOP'
+<%= validation_key %>
+EOP
+) > /tmp/validation.pem
+awk NF /tmp/validation.pem > /etc/chef/validation.pem
+rm /tmp/validation.pem
+chmod 0600 /etc/chef/validation.pem
+
+<% if @chef_config[:encrypted_data_bag_secret] -%>
+(
+cat <<'EOP'
+<%= encrypted_data_bag_secret %>
+EOP
+) > /tmp/encrypted_data_bag_secret
+awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret
+rm /tmp/encrypted_data_bag_secret
+chmod 0600 /etc/chef/encrypted_data_bag_secret
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%>
+mkdir -p /etc/chef/ohai/hints
+
+<% @chef_config[:knife][:hints].each do |name, hash| -%>
+(
+cat <<'EOP'
+<%= hash.to_json %>
+EOP
+) > /etc/chef/ohai/hints/<%= name %>.json
+<% end -%>
+<% end -%>
+
+(
+cat <<'EOP'
+<%= config_content %>
+EOP
+) > /etc/chef/client.rb
+
+(
+cat <<'EOP'
+<%= first_boot.to_json %>
+EOP
+) > /etc/chef/first-boot.json
+
+<%= start_chef %>'
diff --git a/lib/chef/knife/bootstrap/chef-full.erb b/lib/chef/knife/bootstrap/chef-full.erb
new file mode 100644
index 0000000000..771ef85884
--- /dev/null
+++ b/lib/chef/knife/bootstrap/chef-full.erb
@@ -0,0 +1,73 @@
+bash -c '
+<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%>
+
+exists() {
+ if command -v $1 &>/dev/null
+ then
+ return 0
+ else
+ return 1
+ fi
+}
+
+install_sh="http://opscode.com/chef/install.sh"
+version_string="-v <%= chef_version %>"
+
+if ! exists /usr/bin/chef-client; then
+ if exists wget; then
+ bash <(wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> ${install_sh} -O -) ${version_string}
+ else
+ if exists curl; then
+ bash <(curl -L <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> ${install_sh}) ${version_string}
+ fi
+ fi
+fi
+
+mkdir -p /etc/chef
+
+(
+cat <<'EOP'
+<%= validation_key %>
+EOP
+) > /tmp/validation.pem
+awk NF /tmp/validation.pem > /etc/chef/validation.pem
+rm /tmp/validation.pem
+chmod 0600 /etc/chef/validation.pem
+
+<% if @chef_config[:encrypted_data_bag_secret] -%>
+(
+cat <<'EOP'
+<%= encrypted_data_bag_secret %>
+EOP
+) > /tmp/encrypted_data_bag_secret
+awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret
+rm /tmp/encrypted_data_bag_secret
+chmod 0600 /etc/chef/encrypted_data_bag_secret
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%>
+mkdir -p /etc/chef/ohai/hints
+
+<% @chef_config[:knife][:hints].each do |name, hash| -%>
+(
+cat <<'EOP'
+<%= hash.to_json %>
+EOP
+) > /etc/chef/ohai/hints/<%= name %>.json
+<% end -%>
+<% end -%>
+
+(
+cat <<'EOP'
+<%= config_content %>
+EOP
+) > /etc/chef/client.rb
+
+(
+cat <<'EOP'
+<%= first_boot.to_json %>
+EOP
+) > /etc/chef/first-boot.json
+
+<%= start_chef %>'
diff --git a/lib/chef/knife/bootstrap/fedora13-gems.erb b/lib/chef/knife/bootstrap/fedora13-gems.erb
new file mode 100644
index 0000000000..a8448342df
--- /dev/null
+++ b/lib/chef/knife/bootstrap/fedora13-gems.erb
@@ -0,0 +1,58 @@
+bash -c '
+<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%>
+
+yum install -y ruby ruby-devel gcc gcc-c++ automake autoconf rubygems make
+
+gem update --system
+gem update
+gem install ohai --no-rdoc --no-ri --verbose
+gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %>
+
+mkdir -p /etc/chef
+
+(
+cat <<'EOP'
+<%= validation_key %>
+EOP
+) > /tmp/validation.pem
+awk NF /tmp/validation.pem > /etc/chef/validation.pem
+rm /tmp/validation.pem
+chmod 0600 /etc/chef/validation.pem
+
+<% if @chef_config[:encrypted_data_bag_secret] -%>
+(
+cat <<'EOP'
+<%= encrypted_data_bag_secret %>
+EOP
+) > /tmp/encrypted_data_bag_secret
+awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret
+rm /tmp/encrypted_data_bag_secret
+chmod 0600 /etc/chef/encrypted_data_bag_secret
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%>
+mkdir -p /etc/chef/ohai/hints
+
+<% @chef_config[:knife][:hints].each do |name, hash| -%>
+(
+cat <<'EOP'
+<%= hash.to_json %>
+EOP
+) > /etc/chef/ohai/hints/<%= name %>.json
+<% end -%>
+<% end -%>
+
+(
+cat <<'EOP'
+<%= config_content %>
+EOP
+) > /etc/chef/client.rb
+
+(
+cat <<'EOP'
+<%= first_boot.to_json %>
+EOP
+) > /etc/chef/first-boot.json
+
+<%= start_chef %>'
diff --git a/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb b/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb
new file mode 100644
index 0000000000..0e44361d82
--- /dev/null
+++ b/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb
@@ -0,0 +1,65 @@
+bash -c '
+<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%>
+
+if [ ! -f /usr/bin/chef-client ]; then
+ apt-get install -y wget
+ echo "chef chef/chef_server_url string <%= @chef_config[:chef_server_url] %>" | debconf-set-selections
+ [ -f /etc/apt/sources.list.d/opscode.list ] || echo "deb http://apt.opscode.com <%= chef_version.to_f == 0.10 ? "lucid-0.10" : "lucid" %> main" > /etc/apt/sources.list.d/opscode.list
+ wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>-O- http://apt.opscode.com/packages@opscode.com.gpg.key | apt-key add -
+fi
+apt-get update
+apt-get install -y chef
+
+(
+cat <<'EOP'
+<%= validation_key %>
+EOP
+) > /tmp/validation.pem
+awk NF /tmp/validation.pem > /etc/chef/validation.pem
+rm /tmp/validation.pem
+chmod 0600 /etc/chef/validation.pem
+
+<% if @chef_config[:encrypted_data_bag_secret] -%>
+(
+cat <<'EOP'
+<%= encrypted_data_bag_secret %>
+EOP
+) > /tmp/encrypted_data_bag_secret
+awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret
+rm /tmp/encrypted_data_bag_secret
+chmod 0600 /etc/chef/encrypted_data_bag_secret
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%>
+mkdir -p /etc/chef/ohai/hints
+
+<% @chef_config[:knife][:hints].each do |name, hash| -%>
+(
+cat <<'EOP'
+<%= hash.to_json %>
+EOP
+) > /etc/chef/ohai/hints/<%= name %>.json
+<% end -%>
+<% end -%>
+
+<% unless @chef_config[:validation_client_name] == "chef-validator" -%>
+[ `grep -qx "validation_client_name \"<%= @chef_config[:validation_client_name] %>\"" /etc/chef/client.rb` ] || echo "validation_client_name \"<%= @chef_config[:validation_client_name] %>\"" >> /etc/chef/client.rb
+<% end -%>
+
+<% if @config[:chef_node_name] %>
+[ `grep -qx "node_name \"<%= @config[:chef_node_name] %>\"" /etc/chef/client.rb` ] || echo "node_name \"<%= @config[:chef_node_name] %>\"" >> /etc/chef/client.rb
+<% end -%>
+
+<% if knife_config[:bootstrap_proxy] %>
+echo 'http_proxy "knife_config[:bootstrap_proxy]"' >> /etc/chef/client.rb
+echo 'https_proxy "knife_config[:bootstrap_proxy]"' >> /etc/chef/client.rb
+<% end -%>
+
+(
+cat <<'EOP'
+<%= first_boot.to_json %>
+EOP
+) > /etc/chef/first-boot.json
+
+<%= start_chef %>'
diff --git a/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb b/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb
new file mode 100644
index 0000000000..63448fc4d3
--- /dev/null
+++ b/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb
@@ -0,0 +1,65 @@
+bash -c '
+<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%>
+
+if [ ! -f /usr/bin/chef-client ]; then
+ apt-get update
+ apt-get install -y ruby ruby1.8-dev build-essential wget libruby-extras libruby1.8-extras
+ cd /tmp
+ wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>http://production.cf.rubygems.org/rubygems/rubygems-1.6.2.tgz
+ tar zxf rubygems-1.6.2.tgz
+ cd rubygems-1.6.2
+ ruby setup.rb --no-format-executable
+fi
+
+gem update --no-rdoc --no-ri
+gem install ohai --no-rdoc --no-ri --verbose
+gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %>
+
+mkdir -p /etc/chef
+
+(
+cat <<'EOP'
+<%= validation_key %>
+EOP
+) > /tmp/validation.pem
+awk NF /tmp/validation.pem > /etc/chef/validation.pem
+rm /tmp/validation.pem
+chmod 0600 /etc/chef/validation.pem
+
+<% if @chef_config[:encrypted_data_bag_secret] -%>
+(
+cat <<'EOP'
+<%= encrypted_data_bag_secret %>
+EOP
+) > /tmp/encrypted_data_bag_secret
+awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret
+rm /tmp/encrypted_data_bag_secret
+chmod 0600 /etc/chef/encrypted_data_bag_secret
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%>
+mkdir -p /etc/chef/ohai/hints
+
+<% @chef_config[:knife][:hints].each do |name, hash| -%>
+(
+cat <<'EOP'
+<%= hash.to_json %>
+EOP
+) > /etc/chef/ohai/hints/<%= name %>.json
+<% end -%>
+<% end -%>
+
+(
+cat <<'EOP'
+<%= config_content %>
+EOP
+) > /etc/chef/client.rb
+
+(
+cat <<'EOP'
+<%= first_boot.to_json %>
+EOP
+) > /etc/chef/first-boot.json
+
+<%= start_chef %>'
diff --git a/lib/chef/knife/bootstrap/ubuntu12.04-gems.erb b/lib/chef/knife/bootstrap/ubuntu12.04-gems.erb
new file mode 100644
index 0000000000..e7da7db39b
--- /dev/null
+++ b/lib/chef/knife/bootstrap/ubuntu12.04-gems.erb
@@ -0,0 +1,60 @@
+bash -c '
+<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%>
+
+if [ ! -f /usr/bin/chef-client ]; then
+ aptitude update
+ aptitude install -y ruby ruby1.8-dev build-essential wget libruby1.8 rubygems
+fi
+
+gem update --no-rdoc --no-ri
+gem install ohai --no-rdoc --no-ri --verbose
+gem install chef --no-rdoc --no-ri --verbose <%= bootstrap_version_string %>
+
+mkdir -p /etc/chef
+
+(
+cat <<'EOP'
+<%= validation_key %>
+EOP
+) > /tmp/validation.pem
+awk NF /tmp/validation.pem > /etc/chef/validation.pem
+rm /tmp/validation.pem
+chmod 0600 /etc/chef/validation.pem
+
+<% if @chef_config[:encrypted_data_bag_secret] -%>
+(
+cat <<'EOP'
+<%= encrypted_data_bag_secret %>
+EOP
+) > /tmp/encrypted_data_bag_secret
+awk NF /tmp/encrypted_data_bag_secret > /etc/chef/encrypted_data_bag_secret
+rm /tmp/encrypted_data_bag_secret
+chmod 0600 /etc/chef/encrypted_data_bag_secret
+<% end -%>
+
+<%# Generate Ohai Hints -%>
+<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%>
+mkdir -p /etc/chef/ohai/hints
+
+<% @chef_config[:knife][:hints].each do |name, hash| -%>
+(
+cat <<'EOP'
+<%= hash.to_json %>
+EOP
+) > /etc/chef/ohai/hints/<%= name %>.json
+<% end -%>
+<% end -%>
+
+(
+cat <<'EOP'
+<%= config_content %>
+EOP
+) > /etc/chef/client.rb
+
+(
+cat <<'EOP'
+<%= first_boot.to_json %>
+EOP
+) > /etc/chef/first-boot.json
+
+<%= start_chef %>'
diff --git a/lib/chef/knife/client_bulk_delete.rb b/lib/chef/knife/client_bulk_delete.rb
new file mode 100644
index 0000000000..8bf2c2f116
--- /dev/null
+++ b/lib/chef/knife/client_bulk_delete.rb
@@ -0,0 +1,65 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class ClientBulkDelete < Knife
+
+ deps do
+ require 'chef/api_client'
+ require 'chef/json_compat'
+ end
+
+ banner "knife client bulk delete REGEX (options)"
+
+ def run
+ if name_args.length < 1
+ ui.fatal("You must supply a regular expression to match the results against")
+ exit 42
+ end
+ all_clients = Chef::ApiClient.list(true)
+
+ matcher = /#{name_args[0]}/
+ clients_to_delete = {}
+ all_clients.each do |name, client|
+ next unless name =~ matcher
+ clients_to_delete[client.name] = client
+ end
+
+ if clients_to_delete.empty?
+ ui.info "No clients match the expression /#{name_args[0]}/"
+ exit 0
+ end
+
+ ui.msg("The following clients will be deleted:")
+ ui.msg("")
+ ui.msg(ui.list(clients_to_delete.keys.sort, :columns_down))
+ ui.msg("")
+ ui.confirm("Are you sure you want to delete these clients")
+
+ clients_to_delete.sort.each do |name, client|
+ client.destroy
+ ui.msg("Deleted client #{name}")
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/client_create.rb b/lib/chef/knife/client_create.rb
new file mode 100644
index 0000000000..5b5078b574
--- /dev/null
+++ b/lib/chef/knife/client_create.rb
@@ -0,0 +1,80 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class ClientCreate < Knife
+
+ deps do
+ require 'chef/api_client'
+ require 'chef/json_compat'
+ end
+
+ option :file,
+ :short => "-f FILE",
+ :long => "--file FILE",
+ :description => "Write the key to a file"
+
+ option :admin,
+ :short => "-a",
+ :long => "--admin",
+ :description => "Create the client as an admin",
+ :boolean => true
+
+ banner "knife client create CLIENT (options)"
+
+ def run
+ @client_name = @name_args[0]
+
+ if @client_name.nil?
+ show_usage
+ ui.fatal("You must specify a client name")
+ exit 1
+ end
+
+ client = Chef::ApiClient.new
+ client.name(@client_name)
+ client.admin(config[:admin])
+
+ output = edit_data(client)
+
+ # Chef::ApiClient.save will try to create a client and if it exists will update it instead silently
+ client = output.save
+
+ # We only get a private_key on client creation, not on client update.
+ if client['private_key']
+ ui.info("Created #{output}")
+
+ if config[:file]
+ File.open(config[:file], "w") do |f|
+ f.print(client['private_key'])
+ end
+ else
+ puts client['private_key']
+ end
+ else
+ ui.error "Client '#{client['name']}' already exists"
+ exit 1
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/client_delete.rb b/lib/chef/knife/client_delete.rb
new file mode 100644
index 0000000000..6a6fae7ea0
--- /dev/null
+++ b/lib/chef/knife/client_delete.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class ClientDelete < Knife
+
+ deps do
+ require 'chef/api_client'
+ require 'chef/json_compat'
+ end
+
+ banner "knife client delete CLIENT (options)"
+
+ def run
+ @client_name = @name_args[0]
+
+ if @client_name.nil?
+ show_usage
+ ui.fatal("You must specify a client name")
+ exit 1
+ end
+
+ delete_object(Chef::ApiClient, @client_name)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/client_edit.rb b/lib/chef/knife/client_edit.rb
new file mode 100644
index 0000000000..c81bce902a
--- /dev/null
+++ b/lib/chef/knife/client_edit.rb
@@ -0,0 +1,45 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class ClientEdit < Knife
+
+ deps do
+ require 'chef/api_client'
+ require 'chef/json_compat'
+ end
+
+ banner "knife client edit CLIENT (options)"
+
+ def run
+ @client_name = @name_args[0]
+
+ if @client_name.nil?
+ show_usage
+ ui.fatal("You must specify a client name")
+ exit 1
+ end
+
+ edit_object(Chef::ApiClient, @client_name)
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/client_list.rb b/lib/chef/knife/client_list.rb
new file mode 100644
index 0000000000..da0bf12dc3
--- /dev/null
+++ b/lib/chef/knife/client_list.rb
@@ -0,0 +1,42 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class ClientList < Knife
+
+ deps do
+ require 'chef/api_client'
+ require 'chef/json_compat'
+ end
+
+ banner "knife client list (options)"
+
+ option :with_uri,
+ :short => "-w",
+ :long => "--with-uri",
+ :description => "Show corresponding URIs"
+
+ def run
+ output(format_list_for_display(Chef::ApiClient.list))
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/client_reregister.rb b/lib/chef/knife/client_reregister.rb
new file mode 100644
index 0000000000..73a93ec31d
--- /dev/null
+++ b/lib/chef/knife/client_reregister.rb
@@ -0,0 +1,58 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class ClientReregister < Knife
+
+ deps do
+ require 'chef/api_client'
+ require 'chef/json_compat'
+ end
+
+ banner "knife client reregister CLIENT (options)"
+
+ option :file,
+ :short => "-f FILE",
+ :long => "--file FILE",
+ :description => "Write the key to a file"
+
+ def run
+ @client_name = @name_args[0]
+
+ if @client_name.nil?
+ show_usage
+ ui.fatal("You must specify a client name")
+ exit 1
+ end
+
+ client = Chef::ApiClient.load(@client_name)
+ key = client.save(new_key=true)
+ if config[:file]
+ File.open(config[:file], "w") do |f|
+ f.print(key['private_key'])
+ end
+ else
+ ui.msg key['private_key']
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/client_show.rb b/lib/chef/knife/client_show.rb
new file mode 100644
index 0000000000..5c2ffb4183
--- /dev/null
+++ b/lib/chef/knife/client_show.rb
@@ -0,0 +1,52 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class ClientShow < Knife
+
+ deps do
+ require 'chef/api_client'
+ require 'chef/json_compat'
+ end
+
+ banner "knife client show CLIENT (options)"
+
+ option :attribute,
+ :short => "-a ATTR",
+ :long => "--attribute ATTR",
+ :description => "Show only one attribute"
+
+ def run
+ @client_name = @name_args[0]
+
+ if @client_name.nil?
+ show_usage
+ ui.fatal("You must specify a client name")
+ exit 1
+ end
+
+ client = Chef::ApiClient.load(@client_name)
+ output(format_for_display(client))
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/configure.rb b/lib/chef/knife/configure.rb
new file mode 100644
index 0000000000..0be7093e29
--- /dev/null
+++ b/lib/chef/knife/configure.rb
@@ -0,0 +1,168 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class Configure < Knife
+ attr_reader :chef_server, :new_client_name, :admin_client_name, :admin_client_key
+ attr_reader :chef_repo, :new_client_key, :validation_client_name, :validation_key
+
+ deps do
+ require 'ohai'
+ Chef::Knife::ClientCreate.load_deps
+ end
+
+ banner "knife configure (options)"
+
+ option :repository,
+ :short => "-r REPO",
+ :long => "--repository REPO",
+ :description => "The path to your chef-repo"
+
+ option :initial,
+ :short => "-i",
+ :long => "--initial",
+ :boolean => true,
+ :description => "Create an initial API Client"
+
+ option :admin_client_name,
+ :long => "--admin-client-name NAME",
+ :description => "The existing admin clientname (usually chef-webui)"
+
+ option :admin_client_key,
+ :long => "--admin-client-key PATH",
+ :description => "The path to the admin client's private key (usually a file named webui.pem)"
+
+ option :validation_client_name,
+ :long => "--validation-client-name NAME",
+ :description => "The validation clientname (usually chef-validator)"
+
+ option :validation_key,
+ :long => "--validation-key PATH",
+ :description => "The location of the location of the validation key (usually a file named validation.pem)"
+
+ def configure_chef
+ # We are just faking out the system so that you can do this without a key specified
+ Chef::Config[:node_name] = 'woot'
+ super
+ Chef::Config[:node_name] = nil
+ end
+
+ def run
+ ask_user_for_config_path
+
+ FileUtils.mkdir_p(chef_config_path)
+
+ ask_user_for_config
+
+ ::File.open(config[:config_file], "w") do |f|
+ f.puts <<-EOH
+log_level :info
+log_location STDOUT
+node_name '#{new_client_name}'
+client_key '#{new_client_key}'
+validation_client_name '#{validation_client_name}'
+validation_key '#{validation_key}'
+chef_server_url '#{chef_server}'
+cache_type 'BasicFile'
+cache_options( :path => '#{File.join(chef_config_path, "checksums")}' )
+EOH
+ unless chef_repo.empty?
+ f.puts "cookbook_path [ '#{chef_repo}/cookbooks' ]"
+ end
+ end
+
+ if config[:initial]
+ ui.msg("Creating initial API user...")
+ Chef::Config[:chef_server_url] = chef_server
+ Chef::Config[:node_name] = admin_client_name
+ Chef::Config[:client_key] = admin_client_key
+ client_create = Chef::Knife::ClientCreate.new
+ client_create.name_args = [ new_client_name ]
+ client_create.config[:admin] = true
+ client_create.config[:file] = new_client_key
+ client_create.config[:yes] = true
+ client_create.config[:disable_editing] = true
+ client_create.run
+ else
+ ui.msg("*****")
+ ui.msg("")
+ ui.msg("You must place your client key in:")
+ ui.msg(" #{new_client_key}")
+ ui.msg("Before running commands with Knife!")
+ ui.msg("")
+ ui.msg("*****")
+ ui.msg("")
+ ui.msg("You must place your validation key in:")
+ ui.msg(" #{validation_key}")
+ ui.msg("Before generating instance data with Knife!")
+ ui.msg("")
+ ui.msg("*****")
+ end
+
+ ui.msg("Configuration file written to #{config[:config_file]}")
+ end
+
+ def ask_user_for_config_path
+ config[:config_file] ||= ask_question("Where should I put the config file? ", :default => "#{Chef::Config[:user_home]}/.chef/knife.rb")
+ # have to use expand path to expand the tilde character to the user's home
+ config[:config_file] = File.expand_path(config[:config_file])
+ if File.exists?(config[:config_file])
+ confirm("Overwrite #{config[:config_file]}")
+ end
+ end
+
+ def ask_user_for_config
+ server_name = guess_servername
+ @chef_server = config[:chef_server_url] || ask_question("Please enter the chef server URL: ", :default => "http://#{server_name}:4000")
+ if config[:initial]
+ @new_client_name = config[:node_name] || ask_question("Please enter a clientname for the new client: ", :default => Etc.getlogin)
+ @admin_client_name = config[:admin_client_name] || ask_question("Please enter the existing admin clientname: ", :default => 'chef-webui')
+ @admin_client_key = config[:admin_client_key] || ask_question("Please enter the location of the existing admin client's private key: ", :default => '/etc/chef/webui.pem')
+ @admin_client_key = File.expand_path(@admin_client_key)
+ else
+ @new_client_name = config[:node_name] || ask_question("Please enter an existing username or clientname for the API: ", :default => Etc.getlogin)
+ end
+ @validation_client_name = config[:validation_client_name] || ask_question("Please enter the validation clientname: ", :default => 'chef-validator')
+ @validation_key = config[:validation_key] || ask_question("Please enter the location of the validation key: ", :default => '/etc/chef/validation.pem')
+ @validation_key = File.expand_path(@validation_key)
+ @chef_repo = config[:repository] || ask_question("Please enter the path to a chef repository (or leave blank): ")
+
+ @new_client_key = config[:client_key] || File.join(chef_config_path, "#{@new_client_name}.pem")
+ @new_client_key = File.expand_path(@new_client_key)
+ end
+
+ def guess_servername
+ o = Ohai::System.new
+ o.require_plugin 'os'
+ o.require_plugin 'hostname'
+ o[:fqdn] || 'localhost'
+ end
+
+ def config_file
+ config[:config_file]
+ end
+
+ def chef_config_path
+ File.dirname(config_file)
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/configure_client.rb b/lib/chef/knife/configure_client.rb
new file mode 100644
index 0000000000..838d9a1f58
--- /dev/null
+++ b/lib/chef/knife/configure_client.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class ConfigureClient < Knife
+ banner "knife configure client DIRECTORY"
+
+ def run
+ unless @config_dir = @name_args[0]
+ ui.fatal "You must provide the directory to put the files in"
+ show_usage
+ exit(1)
+ end
+
+ ui.info("Creating client configuration")
+ FileUtils.mkdir_p(@config_dir)
+ ui.info("Writing client.rb")
+ File.open(File.join(@config_dir, "client.rb"), "w") do |file|
+ file.puts('log_level :info')
+ file.puts('log_location STDOUT')
+ file.puts("chef_server_url '#{Chef::Config[:chef_server_url]}'")
+ file.puts("validation_client_name '#{Chef::Config[:validation_client_name]}'")
+ end
+ ui.info("Writing validation.pem")
+ File.open(File.join(@config_dir, 'validation.pem'), "w") do |validation|
+ validation.puts(IO.read(Chef::Config[:validation_key]))
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_bulk_delete.rb b/lib/chef/knife/cookbook_bulk_delete.rb
new file mode 100644
index 0000000000..f8ad74d856
--- /dev/null
+++ b/lib/chef/knife/cookbook_bulk_delete.rb
@@ -0,0 +1,72 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookBulkDelete < Knife
+
+ deps do
+ require 'chef/knife/cookbook_delete'
+ require 'chef/cookbook_version'
+ end
+
+ option :purge, :short => '-p', :long => '--purge', :boolean => true, :description => 'Permanently remove files from backing data store'
+
+ banner "knife cookbook bulk delete REGEX (options)"
+
+ def run
+ unless regex_str = @name_args.first
+ ui.fatal("You must supply a regular expression to match the results against")
+ exit 42
+ end
+
+ regex = Regexp.new(regex_str)
+
+ all_cookbooks = Chef::CookbookVersion.list
+ cookbooks_names = all_cookbooks.keys.grep(regex)
+ cookbooks_to_delete = cookbooks_names.inject({}) { |hash, name| hash[name] = all_cookbooks[name];hash }
+ ui.msg "All versions of the following cookbooks will be deleted:"
+ ui.msg ""
+ ui.msg ui.list(cookbooks_to_delete.keys.sort, :columns_down)
+ ui.msg ""
+
+ unless config[:yes]
+ ui.confirm("Do you really want to delete these cookbooks? (Y/N) ", false)
+
+ if config[:purge]
+ ui.msg("Files that are common to multiple cookbooks are shared, so purging the files may break other cookbooks.")
+ ui.confirm("Are you sure you want to purge files instead of just deleting the cookbooks")
+ end
+ ui.msg ""
+ end
+
+
+ cookbooks_names.each do |cookbook_name|
+ versions = rest.get_rest("cookbooks/#{cookbook_name}")[cookbook_name]["versions"].map {|v| v["version"]}.flatten
+ versions.each do |version|
+ object = rest.delete_rest("cookbooks/#{cookbook_name}/#{version}#{config[:purge] ? "?purge=true" : ""}")
+ ui.info("Deleted cookbook #{cookbook_name.ljust(25)} [#{version}]")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_create.rb b/lib/chef/knife/cookbook_create.rb
new file mode 100644
index 0000000000..c2e92e6b42
--- /dev/null
+++ b/lib/chef/knife/cookbook_create.rb
@@ -0,0 +1,297 @@
+#
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookCreate < Knife
+
+ deps do
+ require 'chef/json_compat'
+ require 'uri'
+ require 'fileutils'
+ end
+
+ banner "knife cookbook create COOKBOOK (options)"
+
+ option :cookbook_path,
+ :short => "-o PATH",
+ :long => "--cookbook-path PATH",
+ :description => "The directory where the cookbook will be created"
+
+ option :readme_format,
+ :short => "-r FORMAT",
+ :long => "--readme-format FORMAT",
+ :description => "Format of the README file, supported formats are 'md' (markdown) and 'rdoc' (rdoc)"
+
+ option :cookbook_license,
+ :short => "-I LICENSE",
+ :long => "--license LICENSE",
+ :description => "License for cookbook, apachev2, gplv2, gplv3, mit or none"
+
+ option :cookbook_copyright,
+ :short => "-C COPYRIGHT",
+ :long => "--copyright COPYRIGHT",
+ :description => "Name of Copyright holder"
+
+ option :cookbook_email,
+ :short => "-m EMAIL",
+ :long => "--email EMAIL",
+ :description => "Email address of cookbook maintainer"
+
+ def run
+ self.config = Chef::Config.merge!(config)
+ if @name_args.length < 1
+ show_usage
+ ui.fatal("You must specify a cookbook name")
+ exit 1
+ end
+
+ if default_cookbook_path_empty? && parameter_empty?(config[:cookbook_path])
+ raise ArgumentError, "Default cookbook_path is not specified in the knife.rb config file, and a value to -o is not provided. Nowhere to write the new cookbook to."
+ end
+
+ cookbook_path = File.expand_path(Array(config[:cookbook_path]).first)
+ cookbook_name = @name_args.first
+ copyright = config[:cookbook_copyright] || "YOUR_COMPANY_NAME"
+ email = config[:cookbook_email] || "YOUR_EMAIL"
+ license = ((config[:cookbook_license] != "false") && config[:cookbook_license]) || "none"
+ readme_format = ((config[:readme_format] != "false") && config[:readme_format]) || "md"
+ create_cookbook(cookbook_path,cookbook_name, copyright, license)
+ create_readme(cookbook_path,cookbook_name,readme_format)
+ create_changelog(cookbook_path,cookbook_name)
+ create_metadata(cookbook_path,cookbook_name, copyright, email, license,readme_format)
+ end
+
+ def create_cookbook(dir, cookbook_name, copyright, license)
+ msg("** Creating cookbook #{cookbook_name}")
+ FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "attributes")}"
+ FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "recipes")}"
+ FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "definitions")}"
+ FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "libraries")}"
+ FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "resources")}"
+ FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "providers")}"
+ FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "files", "default")}"
+ FileUtils.mkdir_p "#{File.join(dir, cookbook_name, "templates", "default")}"
+ unless File.exists?(File.join(dir, cookbook_name, "recipes", "default.rb"))
+ open(File.join(dir, cookbook_name, "recipes", "default.rb"), "w") do |file|
+ file.puts <<-EOH
+#
+# Cookbook Name:: #{cookbook_name}
+# Recipe:: default
+#
+# Copyright #{Time.now.year}, #{copyright}
+#
+EOH
+ case license
+ when "apachev2"
+ file.puts <<-EOH
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+EOH
+ when "gplv2"
+ file.puts <<-EOH
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+EOH
+ when "gplv3"
+ file.puts <<-EOH
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+EOH
+ when "mit"
+ file.puts <<-EOH
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+EOH
+ when "none"
+ file.puts <<-EOH
+# All rights reserved - Do Not Redistribute
+#
+EOH
+ end
+ end
+ end
+ end
+
+ def create_changelog(dir, cookbook_name)
+ msg("** Creating CHANGELOG for cookbook: #{cookbook_name}")
+ unless File.exists?(File.join(dir,cookbook_name,'CHANGELOG.md'))
+ open(File.join(dir, cookbook_name, 'CHANGELOG.md'),'w') do |file|
+ file.puts <<-EOH
+# CHANGELOG for #{cookbook_name}
+
+This file is used to list changes made in each version of #{cookbook_name}.
+
+## 0.1.0:
+
+* Initial release of #{cookbook_name}
+
+- - -
+Check the [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) for help with Markdown.
+
+The [Github Flavored Markdown page](http://github.github.com/github-flavored-markdown/) describes the differences between markdown on github and standard markdown.
+EOH
+ end
+ end
+ end
+
+ def create_readme(dir, cookbook_name,readme_format)
+ msg("** Creating README for cookbook: #{cookbook_name}")
+ unless File.exists?(File.join(dir, cookbook_name, "README.#{readme_format}"))
+ open(File.join(dir, cookbook_name, "README.#{readme_format}"), "w") do |file|
+ case readme_format
+ when "rdoc"
+ file.puts <<-EOH
+= DESCRIPTION:
+
+= REQUIREMENTS:
+
+= ATTRIBUTES:
+
+= USAGE:
+
+EOH
+ when "md","mkd","txt"
+ file.puts <<-EOH
+Description
+===========
+
+Requirements
+============
+
+Attributes
+==========
+
+Usage
+=====
+
+EOH
+ else
+ file.puts <<-EOH
+Description
+
+Requirements
+
+Attributes
+
+Usage
+
+EOH
+ end
+ end
+ end
+ end
+
+ def create_metadata(dir, cookbook_name, copyright, email, license,readme_format)
+ msg("** Creating metadata for cookbook: #{cookbook_name}")
+
+ license_name = case license
+ when "apachev2"
+ "Apache 2.0"
+ when "gplv2"
+ "GNU Public License 2.0"
+ when "gplv3"
+ "GNU Public License 3.0"
+ when "mit"
+ "MIT"
+ when "none"
+ "All rights reserved"
+ end
+
+ unless File.exists?(File.join(dir, cookbook_name, "metadata.rb"))
+ open(File.join(dir, cookbook_name, "metadata.rb"), "w") do |file|
+ if File.exists?(File.join(dir, cookbook_name, "README.#{readme_format}"))
+ long_description = "long_description IO.read(File.join(File.dirname(__FILE__), 'README.#{readme_format}'))"
+ end
+ file.puts <<-EOH
+maintainer "#{copyright}"
+maintainer_email "#{email}"
+license "#{license_name}"
+description "Installs/Configures #{cookbook_name}"
+#{long_description}
+version "0.1.0"
+EOH
+ end
+ end
+ end
+
+ private
+
+ def default_cookbook_path_empty?
+ Chef::Config[:cookbook_path].nil? || Chef::Config[:cookbook_path].empty?
+ end
+
+ def parameter_empty?(parameter)
+ parameter.nil? || parameter.empty?
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_delete.rb b/lib/chef/knife/cookbook_delete.rb
new file mode 100644
index 0000000000..f436d270bd
--- /dev/null
+++ b/lib/chef/knife/cookbook_delete.rb
@@ -0,0 +1,151 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookDelete < Knife
+
+ attr_accessor :cookbook_name, :version
+
+ deps do
+ require 'chef/cookbook_version'
+ end
+
+ option :all, :short => '-a', :long => '--all', :boolean => true, :description => 'delete all versions'
+
+ option :purge, :short => '-p', :long => '--purge', :boolean => true, :description => 'Permanently remove files from backing data store'
+
+ banner "knife cookbook delete COOKBOOK VERSION (options)"
+
+ def run
+ confirm("Files that are common to multiple cookbooks are shared, so purging the files may disable other cookbooks. Are you sure you want to purge files instead of just deleting the cookbook") if config[:purge]
+ @cookbook_name, @version = name_args
+ if @cookbook_name && @version
+ delete_explicit_version
+ elsif @cookbook_name && config[:all]
+ delete_all_versions
+ elsif @cookbook_name && @version.nil?
+ delete_without_explicit_version
+ elsif @cookbook_name.nil?
+ show_usage
+ ui.fatal("You must provide the name of the cookbook to delete")
+ exit(1)
+ end
+ end
+
+ def delete_explicit_version
+ delete_object(Chef::CookbookVersion, "#{@cookbook_name} version #{@version}", "cookbook") do
+ delete_request("cookbooks/#{@cookbook_name}/#{@version}")
+ end
+ end
+
+ def delete_all_versions
+ confirm("Do you really want to delete all versions of #{@cookbook_name}")
+ delete_all_without_confirmation
+ end
+
+ def delete_all_without_confirmation
+ # look up the available versions again just in case the user
+ # got to the list of versions to delete and selected 'all'
+ # and also a specific version
+ @available_versions = nil
+ Array(available_versions).each do |version|
+ delete_version_without_confirmation(version)
+ end
+ end
+
+ def delete_without_explicit_version
+ if available_versions.nil?
+ # we already logged an error or 2 about it, so just bail
+ exit(1)
+ elsif available_versions.size == 1
+ @version = available_versions.first
+ delete_explicit_version
+ else
+ versions_to_delete = ask_which_versions_to_delete
+ delete_versions_without_confirmation(versions_to_delete)
+ end
+ end
+
+ def available_versions
+ @available_versions ||= rest.get_rest("cookbooks/#{@cookbook_name}").map do |name, url_and_version|
+ url_and_version["versions"].map {|url_by_version| url_by_version["version"]}
+ end.flatten
+ rescue Net::HTTPServerException => e
+ if e.to_s =~ /^404/
+ ui.error("Cannot find a cookbook named #{@cookbook_name} to delete")
+ nil
+ else
+ raise
+ end
+ end
+
+ def ask_which_versions_to_delete
+ question = "Which version(s) do you want to delete?\n"
+ valid_responses = {}
+ available_versions.each_with_index do |version, index|
+ valid_responses[(index + 1).to_s] = version
+ question << "#{index + 1}. #{@cookbook_name} #{version}\n"
+ end
+ valid_responses[(available_versions.size + 1).to_s] = :all
+ question << "#{available_versions.size + 1}. All versions\n\n"
+ responses = ask_question(question).split(',').map { |response| response.strip }
+
+ if responses.empty?
+ ui.error("No versions specified, exiting")
+ exit(1)
+ end
+ versions = responses.map do |response|
+ if version = valid_responses[response]
+ version
+ else
+ ui.error("#{response} is not a valid choice, skipping it")
+ end
+ end
+ versions.compact
+ end
+
+ def delete_version_without_confirmation(version)
+ object = delete_request("cookbooks/#{@cookbook_name}/#{version}")
+ output(format_for_display(object)) if config[:print_after]
+ ui.info("Deleted cookbook[#{@cookbook_name}][#{version}]")
+ end
+
+ def delete_versions_without_confirmation(versions)
+ versions.each do |version|
+ if version == :all
+ delete_all_without_confirmation
+ break
+ else
+ delete_version_without_confirmation(version)
+ end
+ end
+ end
+
+ private
+
+ def delete_request(path)
+ path += "?purge=true" if config[:purge]
+ rest.delete_rest(path)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_download.rb b/lib/chef/knife/cookbook_download.rb
new file mode 100644
index 0000000000..1da1121b22
--- /dev/null
+++ b/lib/chef/knife/cookbook_download.rb
@@ -0,0 +1,137 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookDownload < Knife
+
+ attr_reader :version
+ attr_accessor :cookbook_name
+
+ deps do
+ require 'chef/cookbook_version'
+ end
+
+ banner "knife cookbook download COOKBOOK [VERSION] (options)"
+
+ option :latest,
+ :short => "-N",
+ :long => "--latest",
+ :description => "The version of the cookbook to download",
+ :boolean => true
+
+ option :download_directory,
+ :short => "-d DOWNLOAD_DIRECTORY",
+ :long => "--dir DOWNLOAD_DIRECTORY",
+ :description => "The directory to download the cookbook into",
+ :default => Dir.pwd
+
+ option :force,
+ :short => "-f",
+ :long => "--force",
+ :description => "Force download over the download directory if it exists"
+
+ # TODO: tim/cw: 5-23-2010: need to implement knife-side
+ # specificity for downloads - need to implement --platform and
+ # --fqdn here
+ def run
+ @cookbook_name, @version = @name_args
+
+ if @cookbook_name.nil?
+ show_usage
+ ui.fatal("You must specify a cookbook name")
+ exit 1
+ elsif @version.nil?
+ @version = determine_version
+ end
+
+ ui.info("Downloading #{@cookbook_name} cookbook version #{@version}")
+
+ cookbook = rest.get_rest("cookbooks/#{@cookbook_name}/#{@version}")
+ manifest = cookbook.manifest
+
+ basedir = File.join(config[:download_directory], "#{@cookbook_name}-#{cookbook.version}")
+ if File.exists?(basedir)
+ if config[:force]
+ Chef::Log.debug("Deleting #{basedir}")
+ FileUtils.rm_rf(basedir)
+ else
+ ui.fatal("Directory #{basedir} exists, use --force to overwrite")
+ exit
+ end
+ end
+
+ Chef::CookbookVersion::COOKBOOK_SEGMENTS.each do |segment|
+ next unless manifest.has_key?(segment)
+ ui.info("Downloading #{segment}")
+ manifest[segment].each do |segment_file|
+ dest = File.join(basedir, segment_file['path'].gsub('/', File::SEPARATOR))
+ Chef::Log.debug("Downloading #{segment_file['path']} to #{dest}")
+ FileUtils.mkdir_p(File.dirname(dest))
+ rest.sign_on_redirect = false
+ tempfile = rest.get_rest(segment_file['url'], true)
+ FileUtils.mv(tempfile.path, dest)
+ end
+ end
+ ui.info("Cookbook downloaded to #{basedir}")
+ end
+
+ def determine_version
+ if available_versions.size == 1
+ @version = available_versions.first
+ elsif config[:latest]
+ @version = available_versions.map { |v| Chef::Version.new(v) }.sort.last
+ else
+ ask_which_version
+ end
+ end
+
+ def available_versions
+ @available_versions ||= begin
+ versions = Chef::CookbookVersion.available_versions(@cookbook_name).map do |version|
+ Chef::Version.new(version)
+ end
+ versions.sort!
+ versions
+ end
+ @available_versions
+ end
+
+ def ask_which_version
+ question = "Which version do you want to download?\n"
+ valid_responses = {}
+ available_versions.each_with_index do |version, index|
+ valid_responses[(index + 1).to_s] = version
+ question << "#{index + 1}. #{@cookbook_name} #{version}\n"
+ end
+ question += "\n"
+ response = ask_question(question).strip
+
+ unless @version = valid_responses[response]
+ ui.error("'#{response}' is not a valid value.")
+ exit(1)
+ end
+ @version
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_list.rb b/lib/chef/knife/cookbook_list.rb
new file mode 100644
index 0000000000..75f18a154b
--- /dev/null
+++ b/lib/chef/knife/cookbook_list.rb
@@ -0,0 +1,47 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2010, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookList < Knife
+
+ banner "knife cookbook list (options)"
+
+ option :with_uri,
+ :short => "-w",
+ :long => "--with-uri",
+ :description => "Show corresponding URIs"
+
+ option :all_versions,
+ :short => "-a",
+ :long => "--all",
+ :description => "Show all available versions."
+
+ def run
+ env = config[:environment]
+ num_versions = config[:all_versions] ? "num_versions=all" : "num_versions=1"
+ api_endpoint = env ? "/environments/#{env}/cookbooks?#{num_versions}" : "/cookbooks?#{num_versions}"
+ cookbook_versions = rest.get_rest(api_endpoint)
+ ui.output(format_cookbook_list_for_display(cookbook_versions))
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_metadata.rb b/lib/chef/knife/cookbook_metadata.rb
new file mode 100644
index 0000000000..dfa69ae39f
--- /dev/null
+++ b/lib/chef/knife/cookbook_metadata.rb
@@ -0,0 +1,108 @@
+#
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookMetadata < Knife
+
+ deps do
+ require 'chef/cookbook_loader'
+ require 'chef/cookbook/metadata'
+ end
+
+ banner "knife cookbook metadata COOKBOOK (options)"
+
+ option :cookbook_path,
+ :short => "-o PATH:PATH",
+ :long => "--cookbook-path PATH:PATH",
+ :description => "A colon-separated path to look for cookbooks in",
+ :proc => lambda { |o| o.split(":") }
+
+ option :all,
+ :short => "-a",
+ :long => "--all",
+ :description => "Generate metadata for all cookbooks, rather than just a single cookbook"
+
+ def run
+ config[:cookbook_path] ||= Chef::Config[:cookbook_path]
+
+ if config[:all]
+ cl = Chef::CookbookLoader.new(config[:cookbook_path])
+ cl.load_cookbooks
+ cl.each do |cname, cookbook|
+ generate_metadata(cname.to_s)
+ end
+ else
+ cookbook_name = @name_args[0]
+ if cookbook_name.nil? || cookbook_name.empty?
+ ui.error "You must specify the cookbook to generate metadata for, or use the --all option."
+ exit 1
+ end
+ generate_metadata(cookbook_name)
+ end
+ end
+
+ def generate_metadata(cookbook)
+ Array(config[:cookbook_path]).reverse.each do |path|
+ file = File.expand_path(File.join(path, cookbook, 'metadata.rb'))
+ if File.exists?(file)
+ generate_metadata_from_file(cookbook, file)
+ else
+ validate_metadata_json(path, cookbook)
+ end
+ end
+ end
+
+ def generate_metadata_from_file(cookbook, file)
+ ui.info("Generating metadata for #{cookbook} from #{file}")
+ md = Chef::Cookbook::Metadata.new
+ md.name(cookbook)
+ md.from_file(file)
+ json_file = File.join(File.dirname(file), 'metadata.json')
+ File.open(json_file, "w") do |f|
+ f.write(Chef::JSONCompat.to_json_pretty(md))
+ end
+ generated = true
+ Chef::Log.debug("Generated #{json_file}")
+ rescue Exceptions::ObsoleteDependencySyntax, Exceptions::InvalidVersionConstraint => e
+ ui.stderr.puts "ERROR: The cookbook '#{cookbook}' contains invalid or obsolete metadata syntax."
+ ui.stderr.puts "in #{file}:"
+ ui.stderr.puts
+ ui.stderr.puts e.message
+ exit 1
+ end
+
+ def validate_metadata_json(path, cookbook)
+ json_file = File.join(path, cookbook, 'metadata.json')
+ if File.exist?(json_file)
+ Chef::Cookbook::Metadata.validate_json(IO.read(json_file))
+ end
+ rescue Exceptions::ObsoleteDependencySyntax, Exceptions::InvalidVersionConstraint => e
+ ui.stderr.puts "ERROR: The cookbook '#{cookbook}' contains invalid or obsolete metadata syntax."
+ ui.stderr.puts "in #{json_file}:"
+ ui.stderr.puts
+ ui.stderr.puts e.message
+ exit 1
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_metadata_from_file.rb b/lib/chef/knife/cookbook_metadata_from_file.rb
new file mode 100644
index 0000000000..eb1afa8b11
--- /dev/null
+++ b/lib/chef/knife/cookbook_metadata_from_file.rb
@@ -0,0 +1,44 @@
+#
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Matthew Kent (<mkent@magoazul.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# Copyright:: Copyright (c) 2010 Matthew Kent
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookMetadataFromFile < Knife
+
+ deps do
+ require 'chef/cookbook/metadata'
+ end
+
+ banner "knife cookbook metadata from FILE (options)"
+
+ def run
+ file = @name_args[0]
+ cookbook = File.basename(File.dirname(file))
+
+ @metadata = Chef::Knife::CookbookMetadata.new
+ @metadata.generate_metadata_from_file(cookbook, file)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_show.rb b/lib/chef/knife/cookbook_show.rb
new file mode 100644
index 0000000000..3545d20817
--- /dev/null
+++ b/lib/chef/knife/cookbook_show.rb
@@ -0,0 +1,102 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookShow < Knife
+
+ deps do
+ require 'chef/json_compat'
+ require 'uri'
+ require 'chef/cookbook_version'
+ end
+
+ banner "knife cookbook show COOKBOOK [VERSION] [PART] [FILENAME] (options)"
+
+ option :fqdn,
+ :short => "-f FQDN",
+ :long => "--fqdn FQDN",
+ :description => "The FQDN of the host to see the file for"
+
+ option :platform,
+ :short => "-p PLATFORM",
+ :long => "--platform PLATFORM",
+ :description => "The platform to see the file for"
+
+ option :platform_version,
+ :short => "-V VERSION",
+ :long => "--platform-version VERSION",
+ :description => "The platform version to see the file for"
+
+ option :with_uri,
+ :short => "-w",
+ :long => "--with-uri",
+ :description => "Show corresponding URIs"
+
+ def run
+ case @name_args.length
+ when 4 # We are showing a specific file
+ node = Hash.new
+ node[:fqdn] = config[:fqdn] if config.has_key?(:fqdn)
+ node[:platform] = config[:platform] if config.has_key?(:platform)
+ node[:platform_version] = config[:platform_version] if config.has_key?(:platform_version)
+
+ class << node
+ def attribute?(name)
+ has_key?(name)
+ end
+ end
+
+ cookbook_name, segment, filename = @name_args[0], @name_args[2], @name_args[3]
+ cookbook_version = @name_args[1] == 'latest' ? '_latest' : @name_args[1]
+
+ cookbook = rest.get_rest("cookbooks/#{cookbook_name}/#{cookbook_version}")
+ manifest_entry = cookbook.preferred_manifest_record(node, segment, filename)
+ temp_file = rest.get_rest(manifest_entry[:url], true)
+
+ # the temp file is cleaned up elsewhere
+ temp_file.open if temp_file.closed?
+ pretty_print(temp_file.read)
+
+ when 3 # We are showing a specific part of the cookbook
+ cookbook_version = @name_args[1] == 'latest' ? '_latest' : @name_args[1]
+ result = rest.get_rest("cookbooks/#{@name_args[0]}/#{cookbook_version}")
+ output(result.manifest[@name_args[2]])
+ when 2 # We are showing the whole cookbook data
+ cookbook_version = @name_args[1] == 'latest' ? '_latest' : @name_args[1]
+ output(rest.get_rest("cookbooks/#{@name_args[0]}/#{cookbook_version}"))
+ when 1 # We are showing the cookbook versions (all of them)
+ cookbook_name = @name_args[0]
+ env = config[:environment]
+ api_endpoint = env ? "environments/#{env}/cookbooks/#{cookbook_name}" : "cookbooks/#{cookbook_name}"
+ output(format_cookbook_list_for_display(rest.get_rest(api_endpoint)))
+ when 0
+ show_usage
+ ui.fatal("You must specify a cookbook name")
+ exit 1
+ end
+ end
+ end
+ end
+end
+
+
+
+
diff --git a/lib/chef/knife/cookbook_site_download.rb b/lib/chef/knife/cookbook_site_download.rb
new file mode 100644
index 0000000000..645b1728e6
--- /dev/null
+++ b/lib/chef/knife/cookbook_site_download.rb
@@ -0,0 +1,109 @@
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookSiteDownload < Knife
+
+ deps do
+ require 'fileutils'
+ end
+
+ banner "knife cookbook site download COOKBOOK [VERSION] (options)"
+ category "cookbook site"
+
+ option :file,
+ :short => "-f FILE",
+ :long => "--file FILE",
+ :description => "The filename to write to"
+
+ option :force,
+ :long => "--force",
+ :description => "Force download deprecated version"
+
+ def run
+ if current_cookbook_deprecated?
+ message = 'DEPRECATION: This cookbook has been deprecated. '
+ message << "It has been replaced by #{replacement_cookbook}."
+ ui.warn message
+
+ unless config[:force]
+ ui.warn 'Use --force to force download deprecated cookbook.'
+ return
+ end
+ end
+
+ download_cookbook
+ end
+
+ def version
+ @version = desired_cookbook_data['version']
+ end
+
+ private
+ def cookbooks_api_url
+ 'http://cookbooks.opscode.com/api/v1/cookbooks'
+ end
+
+ def current_cookbook_data
+ @current_cookbook_data ||= begin
+ noauth_rest.get_rest "#{cookbooks_api_url}/#{@name_args[0]}"
+ end
+ end
+
+ def current_cookbook_deprecated?
+ current_cookbook_data['deprecated'] == true
+ end
+
+ def desired_cookbook_data
+ @desired_cookbook_data ||= begin
+ uri = if @name_args.length == 1
+ current_cookbook_data['latest_version']
+ else
+ specific_cookbook_version_url
+ end
+
+ noauth_rest.get_rest uri
+ end
+ end
+
+ def download_cookbook
+ ui.info "Downloading #{@name_args[0]} from the cookbooks site at version #{version} to #{download_location}"
+ noauth_rest.sign_on_redirect = false
+ tf = noauth_rest.get_rest desired_cookbook_data["file"], true
+
+ ::FileUtils.cp tf.path, download_location
+ ui.info "Cookbook saved: #{download_location}"
+ end
+
+ def download_location
+ config[:file] ||= File.join Dir.pwd, "#{@name_args[0]}-#{version}.tar.gz"
+ config[:file]
+ end
+
+ def replacement_cookbook
+ replacement = File.basename(current_cookbook_data['replacement'])
+ end
+
+ def specific_cookbook_version_url
+ "#{cookbooks_api_url}/#{@name_args[0]}/versions/#{@name_args[1].gsub('.', '_')}"
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_site_install.rb b/lib/chef/knife/cookbook_site_install.rb
new file mode 100644
index 0000000000..b2e0d84751
--- /dev/null
+++ b/lib/chef/knife/cookbook_site_install.rb
@@ -0,0 +1,155 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+require 'shellwords'
+
+class Chef
+ class Knife
+
+ class CookbookSiteInstall < Knife
+
+ deps do
+ require 'chef/mixin/shell_out'
+ require 'chef/knife/core/cookbook_scm_repo'
+ require 'chef/cookbook/metadata'
+ end
+
+ banner "knife cookbook site install COOKBOOK [VERSION] (options)"
+ category "cookbook site"
+
+ option :no_deps,
+ :short => "-D",
+ :long => "--skip-dependencies",
+ :boolean => true,
+ :default => false,
+ :description => "Skips automatic dependency installation."
+
+ option :cookbook_path,
+ :short => "-o PATH:PATH",
+ :long => "--cookbook-path PATH:PATH",
+ :description => "A colon-separated path to look for cookbooks in",
+ :proc => lambda { |o| o.split(":") }
+
+ option :default_branch,
+ :short => "-B BRANCH",
+ :long => "--branch BRANCH",
+ :description => "Default branch to work with",
+ :default => "master"
+
+ option :use_current_branch,
+ :short => "-b",
+ :long => "--use-current-branch",
+ :description => "Use the current branch",
+ :boolean => true,
+ :default => false
+
+ attr_reader :cookbook_name
+ attr_reader :vendor_path
+
+ def run
+ extend Chef::Mixin::ShellOut
+
+ if config[:cookbook_path]
+ Chef::Config[:cookbook_path] = config[:cookbook_path]
+ else
+ config[:cookbook_path] = Chef::Config[:cookbook_path]
+ end
+
+ @cookbook_name = parse_name_args!
+ # Check to ensure we have a valid source of cookbooks before continuing
+ #
+ @install_path = File.expand_path(config[:cookbook_path].first)
+ ui.info "Installing #@cookbook_name to #{@install_path}"
+
+ @repo = CookbookSCMRepo.new(@install_path, ui, config)
+ #cookbook_path = File.join(vendor_path, name_args[0])
+ upstream_file = File.join(@install_path, "#{@cookbook_name}.tar.gz")
+
+ @repo.sanity_check
+ unless config[:use_current_branch]
+ @repo.reset_to_default_state
+ @repo.prepare_to_import(@cookbook_name)
+ end
+
+ downloader = download_cookbook_to(upstream_file)
+ clear_existing_files(File.join(@install_path, @cookbook_name))
+ extract_cookbook(upstream_file, downloader.version)
+
+ # TODO: it'd be better to store these outside the cookbook repo and
+ # keep them around, e.g., in ~/Library/Caches on OS X.
+ ui.info("removing downloaded tarball")
+ File.unlink(upstream_file)
+
+ if @repo.finalize_updates_to(@cookbook_name, downloader.version)
+ unless config[:use_current_branch]
+ @repo.reset_to_default_state
+ end
+ @repo.merge_updates_from(@cookbook_name, downloader.version)
+ else
+ unless config[:use_current_branch]
+ @repo.reset_to_default_state
+ end
+ end
+
+ unless config[:no_deps]
+ md = Chef::Cookbook::Metadata.new
+ md.from_file(File.join(@install_path, @cookbook_name, "metadata.rb"))
+ md.dependencies.each do |cookbook, version_list|
+ # Doesn't do versions.. yet
+ nv = self.class.new
+ nv.config = config
+ nv.name_args = [ cookbook ]
+ nv.run
+ end
+ end
+ end
+
+ def parse_name_args!
+ if name_args.empty?
+ ui.error("Please specify a cookbook to download and install.")
+ exit 1
+ elsif name_args.size >= 2
+ unless name_args.last.match(/^(\d+)(\.\d+){1,2}$/) and name_args.size == 2
+ ui.error("Installing multiple cookbooks at once is not supported.")
+ exit 1
+ end
+ end
+ name_args.first
+ end
+
+ def download_cookbook_to(download_path)
+ downloader = Chef::Knife::CookbookSiteDownload.new
+ downloader.config[:file] = download_path
+ downloader.name_args = name_args
+ downloader.run
+ downloader
+ end
+
+ def extract_cookbook(upstream_file, version)
+ ui.info("Uncompressing #{@cookbook_name} version #{version}.")
+ shell_out!("tar zxvf #{Shellwords.escape upstream_file}", :cwd => @install_path)
+ end
+
+ def clear_existing_files(cookbook_path)
+ ui.info("Removing pre-existing version.")
+ FileUtils.rmtree(cookbook_path) if File.directory?(cookbook_path)
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_site_list.rb b/lib/chef/knife/cookbook_site_list.rb
new file mode 100644
index 0000000000..96c4ef0eed
--- /dev/null
+++ b/lib/chef/knife/cookbook_site_list.rb
@@ -0,0 +1,62 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookSiteList < Knife
+
+ banner "knife cookbook site list (options)"
+ category "cookbook site"
+
+ option :with_uri,
+ :short => "-w",
+ :long => "--with-uri",
+ :description => "Show corresponding URIs"
+
+ def run
+ if config[:with_uri]
+ cookbooks = Hash.new
+ get_cookbook_list.each{ |k,v| cookbooks[k] = v['cookbook'] }
+ ui.output(format_for_display(cookbooks))
+ else
+ ui.msg(ui.list(get_cookbook_list.keys.sort, :columns_down))
+ end
+ end
+
+ def get_cookbook_list(items=10, start=0, cookbook_collection={})
+ cookbooks_url = "http://cookbooks.opscode.com/api/v1/cookbooks?items=#{items}&start=#{start}"
+ cr = noauth_rest.get_rest(cookbooks_url)
+ cr["items"].each do |cookbook|
+ cookbook_collection[cookbook["cookbook_name"]] = cookbook
+ end
+ new_start = start + cr["items"].length
+ if new_start < cr["total"]
+ get_cookbook_list(items, new_start, cookbook_collection)
+ else
+ cookbook_collection
+ end
+ end
+ end
+ end
+end
+
+
+
+
diff --git a/lib/chef/knife/cookbook_site_search.rb b/lib/chef/knife/cookbook_site_search.rb
new file mode 100644
index 0000000000..5df7d67327
--- /dev/null
+++ b/lib/chef/knife/cookbook_site_search.rb
@@ -0,0 +1,51 @@
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookSiteSearch < Knife
+
+ banner "knife cookbook site search QUERY (options)"
+ category "cookbook site"
+
+ def run
+ output(search_cookbook(name_args[0]))
+ end
+
+ def search_cookbook(query, items=10, start=0, cookbook_collection={})
+ cookbooks_url = "http://cookbooks.opscode.com/api/v1/search?q=#{query}&items=#{items}&start=#{start}"
+ cr = noauth_rest.get_rest(cookbooks_url)
+ cr["items"].each do |cookbook|
+ cookbook_collection[cookbook["cookbook_name"]] = cookbook
+ end
+ new_start = start + cr["items"].length
+ if new_start < cr["total"]
+ search_cookbook(query, items, new_start, cookbook_collection)
+ else
+ cookbook_collection
+ end
+ end
+ end
+ end
+end
+
+
+
+
+
diff --git a/lib/chef/knife/cookbook_site_share.rb b/lib/chef/knife/cookbook_site_share.rb
new file mode 100644
index 0000000000..77c4895dcc
--- /dev/null
+++ b/lib/chef/knife/cookbook_site_share.rb
@@ -0,0 +1,114 @@
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookSiteShare < Knife
+
+ deps do
+ require 'chef/cookbook_uploader'
+ require 'chef/cookbook_site_streaming_uploader'
+ end
+
+ banner "knife cookbook site share COOKBOOK CATEGORY (options)"
+ category "cookbook site"
+
+ option :cookbook_path,
+ :short => "-o PATH:PATH",
+ :long => "--cookbook-path PATH:PATH",
+ :description => "A colon-separated path to look for cookbooks in",
+ :proc => lambda { |o| Chef::Config.cookbook_path = o.split(":") }
+
+ def run
+ if @name_args.length < 2
+ show_usage
+ ui.fatal("You must specify the cookbook name and the category you want to share this cookbook to.")
+ exit 1
+ end
+
+ config[:cookbook_path] ||= Chef::Config[:cookbook_path]
+
+ cookbook_name = @name_args[0]
+ category = @name_args[1]
+ cl = Chef::CookbookLoader.new(config[:cookbook_path])
+ if cl.cookbook_exists?(cookbook_name)
+ cookbook = cl[cookbook_name]
+ Chef::CookbookUploader.new(cookbook,config[:cookbook_path]).validate_cookbooks
+ tmp_cookbook_dir = Chef::CookbookSiteStreamingUploader.create_build_dir(cookbook)
+ begin
+ Chef::Log.debug("Temp cookbook directory is #{tmp_cookbook_dir.inspect}")
+ ui.info("Making tarball #{cookbook_name}.tgz")
+ Chef::Mixin::Command.run_command(:command => "tar -czf #{cookbook_name}.tgz #{cookbook_name}", :cwd => tmp_cookbook_dir)
+ rescue => e
+ ui.error("Error making tarball #{cookbook_name}.tgz: #{e.message}. Set log level to debug (-l debug) for more information.")
+ Chef::Log.debug("\n#{e.backtrace.join("\n")}")
+ exit(1)
+ end
+
+ begin
+ do_upload("#{tmp_cookbook_dir}/#{cookbook_name}.tgz", category, Chef::Config[:node_name], Chef::Config[:client_key])
+ ui.info("Upload complete!")
+ Chef::Log.debug("Removing local staging directory at #{tmp_cookbook_dir}")
+ FileUtils.rm_rf tmp_cookbook_dir
+ rescue => e
+ ui.error("Error uploading cookbook #{cookbook_name} to the Opscode Cookbook Site: #{e.message}. Set log level to debug (-l debug) for more information.")
+ Chef::Log.debug("\n#{e.backtrace.join("\n")}")
+ exit(1)
+ end
+
+ else
+ ui.error("Could not find cookbook #{cookbook_name} in your cookbook path.")
+ exit(1)
+ end
+
+ end
+
+ def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename)
+ uri = "http://cookbooks.opscode.com/api/v1/cookbooks"
+
+ category_string = { 'category'=>cookbook_category }.to_json
+
+ http_resp = Chef::CookbookSiteStreamingUploader.post(uri, user_id, user_secret_filename, {
+ :tarball => File.open(cookbook_filename),
+ :cookbook => category_string
+ })
+
+ res = Chef::JSONCompat.from_json(http_resp.body)
+ if http_resp.code.to_i != 201
+ if res['error_messages']
+ if res['error_messages'][0] =~ /Version already exists/
+ ui.error "The same version of this cookbook already exists on the Opscode Cookbook Site."
+ exit(1)
+ else
+ ui.error "#{res['error_messages'][0]}"
+ exit(1)
+ end
+ else
+ ui.error "Unknown error while sharing cookbook"
+ ui.error "Server response: #{http_resp.body}"
+ exit(1)
+ end
+ end
+ res
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/knife/cookbook_site_show.rb b/lib/chef/knife/cookbook_site_show.rb
new file mode 100644
index 0000000000..a02dd61fc8
--- /dev/null
+++ b/lib/chef/knife/cookbook_site_show.rb
@@ -0,0 +1,60 @@
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookSiteShow < Knife
+
+ banner "knife cookbook site show COOKBOOK [VERSION] (options)"
+ category "cookbook site"
+
+ def run
+ output(format_for_display(get_cookbook_data))
+ end
+
+ def get_cookbook_data
+ case @name_args.length
+ when 1
+ noauth_rest.get_rest("http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}")
+ when 2
+ noauth_rest.get_rest("http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}/versions/#{name_args[1].gsub('.', '_')}")
+ end
+ end
+
+ def get_cookbook_list(items=10, start=0, cookbook_collection={})
+ cookbooks_url = "http://cookbooks.opscode.com/api/v1/cookbooks?items=#{items}&start=#{start}"
+ cr = noauth_rest.get_rest(cookbooks_url)
+ cr["items"].each do |cookbook|
+ cookbook_collection[cookbook["cookbook_name"]] = cookbook
+ end
+ new_start = start + cr["items"].length
+ if new_start < cr["total"]
+ get_cookbook_list(items, new_start, cookbook_collection)
+ else
+ cookbook_collection
+ end
+ end
+ end
+ end
+end
+
+
+
+
+
diff --git a/lib/chef/knife/cookbook_site_unshare.rb b/lib/chef/knife/cookbook_site_unshare.rb
new file mode 100644
index 0000000000..a2828549a0
--- /dev/null
+++ b/lib/chef/knife/cookbook_site_unshare.rb
@@ -0,0 +1,56 @@
+#
+# Author:: Stephen Delano (<stephen@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookSiteUnshare < Knife
+
+ deps do
+ require 'chef/json_compat'
+ end
+
+ banner "knife cookbook site unshare COOKBOOK"
+ category "cookbook site"
+
+ def run
+ @cookbook_name = @name_args[0]
+ if @cookbook_name.nil?
+ show_usage
+ ui.fatal "You must provide the name of the cookbook to unshare"
+ exit 1
+ end
+
+ confirm "Do you really want to unshare the cookbook #{@cookbook_name}"
+
+ begin
+ rest.delete_rest "http://cookbooks.opscode.com/api/v1/cookbooks/#{@name_args[0]}"
+ rescue Net::HTTPServerException => e
+ raise e unless e.message =~ /Forbidden/
+ ui.error "Forbidden: You must be the maintainer of #{@cookbook_name} to unshare it."
+ exit 1
+ end
+
+ ui.info "Unshared cookbook #{@cookbook_name}"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_site_vendor.rb b/lib/chef/knife/cookbook_site_vendor.rb
new file mode 100644
index 0000000000..82575958bd
--- /dev/null
+++ b/lib/chef/knife/cookbook_site_vendor.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+require 'chef/knife/cookbook_site_install'
+
+class Chef::Knife::CookbookSiteVendor < Chef::Knife::CookbookSiteInstall
+
+ def self.load_deps
+ superclass.load_deps
+ end
+
+ def self.options=(new_opts)
+ superclass.options = new_opts
+ end
+
+ def self.options
+ superclass.options
+ end
+
+ banner(<<-B)
+*************************************************
+DEPRECATED: please use knife cookbook site install
+*************************************************
+
+#{superclass.banner}
+B
+
+ category 'deprecated'
+
+end
diff --git a/lib/chef/knife/cookbook_test.rb b/lib/chef/knife/cookbook_test.rb
new file mode 100644
index 0000000000..dc8f12d135
--- /dev/null
+++ b/lib/chef/knife/cookbook_test.rb
@@ -0,0 +1,95 @@
+#
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Matthew Kent (<mkent@magoazul.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# Copyright:: Copyright (c) 2010 Matthew Kent
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookTest < Knife
+
+ deps do
+ require 'chef/checksum_cache'
+ require 'chef/cookbook/syntax_check'
+ end
+
+ banner "knife cookbook test [COOKBOOKS...] (options)"
+
+ option :cookbook_path,
+ :short => "-o PATH:PATH",
+ :long => "--cookbook-path PATH:PATH",
+ :description => "A colon-separated path to look for cookbooks in",
+ :proc => lambda { |o| o.split(":") }
+
+ option :all,
+ :short => "-a",
+ :long => "--all",
+ :description => "Test all cookbooks, rather than just a single cookbook"
+
+ def run
+ config[:cookbook_path] ||= Chef::Config[:cookbook_path]
+
+ checked_a_cookbook = false
+ if config[:all]
+ cl = cookbook_loader
+ cl.load_cookbooks
+ cl.each do |key, cookbook|
+ checked_a_cookbook = true
+ test_cookbook(key)
+ end
+ else
+ @name_args.each do |cb|
+ ui.info "checking #{cb}"
+ next unless cookbook_loader.cookbook_exists?(cb)
+ checked_a_cookbook = true
+ test_cookbook(cb)
+ end
+ end
+ unless checked_a_cookbook
+ ui.warn("No cookbooks to test in #{Array(config[:cookbook_path]).join(',')} - is your cookbook path misconfigured?")
+ end
+ end
+
+ def test_cookbook(cookbook)
+ ui.info("Running syntax check on #{cookbook}")
+ Array(config[:cookbook_path]).reverse.each do |path|
+ syntax_checker = Chef::Cookbook::SyntaxCheck.for_cookbook(cookbook, path)
+ test_ruby(syntax_checker)
+ test_templates(syntax_checker)
+ end
+ end
+
+
+ def test_ruby(syntax_checker)
+ ui.info("Validating ruby files")
+ exit(1) unless syntax_checker.validate_ruby_files
+ end
+
+ def test_templates(syntax_checker)
+ ui.info("Validating templates")
+ exit(1) unless syntax_checker.validate_templates
+ end
+
+ def cookbook_loader
+ @cookbook_loader ||= Chef::CookbookLoader.new(config[:cookbook_path])
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/cookbook_upload.rb b/lib/chef/knife/cookbook_upload.rb
new file mode 100644
index 0000000000..b9ecc02a54
--- /dev/null
+++ b/lib/chef/knife/cookbook_upload.rb
@@ -0,0 +1,295 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Nuo Yan (<yan.nuo@gmail.com>)
+# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class CookbookUpload < Knife
+
+ CHECKSUM = "checksum"
+ MATCH_CHECKSUM = /[0-9a-f]{32,}/
+
+ deps do
+ require 'chef/exceptions'
+ require 'chef/cookbook_loader'
+ require 'chef/cookbook_uploader'
+ end
+
+ banner "knife cookbook upload [COOKBOOKS...] (options)"
+
+ option :cookbook_path,
+ :short => "-o PATH:PATH",
+ :long => "--cookbook-path PATH:PATH",
+ :description => "A colon-separated path to look for cookbooks in",
+ :proc => lambda { |o| o.split(":") }
+
+ option :freeze,
+ :long => '--freeze',
+ :description => 'Freeze this version of the cookbook so that it cannot be overwritten',
+ :boolean => true
+
+ option :all,
+ :short => "-a",
+ :long => "--all",
+ :description => "Upload all cookbooks, rather than just a single cookbook"
+
+ option :force,
+ :long => '--force',
+ :boolean => true,
+ :description => "Update cookbook versions even if they have been frozen"
+
+ option :environment,
+ :short => '-E',
+ :long => '--environment ENVIRONMENT',
+ :description => "Set ENVIRONMENT's version dependency match the version you're uploading.",
+ :default => nil
+
+ option :depends,
+ :short => "-d",
+ :long => "--include-dependencies",
+ :description => "Also upload cookbook dependencies"
+
+ def run
+ # Sanity check before we load anything from the server
+ unless config[:all]
+ if @name_args.empty?
+ show_usage
+ ui.fatal("You must specify the --all flag or at least one cookbook name")
+ exit 1
+ end
+ end
+
+ config[:cookbook_path] ||= Chef::Config[:cookbook_path]
+
+ if @name_args.empty? and ! config[:all]
+ show_usage
+ ui.fatal("You must specify the --all flag or at least one cookbook name")
+ exit 1
+ end
+
+ assert_environment_valid!
+ version_constraints_to_update = {}
+ upload_failures = 0
+ upload_ok = 0
+
+ # Get a list of cookbooks and their versions from the server
+ # to check for the existence of a cookbook's dependencies.
+ @server_side_cookbooks = Chef::CookbookVersion.list_all_versions
+ justify_width = @server_side_cookbooks.map {|name| name.size}.max.to_i + 2
+ if config[:all]
+
+ cbs = []
+ cookbook_repo.each do |cookbook_name, cookbook|
+ cbs << cookbook
+ cookbook.freeze_version if config[:freeze]
+ version_constraints_to_update[cookbook_name] = cookbook.version
+ end
+ begin
+ upload(cbs, justify_width)
+ rescue Exceptions::CookbookFrozen
+ ui.warn("Not updating version constraints for some cookbooks in the environment as the cookbook is frozen.")
+ end
+ ui.info("Uploaded all cookbooks.")
+ else
+ if @name_args.empty?
+ show_usage
+ ui.error("You must specify the --all flag or at least one cookbook name")
+ exit 1
+ end
+
+ cookbooks_to_upload.each do |cookbook_name, cookbook|
+ cookbook.freeze_version if config[:freeze]
+ begin
+ upload([cookbook], justify_width)
+ upload_ok += 1
+ version_constraints_to_update[cookbook_name] = cookbook.version
+ rescue Exceptions::CookbookNotFoundInRepo => e
+ upload_failures += 1
+ ui.error("Could not find cookbook #{cookbook_name} in your cookbook path, skipping it")
+ Log.debug(e)
+ upload_failures += 1
+ rescue Exceptions::CookbookFrozen
+ ui.warn("Not updating version constraints for #{cookbook_name} in the environment as the cookbook is frozen.")
+ upload_failures += 1
+ end
+ end
+
+ upload_failures += @name_args.length - @cookbooks_to_upload.length
+
+ if upload_failures == 0
+ ui.info "Uploaded #{upload_ok} cookbook#{upload_ok > 1 ? "s" : ""}."
+ elsif upload_failures > 0 && upload_ok > 0
+ ui.warn "Uploaded #{upload_ok} cookbook#{upload_ok > 1 ? "s" : ""} ok but #{upload_failures} " +
+ "cookbook#{upload_failures > 1 ? "s" : ""} upload failed."
+ elsif upload_failures > 0 && upload_ok == 0
+ ui.error "Failed to upload #{upload_failures} cookbook#{upload_failures > 1 ? "s" : ""}."
+ exit 1
+ end
+ end
+
+ unless version_constraints_to_update.empty?
+ update_version_constraints(version_constraints_to_update) if config[:environment]
+ end
+ end
+
+ def cookbooks_to_upload
+ @cookbooks_to_upload ||=
+ if config[:all]
+ cookbook_repo.load_cookbooks
+ else
+ upload_set = {}
+ @name_args.each do |cookbook_name|
+ begin
+ if ! upload_set.has_key?(cookbook_name)
+ upload_set[cookbook_name] = cookbook_repo[cookbook_name]
+ if config[:depends]
+ upload_set[cookbook_name].metadata.dependencies.each { |dep, ver| @name_args << dep }
+ end
+ end
+ rescue Exceptions::CookbookNotFoundInRepo => e
+ ui.error("Could not find cookbook #{cookbook_name} in your cookbook path, skipping it")
+ Log.debug(e)
+ end
+ end
+ upload_set
+ end
+ end
+
+ def cookbook_repo
+ @cookbook_loader ||= begin
+ Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, config[:cookbook_path]) }
+ Chef::CookbookLoader.new(config[:cookbook_path])
+ end
+ end
+
+ def update_version_constraints(new_version_constraints)
+ new_version_constraints.each do |cookbook_name, version|
+ environment.cookbook_versions[cookbook_name] = "= #{version}"
+ end
+ environment.save
+ end
+
+ def environment
+ @environment ||= config[:environment] ? Environment.load(config[:environment]) : nil
+ end
+
+ def warn_about_cookbook_shadowing
+ unless cookbook_repo.merged_cookbooks.empty?
+ ui.warn "* " * 40
+ ui.warn(<<-WARNING)
+The cookbooks: #{cookbook_repo.merged_cookbooks.join(', ')} exist in multiple places in your cookbook_path.
+A composite version of these cookbooks has been compiled for uploading.
+
+#{ui.color('IMPORTANT:', :red, :bold)} In a future version of Chef, this behavior will be removed and you will no longer
+be able to have the same version of a cookbook in multiple places in your cookbook_path.
+WARNING
+ ui.warn "The affected cookbooks are located:"
+ ui.output ui.format_for_display(cookbook_repo.merged_cookbook_paths)
+ ui.warn "* " * 40
+ end
+ end
+
+ private
+
+ def assert_environment_valid!
+ environment
+ rescue Net::HTTPServerException => e
+ if e.response.code.to_s == "404"
+ ui.error "The environment #{config[:environment]} does not exist on the server, aborting."
+ Log.debug(e)
+ exit 1
+ else
+ raise
+ end
+ end
+
+ def upload(cookbooks, justify_width)
+ cookbooks.each do |cb|
+ ui.info("Uploading #{cb.name.to_s.ljust(justify_width + 10)} [#{cb.version}]")
+ check_for_broken_links!(cb)
+ check_for_dependencies!(cb)
+ end
+ Chef::CookbookUploader.new(cookbooks, config[:cookbook_path], :force => config[:force]).upload_cookbooks
+ rescue Net::HTTPServerException => e
+ case e.response.code
+ when "409"
+ ui.error "Version #{cookbook.version} of cookbook #{cookbook.name} is frozen. Use --force to override."
+ Log.debug(e)
+ raise Exceptions::CookbookFrozen
+ else
+ raise
+ end
+ end
+
+ def check_for_broken_links!(cookbook)
+ # MUST!! dup the cookbook version object--it memoizes its
+ # manifest object, but the manifest becomes invalid when you
+ # regenerate the metadata
+ broken_files = cookbook.dup.manifest_records_by_path.select do |path, info|
+ info[CHECKSUM].nil? || info[CHECKSUM] !~ MATCH_CHECKSUM
+ end
+ unless broken_files.empty?
+ broken_filenames = Array(broken_files).map {|path, info| path}
+ ui.error "The cookbook #{cookbook.name} has one or more broken files"
+ ui.error "This is probably caused by broken symlinks in the cookbook directory"
+ ui.error "The broken file(s) are: #{broken_filenames.join(' ')}"
+ exit 1
+ end
+ end
+
+ def check_for_dependencies!(cookbook)
+ # for each dependency, check if the version is on the server, or
+ # the version is in the cookbooks being uploaded. If not, exit and warn the user.
+ cookbook.metadata.dependencies.each do |cookbook_name, version|
+ unless check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version)
+ ui.error "Cookbook #{cookbook.name} depends on cookbook #{cookbook_name} version #{version},"
+ ui.error "which is not currently being uploaded and cannot be found on the server."
+ exit 1
+ end
+ end
+ end
+
+ def check_server_side_cookbooks(cookbook_name, version)
+ if @server_side_cookbooks[cookbook_name].nil?
+ false
+ else
+ versions = @server_side_cookbooks[cookbook_name]['versions'].collect {|versions| versions["version"]}
+ Log.debug "Versions of cookbook '#{cookbook_name}' returned by the server: #{versions.join(", ")}"
+ @server_side_cookbooks[cookbook_name]["versions"].each do |versions_hash|
+ if Chef::VersionConstraint.new(version).include?(versions_hash["version"])
+ Log.debug "Matched cookbook '#{cookbook_name}' with constraint '#{version}' to cookbook version '#{versions_hash['version']}' on the server"
+ return true
+ end
+ end
+ false
+ end
+ end
+
+ def check_uploading_cookbooks(cookbook_name, version)
+ if (! cookbooks_to_upload[cookbook_name].nil?) && Chef::VersionConstraint.new(version).include?(cookbooks_to_upload[cookbook_name].version)
+ Log.debug "Matched cookbook '#{cookbook_name}' with constraint '#{version}' to a local cookbook."
+ return true
+ end
+ false
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb
new file mode 100644
index 0000000000..71dc008d39
--- /dev/null
+++ b/lib/chef/knife/core/bootstrap_context.rb
@@ -0,0 +1,106 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/run_list'
+class Chef
+ class Knife
+ module Core
+ # Instances of BootstrapContext are the context objects (i.e., +self+) for
+ # bootstrap templates. For backwards compatability, they +must+ set the
+ # following instance variables:
+ # * @config - a hash of knife's config values
+ # * @run_list - the run list for the node to boostrap
+ #
+ class BootstrapContext
+
+ def initialize(config, run_list, chef_config)
+ @config = config
+ @run_list = run_list
+ @chef_config = chef_config
+ end
+
+ def bootstrap_version_string
+ if @config[:prerelease]
+ "--prerelease"
+ else
+ "--version #{chef_version}"
+ end
+ end
+
+ def bootstrap_environment
+ @chef_config[:environment] || '_default'
+ end
+
+ def validation_key
+ IO.read(@chef_config[:validation_key])
+ end
+
+ def encrypted_data_bag_secret
+ IO.read(@chef_config[:encrypted_data_bag_secret])
+ end
+
+ def config_content
+ client_rb = <<-CONFIG
+log_level :info
+log_location STDOUT
+chef_server_url "#{@chef_config[:chef_server_url]}"
+validation_client_name "#{@chef_config[:validation_client_name]}"
+CONFIG
+ if @config[:chef_node_name]
+ client_rb << %Q{node_name "#{@config[:chef_node_name]}"\n}
+ else
+ client_rb << "# Using default node name (fqdn)\n"
+ end
+
+ if knife_config[:bootstrap_proxy]
+ client_rb << %Q{http_proxy "#{knife_config[:bootstrap_proxy]}"\n}
+ client_rb << %Q{https_proxy "#{knife_config[:bootstrap_proxy]}"\n}
+ end
+
+ if @chef_config[:encrypted_data_bag_secret]
+ client_rb << %Q{encrypted_data_bag_secret "/etc/chef/encrypted_data_bag_secret"\n}
+ end
+
+ client_rb
+ end
+
+ def start_chef
+ # If the user doesn't have a client path configure, let bash use the PATH for what it was designed for
+ client_path = @chef_config[:chef_client_path] || 'chef-client'
+ s = "#{client_path} -j /etc/chef/first-boot.json"
+ s << " -E #{bootstrap_environment}" if chef_version.to_f != 0.9 # only use the -E option on Chef 0.10+
+ s
+ end
+
+ def knife_config
+ @chef_config.key?(:knife) ? @chef_config[:knife] : {}
+ end
+
+ def chef_version
+ knife_config[:bootstrap_version] || Chef::VERSION
+ end
+
+ def first_boot
+ (@config[:first_boot_attributes] || {}).merge(:run_list => @run_list)
+ end
+
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/core/cookbook_scm_repo.rb b/lib/chef/knife/core/cookbook_scm_repo.rb
new file mode 100644
index 0000000000..727cff3153
--- /dev/null
+++ b/lib/chef/knife/core/cookbook_scm_repo.rb
@@ -0,0 +1,160 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Knife
+ class CookbookSCMRepo
+
+ DIRTY_REPO = /^[\s]+M/
+
+ include Chef::Mixin::ShellOut
+
+ attr_reader :repo_path
+ attr_reader :default_branch
+ attr_reader :use_current_branch
+ attr_reader :ui
+
+ def initialize(repo_path, ui, opts={})
+ @repo_path = repo_path
+ @ui = ui
+ @default_branch = 'master'
+ @use_current_branch = false
+ apply_opts(opts)
+ end
+
+ def sanity_check
+ unless ::File.directory?(repo_path)
+ ui.error("The cookbook repo path #{repo_path} does not exist or is not a directory")
+ exit 1
+ end
+ unless git_repo?(repo_path)
+ ui.error "The cookbook repo #{repo_path} is not a git repository."
+ ui.info("Use `git init` to initialize a git repo")
+ exit 1
+ end
+ if use_current_branch
+ @default_branch = get_current_branch()
+ end
+ unless branch_exists?(default_branch)
+ ui.error "The default branch '#{default_branch}' does not exist"
+ ui.info "If this is a new git repo, make sure you have at least one commit before installing cookbooks"
+ exit 1
+ end
+ cmd = git('status --porcelain')
+ if cmd.stdout =~ DIRTY_REPO
+ ui.error "You have uncommitted changes to your cookbook repo (#{repo_path}):"
+ ui.msg cmd.stdout
+ ui.info "Commit or stash your changes before importing cookbooks"
+ exit 1
+ end
+ # TODO: any untracked files in the cookbook directory will get nuked later
+ # make this an error condition also.
+ true
+ end
+
+ def reset_to_default_state
+ ui.info("Checking out the #{default_branch} branch.")
+ git("checkout #{default_branch}")
+ end
+
+ def prepare_to_import(cookbook_name)
+ branch = "chef-vendor-#{cookbook_name}"
+ if branch_exists?(branch)
+ ui.info("Pristine copy branch (#{branch}) exists, switching to it.")
+ git("checkout #{branch}")
+ else
+ ui.info("Creating pristine copy branch #{branch}")
+ git("checkout -b #{branch}")
+ end
+ end
+
+ def finalize_updates_to(cookbook_name, version)
+ if update_count = updated?(cookbook_name)
+ ui.info "#{update_count} files updated, committing changes"
+ git("add #{cookbook_name}")
+ git("commit -m \"Import #{cookbook_name} version #{version}\" -- #{cookbook_name}")
+ ui.info("Creating tag cookbook-site-imported-#{cookbook_name}-#{version}")
+ git("tag -f cookbook-site-imported-#{cookbook_name}-#{version}")
+ true
+ else
+ ui.info("No changes made to #{cookbook_name}")
+ false
+ end
+ end
+
+ def merge_updates_from(cookbook_name, version)
+ branch = "chef-vendor-#{cookbook_name}"
+ Dir.chdir(repo_path) do
+ if system("git merge #{branch}")
+ ui.info("Cookbook #{cookbook_name} version #{version} successfully installed")
+ else
+ ui.error("You have merge conflicts - please resolve manually")
+ ui.info("Merge status (cd #{repo_path}; git status):")
+ system("git status")
+ exit 3
+ end
+ end
+ end
+
+ def updated?(cookbook_name)
+ update_count = git("status --porcelain -- #{cookbook_name}").stdout.strip.lines.count
+ update_count == 0 ? nil : update_count
+ end
+
+ def branch_exists?(branch_name)
+ git("branch --no-color").stdout.lines.any? {|l| l =~ /\s#{Regexp.escape(branch_name)}(?:\s|$)/ }
+ end
+
+ def get_current_branch()
+ ref = git("symbolic-ref HEAD").stdout
+ ref.chomp.split('/')[2]
+ end
+
+ private
+
+ def git_repo?(directory)
+ if File.directory?(File.join(directory, '.git'))
+ return true
+ elsif File.dirname(directory) == directory
+ return false
+ else
+ git_repo?(File.dirname(directory))
+ end
+ end
+
+ def apply_opts(opts)
+ opts.each do |option, value|
+ case option.to_s
+ when 'default_branch'
+ @default_branch = value
+ when 'use_current_branch'
+ @use_current_branch = value
+ end
+ end
+ end
+
+ def git(command)
+ shell_out!("git #{command}", :cwd => repo_path)
+ end
+
+ end
+ end
+end
+
diff --git a/lib/chef/knife/core/generic_presenter.rb b/lib/chef/knife/core/generic_presenter.rb
new file mode 100644
index 0000000000..0866f10147
--- /dev/null
+++ b/lib/chef/knife/core/generic_presenter.rb
@@ -0,0 +1,204 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife/core/text_formatter'
+
+class Chef
+ class Knife
+ module Core
+
+ #==Chef::Knife::Core::GenericPresenter
+ # The base presenter class for displaying structured data in knife commands.
+ # This is not an abstract base class, and it is suitable for displaying
+ # most kinds of objects that knife needs to display.
+ class GenericPresenter
+
+ attr_reader :ui
+ attr_reader :config
+
+ # Instaniates a new GenericPresenter. This is generally handled by the
+ # Chef::Knife::UI object, though you need to match the signature of this
+ # method if you intend to use your own presenter instead.
+ def initialize(ui, config)
+ @ui, @config = ui, config
+ end
+
+ # Is the selected output format a data interchange format?
+ # Returns true if the selected output format is json or yaml, false
+ # otherwise. Knife search uses this to adjust its data output so as not
+ # to produce invalid JSON output.
+ def interchange?
+ case parse_format_option
+ when :json, :yaml
+ true
+ else
+ false
+ end
+ end
+
+ # Returns a String representation of +data+ that is suitable for output
+ # to a terminal or perhaps for data interchange with another program.
+ # The representation of the +data+ depends on the value of the
+ # `config[:format]` setting.
+ def format(data)
+ case parse_format_option
+ when :summary
+ summarize(data)
+ when :text
+ text_format(data)
+ when :json
+ Chef::JSONCompat.to_json_pretty(data)
+ when :yaml
+ require 'yaml'
+ YAML::dump(data)
+ when :pp
+ require 'stringio'
+ # If you were looking for some attribute and there is only one match
+ # just dump the attribute value
+ if config[:attribute] and data.length == 1
+ data.values[0]
+ else
+ out = StringIO.new
+ PP.pp(data, out)
+ out.string
+ end
+ end
+ end
+
+ # Converts the user-supplied value of `config[:format]` to a Symbol
+ # representing the desired output format.
+ # ===Returns
+ # returns one of :summary, :text, :json, :yaml, or :pp
+ # ===Raises
+ # Raises an ArgumentError if the desired output format could not be
+ # determined from the value of `config[:format]`
+ def parse_format_option
+ case config[:format]
+ when "summary", /^s/, nil
+ :summary
+ when "text", /^t/
+ :text
+ when "json", /^j/
+ :json
+ when "yaml", /^y/
+ :yaml
+ when "pp", /^p/
+ :pp
+ else
+ raise ArgumentError, "Unknown output format #{config[:format]}"
+ end
+ end
+
+ # Summarize the data. Defaults to text format output,
+ # which may not be very summary-like
+ def summarize(data)
+ text_format(data)
+ end
+
+ # Converts the +data+ to a String in the text format. Uses
+ # Chef::Knife::Core::TextFormatter
+ def text_format(data)
+ TextFormatter.new(data, ui).formatted_data
+ end
+
+ def format_list_for_display(list)
+ config[:with_uri] ? list : list.keys.sort { |a,b| a <=> b }
+ end
+
+ def format_for_display(data)
+ if formatting_subset_of_data?
+ format_data_subset_for_display(data)
+ elsif config[:id_only]
+ name_or_id_for(data)
+ elsif config[:environment] && data.respond_to?(:chef_environment)
+ {"chef_environment" => data.chef_environment}
+ else
+ data
+ end
+ end
+
+ def format_data_subset_for_display(data)
+ subset = if config[:attribute]
+ result = {}
+ Array(config[:attribute]).each do |nested_value_spec|
+ nested_value = extract_nested_value(data, nested_value_spec)
+ result[nested_value_spec] = nested_value
+ end
+ result
+ elsif config[:run_list]
+ run_list = data.run_list.run_list
+ { "run_list" => run_list }
+ else
+ raise ArgumentError, "format_data_subset_for_display requires attribute, run_list, or id_only config option to be set"
+ end
+ {name_or_id_for(data) => subset }
+ end
+
+ def name_or_id_for(data)
+ data.respond_to?(:name) ? data.name : data["id"]
+ end
+
+ def formatting_subset_of_data?
+ config[:attribute] || config[:run_list]
+ end
+
+
+ def extract_nested_value(data, nested_value_spec)
+ nested_value_spec.split(".").each do |attr|
+ if data.nil?
+ nil # don't get no method error on nil
+ elsif data.respond_to?(attr.to_sym)
+ data = data.send(attr.to_sym)
+ elsif data.respond_to?(:[])
+ data = data[attr]
+ else
+ data = begin
+ data.send(attr.to_sym)
+ rescue NoMethodError
+ nil
+ end
+ end
+ end
+ ( !data.kind_of?(Array) && data.respond_to?(:to_hash) ) ? data.to_hash : data
+ end
+
+ def format_cookbook_list_for_display(item)
+ if config[:with_uri]
+ item.inject({}) do |collected, (cookbook, versions)|
+ collected[cookbook] = Hash.new
+ versions['versions'].each do |ver|
+ collected[cookbook][ver['version']] = ver['url']
+ end
+ collected
+ end
+ else
+ versions_by_cookbook = item.inject({}) do |collected, ( cookbook, versions )|
+ collected[cookbook] = versions["versions"].map {|v| v['version']}
+ collected
+ end
+ key_length = versions_by_cookbook.empty? ? 0 : versions_by_cookbook.keys.map {|name| name.size }.max + 2
+ versions_by_cookbook.sort.map do |cookbook, versions|
+ "#{cookbook.ljust(key_length)} #{versions.join(' ')}"
+ end
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/core/node_editor.rb b/lib/chef/knife/core/node_editor.rb
new file mode 100644
index 0000000000..22ba3eaa25
--- /dev/null
+++ b/lib/chef/knife/core/node_editor.rb
@@ -0,0 +1,130 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/json_compat'
+require 'chef/node'
+
+class Chef
+ class Knife
+ class NodeEditor
+
+ attr_reader :node
+ attr_reader :ui
+ attr_reader :config
+
+ def initialize(node, ui, config)
+ @node, @ui, @config = node, ui, config
+ end
+
+ def edit_node
+ abort "You specified the --disable_editing option, nothing to edit" if config[:disable_editing]
+ assert_editor_set!
+
+ updated_node_data = edit_data(view)
+ apply_updates(updated_node_data)
+ @updated_node
+ end
+
+ def view
+ result = {}
+ result["name"] = node.name
+ result["chef_environment"] = node.chef_environment
+ result["normal"] = node.normal_attrs
+ result["run_list"] = node.run_list
+
+ if config[:all_attributes]
+ result["default"] = node.default_attrs
+ result["override"] = node.override_attrs
+ result["automatic"] = node.automatic_attrs
+ end
+ Chef::JSONCompat.to_json_pretty(result)
+ end
+
+ def edit_data(text)
+ edited_data = tempfile_for(text) {|filename| system("#{config[:editor]} #{filename}")}
+ Chef::JSONCompat.from_json(edited_data)
+ end
+
+ def apply_updates(updated_data)
+ if node.name and node.name != updated_data["name"]
+ ui.warn "Changing the name of a node results in a new node being created, #{node.name} will not be modified or removed."
+ confirm = ui.confirm "Proceed with creation of new node"
+ end
+
+ @updated_node = Node.new.tap do |n|
+ n.name( updated_data["name"] )
+ n.chef_environment( updated_data["chef_environment"] )
+ n.run_list( updated_data["run_list"])
+ n.normal_attrs = updated_data["normal"]
+
+ if config[:all_attributes]
+ n.default_attrs = updated_data["default"]
+ n.override_attrs = updated_data["override"]
+ n.automatic_attrs = updated_data["automatic"]
+ else
+ n.default_attrs = node.default_attrs
+ n.override_attrs = node.override_attrs
+ n.automatic_attrs = node.automatic_attrs
+ end
+ end
+ end
+
+ def updated?
+ pristine_copy = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(node), :create_additions => false)
+ updated_copy = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(@updated_node), :create_additions => false)
+ unless pristine_copy == updated_copy
+ updated_properties = %w{name normal chef_environment run_list default override automatic}.reject do |key|
+ pristine_copy[key] == updated_copy[key]
+ end
+ end
+ ( pristine_copy != updated_copy ) && updated_properties
+ end
+
+ private
+
+ def abort(message)
+ ui.error(message)
+ exit 1
+ end
+
+ def assert_editor_set!
+ unless config[:editor]
+ abort "You must set your EDITOR environment variable or configure your editor via knife.rb"
+ end
+ end
+
+ def tempfile_for(data)
+ # TODO: include useful info like the node name in the temp file
+ # name
+ basename = "knife-edit-" << rand(1_000_000_000_000_000).to_s.rjust(15, '0') << '.js'
+ filename = File.join(Dir.tmpdir, basename)
+ File.open(filename, "w+") do |f|
+ f.sync = true
+ f.puts data
+ end
+
+ yield filename
+
+ IO.read(filename)
+ ensure
+ File.unlink(filename)
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/core/node_presenter.rb b/lib/chef/knife/core/node_presenter.rb
new file mode 100644
index 0000000000..a35baf2264
--- /dev/null
+++ b/lib/chef/knife/core/node_presenter.rb
@@ -0,0 +1,137 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife/core/text_formatter'
+require 'chef/knife/core/generic_presenter'
+
+class Chef
+ class Knife
+ module Core
+
+ # This module may be included into a knife subcommand class to automatically
+ # add configuration options used by the NodePresenter
+ module NodeFormattingOptions
+ # :nodoc:
+ # Would prefer to do this in a rational way, but can't be done b/c of
+ # Mixlib::CLI's design :(
+ def self.included(includer)
+ includer.class_eval do
+ option :medium_output,
+ :short => '-m',
+ :long => '--medium',
+ :boolean => true,
+ :default => false,
+ :description => 'Include normal attributes in the output'
+
+ option :long_output,
+ :short => '-l',
+ :long => '--long',
+ :boolean => true,
+ :default => false,
+ :description => 'Include all attributes in the output'
+ end
+ end
+ end
+
+ #==Chef::Knife::Core::NodePresenter
+ # A customized presenter for Chef::Node objects. Supports variable-length
+ # output formats for displaying node data
+ class NodePresenter < GenericPresenter
+
+ def format(data)
+ if parse_format_option == :json
+ summarize_json(data)
+ else
+ super
+ end
+ end
+
+ def summarize_json(data)
+ if data.kind_of?(Chef::Node)
+ node = data
+ result = {}
+
+ result["name"] = node.name
+ result["chef_environment"] = node.chef_environment
+ result["run_list"] = node.run_list
+ result["normal"] = node.normal_attrs
+
+ if config[:long_output]
+ result["default"] = node.default_attrs
+ result["override"] = node.override_attrs
+ result["automatic"] = node.automatic_attrs
+ end
+
+ Chef::JSONCompat.to_json_pretty(result)
+ else
+ Chef::JSONCompat.to_json_pretty(data)
+ end
+ end
+
+ # Converts a Chef::Node object to a string suitable for output to a
+ # terminal. If config[:medium_output] or config[:long_output] are set
+ # the volume of output is adjusted accordingly. Uses colors if enabled
+ # in the the ui object.
+ def summarize(data)
+ if data.kind_of?(Chef::Node)
+ node = data
+ # special case ec2 with their split horizon whatsis.
+ ip = (node[:ec2] && node[:ec2][:public_ipv4]) || node[:ipaddress]
+
+ summarized=<<-SUMMARY
+#{ui.color('Node Name:', :bold)} #{ui.color(node.name, :bold)}
+#{key('Environment:')} #{node.chef_environment}
+#{key('FQDN:')} #{node[:fqdn]}
+#{key('IP:')} #{ip}
+#{key('Run List:')} #{node.run_list}
+#{key('Roles:')} #{Array(node[:roles]).join(', ')}
+#{key('Recipes:')} #{Array(node[:recipes]).join(', ')}
+#{key('Platform:')} #{node[:platform]} #{node[:platform_version]}
+#{key('Tags:')} #{Array(node[:tags]).join(', ')}
+SUMMARY
+ if config[:medium_output] || config[:long_output]
+ summarized +=<<-MORE
+#{key('Attributes:')}
+#{text_format(node.normal_attrs)}
+MORE
+ end
+ if config[:long_output]
+ summarized +=<<-MOST
+#{key('Default Attributes:')}
+#{text_format(node.default_attrs)}
+#{key('Override Attributes:')}
+#{text_format(node.override_attrs)}
+#{key('Automatic Attributes (Ohai Data):')}
+#{text_format(node.automatic_attrs)}
+MOST
+ end
+ summarized
+ else
+ super
+ end
+ end
+
+ def key(key_text)
+ ui.color(key_text, :cyan)
+ end
+
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/core/object_loader.rb b/lib/chef/knife/core/object_loader.rb
new file mode 100644
index 0000000000..1d207c10d1
--- /dev/null
+++ b/lib/chef/knife/core/object_loader.rb
@@ -0,0 +1,112 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Knife
+ module Core
+ class ObjectLoader
+
+ attr_reader :ui
+ attr_reader :klass
+
+ class ObjectType
+ FILE = 1
+ FOLDER = 2
+ end
+
+ def initialize(klass, ui)
+ @klass = klass
+ @ui = ui
+ end
+
+ def load_from(repo_location, *components)
+ unless object_file = find_file(repo_location, *components)
+ ui.error "Could not find or open file '#{components.last}' in current directory or in '#{repo_location}/#{components.join('/')}'"
+ exit 1
+ end
+ object_from_file(object_file)
+ end
+
+ # When someone makes this awesome, please update the above error message.
+ def find_file(repo_location, *components)
+ if file_exists_and_is_readable?(File.expand_path( components.last ))
+ File.expand_path( components.last )
+ else
+ relative_path = File.join(Dir.pwd, repo_location, *components)
+ if file_exists_and_is_readable?(relative_path)
+ relative_path
+ else
+ nil
+ end
+ end
+ end
+
+ # Find all objects in the given location
+ # If the object type is File it will look for all *.{json,rb}
+ # files, otherwise it will lookup for folders only (useful for
+ # data_bags)
+ #
+ # @param [String] path - base look up location
+ #
+ # @return [Array<String>] basenames of the found objects
+ #
+ # @api public
+ def find_all_objects(path)
+ path = File.join(path, '*')
+ path << '.{json,rb}'
+ objects = Dir.glob(File.expand_path(path))
+ objects.map { |o| File.basename(o) }
+ end
+
+ def find_all_object_dirs(path)
+ path = File.join(path, '*')
+ objects = Dir.glob(File.expand_path(path))
+ objects.delete_if { |o| !File.directory?(o) }
+ objects.map { |o| File.basename(o) }
+ end
+
+ def object_from_file(filename)
+ case filename
+ when /\.(js|json)$/
+ r = Yajl::Parser.parse(IO.read(filename))
+
+ # Chef::DataBagItem doesn't work well with the json_create method
+ if @klass == Chef::DataBagItem
+ r
+ else
+ @klass.json_create(r)
+ end
+ when /\.rb$/
+ r = klass.new
+ r.from_file(filename)
+ r
+ else
+ ui.fatal("File must end in .js, .json, or .rb")
+ exit 30
+ end
+ end
+
+ def file_exists_and_is_readable?(file)
+ File.exists?(file) && File.readable?(file)
+ end
+
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb
new file mode 100644
index 0000000000..314f54bc0b
--- /dev/null
+++ b/lib/chef/knife/core/subcommand_loader.rb
@@ -0,0 +1,112 @@
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/version'
+class Chef
+ class Knife
+ class SubcommandLoader
+
+ CHEF_FILE_IN_GEM = /chef-[\d]+\.[\d]+\.[\d]+/
+ CURRENT_CHEF_GEM = /chef-#{Regexp.escape(Chef::VERSION)}/
+
+ attr_reader :chef_config_dir
+ attr_reader :env
+
+ def initialize(chef_config_dir, env=ENV)
+ @chef_config_dir, @env = chef_config_dir, env
+ @forced_activate = {}
+ end
+
+ # Load all the sub-commands
+ def load_commands
+ subcommand_files.each { |subcommand| Kernel.load subcommand }
+ true
+ end
+
+ # Returns an Array of paths to knife commands located in chef_config_dir/plugins/knife/
+ # and ~/.chef/plugins/knife/
+ def site_subcommands
+ user_specific_files = []
+
+ if chef_config_dir
+ user_specific_files.concat Dir.glob(File.expand_path("plugins/knife/*.rb", chef_config_dir))
+ end
+
+ # finally search ~/.chef/plugins/knife/*.rb
+ user_specific_files.concat Dir.glob(File.join(env['HOME'], '.chef', 'plugins', 'knife', '*.rb')) if env['HOME']
+
+ user_specific_files
+ end
+
+ # Returns a Hash of paths to knife commands built-in to chef, or installed via gem.
+ # If rubygems is not installed, falls back to globbing the knife directory.
+ # The Hash is of the form {"relative/path" => "/absolute/path"}
+ #--
+ # Note: the "right" way to load the plugins is to require the relative path, i.e.,
+ # require 'chef/knife/command'
+ # but we're getting frustrated by bugs at every turn, and it's slow besides. So
+ # subcommand loader has been modified to load the plugins by using Kernel.load
+ # with the absolute path.
+ def gem_and_builtin_subcommands
+ # search all gems for chef/knife/*.rb
+ require 'rubygems'
+ find_subcommands_via_rubygems
+ rescue LoadError
+ find_subcommands_via_dirglob
+ end
+
+ def subcommand_files
+ @subcommand_files ||= (gem_and_builtin_subcommands.values + site_subcommands).flatten.uniq
+ end
+
+ def find_subcommands_via_dirglob
+ # The "require paths" of the core knife subcommands bundled with chef
+ files = Dir[File.expand_path('../../../knife/*.rb', __FILE__)]
+ subcommand_files = {}
+ files.each do |knife_file|
+ rel_path = knife_file[/#{CHEF_ROOT}#{Regexp.escape(File::SEPARATOR)}(.*)\.rb/,1]
+ subcommand_files[rel_path] = knife_file
+ end
+ subcommand_files
+ end
+
+ def find_subcommands_via_rubygems
+ files = Gem.find_files 'chef/knife/*.rb'
+ files.reject! {|f| from_old_gem?(f) }
+ subcommand_files = {}
+ files.each do |file|
+ rel_path = file[/(#{Regexp.escape File.join('chef', 'knife', '')}.*)\.rb/, 1]
+ subcommand_files[rel_path] = file
+ end
+
+ subcommand_files.merge(find_subcommands_via_dirglob)
+ end
+
+ private
+
+ # wow, this is a sad hack :(
+ # Gem.find_files finds files in all versions of a gem, which
+ # means that if chef 0.10 and 0.9.x are installed, we'll try to
+ # require, e.g., chef/knife/ec2_server_create, which will cause
+ # a gem activation error. So remove files from older chef gems.
+ def from_old_gem?(path)
+ path =~ CHEF_FILE_IN_GEM && path !~ CURRENT_CHEF_GEM
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/core/text_formatter.rb b/lib/chef/knife/core/text_formatter.rb
new file mode 100644
index 0000000000..60328488b2
--- /dev/null
+++ b/lib/chef/knife/core/text_formatter.rb
@@ -0,0 +1,86 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Knife
+ module Core
+ class TextFormatter
+
+ attr_reader :data
+ attr_reader :ui
+
+ def initialize(data, ui)
+ @ui = ui
+ @data = if data.respond_to?(:display_hash)
+ data.display_hash
+ elsif data.kind_of?(Array)
+ data
+ elsif data.respond_to?(:to_hash)
+ data.to_hash
+ else
+ data
+ end
+ end
+
+ def formatted_data
+ @formatted_data ||= text_format(data)
+ end
+
+ def text_format(data)
+ buffer = ''
+
+ if data.respond_to?(:keys)
+ justify_width = data.keys.map {|k| k.to_s.size }.max.to_i + 1
+ data.sort.each do |key, value|
+ # key: ['value'] should be printed as key: value
+ if value.kind_of?(Array) && value.size == 1 && is_singleton(value[0])
+ value = value[0]
+ end
+ if is_singleton(value)
+ # Strings are printed as key: value.
+ justified_key = ui.color("#{key}:".ljust(justify_width), :cyan)
+ buffer << "#{justified_key} #{value}\n"
+ else
+ # Arrays and hashes get indented on their own lines.
+ buffer << ui.color("#{key}:\n", :cyan)
+ lines = text_format(value).split("\n")
+ lines.each { |line| buffer << " #{line}\n" }
+ end
+ end
+ elsif data.kind_of?(Array)
+ data.each_index do |index|
+ item = data[index]
+ buffer << text_format(data[index])
+ # Separate items with newlines if it's an array of hashes or an
+ # array of arrays
+ buffer << "\n" if !is_singleton(data[index]) && index != data.size-1
+ end
+ else
+ buffer << "#{data}\n"
+ end
+ buffer
+ end
+
+ def is_singleton(value)
+ !(value.kind_of?(Array) || value.respond_to?(:keys))
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb
new file mode 100644
index 0000000000..85e9612315
--- /dev/null
+++ b/lib/chef/knife/core/ui.rb
@@ -0,0 +1,219 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'forwardable'
+require 'chef/platform'
+require 'chef/knife/core/generic_presenter'
+
+class Chef
+ class Knife
+
+ #==Chef::Knife::UI
+ # The User Interaction class used by knife.
+ class UI
+
+ extend Forwardable
+
+ attr_reader :stdout
+ attr_reader :stderr
+ attr_reader :stdin
+ attr_reader :config
+
+ attr_reader :presenter
+
+ def_delegator :@presenter, :format_list_for_display
+ def_delegator :@presenter, :format_for_display
+ def_delegator :@presenter, :format_cookbook_list_for_display
+
+ def initialize(stdout, stderr, stdin, config)
+ @stdout, @stderr, @stdin, @config = stdout, stderr, stdin, config
+ @presenter = Chef::Knife::Core::GenericPresenter.new(self, config)
+ end
+
+ # Creates a new +presenter_class+ object and uses it to format structured
+ # data for display. By default, a Chef::Knife::Core::GenericPresenter
+ # object is used.
+ def use_presenter(presenter_class)
+ @presenter = presenter_class.new(self, config)
+ end
+
+ def highline
+ @highline ||= begin
+ require 'highline'
+ HighLine.new
+ end
+ end
+
+ # Prints a message to stdout. Aliased as +info+ for compatibility with
+ # the logger API.
+ def msg(message)
+ stdout.puts message
+ end
+
+ alias :info :msg
+
+ # Prints a msg to stderr. Used for warn, error, and fatal.
+ def err(message)
+ stderr.puts message
+ end
+
+ # Print a warning message
+ def warn(message)
+ err("#{color('WARNING:', :yellow, :bold)} #{message}")
+ end
+
+ # Print an error message
+ def error(message)
+ err("#{color('ERROR:', :red, :bold)} #{message}")
+ end
+
+ # Print a message describing a fatal error.
+ def fatal(message)
+ err("#{color('FATAL:', :red, :bold)} #{message}")
+ end
+
+ def color(string, *colors)
+ if color?
+ highline.color(string, *colors)
+ else
+ string
+ end
+ end
+
+ # Should colored output be used? For output to a terminal, this is
+ # determined by the value of `config[:color]`. When output is not to a
+ # terminal, colored output is never used
+ def color?
+ Chef::Config[:color] && stdout.tty? && !Chef::Platform.windows?
+ end
+
+ def ask(*args, &block)
+ highline.ask(*args, &block)
+ end
+
+ def list(*args)
+ highline.list(*args)
+ end
+
+ # Formats +data+ using the configured presenter and outputs the result
+ # via +msg+. Formatting can be customized by configuring a different
+ # presenter. See +use_presenter+
+ def output(data)
+ msg @presenter.format(data)
+ end
+
+ # Determines if the output format is a data interchange format, i.e.,
+ # JSON or YAML
+ def interchange?
+ @presenter.interchange?
+ end
+
+ def ask_question(question, opts={})
+ question = question + "[#{opts[:default]}] " if opts[:default]
+
+ if opts[:default] and config[:defaults]
+ opts[:default]
+ else
+ stdout.print question
+ a = stdin.readline.strip
+
+ if opts[:default]
+ a.empty? ? opts[:default] : a
+ else
+ a
+ end
+ end
+ end
+
+ def pretty_print(data)
+ stdout.puts data
+ end
+
+ def edit_data(data, parse_output=true)
+ output = Chef::JSONCompat.to_json_pretty(data)
+
+ if (!config[:disable_editing])
+ filename = "knife-edit-"
+ 0.upto(20) { filename += rand(9).to_s }
+ filename << ".js"
+ filename = File.join(Dir.tmpdir, filename)
+ tf = File.open(filename, "w")
+ tf.sync = true
+ tf.puts output
+ tf.close
+ raise "Please set EDITOR environment variable" unless system("#{config[:editor]} #{tf.path}")
+ tf = File.open(filename, "r")
+ output = tf.gets(nil)
+ tf.close
+ File.unlink(filename)
+ end
+
+ parse_output ? Chef::JSONCompat.from_json(output) : output
+ end
+
+ def edit_object(klass, name)
+ object = klass.load(name)
+
+ output = edit_data(object)
+
+ # Only make the save if the user changed the object.
+ #
+ # Output JSON for the original (object) and edited (output), then parse
+ # them without reconstituting the objects into real classes
+ # (create_additions=false). Then, compare the resulting simple objects,
+ # which will be Array/Hash/String/etc.
+ #
+ # We wouldn't have to do these shenanigans if all the editable objects
+ # implemented to_hash, or if to_json against a hash returned a string
+ # with stable key order.
+ object_parsed_again = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(object), :create_additions => false)
+ output_parsed_again = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(output), :create_additions => false)
+ if object_parsed_again != output_parsed_again
+ output.save
+ self.msg("Saved #{output}")
+ else
+ self.msg("Object unchanged, not saving")
+ end
+ output(format_for_display(object)) if config[:print_after]
+ end
+
+ def confirm(question, append_instructions=true)
+ return true if config[:yes]
+
+ stdout.print question
+ stdout.print "? (Y/N) " if append_instructions
+ answer = stdin.readline
+ answer.chomp!
+ case answer
+ when "Y", "y"
+ true
+ when "N", "n"
+ self.msg("You said no, so I'm done here.")
+ exit 3
+ else
+ self.msg("I have no idea what to do with #{answer}")
+ self.msg("Just say Y or N, please.")
+ confirm(question)
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/data_bag_create.rb b/lib/chef/knife/data_bag_create.rb
new file mode 100644
index 0000000000..e644ab78d3
--- /dev/null
+++ b/lib/chef/knife/data_bag_create.rb
@@ -0,0 +1,93 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright (c) 2009-2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class DataBagCreate < Knife
+
+ deps do
+ require 'chef/data_bag'
+ require 'chef/encrypted_data_bag_item'
+ end
+
+ banner "knife data bag create BAG [ITEM] (options)"
+ category "data bag"
+
+ option :secret,
+ :short => "-s SECRET",
+ :long => "--secret ",
+ :description => "The secret key to use to encrypt data bag item values"
+
+ option :secret_file,
+ :long => "--secret-file SECRET_FILE",
+ :description => "A file containing the secret key to use to encrypt data bag item values"
+
+ def read_secret
+ if config[:secret]
+ config[:secret]
+ else
+ Chef::EncryptedDataBagItem.load_secret(config[:secret_file])
+ end
+ end
+
+ def use_encryption
+ if config[:secret] && config[:secret_file]
+ ui.fatal("please specify only one of --secret, --secret-file")
+ exit(1)
+ end
+ config[:secret] || config[:secret_file]
+ end
+
+ def run
+ @data_bag_name, @data_bag_item_name = @name_args
+
+ if @data_bag_name.nil?
+ show_usage
+ ui.fatal("You must specify a data bag name")
+ exit 1
+ end
+
+ # create the data bag
+ begin
+ rest.post_rest("data", { "name" => @data_bag_name })
+ ui.info("Created data_bag[#{@data_bag_name}]")
+ rescue Net::HTTPServerException => e
+ raise unless e.to_s =~ /^409/
+ ui.info("Data bag #{@data_bag_name} already exists")
+ end
+
+ # if an item is specified, create it, as well
+ if @data_bag_item_name
+ create_object({ "id" => @data_bag_item_name }, "data_bag_item[#{@data_bag_item_name}]") do |output|
+ item = Chef::DataBagItem.from_hash(
+ if use_encryption
+ Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret)
+ else
+ output
+ end)
+ item.data_bag(@data_bag_name)
+ rest.post_rest("data/#{@data_bag_name}", item)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/data_bag_delete.rb b/lib/chef/knife/data_bag_delete.rb
new file mode 100644
index 0000000000..f8e52018a6
--- /dev/null
+++ b/lib/chef/knife/data_bag_delete.rb
@@ -0,0 +1,51 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class DataBagDelete < Knife
+
+ deps do
+ require 'chef/data_bag'
+ end
+
+ banner "knife data bag delete BAG [ITEM] (options)"
+ category "data bag"
+
+ def run
+ if @name_args.length == 2
+ delete_object(Chef::DataBagItem, @name_args[1], "data_bag_item") do
+ rest.delete_rest("data/#{@name_args[0]}/#{@name_args[1]}")
+ end
+ elsif @name_args.length == 1
+ delete_object(Chef::DataBag, @name_args[0], "data_bag") do
+ rest.delete_rest("data/#{@name_args[0]}")
+ end
+ else
+ show_usage
+ ui.fatal("You must specify at least a data bag name")
+ exit 1
+ end
+ end
+ end
+ end
+end
+
+
diff --git a/lib/chef/knife/data_bag_edit.rb b/lib/chef/knife/data_bag_edit.rb
new file mode 100644
index 0000000000..234c77177d
--- /dev/null
+++ b/lib/chef/knife/data_bag_edit.rb
@@ -0,0 +1,94 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright (c) 2009-2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class DataBagEdit < Knife
+
+ deps do
+ require 'chef/data_bag_item'
+ require 'chef/encrypted_data_bag_item'
+ end
+
+ banner "knife data bag edit BAG ITEM (options)"
+ category "data bag"
+
+ option :secret,
+ :short => "-s SECRET",
+ :long => "--secret ",
+ :description => "The secret key to use to encrypt data bag item values"
+
+ option :secret_file,
+ :long => "--secret-file SECRET_FILE",
+ :description => "A file containing the secret key to use to encrypt data bag item values"
+
+ def read_secret
+ if config[:secret]
+ config[:secret]
+ else
+ Chef::EncryptedDataBagItem.load_secret(config[:secret_file])
+ end
+ end
+
+ def use_encryption
+ if config[:secret] && config[:secret_file]
+ stdout.puts "please specify only one of --secret, --secret-file"
+ exit(1)
+ end
+ config[:secret] || config[:secret_file]
+ end
+
+ def load_item(bag, item_name)
+ item = Chef::DataBagItem.load(bag, item_name)
+ if use_encryption
+ Chef::EncryptedDataBagItem.new(item, read_secret).to_hash
+ else
+ item
+ end
+ end
+
+ def edit_item(item)
+ output = edit_data(item)
+ if use_encryption
+ Chef::EncryptedDataBagItem.encrypt_data_bag_item(output, read_secret)
+ else
+ output
+ end
+ end
+
+ def run
+ if @name_args.length != 2
+ stdout.puts "You must supply the data bag and an item to edit!"
+ stdout.puts opt_parser
+ exit 1
+ end
+ item = load_item(@name_args[0], @name_args[1])
+ output = edit_item(item)
+ rest.put_rest("data/#{@name_args[0]}/#{@name_args[1]}", output)
+ stdout.puts("Saved data_bag_item[#{@name_args[1]}]")
+ ui.output(output) if config[:print_after]
+ end
+ end
+ end
+end
+
+
+
diff --git a/lib/chef/knife/data_bag_from_file.rb b/lib/chef/knife/data_bag_from_file.rb
new file mode 100644
index 0000000000..275cbeac52
--- /dev/null
+++ b/lib/chef/knife/data_bag_from_file.rb
@@ -0,0 +1,136 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class DataBagFromFile < Knife
+
+ deps do
+ require 'chef/data_bag'
+ require 'chef/data_bag_item'
+ require 'chef/knife/core/object_loader'
+ require 'chef/json_compat'
+ require 'chef/encrypted_data_bag_item'
+ end
+
+ banner "knife data bag from file BAG FILE|FOLDER [FILE|FOLDER..] (options)"
+ category "data bag"
+
+ option :secret,
+ :short => "-s SECRET",
+ :long => "--secret ",
+ :description => "The secret key to use to encrypt data bag item values"
+
+ option :secret_file,
+ :long => "--secret-file SECRET_FILE",
+ :description => "A file containing the secret key to use to encrypt data bag item values"
+
+ option :all,
+ :short => "-a",
+ :long => "--all",
+ :description => "Upload all data bags or all items for specified data bags"
+
+ def read_secret
+ if config[:secret]
+ config[:secret]
+ else
+ Chef::EncryptedDataBagItem.load_secret(config[:secret_file])
+ end
+ end
+
+ def use_encryption
+ if config[:secret] && config[:secret_file]
+ ui.fatal("please specify only one of --secret, --secret-file")
+ exit(1)
+ end
+ config[:secret] || config[:secret_file]
+ end
+
+ def loader
+ @loader ||= Knife::Core::ObjectLoader.new(DataBagItem, ui)
+ end
+
+ def run
+ if config[:all] == true
+ load_all_data_bags(@name_args)
+ else
+ if @name_args.size < 2
+ ui.msg(opt_parser)
+ exit(1)
+ end
+ @data_bag = @name_args.shift
+ load_data_bag_items(@data_bag, @name_args)
+ end
+ end
+
+ private
+ def data_bags_path
+ @data_bag_path ||= "data_bags"
+ end
+
+ def find_all_data_bags
+ loader.find_all_object_dirs("./#{data_bags_path}")
+ end
+
+ def find_all_data_bag_items(data_bag)
+ loader.find_all_objects("./#{data_bags_path}/#{data_bag}")
+ end
+
+ def load_all_data_bags(args)
+ data_bags = args.empty? ? find_all_data_bags : [args.shift]
+ data_bags.each do |data_bag|
+ load_data_bag_items(data_bag)
+ end
+ end
+
+ def load_data_bag_items(data_bag, items = nil)
+ items ||= find_all_data_bag_items(data_bag)
+ item_paths = normalize_item_paths(items)
+ item_paths.each do |item_path|
+ item = loader.load_from("#{data_bags_path}", data_bag, item_path)
+ item = if use_encryption
+ secret = read_secret
+ Chef::EncryptedDataBagItem.encrypt_data_bag_item(item, secret)
+ else
+ item
+ end
+ dbag = Chef::DataBagItem.new
+ dbag.data_bag(data_bag)
+ dbag.raw_data = item
+ dbag.save
+ ui.info("Updated data_bag_item[#{dbag.data_bag}::#{dbag.id}]")
+ end
+ end
+
+ def normalize_item_paths(args)
+ paths = Array.new
+ args.each do |path|
+ if File.directory?(path)
+ paths.concat(Dir.glob(File.join(path, "*.json")))
+ else
+ paths << path
+ end
+ end
+ paths
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/data_bag_list.rb b/lib/chef/knife/data_bag_list.rb
new file mode 100644
index 0000000000..31dcf984f6
--- /dev/null
+++ b/lib/chef/knife/data_bag_list.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class DataBagList < Knife
+
+ deps do
+ require 'chef/data_bag'
+ end
+
+ banner "knife data bag list (options)"
+ category "data bag"
+
+ option :with_uri,
+ :short => "-w",
+ :long => "--with-uri",
+ :description => "Show corresponding URIs"
+
+ def run
+ output(format_list_for_display(Chef::DataBag.list))
+ end
+ end
+ end
+end
+
+
+
+
diff --git a/lib/chef/knife/data_bag_show.rb b/lib/chef/knife/data_bag_show.rb
new file mode 100644
index 0000000000..81b1425f78
--- /dev/null
+++ b/lib/chef/knife/data_bag_show.rb
@@ -0,0 +1,81 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright (c) 2009-2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class DataBagShow < Knife
+
+ deps do
+ require 'chef/data_bag'
+ require 'chef/encrypted_data_bag_item'
+ end
+
+ banner "knife data bag show BAG [ITEM] (options)"
+ category "data bag"
+
+ option :secret,
+ :short => "-s SECRET",
+ :long => "--secret ",
+ :description => "The secret key to use to decrypt data bag item values"
+
+ option :secret_file,
+ :long => "--secret-file SECRET_FILE",
+ :description => "A file containing the secret key to use to decrypt data bag item values"
+
+ def read_secret
+ if config[:secret]
+ config[:secret]
+ else
+ Chef::EncryptedDataBagItem.load_secret(config[:secret_file])
+ end
+ end
+
+ def use_encryption
+ if config[:secret] && config[:secret_file]
+ stdout.puts "please specify only one of --secret, --secret-file"
+ exit(1)
+ end
+ config[:secret] || config[:secret_file]
+ end
+
+ def run
+ display = case @name_args.length
+ when 2
+ if use_encryption
+ raw = Chef::EncryptedDataBagItem.load(@name_args[0],
+ @name_args[1],
+ read_secret)
+ format_for_display(raw.to_hash)
+ else
+ format_for_display(Chef::DataBagItem.load(@name_args[0], @name_args[1]).raw_data)
+ end
+ when 1
+ format_list_for_display(Chef::DataBag.load(@name_args[0]))
+ else
+ stdout.puts opt_parser
+ exit(1)
+ end
+ output(display)
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/delete.rb b/lib/chef/knife/delete.rb
new file mode 100644
index 0000000000..d7482b3085
--- /dev/null
+++ b/lib/chef/knife/delete.rb
@@ -0,0 +1,33 @@
+require 'chef/chef_fs/knife'
+require 'chef/chef_fs/file_system'
+
+class Chef
+ class Knife
+ class Delete < Chef::ChefFS::Knife
+ banner "knife delete [PATTERN1 ... PATTERNn]"
+
+ common_options
+
+ option :recurse,
+ :long => '--[no-]recurse',
+ :boolean => true,
+ :default => false,
+ :description => "Delete directories recursively."
+
+ def run
+ # Get the matches (recursively)
+ pattern_args.each do |pattern|
+ Chef::ChefFS::FileSystem.list(chef_fs, pattern) do |result|
+ begin
+ result.delete(config[:recurse])
+ puts "Deleted #{result.path_for_printing}"
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ STDERR.puts "#{result.path_for_printing}: No such file or directory"
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/diff.rb b/lib/chef/knife/diff.rb
new file mode 100644
index 0000000000..57d3bf3f0c
--- /dev/null
+++ b/lib/chef/knife/diff.rb
@@ -0,0 +1,46 @@
+require 'chef/chef_fs/knife'
+require 'chef/chef_fs/command_line'
+
+class Chef
+ class Knife
+ class Diff < Chef::ChefFS::Knife
+ banner "knife diff PATTERNS"
+
+ common_options
+
+ option :recurse,
+ :long => '--[no-]recurse',
+ :boolean => true,
+ :default => true,
+ :description => "List directories recursively."
+
+ option :name_only,
+ :long => '--name-only',
+ :boolean => true,
+ :description => "Only show names of modified files."
+
+ option :name_status,
+ :long => '--name-status',
+ :boolean => true,
+ :description => "Only show names and statuses of modified files: Added, Deleted, Modified, and Type Changed."
+
+ def run
+ if config[:name_only]
+ output_mode = :name_only
+ end
+ if config[:name_status]
+ output_mode = :name_status
+ end
+ patterns = pattern_args_from(name_args.length > 0 ? name_args : [ "" ])
+
+ # Get the matches (recursively)
+ patterns.each do |pattern|
+ Chef::ChefFS::CommandLine.diff(pattern, chef_fs, local_fs, config[:recurse] ? nil : 1, output_mode) do |diff|
+ puts diff
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/download.rb b/lib/chef/knife/download.rb
new file mode 100644
index 0000000000..f891e55530
--- /dev/null
+++ b/lib/chef/knife/download.rb
@@ -0,0 +1,47 @@
+require 'chef/chef_fs/knife'
+require 'chef/chef_fs/command_line'
+
+class Chef
+ class Knife
+ class Download < Chef::ChefFS::Knife
+ banner "knife download PATTERNS"
+
+ common_options
+
+ option :recurse,
+ :long => '--[no-]recurse',
+ :boolean => true,
+ :default => true,
+ :description => "List directories recursively."
+
+ option :purge,
+ :long => '--[no-]purge',
+ :boolean => true,
+ :default => false,
+ :description => "Delete matching local files and directories that do not exist remotely."
+
+ option :force,
+ :long => '--[no-]force',
+ :boolean => true,
+ :default => false,
+ :description => "Force upload of files even if they match (quicker and harmless, but doesn't print out what it changed)"
+
+ option :dry_run,
+ :long => '--dry-run',
+ :short => '-n',
+ :boolean => true,
+ :default => false,
+ :description => "Don't take action, only print what would happen"
+
+ def run
+ patterns = pattern_args_from(name_args.length > 0 ? name_args : [ "" ])
+
+ # Get the matches (recursively)
+ patterns.each do |pattern|
+ Chef::ChefFS::FileSystem.copy_to(pattern, chef_fs, local_fs, config[:recurse] ? nil : 1, config)
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/environment_create.rb b/lib/chef/knife/environment_create.rb
new file mode 100644
index 0000000000..6bc00d52b9
--- /dev/null
+++ b/lib/chef/knife/environment_create.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Stephen Delano (<stephen@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class EnvironmentCreate < Knife
+
+ deps do
+ require 'chef/environment'
+ require 'chef/json_compat'
+ end
+
+ banner "knife environment create ENVIRONMENT (options)"
+
+ option :description,
+ :short => "-d DESCRIPTION",
+ :long => "--description DESCRIPTION",
+ :description => "The environment description"
+
+ def run
+ env_name = @name_args[0]
+
+ if env_name.nil?
+ show_usage
+ ui.fatal("You must specify an environment name")
+ exit 1
+ end
+
+ env = Chef::Environment.new
+ env.name(env_name)
+ env.description(config[:description]) if config[:description]
+ create_object(env)
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/environment_delete.rb b/lib/chef/knife/environment_delete.rb
new file mode 100644
index 0000000000..e17841f805
--- /dev/null
+++ b/lib/chef/knife/environment_delete.rb
@@ -0,0 +1,45 @@
+#
+# Author:: Stephen Delano (<stephen@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class EnvironmentDelete < Knife
+
+ deps do
+ require 'chef/environment'
+ require 'chef/json_compat'
+ end
+
+ banner "knife environment delete ENVIRONMENT (options)"
+
+ def run
+ env_name = @name_args[0]
+
+ if env_name.nil?
+ show_usage
+ ui.fatal("You must specify an environment name")
+ exit 1
+ end
+
+ delete_object(Chef::Environment, env_name)
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/environment_edit.rb b/lib/chef/knife/environment_edit.rb
new file mode 100644
index 0000000000..54962ac20d
--- /dev/null
+++ b/lib/chef/knife/environment_edit.rb
@@ -0,0 +1,45 @@
+#
+# Author:: Stephen Delano (<stephen@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class EnvironmentEdit < Knife
+
+ deps do
+ require 'chef/environment'
+ require 'chef/json_compat'
+ end
+
+ banner "knife environment edit ENVIRONMENT (options)"
+
+ def run
+ env_name = @name_args[0]
+
+ if env_name.nil?
+ show_usage
+ ui.fatal("You must specify an environment name")
+ exit 1
+ end
+
+ edit_object(Chef::Environment, env_name)
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/environment_from_file.rb b/lib/chef/knife/environment_from_file.rb
new file mode 100644
index 0000000000..af72f84622
--- /dev/null
+++ b/lib/chef/knife/environment_from_file.rb
@@ -0,0 +1,83 @@
+#
+# Author:: Stephen Delano (<stephen@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Knife
+ class EnvironmentFromFile < Knife
+
+ deps do
+ require 'chef/environment'
+ require 'chef/knife/core/object_loader'
+ end
+
+ banner "knife environment from file FILE [FILE..] (options)"
+
+ option :all,
+ :short => "-a",
+ :long => "--all",
+ :description => "Upload all environments"
+
+ def loader
+ @loader ||= Knife::Core::ObjectLoader.new(Chef::Environment, ui)
+ end
+
+ def environments_path
+ @environments_path ||= "environments"
+ end
+
+ def find_all_environments
+ loader.find_all_objects("./#{environments_path}/")
+ end
+
+ def load_all_environments
+ environments = find_all_environments
+ if environments.empty?
+ ui.fatal("Unable to find any environment files in '#{environments_path}'")
+ exit(1)
+ end
+ environments.each do |env|
+ load_environment(env)
+ end
+ end
+
+ def load_environment(env)
+ updated = loader.load_from("environments", env)
+ updated.save
+ output(format_for_display(updated)) if config[:print_after]
+ ui.info("Updated Environment #{updated.name}")
+ end
+
+
+ def run
+ if config[:all] == true
+ load_all_environments
+ else
+ if @name_args[0].nil?
+ show_usage
+ ui.fatal("You must specify a file to load")
+ exit 1
+ end
+
+ @name_args.each do |arg|
+ load_environment(arg)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/environment_list.rb b/lib/chef/knife/environment_list.rb
new file mode 100644
index 0000000000..4e70818093
--- /dev/null
+++ b/lib/chef/knife/environment_list.rb
@@ -0,0 +1,42 @@
+#
+# Author:: Stephen Delano (<stephen@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class EnvironmentList < Knife
+
+ deps do
+ require 'chef/environment'
+ require 'chef/json_compat'
+ end
+
+ banner "knife environment list (options)"
+
+ option :with_uri,
+ :short => "-w",
+ :long => "--with-uri",
+ :description => "Show corresponding URIs"
+
+ def run
+ output(format_list_for_display(Chef::Environment.list))
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/environment_show.rb b/lib/chef/knife/environment_show.rb
new file mode 100644
index 0000000000..a2b1636f47
--- /dev/null
+++ b/lib/chef/knife/environment_show.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Stephen Delano (<stephen@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class EnvironmentShow < Knife
+
+ deps do
+ require 'chef/environment'
+ require 'chef/json_compat'
+ end
+
+ @attrs_to_show = []
+ option :attribute,
+ :short => "-a [ATTR]",
+ :long => "--attribute [ATTR]",
+ :proc => lambda {|val| @attrs_to_show << val},
+ :description => "Show one or more attributes"
+
+ banner "knife environment show ENVIRONMENT (options)"
+
+ def run
+ env_name = @name_args[0]
+
+ if env_name.nil?
+ show_usage
+ ui.fatal("You must specify an environment name")
+ exit 1
+ end
+
+ env = Chef::Environment.load(env_name)
+ output(format_for_display(env))
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/exec.rb b/lib/chef/knife/exec.rb
new file mode 100644
index 0000000000..3e8196c616
--- /dev/null
+++ b/lib/chef/knife/exec.rb
@@ -0,0 +1,86 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef::Knife::Exec < Chef::Knife
+
+ banner "knife exec [SCRIPT] (options)"
+
+ option :exec,
+ :short => "-E CODE",
+ :long => "--exec CODE",
+ :description => "a string of Chef code to execute"
+
+ option :script_path,
+ :short => "-p PATH:PATH",
+ :long => "--script-path PATH:PATH",
+ :description => "A colon-separated path to look for scripts in",
+ :proc => lambda { |o| o.split(":") }
+
+ deps do
+ require 'chef/shell/ext'
+ end
+
+ def run
+ config[:script_path] ||= Array(Chef::Config[:script_path])
+
+ # Default script paths are chef-repo/.chef/scripts and ~/.chef/scripts
+ config[:script_path] << File.join(Chef::Knife.chef_config_dir, 'scripts') if Chef::Knife.chef_config_dir
+ config[:script_path] << File.join(ENV['HOME'], '.chef', 'scripts') if ENV['HOME']
+
+ scripts = Array(name_args)
+ context = Object.new
+ Shell::Extensions.extend_context_object(context)
+ if config[:exec]
+ context.instance_eval(config[:exec], "-E Argument", 0)
+ elsif !scripts.empty?
+ scripts.each do |script|
+ file = find_script(script)
+ context.instance_eval(IO.read(file), file, 0)
+ end
+ else
+ script = STDIN.read
+ context.instance_eval(script, "STDIN", 0)
+ end
+ end
+
+ def find_script(x)
+ # Try to find a script. First try expanding the path given.
+ script = File.expand_path(x)
+ return script if File.exists?(script)
+
+ # Failing that, try searching the script path. If we can't find
+ # anything, fail gracefully.
+ Chef::Log.debug("Searching script_path: #{config[:script_path].inspect}")
+
+ config[:script_path].each do |path|
+ path = File.expand_path(path)
+ test = File.join(path, x)
+ Chef::Log.debug("Testing: #{test}")
+ if File.exists?(test)
+ script = test
+ Chef::Log.debug("Found: #{test}")
+ return script
+ end
+ end
+ ui.error("\"#{x}\" not found in current directory or script_path, giving up.")
+ exit(1)
+ end
+
+end
diff --git a/lib/chef/knife/help.rb b/lib/chef/knife/help.rb
new file mode 100644
index 0000000000..13fe674704
--- /dev/null
+++ b/lib/chef/knife/help.rb
@@ -0,0 +1,103 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Knife
+ class Help < Chef::Knife
+
+ banner "knife help [list|TOPIC]"
+
+ def run
+ if name_args.empty?
+ ui.info "Usage: knife SUBCOMMAND (options)"
+ ui.msg ""
+ # This command is atypical, the user is likely not interested in usage of
+ # this command, but knife in general. So hack the banner.
+ opt_parser.banner = "General Knife Options:"
+ ui.msg opt_parser.to_s
+ ui.msg ""
+ ui.info "For further help:"
+ ui.info(<<-MOAR_HELP)
+ knife help list list help topics
+ knife help knife show general knife help
+ knife help TOPIC display the manual for TOPIC
+ knife SUBCOMMAND --help show the options for a command
+MOAR_HELP
+ exit 1
+ else
+ @query = name_args.join('-')
+ end
+
+
+
+ case @query
+ when 'topics', 'list'
+ print_help_topics
+ exit 1
+ when 'intro', 'knife'
+ @topic = 'knife'
+ else
+ @topic = find_manpages_for_query(@query)
+ end
+
+ manpage_path = find_manpage_path(@topic)
+ exec "man #{manpage_path}"
+ end
+
+ def help_topics
+ # The list of help topics is generated by a rake task from the available man pages
+ # This constant is provided in help_topics.rb which is automatically required/loaded by the knife subcommand loader.
+ HELP_TOPICS
+ end
+
+ def print_help_topics
+ ui.info "Available help topics are: "
+ help_topics.collect {|t| t.gsub(/knife-/, '') }.sort.each do |topic|
+ ui.msg " #{topic}"
+ end
+ end
+
+ def find_manpages_for_query(query)
+ possibilities = help_topics.select do |manpage|
+ ::File.fnmatch("knife-#{query}*", manpage) || ::File.fnmatch("#{query}*", manpage)
+ end
+ if possibilities.empty?
+ ui.error "No help found for '#{query}'"
+ ui.msg ""
+ print_help_topics
+ exit 1
+ elsif possibilities.size == 1
+ possibilities.first
+ else
+ ui.info "Multiple help topics match your query. Pick one:"
+ ui.highline.choose(*possibilities)
+ end
+ end
+
+ def find_manpage_path(topic)
+ if ::File.exists?(::File.expand_path("../distro/common/man/man1/#{topic}.1", CHEF_ROOT))
+ # If we've provided the man page in the gem, give that
+ return ::File.expand_path("../distro/common/man/man1/#{topic}.1", CHEF_ROOT)
+ else
+ # Otherwise, we'll just be using MANPATH
+ topic
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/help_topics.rb b/lib/chef/knife/help_topics.rb
new file mode 100644
index 0000000000..8427204fd6
--- /dev/null
+++ b/lib/chef/knife/help_topics.rb
@@ -0,0 +1,4 @@
+# Do not edit this file by hand
+# This file is autogenerated by the docs:list rake task from the available manpages
+
+HELP_TOPICS = ["chef-shell", "knife-bootstrap", "knife-client", "knife-configure", "knife-cookbook-site", "knife-cookbook", "knife-data-bag", "knife-environment", "knife-exec", "knife-index", "knife-node", "knife-role", "knife-search", "knife-ssh", "knife-status", "knife-tag", "knife", "shef"]
diff --git a/lib/chef/knife/index_rebuild.rb b/lib/chef/knife/index_rebuild.rb
new file mode 100644
index 0000000000..b35da77619
--- /dev/null
+++ b/lib/chef/knife/index_rebuild.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class IndexRebuild < Knife
+
+ banner "knife index rebuild (options)"
+ option :yes,
+ :short => "-y",
+ :long => "--yes",
+ :boolean => true,
+ :description => "don't bother to ask if I'm sure"
+
+ def run
+ nag
+ output rest.post_rest("/search/reindex", {})
+ end
+
+ def nag
+ unless config[:yes]
+ yea_or_nay = ask_question("This operation is destructive. Rebuilding the index may take some time. You sure? (yes/no): ")
+ unless yea_or_nay =~ /^y/i
+ puts "aborting"
+ exit 7
+ end
+ end
+ end
+
+
+ end
+ end
+end
diff --git a/lib/chef/knife/list.rb b/lib/chef/knife/list.rb
new file mode 100644
index 0000000000..30fcb5fa35
--- /dev/null
+++ b/lib/chef/knife/list.rb
@@ -0,0 +1,109 @@
+require 'chef/chef_fs/knife'
+require 'chef/chef_fs/file_system'
+
+class Chef
+ class Knife
+ class List < Chef::ChefFS::Knife
+ banner "knife list [-dR] [PATTERN1 ... PATTERNn]"
+
+ common_options
+
+ option :recursive,
+ :short => '-R',
+ :boolean => true,
+ :description => "List directories recursively."
+ option :bare_directories,
+ :short => '-d',
+ :boolean => true,
+ :description => "When directories match the pattern, do not show the directories' children."
+
+ def run
+ patterns = name_args.length == 0 ? [""] : name_args
+
+ # Get the matches (recursively)
+ results = []
+ dir_results = []
+ pattern_args_from(patterns).each do |pattern|
+ Chef::ChefFS::FileSystem.list(chef_fs, pattern) do |result|
+ if result.dir? && !config[:bare_directories]
+ dir_results += add_dir_result(result)
+ elsif result.exists?
+ results << result
+ elsif pattern.exact_path
+ STDERR.puts "#{format_path(result.path)}: No such file or directory"
+ end
+ end
+ end
+
+ results = results.sort_by { |result| result.path }
+ dir_results = dir_results.sort_by { |result| result[0].path }
+
+ if results.length == 0 && dir_results.length == 1
+ results = dir_results[0][1]
+ dir_results = []
+ end
+
+ print_result_paths results
+ dir_results.each do |result, children|
+ puts ""
+ puts "#{format_path(result.path)}:"
+ print_results(children.map { |result| result.name }.sort, "")
+ end
+ end
+
+ def add_dir_result(result)
+ begin
+ children = result.children.sort_by { |child| child.name }
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ STDERR.puts "#{format_path(result.path)}: No such file or directory"
+ return []
+ end
+
+ result = [ [ result, children ] ]
+ if config[:recursive]
+ children.each do |child|
+ if child.dir?
+ result += add_dir_result(child)
+ end
+ end
+ end
+ result
+ end
+
+ def list_dirs_recursive(children)
+ results = children.select { |child| child.dir? }.to_a
+ results.each do |child|
+ results += list_dirs_recursive(child.children)
+ end
+ results
+ end
+
+ def print_result_paths(results, indent = "")
+ print_results(results.map { |result| format_path(result.path) }, indent)
+ end
+
+ def print_results(results, indent)
+ return if results.length == 0
+
+ print_space = results.map { |result| result.length }.max + 2
+ # TODO: tput cols is not cross platform
+ columns = $stdout.isatty ? Integer(`tput cols`) : 0
+ current_column = 0
+ results.each do |result|
+ if current_column != 0 && current_column + print_space > columns
+ puts ""
+ current_column = 0
+ end
+ if current_column == 0
+ print indent
+ current_column += indent.length
+ end
+ print result + (' ' * (print_space - result.length))
+ current_column += print_space
+ end
+ puts ""
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/node_bulk_delete.rb b/lib/chef/knife/node_bulk_delete.rb
new file mode 100644
index 0000000000..89b2abe6ae
--- /dev/null
+++ b/lib/chef/knife/node_bulk_delete.rb
@@ -0,0 +1,80 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class NodeBulkDelete < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ end
+
+ banner "knife node bulk delete REGEX (options)"
+
+ def run
+ if name_args.length < 1
+ ui.fatal("You must supply a regular expression to match the results against")
+ exit 42
+ end
+
+
+ nodes_to_delete = {}
+ matcher = /#{name_args[0]}/
+
+ all_nodes.each do |name, node|
+ next unless name =~ matcher
+ nodes_to_delete[name] = node
+ end
+
+ if nodes_to_delete.empty?
+ ui.msg "No nodes match the expression /#{name_args[0]}/"
+ exit 0
+ end
+
+ ui.msg("The following nodes will be deleted:")
+ ui.msg("")
+ ui.msg(ui.list(nodes_to_delete.keys.sort, :columns_down))
+ ui.msg("")
+ ui.confirm("Are you sure you want to delete these nodes")
+
+
+ nodes_to_delete.sort.each do |name, node|
+ node.destroy
+ ui.msg("Deleted node #{name}")
+ end
+ end
+
+ def all_nodes
+ node_uris_by_name = Chef::Node.list
+
+ node_uris_by_name.keys.inject({}) do |nodes_by_name, name|
+ nodes_by_name[name] = Chef::Node.new.tap {|n| n.name(name)}
+ nodes_by_name
+ end
+ end
+
+ end
+ end
+end
+
+
+
+
diff --git a/lib/chef/knife/node_create.rb b/lib/chef/knife/node_create.rb
new file mode 100644
index 0000000000..7f50a30c80
--- /dev/null
+++ b/lib/chef/knife/node_create.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class NodeCreate < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ end
+
+ banner "knife node create NODE (options)"
+
+ def run
+ @node_name = @name_args[0]
+
+ if @node_name.nil?
+ show_usage
+ ui.fatal("You must specify a node name")
+ exit 1
+ end
+
+ node = Chef::Node.new
+ node.name(@node_name)
+ create_object(node)
+ end
+ end
+ end
+end
+
+
+
diff --git a/lib/chef/knife/node_delete.rb b/lib/chef/knife/node_delete.rb
new file mode 100644
index 0000000000..a645d32035
--- /dev/null
+++ b/lib/chef/knife/node_delete.rb
@@ -0,0 +1,47 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class NodeDelete < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ end
+
+ banner "knife node delete NODE (options)"
+
+ def run
+ @node_name = @name_args[0]
+
+ if @node_name.nil?
+ show_usage
+ ui.fatal("You must specify a node name")
+ exit 1
+ end
+
+ delete_object(Chef::Node, @node_name)
+ end
+
+ end
+ end
+end
+
diff --git a/lib/chef/knife/node_edit.rb b/lib/chef/knife/node_edit.rb
new file mode 100644
index 0000000000..0d6b8fcf6c
--- /dev/null
+++ b/lib/chef/knife/node_edit.rb
@@ -0,0 +1,72 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+
+ class NodeEdit < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ require 'chef/knife/core/node_editor'
+ end
+
+ banner "knife node edit NODE (options)"
+
+ option :all_attributes,
+ :short => "-a",
+ :long => "--all",
+ :boolean => true,
+ :description => "Display all attributes when editing"
+
+ def run
+ if node_name.nil?
+ show_usage
+ ui.fatal("You must specify a node name")
+ exit 1
+ end
+
+ updated_node = node_editor.edit_node
+ if updated_values = node_editor.updated?
+ ui.info "Saving updated #{updated_values.join(', ')} on node #{node.name}"
+ updated_node.save
+ else
+ ui.info "Node not updated, skipping node save"
+ end
+ end
+
+ def node_name
+ @node_name ||= @name_args[0]
+ end
+
+ def node_editor
+ @node_editor ||= Knife::NodeEditor.new(node, ui, config)
+ end
+
+ def node
+ @node ||= Chef::Node.load(node_name)
+ end
+
+ end
+ end
+end
+
+
diff --git a/lib/chef/knife/node_from_file.rb b/lib/chef/knife/node_from_file.rb
new file mode 100644
index 0000000000..d69392a8db
--- /dev/null
+++ b/lib/chef/knife/node_from_file.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class NodeFromFile < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ require 'chef/knife/core/object_loader'
+ end
+
+ banner "knife node from file FILE (options)"
+
+ def loader
+ @loader ||= Knife::Core::ObjectLoader.new(Chef::Node, ui)
+ end
+
+ def run
+ updated = loader.load_from('nodes', @name_args[0])
+
+ updated.save
+
+ output(format_for_display(updated)) if config[:print_after]
+
+ ui.info("Updated Node #{updated.name}!")
+ end
+
+ end
+ end
+end
+
diff --git a/lib/chef/knife/node_list.rb b/lib/chef/knife/node_list.rb
new file mode 100644
index 0000000000..3926d101cf
--- /dev/null
+++ b/lib/chef/knife/node_list.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class NodeList < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ end
+
+ banner "knife node list (options)"
+
+ option :with_uri,
+ :short => "-w",
+ :long => "--with-uri",
+ :description => "Show corresponding URIs"
+
+ def run
+ env = Chef::Config[:environment]
+ output(format_list_for_display( env ? Chef::Node.list_by_environment(env) : Chef::Node.list ))
+ end
+
+ end
+ end
+end
+
+
diff --git a/lib/chef/knife/node_run_list_add.rb b/lib/chef/knife/node_run_list_add.rb
new file mode 100644
index 0000000000..dcd41ae997
--- /dev/null
+++ b/lib/chef/knife/node_run_list_add.rb
@@ -0,0 +1,75 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class NodeRunListAdd < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ end
+
+ banner "knife node run_list add [NODE] [ENTRY[,ENTRY]] (options)"
+
+ option :after,
+ :short => "-a ITEM",
+ :long => "--after ITEM",
+ :description => "Place the ENTRY in the run list after ITEM"
+
+ def run
+ node = Chef::Node.load(@name_args[0])
+ if @name_args.size > 2
+ # Check for nested lists and create a single plain one
+ entries = @name_args[1..-1].map do |entry|
+ entry.split(',').map { |e| e.strip }
+ end.flatten
+ else
+ # Convert to array and remove the extra spaces
+ entries = @name_args[1].split(',').map { |e| e.strip }
+ end
+
+ add_to_run_list(node, entries, config[:after])
+
+ node.save
+
+ config[:run_list] = true
+
+ output(format_for_display(node))
+ end
+
+ def add_to_run_list(node, entries, after=nil)
+ if after
+ nlist = []
+ node.run_list.each do |entry|
+ nlist << entry
+ if entry == after
+ entries.each { |e| nlist << e }
+ end
+ end
+ node.run_list.reset!(nlist)
+ else
+ entries.each { |e| node.run_list << e }
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/node_run_list_remove.rb b/lib/chef/knife/node_run_list_remove.rb
new file mode 100644
index 0000000000..8519fd590a
--- /dev/null
+++ b/lib/chef/knife/node_run_list_remove.rb
@@ -0,0 +1,48 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class NodeRunListRemove < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ end
+
+ banner "knife node run_list remove [NODE] [ENTRIES] (options)"
+
+ def run
+ node = Chef::Node.load(@name_args[0])
+ entries = @name_args[1].split(',')
+
+ entries.each { |e| node.run_list.remove(e) }
+
+ node.save
+
+ config[:run_list] = true
+
+ output(format_for_display(node))
+ end
+
+ end
+ end
+end
+
diff --git a/lib/chef/knife/node_show.rb b/lib/chef/knife/node_show.rb
new file mode 100644
index 0000000000..4b0be18890
--- /dev/null
+++ b/lib/chef/knife/node_show.rb
@@ -0,0 +1,73 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+require 'chef/knife/core/node_presenter'
+
+class Chef
+ class Knife
+ class NodeShow < Knife
+
+ include Knife::Core::NodeFormattingOptions
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ end
+
+ banner "knife node show NODE (options)"
+
+ @attrs_to_show = []
+ option :attribute,
+ :short => "-a [ATTR]",
+ :long => "--attribute [ATTR]",
+ :proc => lambda {|val| @attrs_to_show << val},
+ :description => "Show one or more attributes"
+
+ option :run_list,
+ :short => "-r",
+ :long => "--run-list",
+ :description => "Show only the run list"
+
+ option :environment,
+ :short => "-E",
+ :long => "--environment",
+ :description => "Show only the Chef environment"
+
+ def run
+ ui.use_presenter Knife::Core::NodePresenter
+ @node_name = @name_args[0]
+
+ if @node_name.nil?
+ show_usage
+ ui.fatal("You must specify a node name")
+ exit 1
+ end
+
+ node = Chef::Node.load(@node_name)
+ output(format_for_display(node))
+ self.class.attrs_to_show = []
+ end
+
+ def self.attrs_to_show=(attrs)
+ @attrs_to_show = attrs
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/raw.rb b/lib/chef/knife/raw.rb
new file mode 100644
index 0000000000..ad5d5f33ef
--- /dev/null
+++ b/lib/chef/knife/raw.rb
@@ -0,0 +1,108 @@
+require 'json'
+
+class Chef
+ class Knife
+ class Raw < Chef::Knife
+ banner "knife raw REQUEST_PATH"
+
+ option :method,
+ :long => '--method METHOD',
+ :short => '-m METHOD',
+ :default => "GET",
+ :description => "Request method (GET, POST, PUT or DELETE)"
+
+ option :pretty,
+ :long => '--[no-]pretty',
+ :boolean => true,
+ :default => true,
+ :description => "Pretty-print JSON output"
+
+ option :input,
+ :long => '--input FILE',
+ :short => '-i FILE',
+ :description => "Name of file to use for PUT or POST"
+
+ def run
+ if name_args.length == 0
+ show_usage
+ ui.fatal("You must provide the path you want to hit on the server")
+ exit(1)
+ elsif name_args.length > 1
+ show_usage
+ ui.fatal("Only one path accepted for knife raw")
+ exit(1)
+ end
+
+ path = name_args[0]
+ data = false
+ if config[:input]
+ data = IO.read(config[:input])
+ end
+ chef_rest = Chef::REST.new(Chef::Config[:chef_server_url])
+ puts api_request(chef_rest, config[:method].to_sym, chef_rest.create_url(name_args[0]), {}, data)
+ end
+
+ ACCEPT_ENCODING = "Accept-Encoding".freeze
+ ENCODING_GZIP_DEFLATE = "gzip;q=1.0,deflate;q=0.6,identity;q=0.3".freeze
+
+ def redirected_to(response)
+ return nil unless response.kind_of?(Net::HTTPRedirection)
+ # Net::HTTPNotModified is undesired subclass of Net::HTTPRedirection so test for this
+ return nil if response.kind_of?(Net::HTTPNotModified)
+ response['location']
+ end
+
+ def api_request(chef_rest, method, url, headers={}, data=false)
+ json_body = data
+# json_body = data ? Chef::JSONCompat.to_json(data) : nil
+ # Force encoding to binary to fix SSL related EOFErrors
+ # cf. http://tickets.opscode.com/browse/CHEF-2363
+ # http://redmine.ruby-lang.org/issues/5233
+# json_body.force_encoding(Encoding::BINARY) if json_body.respond_to?(:force_encoding)
+ headers = build_headers(chef_rest, method, url, headers, json_body)
+
+ chef_rest.retriable_rest_request(method, url, json_body, headers) do |rest_request|
+ response = rest_request.call {|r| r.read_body}
+
+ response_body = chef_rest.decompress_body(response)
+
+ if response.kind_of?(Net::HTTPSuccess)
+ if config[:pretty] && response['content-type'] =~ /json/
+ JSON.pretty_generate(JSON.parse(response_body, :create_additions => false))
+ else
+ response_body
+ end
+ elsif redirect_location = redirected_to(response)
+ raise "Redirected to #{create_url(redirect_location)}"
+ follow_redirect {api_request(:GET, create_url(redirect_location))}
+ else
+ # have to decompress the body before making an exception for it. But the body could be nil.
+ response.body.replace(chef_rest.decompress_body(response)) if response.body.respond_to?(:replace)
+
+ if response['content-type'] =~ /json/
+ exception = response_body
+ msg = "HTTP Request Returned #{response.code} #{response.message}: "
+ msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s)
+ Chef::Log.info(msg)
+ end
+ puts response.body
+ response.error!
+ end
+ end
+ end
+
+ def build_headers(chef_rest, method, url, headers={}, json_body=false, raw=false)
+# headers = @default_headers.merge(headers)
+ #headers['Accept'] = "application/json" unless raw
+ headers['Accept'] = "application/json" unless raw
+ headers["Content-Type"] = 'application/json' if json_body
+ headers['Content-Length'] = json_body.bytesize.to_s if json_body
+ headers[Chef::REST::RESTRequest::ACCEPT_ENCODING] = Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE
+ headers.merge!(chef_rest.authentication_headers(method, url, json_body)) if chef_rest.sign_requests?
+ headers.merge!(Chef::Config[:custom_http_headers]) if Chef::Config[:custom_http_headers]
+ headers
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/recipe_list.rb b/lib/chef/knife/recipe_list.rb
new file mode 100644
index 0000000000..ed7d2a9509
--- /dev/null
+++ b/lib/chef/knife/recipe_list.rb
@@ -0,0 +1,32 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+class Chef::Knife::RecipeList < Chef::Knife
+
+ banner "knife recipe list [PATTERN]"
+
+ def run
+ recipes = rest.get_rest('cookbooks/_recipes')
+ if pattern = @name_args.first
+ recipes = recipes.grep(Regexp.new(pattern))
+ end
+ output(recipes)
+ end
+
+end
diff --git a/lib/chef/knife/role_bulk_delete.rb b/lib/chef/knife/role_bulk_delete.rb
new file mode 100644
index 0000000000..8b24f55846
--- /dev/null
+++ b/lib/chef/knife/role_bulk_delete.rb
@@ -0,0 +1,70 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class RoleBulkDelete < Knife
+
+ deps do
+ require 'chef/role'
+ require 'chef/json_compat'
+ end
+
+ banner "knife role bulk delete REGEX (options)"
+
+ def run
+ if @name_args.length < 1
+ ui.error("You must supply a regular expression to match the results against")
+ exit 1
+ end
+
+ all_roles = Chef::Role.list(true)
+
+ matcher = /#{@name_args[0]}/
+ roles_to_delete = {}
+ all_roles.each do |name, role|
+ next unless name =~ matcher
+ roles_to_delete[role.name] = role
+ end
+
+ if roles_to_delete.empty?
+ ui.info "No roles match the expression /#{@name_args[0]}/"
+ exit 0
+ end
+
+ ui.msg("The following roles will be deleted:")
+ ui.msg("")
+ ui.msg(ui.list(roles_to_delete.keys.sort, :columns_down))
+ ui.msg("")
+ ui.confirm("Are you sure you want to delete these roles")
+
+ roles_to_delete.sort.each do |name, role|
+ role.destroy
+ ui.msg("Deleted role #{name}")
+ end
+ end
+ end
+ end
+end
+
+
+
+
+
diff --git a/lib/chef/knife/role_create.rb b/lib/chef/knife/role_create.rb
new file mode 100644
index 0000000000..e9e363e870
--- /dev/null
+++ b/lib/chef/knife/role_create.rb
@@ -0,0 +1,55 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class RoleCreate < Knife
+
+ deps do
+ require 'chef/role'
+ require 'chef/json_compat'
+ end
+
+ banner "knife role create ROLE (options)"
+
+ option :description,
+ :short => "-d DESC",
+ :long => "--description DESC",
+ :description => "The role description"
+
+ def run
+ @role_name = @name_args[0]
+
+ if @role_name.nil?
+ show_usage
+ ui.fatal("You must specify a role name")
+ exit 1
+ end
+
+ role = Chef::Role.new
+ role.name(@role_name)
+ role.description(config[:description]) if config[:description]
+ create_object(role)
+ end
+ end
+ end
+end
+
+
diff --git a/lib/chef/knife/role_delete.rb b/lib/chef/knife/role_delete.rb
new file mode 100644
index 0000000000..b823f37359
--- /dev/null
+++ b/lib/chef/knife/role_delete.rb
@@ -0,0 +1,47 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class RoleDelete < Knife
+
+ deps do
+ require 'chef/role'
+ require 'chef/json_compat'
+ end
+
+ banner "knife role delete ROLE (options)"
+
+ def run
+ @role_name = @name_args[0]
+
+ if @role_name.nil?
+ show_usage
+ ui.fatal("You must specify a role name")
+ exit 1
+ end
+
+ delete_object(Chef::Role, @role_name)
+ end
+
+ end
+ end
+end
+
diff --git a/lib/chef/knife/role_edit.rb b/lib/chef/knife/role_edit.rb
new file mode 100644
index 0000000000..b0580988a0
--- /dev/null
+++ b/lib/chef/knife/role_edit.rb
@@ -0,0 +1,48 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class RoleEdit < Knife
+
+ deps do
+ require 'chef/role'
+ require 'chef/json_compat'
+ end
+
+ banner "knife role edit ROLE (options)"
+
+ def run
+ @role_name = @name_args[0]
+
+ if @role_name.nil?
+ show_usage
+ ui.fatal("You must specify a role name")
+ exit 1
+ end
+
+ ui.edit_object(Chef::Role, @role_name)
+ end
+ end
+ end
+end
+
+
+
diff --git a/lib/chef/knife/role_from_file.rb b/lib/chef/knife/role_from_file.rb
new file mode 100644
index 0000000000..c80218b753
--- /dev/null
+++ b/lib/chef/knife/role_from_file.rb
@@ -0,0 +1,56 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class RoleFromFile < Knife
+
+ deps do
+ require 'chef/role'
+ require 'chef/knife/core/object_loader'
+ require 'chef/json_compat'
+ end
+
+ banner "knife role from file FILE [FILE..] (options)"
+
+ def loader
+ @loader ||= Knife::Core::ObjectLoader.new(Chef::Role, ui)
+ end
+
+ def run
+ @name_args.each do |arg|
+ updated = loader.load_from("roles", arg)
+
+ updated.save
+
+ output(format_for_display(updated)) if config[:print_after]
+
+ ui.info("Updated Role #{updated.name}!")
+ end
+ end
+
+ end
+ end
+end
+
+
+
+
+
diff --git a/lib/chef/knife/role_list.rb b/lib/chef/knife/role_list.rb
new file mode 100644
index 0000000000..0f105b2188
--- /dev/null
+++ b/lib/chef/knife/role_list.rb
@@ -0,0 +1,43 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class RoleList < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ end
+
+ banner "knife role list (options)"
+
+ option :with_uri,
+ :short => "-w",
+ :long => "--with-uri",
+ :description => "Show corresponding URIs"
+
+ def run
+ output(format_list_for_display(Chef::Role.list))
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/role_show.rb b/lib/chef/knife/role_show.rb
new file mode 100644
index 0000000000..2f09794cbb
--- /dev/null
+++ b/lib/chef/knife/role_show.rb
@@ -0,0 +1,54 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class RoleShow < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/json_compat'
+ end
+
+ banner "knife role show ROLE (options)"
+
+ option :attribute,
+ :short => "-a ATTR",
+ :long => "--attribute ATTR",
+ :description => "Show only one attribute"
+
+ def run
+ @role_name = @name_args[0]
+
+ if @role_name.nil?
+ show_usage
+ ui.fatal("You must specify a role name")
+ exit 1
+ end
+
+ role = Chef::Role.load(@role_name)
+ output(format_for_display(config[:environment] ? role.environment(config[:environment]) : role))
+ end
+
+ end
+ end
+end
+
+
diff --git a/lib/chef/knife/search.rb b/lib/chef/knife/search.rb
new file mode 100644
index 0000000000..da739c4e62
--- /dev/null
+++ b/lib/chef/knife/search.rb
@@ -0,0 +1,141 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+require 'chef/knife/core/node_presenter'
+
+class Chef
+ class Knife
+ class Search < Knife
+
+ deps do
+ require 'chef/node'
+ require 'chef/environment'
+ require 'chef/api_client'
+ require 'chef/search/query'
+ end
+
+ include Knife::Core::NodeFormattingOptions
+
+ banner "knife search INDEX QUERY (options)"
+
+ option :sort,
+ :short => "-o SORT",
+ :long => "--sort SORT",
+ :description => "The order to sort the results in",
+ :default => nil
+
+ option :start,
+ :short => "-b ROW",
+ :long => "--start ROW",
+ :description => "The row to start returning results at",
+ :default => 0,
+ :proc => lambda { |i| i.to_i }
+
+ option :rows,
+ :short => "-R INT",
+ :long => "--rows INT",
+ :description => "The number of rows to return",
+ :default => 1000,
+ :proc => lambda { |i| i.to_i }
+
+ option :attribute,
+ :short => "-a ATTR",
+ :long => "--attribute ATTR",
+ :description => "Show only one attribute"
+
+ option :run_list,
+ :short => "-r",
+ :long => "--run-list",
+ :description => "Show only the run list"
+
+ option :id_only,
+ :short => "-i",
+ :long => "--id-only",
+ :description => "Show only the ID of matching objects"
+
+ option :query,
+ :short => "-q QUERY",
+ :long => "--query QUERY",
+ :description => "The search query; useful to protect queries starting with -"
+
+ def run
+ if config[:query] && @name_args[1]
+ ui.error "please specify query as an argument or an option via -q, not both"
+ ui.msg opt_parser
+ exit 1
+ end
+ raw_query = config[:query] || @name_args[1]
+ if !raw_query || raw_query.empty?
+ ui.error "no query specified"
+ ui.msg opt_parser
+ exit 1
+ end
+
+ if name_args[0].nil?
+ ui.error "you must specify an item type to search for"
+ exit 1
+ end
+
+ if name_args[0] == 'node'
+ ui.use_presenter Knife::Core::NodePresenter
+ end
+
+
+ q = Chef::Search::Query.new
+ query = URI.escape(raw_query,
+ Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
+
+ result_items = []
+ result_count = 0
+
+ rows = config[:rows]
+ start = config[:start]
+ begin
+ q.search(@name_args[0], query, config[:sort], start, rows) do |item|
+ formatted_item = format_for_display(item)
+ # if formatted_item.respond_to?(:has_key?) && !formatted_item.has_key?('id')
+ # formatted_item['id'] = item.has_key?('id') ? item['id'] : item.name
+ # end
+ result_items << formatted_item
+ result_count += 1
+ end
+ rescue Net::HTTPServerException => e
+ msg = Chef::JSONCompat.from_json(e.response.body)["error"].first
+ ui.error("knife search failed: #{msg}")
+ exit 1
+ end
+
+ if ui.interchange?
+ output({:results => result_count, :rows => result_items})
+ else
+ ui.msg "#{result_count} items found"
+ ui.msg("\n")
+ result_items.each do |item|
+ output(item)
+ ui.msg("\n")
+ end
+ end
+ end
+ end
+ end
+end
+
+
+
+
diff --git a/lib/chef/knife/show.rb b/lib/chef/knife/show.rb
new file mode 100644
index 0000000000..7075315b08
--- /dev/null
+++ b/lib/chef/knife/show.rb
@@ -0,0 +1,32 @@
+require 'chef/chef_fs/knife'
+require 'chef/chef_fs/file_system'
+
+class Chef
+ class Knife
+ class Show < Chef::ChefFS::Knife
+ banner "knife show [PATTERN1 ... PATTERNn]"
+
+ common_options
+
+ def run
+ # Get the matches (recursively)
+ pattern_args.each do |pattern|
+ Chef::ChefFS::FileSystem.list(chef_fs, pattern) do |result|
+ if result.dir?
+ STDERR.puts "#{result.path_for_printing}: is a directory" if pattern.exact_path
+ else
+ begin
+ value = result.read
+ puts "#{result.path_for_printing}:"
+ output(format_for_display(value))
+ rescue Chef::ChefFS::FileSystem::NotFoundError
+ STDERR.puts "#{result.path_for_printing}: No such file or directory"
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/knife/ssh.rb b/lib/chef/knife/ssh.rb
new file mode 100644
index 0000000000..a1b37723a6
--- /dev/null
+++ b/lib/chef/knife/ssh.rb
@@ -0,0 +1,444 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class Ssh < Knife
+
+ deps do
+ require 'net/ssh'
+ require 'net/ssh/multi'
+ require 'readline'
+ require 'chef/exceptions'
+ require 'chef/search/query'
+ require 'chef/mixin/shell_out'
+ require 'mixlib/shellout'
+ end
+
+ include Chef::Mixin::ShellOut
+
+ attr_writer :password
+
+ banner "knife ssh QUERY COMMAND (options)"
+
+ option :concurrency,
+ :short => "-C NUM",
+ :long => "--concurrency NUM",
+ :description => "The number of concurrent connections",
+ :default => nil,
+ :proc => lambda { |o| o.to_i }
+
+ option :attribute,
+ :short => "-a ATTR",
+ :long => "--attribute ATTR",
+ :description => "The attribute to use for opening the connection - default depends on the context",
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_attribute] = key.strip }
+
+ option :manual,
+ :short => "-m",
+ :long => "--manual-list",
+ :boolean => true,
+ :description => "QUERY is a space separated list of servers",
+ :default => false
+
+ option :ssh_user,
+ :short => "-x USERNAME",
+ :long => "--ssh-user USERNAME",
+ :description => "The ssh username"
+
+ option :ssh_password,
+ :short => "-P PASSWORD",
+ :long => "--ssh-password PASSWORD",
+ :description => "The ssh password"
+
+ option :ssh_port,
+ :short => "-p PORT",
+ :long => "--ssh-port PORT",
+ :description => "The ssh port",
+ :default => "22",
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
+
+ option :ssh_gateway,
+ :short => "-G GATEWAY",
+ :long => "--ssh-gateway GATEWAY",
+ :description => "The ssh gateway",
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key }
+
+ option :identity_file,
+ :short => "-i IDENTITY_FILE",
+ :long => "--identity-file IDENTITY_FILE",
+ :description => "The SSH identity file used for authentication"
+
+ option :host_key_verify,
+ :long => "--[no-]host-key-verify",
+ :description => "Verify host key, enabled by default.",
+ :boolean => true,
+ :default => true
+
+ def session
+ config[:on_error] ||= :skip
+ ssh_error_handler = Proc.new do |server|
+ if config[:manual]
+ node_name = server.host
+ else
+ @action_nodes.each do |n|
+ node_name = n if format_for_display(n)[config[:attribute]] == server.host
+ end
+ end
+ case config[:on_error]
+ when :skip
+ ui.warn "Failed to connect to #{node_name} -- #{$!.class.name}: #{$!.message}"
+ $!.backtrace.each { |l| Chef::Log.debug(l) }
+ when :raise
+ #Net::SSH::Multi magic to force exception to be re-raised.
+ throw :go, :raise
+ end
+ end
+
+ @session ||= Net::SSH::Multi.start(:concurrent_connections => config[:concurrency], :on_error => ssh_error_handler)
+ end
+
+ def configure_session
+ list = case config[:manual]
+ when true
+ @name_args[0].split(" ")
+ when false
+ r = Array.new
+ q = Chef::Search::Query.new
+ @action_nodes = q.search(:node, @name_args[0])[0]
+ @action_nodes.each do |item|
+ # we should skip the loop to next iteration if the item returned by the search is nil
+ next if item.nil?
+ # if a command line attribute was not passed, and we have a cloud public_hostname, use that.
+ # see #configure_attribute for the source of config[:attribute] and config[:override_attribute]
+ if !config[:override_attribute] && item[:cloud] and item[:cloud][:public_hostname]
+ i = item[:cloud][:public_hostname]
+ elsif config[:override_attribute]
+ i = extract_nested_value(item, config[:override_attribute])
+ else
+ i = extract_nested_value(item, config[:attribute])
+ end
+ # next if we couldn't find the specified attribute in the returned node object
+ next if i.nil?
+ r.push(i)
+ end
+ r
+ end
+ if list.length == 0
+ if @action_nodes.length == 0
+ ui.fatal("No nodes returned from search!")
+ else
+ ui.fatal("#{@action_nodes.length} #{@action_nodes.length > 1 ? "nodes":"node"} found, " +
+ "but do not have the required attribute to stablish the connection. " +
+ "Try setting another attribute to open the connection using --attribute.")
+ end
+ exit 10
+ end
+ session_from_list(list)
+ end
+
+ def session_from_list(list)
+ config[:ssh_gateway] ||= Chef::Config[:knife][:ssh_gateway]
+ if config[:ssh_gateway]
+ gw_host, gw_user = config[:ssh_gateway].split('@').reverse
+ gw_host, gw_port = gw_host.split(':')
+ gw_opts = gw_port ? { :port => gw_port } : {}
+
+ session.via(gw_host, gw_user || config[:ssh_user], gw_opts)
+ end
+
+ list.each do |item|
+ Chef::Log.debug("Adding #{item}")
+
+ hostspec = config[:ssh_user] ? "#{config[:ssh_user]}@#{item}" : item
+ session_opts = {}
+ session_opts[:keys] = File.expand_path(config[:identity_file]) if config[:identity_file]
+ session_opts[:keys_only] = true if config[:identity_file]
+ session_opts[:password] = config[:ssh_password] if config[:ssh_password]
+ session_opts[:port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
+ session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug
+
+ if !config[:host_key_verify]
+ session_opts[:paranoid] = false
+ session_opts[:user_known_hosts_file] = "/dev/null"
+ end
+
+ session.use(hostspec, session_opts)
+
+ @longest = item.length if item.length > @longest
+ end
+
+ session
+ end
+
+ def fixup_sudo(command)
+ command.sub(/^sudo/, 'sudo -p \'knife sudo password: \'')
+ end
+
+ def print_data(host, data)
+ if data =~ /\n/
+ data.split(/\n/).each { |d| print_data(host, d) }
+ else
+ padding = @longest - host.length
+ str = ui.color(host, :cyan) + (" " * (padding + 1)) + data
+ ui.msg(str)
+ end
+ end
+
+ def ssh_command(command, subsession=nil)
+ exit_status = 0
+ subsession ||= session
+ command = fixup_sudo(command)
+ command.force_encoding('binary') if command.respond_to?(:force_encoding)
+ subsession.open_channel do |ch|
+ ch.request_pty
+ ch.exec command do |ch, success|
+ raise ArgumentError, "Cannot execute #{command}" unless success
+ ch.on_data do |ichannel, data|
+ print_data(ichannel[:host], data)
+ if data =~ /^knife sudo password: /
+ ichannel.send_data("#{get_password}\n")
+ end
+ end
+ ch.on_request "exit-status" do |ichannel, data|
+ exit_status = data.read_long
+ end
+ end
+ end
+ session.loop
+ exit_status
+ end
+
+ def get_password
+ @password ||= ui.ask("Enter your password: ") { |q| q.echo = false }
+ end
+
+ # Present the prompt and read a single line from the console. It also
+ # detects ^D and returns "exit" in that case. Adds the input to the
+ # history, unless the input is empty. Loops repeatedly until a non-empty
+ # line is input.
+ def read_line
+ loop do
+ command = reader.readline("#{ui.color('knife-ssh>', :bold)} ", true)
+
+ if command.nil?
+ command = "exit"
+ puts(command)
+ else
+ command.strip!
+ end
+
+ unless command.empty?
+ return command
+ end
+ end
+ end
+
+ def reader
+ Readline
+ end
+
+ def interactive
+ puts "Connected to #{ui.list(session.servers_for.collect { |s| ui.color(s.host, :cyan) }, :inline, " and ")}"
+ puts
+ puts "To run a command on a list of servers, do:"
+ puts " on SERVER1 SERVER2 SERVER3; COMMAND"
+ puts " Example: on latte foamy; echo foobar"
+ puts
+ puts "To exit interactive mode, use 'quit!'"
+ puts
+ while 1
+ command = read_line
+ case command
+ when 'quit!'
+ puts 'Bye!'
+ break
+ when /^on (.+?); (.+)$/
+ raw_list = $1.split(" ")
+ server_list = Array.new
+ session.servers.each do |session_server|
+ server_list << session_server if raw_list.include?(session_server.host)
+ end
+ command = $2
+ ssh_command(command, session.on(*server_list))
+ else
+ ssh_command(command)
+ end
+ end
+ end
+
+ def screen
+ tf = Tempfile.new("knife-ssh-screen")
+ if File.exist? "#{ENV["HOME"]}/.screenrc"
+ tf.puts("source #{ENV["HOME"]}/.screenrc")
+ end
+ tf.puts("caption always '%-Lw%{= BW}%50>%n%f* %t%{-}%+Lw%<'")
+ tf.puts("hardstatus alwayslastline 'knife ssh #{@name_args[0]}'")
+ window = 0
+ session.servers_for.each do |server|
+ tf.print("screen -t \"#{server.host}\" #{window} ssh ")
+ tf.print("-i #{config[:identity_file]} ") if config[:identity_file]
+ server.user ? tf.puts("#{server.user}@#{server.host}") : tf.puts(server.host)
+ window += 1
+ end
+ tf.close
+ exec("screen -c #{tf.path}")
+ end
+
+ def tmux
+ ssh_dest = lambda do |server|
+ identity = "-i #{config[:identity_file]} " if config[:identity_file]
+ prefix = server.user ? "#{server.user}@" : ""
+ "'ssh #{identity}#{prefix}#{server.host}'"
+ end
+
+ new_window_cmds = lambda do
+ if session.servers_for.size > 1
+ [""] + session.servers_for[1..-1].map do |server|
+ "new-window -a -n '#{server.host}' #{ssh_dest.call(server)}"
+ end
+ else
+ []
+ end.join(" \\; ")
+ end
+
+ tmux_name = "'knife ssh #{@name_args[0].gsub(/:/,'=')}'"
+ begin
+ server = session.servers_for.first
+ cmd = ["tmux new-session -d -s #{tmux_name}",
+ "-n '#{server.host}'", ssh_dest.call(server),
+ new_window_cmds.call].join(" ")
+ shell_out!(cmd)
+ exec("tmux attach-session -t #{tmux_name}")
+ rescue Chef::Exceptions::Exec
+ end
+ end
+
+ def macterm
+ begin
+ require 'appscript'
+ rescue LoadError
+ STDERR.puts "you need the rb-appscript gem to use knife ssh macterm. `(sudo) gem install rb-appscript` to install"
+ raise
+ end
+
+ Appscript.app("/Applications/Utilities/Terminal.app").windows.first.activate
+ Appscript.app("System Events").application_processes["Terminal.app"].keystroke("n", :using=>:command_down)
+ term = Appscript.app('Terminal')
+ window = term.windows.first.get
+
+ (session.servers_for.size - 1).times do |i|
+ window.activate
+ Appscript.app("System Events").application_processes["Terminal.app"].keystroke("t", :using=>:command_down)
+ end
+
+ session.servers_for.each_with_index do |server, tab_number|
+ cmd = "unset PROMPT_COMMAND; echo -e \"\\033]0;#{server.host}\\007\"; ssh #{server.user ? "#{server.user}@#{server.host}" : server.host}"
+ Appscript.app('Terminal').do_script(cmd, :in => window.tabs[tab_number + 1].get)
+ end
+ end
+
+ def configure_attribute
+ # Setting 'knife[:ssh_attribute] = "foo"' in knife.rb => Chef::Config[:knife][:ssh_attribute] == 'foo'
+ # Running 'knife ssh -a foo' => both Chef::Config[:knife][:ssh_attribute] && config[:attribute] == foo
+ # Thus we can differentiate between a config file value and a command line override at this point by checking config[:attribute]
+ # We can tell here if fqdn was passed from the command line, rather than being the default, by checking config[:attribute]
+ # However, after here, we cannot tell these things, so we must preserve config[:attribute]
+ config[:override_attribute] = config[:attribute] || Chef::Config[:knife][:ssh_attribute]
+ config[:attribute] = (Chef::Config[:knife][:ssh_attribute] ||
+ config[:attribute] ||
+ "fqdn").strip
+ end
+
+ def cssh
+ cssh_cmd = nil
+ %w[csshX cssh].each do |cmd|
+ begin
+ # Unix and Mac only
+ cssh_cmd = shell_out!("which #{cmd}").stdout.strip
+ break
+ rescue Mixlib::ShellOut::ShellCommandFailed
+ end
+ end
+ raise Chef::Exceptions::Exec, "no command found for cssh" unless cssh_cmd
+
+ session.servers_for.each do |server|
+ cssh_cmd << " #{server.user ? "#{server.user}@#{server.host}" : server.host}"
+ end
+ Chef::Log.debug("starting cssh session with command: #{cssh_cmd}")
+ exec(cssh_cmd)
+ end
+
+ def get_stripped_unfrozen_value(value)
+ return nil if value.nil?
+ value.strip
+ end
+
+ def configure_user
+ config[:ssh_user] = get_stripped_unfrozen_value(config[:ssh_user] ||
+ Chef::Config[:knife][:ssh_user])
+ end
+
+ def configure_identity_file
+ config[:identity_file] = get_stripped_unfrozen_value(config[:identity_file] ||
+ Chef::Config[:knife][:ssh_identity_file])
+ end
+
+ def extract_nested_value(data_structure, path_spec)
+ ui.presenter.extract_nested_value(data_structure, path_spec)
+ end
+
+ def run
+ extend Chef::Mixin::Command
+
+ @longest = 0
+
+ configure_attribute
+ configure_user
+ configure_identity_file
+ configure_session
+
+ exit_status =
+ case @name_args[1]
+ when "interactive"
+ interactive
+ when "screen"
+ screen
+ when "tmux"
+ tmux
+ when "macterm"
+ macterm
+ when "cssh"
+ cssh
+ when "csshx"
+ Chef::Log.warn("knife ssh csshx will be deprecated in a future release")
+ Chef::Log.warn("please use knife ssh cssh instead")
+ cssh
+ else
+ ssh_command(@name_args[1..-1].join(" "))
+ end
+
+ session.close
+ exit_status
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/status.rb b/lib/chef/knife/status.rb
new file mode 100644
index 0000000000..ceb394ce3a
--- /dev/null
+++ b/lib/chef/knife/status.rb
@@ -0,0 +1,119 @@
+#
+# Author:: Ian Meyer (<ianmmeyer@gmail.com>)
+# Copyright:: Copyright (c) 2010 Ian Meyer
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class Status < Knife
+
+ deps do
+ require 'highline'
+ require 'chef/search/query'
+ end
+
+ banner "knife status QUERY (options)"
+
+ option :run_list,
+ :short => "-r",
+ :long => "--run-list",
+ :description => "Show the run list"
+
+ option :sort_reverse,
+ :short => "-s",
+ :long => "--sort-reverse",
+ :description => "Sort the status list by last run time descending"
+
+ option :hide_healthy,
+ :short => "-H",
+ :long => "--hide-healthy",
+ :description => "Hide nodes that have run chef in the last hour"
+
+ def highline
+ @h ||= HighLine.new
+ end
+
+ def run
+ all_nodes = []
+ q = Chef::Search::Query.new
+ query = @name_args[0] || "*:*"
+ q.search(:node, query) do |node|
+ all_nodes << node
+ end
+ all_nodes.sort { |n1, n2|
+ if (config[:sort_reverse] || Chef::Config[:knife][:sort_status_reverse])
+ (n2["ohai_time"] or 0) <=> (n1["ohai_time"] or 0)
+ else
+ (n1["ohai_time"] or 0) <=> (n2["ohai_time"] or 0)
+ end
+ }.each do |node|
+ if node.has_key?("ec2")
+ fqdn = node['ec2']['public_hostname']
+ ipaddress = node['ec2']['public_ipv4']
+ else
+ fqdn = node['fqdn']
+ ipaddress = node['ipaddress']
+ end
+ hours, minutes, seconds = time_difference_in_hms(node["ohai_time"])
+ hours_text = "#{hours} hour#{hours == 1 ? ' ' : 's'}"
+ minutes_text = "#{minutes} minute#{minutes == 1 ? ' ' : 's'}"
+ run_list = ", #{node.run_list}." if config[:run_list]
+ if hours > 24
+ color = :red
+ text = hours_text
+ elsif hours >= 1
+ color = :yellow
+ text = hours_text
+ else
+ color = :green
+ text = minutes_text
+ end
+
+ line_parts = Array.new
+ line_parts << @ui.color(text, color) + " ago" << node.name
+ line_parts << fqdn if fqdn
+ line_parts << ipaddress if ipaddress
+ line_parts << run_list if run_list
+
+ if node['platform']
+ platform = node['platform']
+ if node['platform_version']
+ platform << " #{node['platform_version']}"
+ end
+ line_parts << platform
+ end
+ highline.say(line_parts.join(', ') + '.') unless (config[:hide_healthy] && hours < 1)
+ end
+
+ end
+
+ # :nodoc:
+ # TODO: this is duplicated from StatusHelper in the Webui. dedup.
+ def time_difference_in_hms(unix_time)
+ now = Time.now.to_i
+ difference = now - unix_time.to_i
+ hours = (difference / 3600).to_i
+ difference = difference % 3600
+ minutes = (difference / 60).to_i
+ seconds = (difference % 60)
+ return [hours, minutes, seconds]
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/tag_create.rb b/lib/chef/knife/tag_create.rb
new file mode 100644
index 0000000000..d3ca95242d
--- /dev/null
+++ b/lib/chef/knife/tag_create.rb
@@ -0,0 +1,52 @@
+#
+# Author:: Ryan Davis (<ryand-ruby@zenspider.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Copyright:: Copyright (c) 2011 Ryan Davis and Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class TagCreate < Knife
+
+ deps do
+ require 'chef/node'
+ end
+
+ banner "knife tag create NODE TAG ..."
+
+ def run
+ name = @name_args[0]
+ tags = @name_args[1..-1]
+
+ if name.nil? || tags.nil? || tags.empty?
+ show_usage
+ ui.fatal("You must specify a node name and at least one tag.")
+ exit 1
+ end
+
+ node = Chef::Node.load name
+ tags.each do |tag|
+ (node.tags << tag).uniq!
+ end
+ node.save
+ ui.info("Created tags #{tags.join(", ")} for node #{name}.")
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/tag_delete.rb b/lib/chef/knife/tag_delete.rb
new file mode 100644
index 0000000000..10751db216
--- /dev/null
+++ b/lib/chef/knife/tag_delete.rb
@@ -0,0 +1,60 @@
+#
+# Author:: Ryan Davis (<ryand-ruby@zenspider.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Copyright:: Copyright (c) 2011 Ryan Davis and Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class TagDelete < Knife
+
+ deps do
+ require 'chef/node'
+ end
+
+ banner "knife tag delete NODE TAG ..."
+
+ def run
+ name = @name_args[0]
+ tags = @name_args[1..-1]
+
+ if name.nil? || tags.nil? || tags.empty?
+ show_usage
+ ui.fatal("You must specify a node name and at least one tag.")
+ exit 1
+ end
+
+ node = Chef::Node.load name
+ deleted_tags = Array.new
+ tags.each do |tag|
+ unless node.tags.delete(tag).nil?
+ deleted_tags << tag
+ end
+ end
+ node.save
+ message = if deleted_tags.empty?
+ "Nothing has changed. The tags requested to be deleted do not exist."
+ else
+ "Deleted tags #{deleted_tags.join(", ")} for node #{name}."
+ end
+ ui.info(message)
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/tag_list.rb b/lib/chef/knife/tag_list.rb
new file mode 100644
index 0000000000..499eb8578c
--- /dev/null
+++ b/lib/chef/knife/tag_list.rb
@@ -0,0 +1,47 @@
+#
+# Author:: Ryan Davis (<ryand-ruby@zenspider.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Copyright:: Copyright (c) 2011 Ryan Davis and Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/knife'
+
+class Chef
+ class Knife
+ class TagList < Knife
+
+ deps do
+ require 'chef/node'
+ end
+
+ banner "knife tag list NODE"
+
+ def run
+ name = @name_args[0]
+
+ if name.nil?
+ show_usage
+ ui.fatal("You must specify a node name.")
+ exit 1
+ end
+
+ node = Chef::Node.load(name)
+ output(node.tags)
+ end
+ end
+ end
+end
diff --git a/lib/chef/knife/upload.rb b/lib/chef/knife/upload.rb
new file mode 100644
index 0000000000..ff8616543e
--- /dev/null
+++ b/lib/chef/knife/upload.rb
@@ -0,0 +1,47 @@
+require 'chef/chef_fs/knife'
+require 'chef/chef_fs/command_line'
+
+class Chef
+ class Knife
+ class Upload < Chef::ChefFS::Knife
+ banner "knife upload PATTERNS"
+
+ common_options
+
+ option :recurse,
+ :long => '--[no-]recurse',
+ :boolean => true,
+ :default => true,
+ :description => "List directories recursively."
+
+ option :purge,
+ :long => '--[no-]purge',
+ :boolean => true,
+ :default => false,
+ :description => "Delete matching local files and directories that do not exist remotely."
+
+ option :force,
+ :long => '--[no-]force',
+ :boolean => true,
+ :default => false,
+ :description => "Force upload of files even if they match (quicker and harmless, but doesn't print out what it changed)"
+
+ option :dry_run,
+ :long => '--dry-run',
+ :short => '-n',
+ :boolean => true,
+ :default => false,
+ :description => "Don't take action, only print what would happen"
+
+ def run
+ patterns = pattern_args_from(name_args.length > 0 ? name_args : [ "" ])
+
+ # Get the matches (recursively)
+ patterns.each do |pattern|
+ Chef::ChefFS::FileSystem.copy_to(pattern, local_fs, chef_fs, config[:recurse] ? nil : 1, config)
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/log.rb b/lib/chef/log.rb
new file mode 100644
index 0000000000..7355ec7574
--- /dev/null
+++ b/lib/chef/log.rb
@@ -0,0 +1,39 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: AJ Christensen (<@aj@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Copyright:: Copyright (c) 2008 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 'logger'
+require 'mixlib/log'
+
+class Chef
+ class Log
+ extend Mixlib::Log
+
+ # Force initialization of the primary log device (@logger)
+ init
+
+
+ class Formatter
+ def self.show_time=(*args)
+ Mixlib::Log::Formatter.show_time = *args
+ end
+ end
+
+ end
+end
+
diff --git a/lib/chef/mash.rb b/lib/chef/mash.rb
new file mode 100644
index 0000000000..edd254cb77
--- /dev/null
+++ b/lib/chef/mash.rb
@@ -0,0 +1,225 @@
+# Copyright (c) 2009 Dan Kubb
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+# ---
+# ---
+
+# Some portions of blank.rb and mash.rb are verbatim copies of software
+# licensed under the MIT license. That license is included below:
+
+# Copyright (c) 2005-2008 David Heinemeier Hansson
+
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+# This class has dubious semantics and we only have it so that people can write
+# params[:key] instead of params['key'].
+class Mash < Hash
+
+ # @param constructor<Object>
+ # The default value for the mash. Defaults to an empty hash.
+ #
+ # @details [Alternatives]
+ # If constructor is a Hash, a new mash will be created based on the keys of
+ # the hash and no default value will be set.
+ def initialize(constructor = {})
+ if constructor.is_a?(Hash)
+ super()
+ update(constructor)
+ else
+ super(constructor)
+ end
+ end
+
+ # @param orig<Object> Mash being copied
+ #
+ # @return [Object] A new copied Mash
+ def initialize_copy(orig)
+ super
+ # Handle nested values
+ each do |k,v|
+ if v.kind_of?(Mash) || v.is_a?(Array)
+ self[k] = v.dup
+ end
+ end
+ self
+ end
+
+ # @param key<Object> The default value for the mash. Defaults to nil.
+ #
+ # @details [Alternatives]
+ # If key is a Symbol and it is a key in the mash, then the default value will
+ # be set to the value matching the key.
+ def default(key = nil)
+ if key.is_a?(Symbol) && include?(key = key.to_s)
+ self[key]
+ else
+ super
+ end
+ end
+
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
+
+ # @param key<Object> The key to set.
+ # @param value<Object>
+ # The value to set the key to.
+ #
+ # @see Mash#convert_key
+ # @see Mash#convert_value
+ def []=(key, value)
+ regular_writer(convert_key(key), convert_value(value))
+ end
+
+ # @param other_hash<Hash>
+ # A hash to update values in the mash with. The keys and the values will be
+ # converted to Mash format.
+ #
+ # @return [Mash] The updated mash.
+ def update(other_hash)
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
+ self
+ end
+
+ alias_method :merge!, :update
+
+ # @param key<Object> The key to check for. This will be run through convert_key.
+ #
+ # @return [Boolean] True if the key exists in the mash.
+ def key?(key)
+ super(convert_key(key))
+ end
+
+ # def include? def has_key? def member?
+ alias_method :include?, :key?
+ alias_method :has_key?, :key?
+ alias_method :member?, :key?
+
+ # @param key<Object> The key to fetch. This will be run through convert_key.
+ # @param *extras<Array> Default value.
+ #
+ # @return [Object] The value at key or the default value.
+ def fetch(key, *extras)
+ super(convert_key(key), *extras)
+ end
+
+ # @param *indices<Array>
+ # The keys to retrieve values for. These will be run through +convert_key+.
+ #
+ # @return [Array] The values at each of the provided keys
+ def values_at(*indices)
+ indices.collect {|key| self[convert_key(key)]}
+ end
+
+ # @param hash<Hash> The hash to merge with the mash.
+ #
+ # @return [Mash] A new mash with the hash values merged in.
+ def merge(hash)
+ self.dup.update(hash)
+ end
+
+ # @param key<Object>
+ # The key to delete from the mash.\
+ def delete(key)
+ super(convert_key(key))
+ end
+
+ # @param *rejected<Array[(String, Symbol)] The mash keys to exclude.
+ #
+ # @return [Mash] A new mash without the selected keys.
+ #
+ # @example
+ # { :one => 1, :two => 2, :three => 3 }.except(:one)
+ # #=> { "two" => 2, "three" => 3 }
+ def except(*keys)
+ super(*keys.map {|k| convert_key(k)})
+ end
+
+ # Used to provide the same interface as Hash.
+ #
+ # @return [Mash] This mash unchanged.
+ def stringify_keys!; self end
+
+ # @return [Hash] The mash as a Hash with symbolized keys.
+ def symbolize_keys
+ h = Hash.new(default)
+ each { |key, val| h[key.to_sym] = val }
+ h
+ end
+
+ # @return [Hash] The mash as a Hash with string keys.
+ def to_hash
+ Hash.new(default).merge(self)
+ end
+
+ # @return [Mash] Convert a Hash into a Mash
+ # The input Hash's default value is maintained
+ def self.from_hash(hash)
+ mash = Mash.new(hash)
+ mash.default = hash.default
+ mash
+ end
+
+ protected
+ # @param key<Object> The key to convert.
+ #
+ # @param [Object]
+ # The converted key. If the key was a symbol, it will be converted to a
+ # string.
+ #
+ # @api private
+ def convert_key(key)
+ key.kind_of?(Symbol) ? key.to_s : key
+ end
+
+ # @param value<Object> The value to convert.
+ #
+ # @return [Object]
+ # The converted value. A Hash or an Array of hashes, will be converted to
+ # their Mash equivalents.
+ #
+ # @api private
+ def convert_value(value)
+ if value.class == Hash
+ Mash.from_hash(value)
+ elsif value.is_a?(Array)
+ value.collect { |e| convert_value(e) }
+ else
+ value
+ end
+ end
+end
diff --git a/lib/chef/mixin/check_helper.rb b/lib/chef/mixin/check_helper.rb
new file mode 100644
index 0000000000..b3a7835e09
--- /dev/null
+++ b/lib/chef/mixin/check_helper.rb
@@ -0,0 +1,31 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+ module Mixin
+ module CheckHelper
+ def set_if_args(thing, arguments)
+ raise ArgumentError, "Must call set_if_args with a block!" unless Kernel.block_given?
+ if arguments != nil
+ yield(arguments)
+ else
+ thing
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/mixin/checksum.rb b/lib/chef/mixin/checksum.rb
new file mode 100644
index 0000000000..7b716b6285
--- /dev/null
+++ b/lib/chef/mixin/checksum.rb
@@ -0,0 +1,32 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 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 'digest/sha2'
+require 'chef/checksum_cache'
+
+class Chef
+ module Mixin
+ module Checksum
+
+ def checksum(file)
+ Chef::ChecksumCache.checksum_for_file(file)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/command.rb b/lib/chef/mixin/command.rb
new file mode 100644
index 0000000000..55c028ff5f
--- /dev/null
+++ b/lib/chef/mixin/command.rb
@@ -0,0 +1,164 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/log'
+require 'chef/exceptions'
+require 'tmpdir'
+require 'fcntl'
+require 'etc'
+
+class Chef
+ module Mixin
+ module Command
+ extend self
+
+ # NOTE: run_command is deprecated in favor of using Chef::Shellout which now comes from the mixlib-shellout gem. NOTE #
+
+ if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ require 'chef/mixin/command/windows'
+ include ::Chef::Mixin::Command::Windows
+ extend ::Chef::Mixin::Command::Windows
+ else
+ require 'chef/mixin/command/unix'
+ include ::Chef::Mixin::Command::Unix
+ extend ::Chef::Mixin::Command::Unix
+ end
+
+ # === Parameters
+ # args<Hash>: A number of required and optional arguments
+ # command<String>, <Array>: A complete command with options to execute or a command and options as an Array
+ # creates<String>: The absolute path to a file that prevents the command from running if it exists
+ # cwd<String>: Working directory to execute command in, defaults to Dir.tmpdir
+ # timeout<String>: How many seconds to wait for the command to execute before timing out
+ # returns<String>: The single exit value command is expected to return, otherwise causes an exception
+ # ignore_failure<Boolean>: Whether to raise an exception on failure, or just return the status
+ # output_on_failure<Boolean>: Return output in raised exception regardless of Log.level
+ #
+ # user<String>: The UID or user name of the user to execute the command as
+ # group<String>: The GID or group name of the group to execute the command as
+ # environment<Hash>: Pairs of environment variable names and their values to set before execution
+ #
+ # === Returns
+ # Returns the exit status of args[:command]
+ def run_command(args={})
+ command_output = ""
+
+ args[:ignore_failure] ||= false
+ args[:output_on_failure] ||= false
+
+ # TODO: This is the wrong place for this responsibility.
+ if args.has_key?(:creates)
+ if File.exists?(args[:creates])
+ Chef::Log.debug("Skipping #{args[:command]} - creates #{args[:creates]} exists.")
+ return false
+ end
+ end
+
+ status, stdout, stderr = output_of_command(args[:command], args)
+ command_output << "STDOUT: #{stdout}"
+ command_output << "STDERR: #{stderr}"
+ handle_command_failures(status, command_output, args)
+
+ status
+ end
+
+ def output_of_command(command, args)
+ Chef::Log.debug("Executing #{command}")
+ stderr_string, stdout_string, status = "", "", nil
+
+ exec_processing_block = lambda do |pid, stdin, stdout, stderr|
+ stdout_string, stderr_string = stdout.string.chomp, stderr.string.chomp
+ end
+
+ args[:cwd] ||= Dir.tmpdir
+ unless ::File.directory?(args[:cwd])
+ raise Chef::Exceptions::Exec, "#{args[:cwd]} does not exist or is not a directory"
+ end
+
+ Dir.chdir(args[:cwd]) do
+ if args[:timeout]
+ begin
+ Timeout.timeout(args[:timeout]) do
+ status = popen4(command, args, &exec_processing_block)
+ end
+ rescue Timeout::Error => e
+ Chef::Log.error("#{command} exceeded timeout #{args[:timeout]}")
+ raise(e)
+ end
+ else
+ status = popen4(command, args, &exec_processing_block)
+ end
+
+ Chef::Log.debug("---- Begin output of #{command} ----")
+ Chef::Log.debug("STDOUT: #{stdout_string}")
+ Chef::Log.debug("STDERR: #{stderr_string}")
+ Chef::Log.debug("---- End output of #{command} ----")
+ Chef::Log.debug("Ran #{command} returned #{status.exitstatus}")
+ end
+
+ return status, stdout_string, stderr_string
+ end
+
+ def handle_command_failures(status, command_output, opts={})
+ unless opts[:ignore_failure]
+ opts[:returns] ||= 0
+ unless Array(opts[:returns]).include?(status.exitstatus)
+ # if the log level is not debug, through output of command when we fail
+ output = ""
+ if Chef::Log.level == :debug || opts[:output_on_failure]
+ output << "\n---- Begin output of #{opts[:command]} ----\n"
+ output << command_output.to_s
+ output << "\n---- End output of #{opts[:command]} ----\n"
+ end
+ raise Chef::Exceptions::Exec, "#{opts[:command]} returned #{status.exitstatus}, expected #{opts[:returns]}#{output}"
+ end
+ end
+ end
+
+ # Call #run_command but set LC_ALL to the system's current environment so it doesn't get changed to C.
+ #
+ # === Parameters
+ # args<Hash>: A number of required and optional arguments that will be handed out to #run_command
+ #
+ # === Returns
+ # Returns the result of #run_command
+ def run_command_with_systems_locale(args={})
+ args[:environment] ||= {}
+ args[:environment]["LC_ALL"] = ENV["LC_ALL"]
+ run_command args
+ end
+
+ # def popen4(cmd, args={}, &b)
+ # @@os_handler.popen4(cmd, args, &b)
+ # end
+
+ # module_function :popen4
+
+ def chdir_or_tmpdir(dir, &block)
+ dir ||= Dir.tmpdir
+ unless File.directory?(dir)
+ raise Chef::Exceptions::Exec, "#{dir} does not exist or is not a directory"
+ end
+ Dir.chdir(dir) do
+ block.call
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/command/unix.rb b/lib/chef/mixin/command/unix.rb
new file mode 100644
index 0000000000..b63a02663b
--- /dev/null
+++ b/lib/chef/mixin/command/unix.rb
@@ -0,0 +1,220 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Mixin
+ module Command
+ module Unix
+ # This is taken directly from Ara T Howard's Open4 library, and then
+ # modified to suit the needs of Chef. Any bugs here are most likely
+ # my own, and not Ara's.
+ #
+ # The original appears in external/open4.rb in its unmodified form.
+ #
+ # Thanks Ara!
+ def popen4(cmd, args={}, &b)
+ # Ruby 1.8 suffers from intermittent segfaults believed to be due to GC while IO.select
+ # See CHEF-2916 / CHEF-1305
+ GC.disable
+
+ # Waitlast - this is magic.
+ #
+ # Do we wait for the child process to die before we yield
+ # to the block, or after? That is the magic of waitlast.
+ #
+ # By default, we are waiting before we yield the block.
+ args[:waitlast] ||= false
+
+ args[:user] ||= nil
+ unless args[:user].kind_of?(Integer)
+ args[:user] = Etc.getpwnam(args[:user]).uid if args[:user]
+ end
+ args[:group] ||= nil
+ unless args[:group].kind_of?(Integer)
+ args[:group] = Etc.getgrnam(args[:group]).gid if args[:group]
+ end
+ args[:environment] ||= {}
+
+ # Default on C locale so parsing commands output can be done
+ # independently of the node's default locale.
+ # "LC_ALL" could be set to nil, in which case we also must ignore it.
+ unless args[:environment].has_key?("LC_ALL")
+ args[:environment]["LC_ALL"] = "C"
+ end
+
+ pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe
+
+ verbose = $VERBOSE
+ begin
+ $VERBOSE = nil
+ ps.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
+
+ cid = fork {
+ pw.last.close
+ STDIN.reopen pw.first
+ pw.first.close
+
+ pr.first.close
+ STDOUT.reopen pr.last
+ pr.last.close
+
+ pe.first.close
+ STDERR.reopen pe.last
+ pe.last.close
+
+ STDOUT.sync = STDERR.sync = true
+
+ if args[:group]
+ Process.egid = args[:group]
+ Process.gid = args[:group]
+ end
+
+ if args[:user]
+ Process.euid = args[:user]
+ Process.uid = args[:user]
+ end
+
+ args[:environment].each do |key,value|
+ ENV[key] = value
+ end
+
+ if args[:umask]
+ umask = ((args[:umask].respond_to?(:oct) ? args[:umask].oct : args[:umask].to_i) & 007777)
+ File.umask(umask)
+ end
+
+ begin
+ if cmd.kind_of?(Array)
+ exec(*cmd)
+ else
+ exec(cmd)
+ end
+ raise 'forty-two'
+ rescue Exception => e
+ Marshal.dump(e, ps.last)
+ ps.last.flush
+ end
+ ps.last.close unless (ps.last.closed?)
+ exit!
+ }
+ ensure
+ $VERBOSE = verbose
+ end
+
+ [pw.first, pr.last, pe.last, ps.last].each{|fd| fd.close}
+
+ begin
+ e = Marshal.load ps.first
+ raise(Exception === e ? e : "unknown failure!")
+ rescue EOFError # If we get an EOF error, then the exec was successful
+ 42
+ ensure
+ ps.first.close
+ end
+
+ pw.last.sync = true
+
+ pi = [pw.last, pr.first, pe.first]
+
+ if b
+ begin
+ if args[:waitlast]
+ b[cid, *pi]
+ # send EOF so that if the child process is reading from STDIN
+ # it will actually finish up and exit
+ pi[0].close_write
+ Process.waitpid2(cid).last
+ else
+ # This took some doing.
+ # The trick here is to close STDIN
+ # Then set our end of the childs pipes to be O_NONBLOCK
+ # Then wait for the child to die, which means any IO it
+ # wants to do must be done - it's dead. If it isn't,
+ # it's because something totally skanky is happening,
+ # and we don't care.
+ o = StringIO.new
+ e = StringIO.new
+
+ pi[0].close
+
+ stdout = pi[1]
+ stderr = pi[2]
+
+ stdout.sync = true
+ stderr.sync = true
+
+ stdout.fcntl(Fcntl::F_SETFL, pi[1].fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
+ stderr.fcntl(Fcntl::F_SETFL, pi[2].fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
+
+ stdout_finished = false
+ stderr_finished = false
+
+ results = nil
+
+ while !stdout_finished || !stderr_finished
+ begin
+ channels_to_watch = []
+ channels_to_watch << stdout if !stdout_finished
+ channels_to_watch << stderr if !stderr_finished
+ ready = IO.select(channels_to_watch, nil, nil, 1.0)
+ rescue Errno::EAGAIN
+ ensure
+ results = Process.waitpid2(cid, Process::WNOHANG)
+ if results
+ stdout_finished = true
+ stderr_finished = true
+ end
+ end
+
+ if ready && ready.first.include?(stdout)
+ line = results ? stdout.gets(nil) : stdout.gets
+ if line
+ o.write(line)
+ else
+ stdout_finished = true
+ end
+ end
+ if ready && ready.first.include?(stderr)
+ line = results ? stderr.gets(nil) : stderr.gets
+ if line
+ e.write(line)
+ else
+ stderr_finished = true
+ end
+ end
+ end
+ results = Process.waitpid2(cid) unless results
+ o.rewind
+ e.rewind
+ b[cid, pi[0], o, e]
+ results.last
+ end
+ ensure
+ pi.each{|fd| fd.close unless fd.closed?}
+ end
+ else
+ [cid, pw.last, pr.first, pe.first]
+ end
+ ensure
+ GC.enable
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/mixin/command/windows.rb b/lib/chef/mixin/command/windows.rb
new file mode 100644
index 0000000000..e3d0cfdb18
--- /dev/null
+++ b/lib/chef/mixin/command/windows.rb
@@ -0,0 +1,76 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 RUBY_VERSION =~ /^1\.8/
+ require 'win32/open3'
+else
+ require 'open3'
+end
+
+class Chef
+ module Mixin
+ module Command
+ module Windows
+ def popen4(cmd, args={}, &b)
+
+ # By default, we are waiting before we yield the block.
+ args[:waitlast] ||= false
+
+ #XXX :user, :group, :environment support?
+
+ Open3.popen3(cmd) do |stdin,stdout,stderr,cid|
+ if b
+ if args[:waitlast]
+ b[cid, stdin, stdout, stderr]
+ # send EOF so that if the child process is reading from STDIN
+ # it will actually finish up and exit
+ stdin.close_write
+ else
+ o = StringIO.new
+ e = StringIO.new
+
+ stdin.close
+
+ stdout.sync = true
+ stderr.sync = true
+
+ line = stdout.gets(nil)
+ if line
+ o.write(line)
+ end
+ line = stderr.gets(nil)
+ if line
+ e.write(line)
+ end
+ o.rewind
+ e.rewind
+ b[cid, stdin, o, e]
+ end
+ else
+ [cid, stdin, stdout, stderr]
+ end
+ end
+ $?
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/mixin/convert_to_class_name.rb b/lib/chef/mixin/convert_to_class_name.rb
new file mode 100644
index 0000000000..7b4ec7ad3f
--- /dev/null
+++ b/lib/chef/mixin/convert_to_class_name.rb
@@ -0,0 +1,65 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Mixin
+ module ConvertToClassName
+ extend self
+
+ def convert_to_class_name(str)
+ str = str.dup
+ str.gsub!(/[^A-Za-z0-9_]/,'_')
+ rname = nil
+ regexp = %r{^(.+?)(_(.+))?$}
+
+ mn = str.match(regexp)
+ if mn
+ rname = mn[1].capitalize
+
+ while mn && mn[3]
+ mn = mn[3].match(regexp)
+ rname << mn[1].capitalize if mn
+ end
+ end
+
+ rname
+ end
+
+ def convert_to_snake_case(str, namespace=nil)
+ str = str.dup
+ str.sub!(/^#{namespace}(\:\:)?/, '') if namespace
+ str.gsub!(/[A-Z]/) {|s| "_" + s}
+ str.downcase!
+ str.sub!(/^\_/, "")
+ str
+ end
+
+ def snake_case_basename(str)
+ with_namespace = convert_to_snake_case(str)
+ with_namespace.split("::").last.sub(/^_/, '')
+ end
+
+ def filename_to_qualified_string(base, filename)
+ file_base = File.basename(filename, ".rb")
+ base.to_s + (file_base == 'default' ? '' : "_#{file_base}")
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/create_path.rb b/lib/chef/mixin/create_path.rb
new file mode 100644
index 0000000000..9b5dba14f2
--- /dev/null
+++ b/lib/chef/mixin/create_path.rb
@@ -0,0 +1,57 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+ module Mixin
+ module CreatePath
+
+ # Creates a given path, including all directories that lead up to it.
+ # Like mkdir_p, but without the leaking.
+ #
+ # === Parameters
+ # file_path<String, Array>:: A string that represents the path to create,
+ # or an Array with the path-parts.
+ #
+ # === Returns
+ # The created file_path.
+ def create_path(file_path)
+ unless file_path.kind_of?(String) || file_path.kind_of?(Array)
+ raise ArgumentError, "file_path must be a string or an array!"
+ end
+
+ if file_path.kind_of?(String)
+ file_path = File.expand_path(file_path).split(File::SEPARATOR)
+ file_path.shift if file_path[0] == ''
+ # Check if path starts with a separator or drive letter (Windows)
+ unless file_path[0].match("^#{File::SEPARATOR}|^[a-zA-Z]:")
+ file_path[0] = "#{File::SEPARATOR}#{file_path[0]}"
+ end
+ end
+
+ file_path.each_index do |i|
+ create_path = File.join(file_path[0, i + 1])
+ unless File.directory?(create_path)
+ Chef::Log.debug("Creating directory #{create_path}")
+ Dir.mkdir(create_path)
+ end
+ end
+ File.expand_path(File.join(file_path))
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/deep_merge.rb b/lib/chef/mixin/deep_merge.rb
new file mode 100644
index 0000000000..c5bbc8d9e6
--- /dev/null
+++ b/lib/chef/mixin/deep_merge.rb
@@ -0,0 +1,142 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Steve Midgley (http://www.misuse.org/science)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# Copyright:: Copyright (c) 2008 Steve Midgley
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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
+ module Mixin
+ # == Chef::Mixin::DeepMerge
+ # Implements a deep merging algorithm for nested data structures.
+ # ==== Notice:
+ # This code was originally imported from deep_merge by Steve Midgley.
+ # deep_merge is available under the MIT license from
+ # http://trac.misuse.org/science/wiki/DeepMerge
+ module DeepMerge
+
+ class InvalidSubtractiveMerge < ArgumentError; end
+
+
+ OLD_KNOCKOUT_PREFIX = "!merge:".freeze
+
+ # Regex to match the "knockout prefix" that was used to indicate
+ # subtractive merging in Chef 10.x and previous. Subtractive merging is
+ # removed as of Chef 11, but we detect attempted use of it and raise an
+ # error (see: raise_if_knockout_used!)
+ OLD_KNOCKOUT_MATCH = %r[!merge].freeze
+
+ extend self
+
+ def merge(first, second)
+ first = Mash.new(first) unless first.kind_of?(Mash)
+ second = Mash.new(second) unless second.kind_of?(Mash)
+
+ DeepMerge.deep_merge(second, first)
+ end
+
+ # Inherited roles use the knockout_prefix array subtraction functionality
+ # This is likely to go away in Chef >= 0.11
+ def role_merge(first, second)
+ first = Mash.new(first) unless first.kind_of?(Mash)
+ second = Mash.new(second) unless second.kind_of?(Mash)
+
+ DeepMerge.deep_merge(second, first)
+ end
+
+ class InvalidParameter < StandardError; end
+
+ # Deep Merge core documentation.
+ # deep_merge! method permits merging of arbitrary child elements. The two top level
+ # elements must be hashes. These hashes can contain unlimited (to stack limit) levels
+ # of child elements. These child elements to not have to be of the same types.
+ # Where child elements are of the same type, deep_merge will attempt to merge them together.
+ # Where child elements are not of the same type, deep_merge will skip or optionally overwrite
+ # the destination element with the contents of the source element at that level.
+ # So if you have two hashes like this:
+ # source = {:x => [1,2,3], :y => 2}
+ # dest = {:x => [4,5,'6'], :y => [7,8,9]}
+ # dest.deep_merge!(source)
+ # Results: {:x => [1,2,3,4,5,'6'], :y => 2}
+ # By default, "deep_merge!" will overwrite any unmergeables and merge everything else.
+ # To avoid this, use "deep_merge" (no bang/exclamation mark)
+ def deep_merge!(source, dest)
+ # if dest doesn't exist, then simply copy source to it
+ if dest.nil?
+ dest = source; return dest
+ end
+
+ raise_if_knockout_used!(source)
+ raise_if_knockout_used!(dest)
+ case source
+ when nil
+ dest
+ when Hash
+ source.each do |src_key, src_value|
+ if dest.kind_of?(Hash)
+ if dest[src_key]
+ dest[src_key] = deep_merge!(src_value, dest[src_key])
+ else # dest[src_key] doesn't exist so we take whatever source has
+ raise_if_knockout_used!(src_value)
+ dest[src_key] = src_value
+ end
+ else # dest isn't a hash, so we overwrite it completely
+ dest = source
+ end
+ end
+ when Array
+ if dest.kind_of?(Array)
+ dest = dest | source
+ else
+ dest = source
+ end
+ when String
+ dest = source
+ else # src_hash is not an array or hash, so we'll have to overwrite dest
+ dest = source
+ end
+ dest
+ end # deep_merge!
+
+ # Checks for attempted use of subtractive merge, which was removed for
+ # Chef 11.0. If subtractive merge use is detected, will raise an
+ # InvalidSubtractiveMerge exception.
+ def raise_if_knockout_used!(obj)
+ if uses_knockout?(obj)
+ raise InvalidSubtractiveMerge, "subtractive merge with !merge is no longer supported"
+ end
+ end
+
+ # Checks for attempted use of subtractive merge in +obj+.
+ def uses_knockout?(obj)
+ case obj
+ when String
+ obj =~ OLD_KNOCKOUT_MATCH
+ when Array
+ obj.any? {|element| element.respond_to?(:gsub) && element =~ OLD_KNOCKOUT_MATCH }
+ else
+ false
+ end
+ end
+
+ def deep_merge(source, dest)
+ deep_merge!(source.dup, dest.dup)
+ end
+
+ end
+ end
+end
+
+
diff --git a/lib/chef/mixin/deprecation.rb b/lib/chef/mixin/deprecation.rb
new file mode 100644
index 0000000000..cc85c4e976
--- /dev/null
+++ b/lib/chef/mixin/deprecation.rb
@@ -0,0 +1,65 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Mixin
+ module Deprecation
+ class DeprecatedObjectProxyBase
+ KEEPERS = %w{__id__ __send__ instance_eval == equal? initialize object_id}
+ instance_methods.each { |method_name| undef_method(method_name) unless KEEPERS.include?(method_name.to_s)}
+ end
+
+ class DeprecatedInstanceVariable < DeprecatedObjectProxyBase
+ def initialize(target, ivar_name, level=nil)
+ @target, @ivar_name = target, ivar_name
+ @level ||= :warn
+ end
+
+ def method_missing(method_name, *args, &block)
+ log_deprecation_msg(caller[0..3])
+ @target.send(method_name, *args, &block)
+ end
+
+ def inspect
+ @target.inspect
+ end
+
+ private
+
+ def log_deprecation_msg(*called_from)
+ called_from = called_from.flatten
+ log("Accessing #{@ivar_name} by the variable @#{@ivar_name} is deprecated. Support will be removed in a future release.")
+ log("Please update your cookbooks to use #{@ivar_name} in place of @#{@ivar_name}. Accessed from:")
+ called_from.each {|l| log(l)}
+ end
+
+ def log(msg)
+ # WTF: I don't get the log prefix (i.e., "[timestamp] LEVEL:") if I
+ # send to Chef::Log. No one but me should use method_missing, ever.
+ Chef::Log.logger.send(@level, msg)
+ end
+
+ end
+
+ def deprecated_ivar(obj, name, level=nil)
+ DeprecatedInstanceVariable.new(obj, name, level)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/enforce_ownership_and_permissions.rb b/lib/chef/mixin/enforce_ownership_and_permissions.rb
new file mode 100644
index 0000000000..9c1e4dda93
--- /dev/null
+++ b/lib/chef/mixin/enforce_ownership_and_permissions.rb
@@ -0,0 +1,39 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/file_access_control'
+
+class Chef
+ module Mixin
+ module EnforceOwnershipAndPermissions
+
+ def access_controls
+ @access_controls ||= Chef::FileAccessControl.new(current_resource, new_resource, self)
+ end
+
+ # will set the proper user, group and
+ # permissions using a platform specific
+ # version of Chef::FileAccessControl
+ def enforce_ownership_and_permissions
+ access_controls.set_all
+ new_resource.updated_by_last_action(true) if access_controls.modified?
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/file_class.rb b/lib/chef/mixin/file_class.rb
new file mode 100644
index 0000000000..ed2cda47db
--- /dev/null
+++ b/lib/chef/mixin/file_class.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Mark Mzyk <mmzyk@opscode.com>
+# Author:: Seth Chisamore <schisamo@opscode.com>
+# Author:: Bryan McLellan <btm@opscode.com>
+# Copyright:: Copyright (c) 2011-2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Mixin
+ module FileClass
+
+ def file_class
+ @host_os_file ||= if Chef::Platform.windows?
+ require 'chef/win32/file'
+ begin
+ Chef::ReservedNames::Win32::File.verify_links_supported!
+ rescue Chef::Exceptions::Win32APIFunctionNotImplemented => e
+ message = "Link resource is not supported on this version of Windows"
+ message << ": #{node[:kernel][:name]}" if node
+ message << " (#{node[:platform_version]})" if node
+ Chef::Log.fatal(message)
+ raise e
+ end
+ Chef::ReservedNames::Win32::File
+ else
+ ::File
+ end
+ end
+ end
+ end
+end
+
+
diff --git a/lib/chef/mixin/from_file.rb b/lib/chef/mixin/from_file.rb
new file mode 100644
index 0000000000..609fe1de55
--- /dev/null
+++ b/lib/chef/mixin/from_file.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Mixin
+ module FromFile
+
+ # Loads a given ruby file, and runs instance_eval against it in the context of the current
+ # object.
+ #
+ # Raises an IOError if the file cannot be found, or is not readable.
+ def from_file(filename)
+ if File.exists?(filename) && File.readable?(filename)
+ self.instance_eval(IO.read(filename), filename, 1)
+ else
+ raise IOError, "Cannot open or read #{filename}!"
+ end
+ end
+
+ # Loads a given ruby file, and runs class_eval against it in the context of the current
+ # object.
+ #
+ # Raises an IOError if the file cannot be found, or is not readable.
+ def class_from_file(filename)
+ if File.exists?(filename) && File.readable?(filename)
+ self.class_eval(IO.read(filename), filename, 1)
+ else
+ raise IOError, "Cannot open or read #{filename}!"
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/get_source_from_package.rb b/lib/chef/mixin/get_source_from_package.rb
new file mode 100644
index 0000000000..6d5cb56a27
--- /dev/null
+++ b/lib/chef/mixin/get_source_from_package.rb
@@ -0,0 +1,42 @@
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2008 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.
+#
+
+
+#
+# mixin to make this syntax work without specifying a source:
+#
+# gem_pacakge "/tmp/foo-x.y.z.gem"
+# rpm_package "/tmp/foo-x.y-z.rpm"
+# dpkg_package "/tmp/foo-x.y.z.deb"
+#
+
+class Chef
+ module Mixin
+ module GetSourceFromPackage
+ def initialize(new_resource, run_context)
+ super
+ # if we're passed something that looks like a filesystem path, with no source, use it
+ # - require at least one '/' in the path to avoid gem_package "foo" breaking if a file named 'foo' exists in the cwd
+ if new_resource.source.nil? && new_resource.package_name.match(/#{::File::SEPARATOR}/) && ::File.exists?(new_resource.package_name)
+ Chef::Log.debug("No package source specified, but #{new_resource.package_name} exists on the filesystem, copying to package source")
+ new_resource.source(@new_resource.package_name)
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/mixin/language.rb b/lib/chef/mixin/language.rb
new file mode 100644
index 0000000000..3aa6a6d800
--- /dev/null
+++ b/lib/chef/mixin/language.rb
@@ -0,0 +1,36 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/dsl/platform_introspection'
+require 'chef/dsl/data_query'
+
+class Chef
+ module Mixin
+
+ # == [DEPRECATED] Chef::Mixin::Language
+ # This module is deprecated and remains only for backwards compatibility.
+ #
+ # See Chef::DSL::PlatformIntrospection and Chef::DSL::DataQuery
+ module Language
+
+ include Chef::DSL::PlatformIntrospection
+ include Chef::DSL::DataQuery
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/language_include_attribute.rb b/lib/chef/mixin/language_include_attribute.rb
new file mode 100644
index 0000000000..283773b25d
--- /dev/null
+++ b/lib/chef/mixin/language_include_attribute.rb
@@ -0,0 +1,29 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/dsl/include_attribute'
+
+class Chef
+ module Mixin
+
+ # DEPRECATED: This is just here for compatibility, use
+ # Chef::DSL::IncludeAttribute instead.
+ LanguageIncludeAttribute = Chef::DSL::IncludeAttribute
+ end
+end
+
diff --git a/lib/chef/mixin/language_include_recipe.rb b/lib/chef/mixin/language_include_recipe.rb
new file mode 100644
index 0000000000..0566046560
--- /dev/null
+++ b/lib/chef/mixin/language_include_recipe.rb
@@ -0,0 +1,26 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/dsl/include_recipe'
+
+class Chef
+ module Mixin
+ LanguageIncludeRecipe = Chef::DSL::IncludeRecipe
+ end
+end
+
diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb
new file mode 100644
index 0000000000..649224f978
--- /dev/null
+++ b/lib/chef/mixin/params_validate.rb
@@ -0,0 +1,225 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+
+ module Mixin
+ module ParamsValidate
+
+ # Takes a hash of options, along with a map to validate them. Returns the original
+ # options hash, plus any changes that might have been made (through things like setting
+ # default values in the validation map)
+ #
+ # For example:
+ #
+ # validate({ :one => "neat" }, { :one => { :kind_of => String }})
+ #
+ # Would raise an exception if the value of :one above is not a kind_of? string. Valid
+ # map options are:
+ #
+ # :default:: Sets the default value for this parameter.
+ # :callbacks:: Takes a hash of Procs, which should return true if the argument is valid.
+ # The key will be inserted into the error message if the Proc does not return true:
+ # "Option #{key}'s value #{value} #{message}!"
+ # :kind_of:: Ensure that the value is a kind_of?(Whatever). If passed an array, it will ensure
+ # that the value is one of those types.
+ # :respond_to:: Ensure that the value has a given method. Takes one method name or an array of
+ # method names.
+ # :required:: Raise an exception if this parameter is missing. Valid values are true or false,
+ # by default, options are not required.
+ # :regex:: Match the value of the paramater against a regular expression.
+ # :equal_to:: Match the value of the paramater with ==. An array means it can be equal to any
+ # of the values.
+ def validate(opts, map)
+ #--
+ # validate works by taking the keys in the validation map, assuming it's a hash, and
+ # looking for _pv_:symbol as methods. Assuming it find them, it calls the right
+ # one.
+ #++
+ raise ArgumentError, "Options must be a hash" unless opts.kind_of?(Hash)
+ raise ArgumentError, "Validation Map must be a hash" unless map.kind_of?(Hash)
+
+ map.each do |key, validation|
+ unless key.kind_of?(Symbol) || key.kind_of?(String)
+ raise ArgumentError, "Validation map keys must be symbols or strings!"
+ end
+ case validation
+ when true
+ _pv_required(opts, key)
+ when false
+ true
+ when Hash
+ validation.each do |check, carg|
+ check_method = "_pv_#{check.to_s}"
+ if self.respond_to?(check_method, true)
+ self.send(check_method, opts, key, carg)
+ else
+ raise ArgumentError, "Validation map has unknown check: #{check}"
+ end
+ end
+ end
+ end
+ opts
+ end
+
+ def set_or_return(symbol, arg, validation)
+ iv_symbol = "@#{symbol.to_s}".to_sym
+ map = {
+ symbol => validation
+ }
+
+ if arg == nil && self.instance_variable_defined?(iv_symbol) == true
+ self.instance_variable_get(iv_symbol)
+ else
+ opts = validate({ symbol => arg }, { symbol => validation })
+ self.instance_variable_set(iv_symbol, opts[symbol])
+ end
+ end
+
+ private
+
+ # Return the value of a parameter, or nil if it doesn't exist.
+ def _pv_opts_lookup(opts, key)
+ if opts.has_key?(key.to_s)
+ opts[key.to_s]
+ elsif opts.has_key?(key.to_sym)
+ opts[key.to_sym]
+ else
+ nil
+ end
+ end
+
+ # Raise an exception if the parameter is not found.
+ def _pv_required(opts, key, is_required=true)
+ if is_required
+ if (opts.has_key?(key.to_s) && !opts[key.to_s].nil?) ||
+ (opts.has_key?(key.to_sym) && !opts[key.to_sym].nil?)
+ true
+ else
+ raise Exceptions::ValidationFailed, "Required argument #{key} is missing!"
+ end
+ end
+ end
+
+ def _pv_equal_to(opts, key, to_be)
+ value = _pv_opts_lookup(opts, key)
+ unless value.nil?
+ passes = false
+ Array(to_be).each do |tb|
+ passes = true if value == tb
+ end
+ unless passes
+ raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}."
+ end
+ end
+ end
+
+ # Raise an exception if the parameter is not a kind_of?(to_be)
+ def _pv_kind_of(opts, key, to_be)
+ value = _pv_opts_lookup(opts, key)
+ unless value.nil?
+ passes = false
+ Array(to_be).each do |tb|
+ passes = true if value.kind_of?(tb)
+ end
+ unless passes
+ raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}."
+ end
+ end
+ end
+
+ # Raise an exception if the parameter does not respond to a given set of methods.
+ def _pv_respond_to(opts, key, method_name_list)
+ value = _pv_opts_lookup(opts, key)
+ unless value.nil?
+ Array(method_name_list).each do |method_name|
+ unless value.respond_to?(method_name)
+ raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!"
+ end
+ end
+ end
+ end
+
+ # Assert that parameter returns false when passed a predicate method.
+ # For example, :cannot_be => :blank will raise a Exceptions::ValidationFailed
+ # error value.blank? returns a 'truthy' (not nil or false) value.
+ #
+ # Note, this will *PASS* if the object doesn't respond to the method.
+ # So, to make sure a value is not nil and not blank, you need to do
+ # both :cannot_be => :blank *and* :cannot_be => :nil (or :required => true)
+ def _pv_cannot_be(opts, key, predicate_method_base_name)
+ value = _pv_opts_lookup(opts, key)
+ predicate_method = (predicate_method_base_name.to_s + "?").to_sym
+
+ if value.respond_to?(predicate_method)
+ if value.send(predicate_method)
+ raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}"
+ end
+ end
+ end
+
+ # Assign a default value to a parameter.
+ def _pv_default(opts, key, default_value)
+ value = _pv_opts_lookup(opts, key)
+ if value == nil
+ opts[key] = default_value
+ end
+ end
+
+ # Check a parameter against a regular expression.
+ def _pv_regex(opts, key, regex)
+ value = _pv_opts_lookup(opts, key)
+ if value != nil
+ passes = false
+ [ regex ].flatten.each do |r|
+ if value != nil
+ if r.match(value.to_s)
+ passes = true
+ end
+ end
+ end
+ unless passes
+ raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}"
+ end
+ end
+ end
+
+ # Check a parameter against a hash of proc's.
+ def _pv_callbacks(opts, key, callbacks)
+ raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash)
+ value = _pv_opts_lookup(opts, key)
+ if value != nil
+ callbacks.each do |message, zeproc|
+ if zeproc.call(value) != true
+ raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!"
+ end
+ end
+ end
+ end
+
+ # Allow a parameter to default to @name
+ def _pv_name_attribute(opts, key, is_name_attribute=true)
+ if is_name_attribute
+ if opts[key] == nil
+ opts[key] = self.instance_variable_get("@name")
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/mixin/path_sanity.rb b/lib/chef/mixin/path_sanity.rb
new file mode 100644
index 0000000000..1d324f54e9
--- /dev/null
+++ b/lib/chef/mixin/path_sanity.rb
@@ -0,0 +1,67 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Mixin
+ module PathSanity
+
+ def enforce_path_sanity(env=ENV)
+ if Chef::Config[:enforce_path_sanity]
+ path_separator = Chef::Platform.windows? ? ';' : ':'
+ existing_paths = env["PATH"].split(path_separator)
+ # ensure the Ruby and Gem bindirs are included
+ # mainly for 'full-stack' Chef installs
+ paths_to_add = []
+ paths_to_add << ruby_bindir unless sane_paths.include?(ruby_bindir)
+ paths_to_add << gem_bindir unless sane_paths.include?(gem_bindir)
+ paths_to_add << sane_paths if sane_paths
+ paths_to_add.flatten!.compact!
+ paths_to_add.each do |sane_path|
+ unless existing_paths.include?(sane_path)
+ env_path = env["PATH"].dup
+ env_path << path_separator unless env["PATH"].empty?
+ env_path << sane_path
+ env["PATH"] = env_path
+ end
+ end
+ end
+ end
+
+ private
+
+ def sane_paths
+ @sane_paths ||= begin
+ if Chef::Platform.windows?
+ %w[]
+ else
+ %w[/usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin]
+ end
+ end
+ end
+
+ def ruby_bindir
+ RbConfig::CONFIG['bindir']
+ end
+
+ def gem_bindir
+ Gem.bindir
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/recipe_definition_dsl_core.rb b/lib/chef/mixin/recipe_definition_dsl_core.rb
new file mode 100644
index 0000000000..ff422d892f
--- /dev/null
+++ b/lib/chef/mixin/recipe_definition_dsl_core.rb
@@ -0,0 +1,33 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+###
+# NOTE: This file and constant are here only for backwards compatibility.
+# New code should use Chef::DSL::Recipe instead.
+#
+# This constant (module name) will eventually be deprecated and then removed.
+###
+
+require 'chef/dsl/recipe'
+
+class Chef
+ module Mixin
+ RecipeDefinitionDSLCore = Chef::DSL::Recipe
+ end
+end
diff --git a/lib/chef/mixin/securable.rb b/lib/chef/mixin/securable.rb
new file mode 100644
index 0000000000..47c388b239
--- /dev/null
+++ b/lib/chef/mixin/securable.rb
@@ -0,0 +1,180 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Mixin
+ module Securable
+
+ def owner(arg=nil)
+ set_or_return(
+ :owner,
+ arg,
+ :regex => Chef::Config[:user_valid_regex]
+ )
+ end
+
+ alias :user :owner
+
+ def group(arg=nil)
+ set_or_return(
+ :group,
+ arg,
+ :regex => Chef::Config[:group_valid_regex]
+ )
+ end
+
+ def mode(arg=nil)
+ set_or_return(
+ :mode,
+ arg,
+ :callbacks => {
+ "not in valid numeric range" => lambda { |m|
+ if m.kind_of?(String)
+ m =~ /^0/ || m="0#{m}"
+ end
+
+ # Windows does not support the sticky or setuid bits
+ if Chef::Platform.windows?
+ Integer(m)<=0777 && Integer(m)>=0
+ else
+ Integer(m)<=07777 && Integer(m)>=0
+ end
+ },
+ }
+ )
+ end
+
+ # TODO should this be separated into different files?
+ if RUBY_PLATFORM =~ /mswin|mingw|windows/
+
+ # === rights_attribute
+ # "meta-method" for dynamically creating rights attributes on resources.
+ #
+ # Multiple rights attributes can be declared. This enables resources to
+ # have multiple rights attributes with separate runtime states.
+ #
+ # For example, +Chef::Resource::RemoteDirectory+ supports different
+ # rights on the directories and files by declaring separate rights
+ # attributes for each (rights and files_rights).
+ #
+ # ==== User Level API
+ # Given a resource that calls
+ #
+ # rights_attribute(:rights)
+ #
+ # Then the resource DSL could be used like this:
+ #
+ # rights :read, ["Administrators","Everyone"]
+ # rights :deny, "Pinky"
+ # rights :full_control, "Users", :applies_to_children => true
+ # rights :write, "John Keiser", :applies_to_children => :containers_only, :applies_to_self => false, :one_level_deep => true
+ #
+ # ==== Internal Data Structure
+ # rights attributes support multiple right declarations
+ # in a single resource block--the data will be merged
+ # into a single internal hash.
+ #
+ # The internal representation is a hash with the following keys:
+ #
+ # * `:permissions`: Integer of Windows permissions flags, 1..2^32
+ # or one of `[:full_control, :modify, :read_execute, :read, :write]`
+ # * `:principals`: String or Array of Strings represnting usernames on
+ # the system.
+ # * `:applies_to_children` (optional): Boolean
+ # * `:applies_to_self` (optional): Boolean
+ # * `:one_level_deep` (optional): Boolean
+ #
+ def self.rights_attribute(name)
+
+ # equivalent to something like:
+ # def rights(permissions=nil, principals=nil, args_hash=nil)
+ define_method(name) do |*args|
+ # Ruby 1.8 compat: default the arguments
+ permissions = args.length >= 1 ? args[0] : nil
+ principals = args.length >= 2 ? args[1] : nil
+ args_hash = args.length >= 3 ? args[2] : nil
+ raise ArgumentError.new("wrong number of arguments (#{args.length} for 3)") if args.length >= 4
+
+ rights = self.instance_variable_get("@#{name.to_s}".to_sym)
+ unless permissions.nil?
+ input = {
+ :permissions => permissions,
+ :principals => principals
+ }
+ input.merge!(args_hash) unless args_hash.nil?
+
+ validations = {:permissions => { :required => true },
+ :principals => { :required => true, :kind_of => [String, Array] },
+ :applies_to_children => { :equal_to => [ true, false, :containers_only, :objects_only ]},
+ :applies_to_self => { :kind_of => [ TrueClass, FalseClass ] },
+ :one_level_deep => { :kind_of => [ TrueClass, FalseClass ] }
+ }
+ validate(input, validations)
+
+ [ permissions ].flatten.each do |permission|
+ if permission.is_a?(Integer)
+ if permission < 0 || permission > 1<<32
+ raise ArgumentError, "permissions flags must be positive and <= 32 bits (#{permission})"
+ end
+ elsif !([:full_control, :modify, :read_execute, :read, :write].include?(permission.to_sym))
+ raise ArgumentError, "permissions parameter must be :full_control, :modify, :read_execute, :read, :write or an integer representing Windows permission flags"
+ end
+ end
+
+ [ principals ].flatten.each do |principal|
+ if !principal.is_a?(String)
+ raise ArgumentError, "principals parameter must be a string or array of strings representing usernames"
+ end
+ end
+
+ if input[:applies_to_children] == false
+ if input[:applies_to_self] == false
+ raise ArgumentError, "'rights' attribute must specify either :applies_to_children or :applies_to_self."
+ end
+ if input[:one_level_deep] == true
+ raise ArgumentError, "'rights' attribute specified :one_level_deep without specifying :applies_to_children."
+ end
+ end
+ rights ||= []
+ rights << input
+ end
+ set_or_return(
+ name,
+ rights,
+ {}
+ )
+ end
+ end
+
+ # create a default 'rights' attribute
+ rights_attribute(:rights)
+ rights_attribute(:deny_rights)
+
+ def inherits(arg=nil)
+ set_or_return(
+ :inherits,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ end # Windows-specific
+
+ end
+ end
+end
diff --git a/lib/chef/mixin/shell_out.rb b/lib/chef/mixin/shell_out.rb
new file mode 100644
index 0000000000..4eaa509f8b
--- /dev/null
+++ b/lib/chef/mixin/shell_out.rb
@@ -0,0 +1,69 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/shell_out'
+require 'chef/config'
+
+class Chef
+ module Mixin
+ module ShellOut
+
+ def shell_out(*command_args)
+ cmd = Mixlib::ShellOut.new(*run_command_compatible_options(command_args))
+ if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.debug?
+ cmd.live_stream = STDOUT
+ end
+ cmd.run_command
+ cmd
+ end
+
+ def shell_out!(*command_args)
+ cmd= shell_out(*command_args)
+ cmd.error!
+ cmd
+ end
+
+ DEPRECATED_OPTIONS =
+ [ [:command_log_level, :log_level],
+ [:command_log_prepend, :log_tag] ]
+
+ # CHEF-3090: Deprecate command_log_level and command_log_prepend
+ # Patterned after https://github.com/opscode/chef/commit/e1509990b559984b43e428d4d801c394e970f432
+ def run_command_compatible_options(command_args)
+ return command_args unless command_args.last.is_a?(Hash)
+
+ _command_args = command_args.dup
+ _options = _command_args.last
+
+ DEPRECATED_OPTIONS.each do |old_option, new_option|
+ # Edge case: someone specifies :command_log_level and 'command_log_level' in the option hash
+ next unless value = _options.delete(old_option) || _options.delete(old_option.to_s)
+ deprecate_option old_option, new_option
+ _options[new_option] = value
+ end
+
+ return _command_args
+ end
+
+ private
+
+ def deprecate_option(old_option, new_option)
+ Chef::Log.logger.warn "DEPRECATION: Chef::Mixin::ShellOut option :#{old_option} is deprecated. Use :#{new_option}"
+ end
+ end
+ end
+end
diff --git a/lib/chef/mixin/template.rb b/lib/chef/mixin/template.rb
new file mode 100644
index 0000000000..78148d2577
--- /dev/null
+++ b/lib/chef/mixin/template.rb
@@ -0,0 +1,100 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 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 'tempfile'
+require 'erubis'
+
+class Chef
+ module Mixin
+ module Template
+
+ module ChefContext
+ def node
+ return @node if @node
+ raise "Could not find a value for node. If you are explicitly setting variables in a template, " +
+ "include a node variable if you plan to use it."
+ end
+ end
+
+ ::Erubis::Context.send(:include, ChefContext)
+
+ # Render a template with Erubis. Takes a template as a string, and a
+ # context hash.
+ def render_template(template, context)
+ begin
+ eruby = Erubis::Eruby.new(template)
+ output = eruby.evaluate(context)
+ rescue Object => e
+ raise TemplateError.new(e, template, context)
+ end
+ Tempfile.open("chef-rendered-template") do |tempfile|
+ tempfile.print(output)
+ tempfile.close
+ yield tempfile
+ end
+ end
+
+ class TemplateError < RuntimeError
+ attr_reader :original_exception, :context
+ SOURCE_CONTEXT_WINDOW = 2
+
+ def initialize(original_exception, template, context)
+ @original_exception, @template, @context = original_exception, template, context
+ end
+
+ def message
+ @original_exception.message
+ end
+
+ def line_number
+ @line_number ||= $1.to_i if original_exception.backtrace.find {|line| line =~ /\(erubis\):(\d+)/ }
+ end
+
+ def source_location
+ "on line ##{line_number}"
+ end
+
+ def source_listing
+ @source_listing ||= begin
+ lines = @template.split(/\n/)
+ if line_number
+ line_index = line_number - 1
+ beginning_line = line_index <= SOURCE_CONTEXT_WINDOW ? 0 : line_index - SOURCE_CONTEXT_WINDOW
+ source_size = SOURCE_CONTEXT_WINDOW * 2 + 1
+ else
+ beginning_line = 0
+ source_size = lines.length
+ end
+ contextual_lines = lines[beginning_line, source_size]
+ output = []
+ contextual_lines.each_with_index do |line, index|
+ line_number = (index+beginning_line+1).to_s.rjust(3)
+ output << "#{line_number}: #{line}"
+ end
+ output.join("\n")
+ end
+ end
+
+ def to_s
+ "\n\n#{self.class} (#{message}) #{source_location}:\n\n" +
+ "#{source_listing}\n\n #{original_exception.backtrace.join("\n ")}\n\n"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/mixin/why_run.rb b/lib/chef/mixin/why_run.rb
new file mode 100644
index 0000000000..22c58c1e54
--- /dev/null
+++ b/lib/chef/mixin/why_run.rb
@@ -0,0 +1,339 @@
+#
+# Author:: Dan DeLeo ( <dan@opscode.com> )
+# Author:: Marc Paradise ( <marc@opscode.com> )
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Mixin
+ module WhyRun
+
+ # ==ConvergeActions
+ # ConvergeActions implements the logic for why run. A ConvergeActions
+ # object wraps a collection of actions, which consist of a descriptive
+ # string and a block/Proc. Actions are executed by calling #converge!
+ # When why_run mode is enabled, each action's description will be
+ # printed, but the block will not be called. Conversely, in normal mode,
+ # the block is called, but the message is not printed.
+ #
+ # In general, this class should be accessed through the API provided by
+ # Chef::Provider.
+ class ConvergeActions
+ attr_reader :actions
+
+ def initialize(resource, run_context, action)
+ @resource, @run_context = resource, run_context
+ @actions = []
+ end
+
+ def events
+ @run_context.events
+ end
+
+ # Adds an action to the list. +descriptions+ can either be an Array of
+ # Strings, or a single String describing the action; +block+ is a
+ # block/proc that implements the action.
+ def add_action(descriptions, &block)
+ @actions << [descriptions, block]
+ end
+
+ # True if there are no actions to execute.
+ def empty?
+ @actions.empty?
+ end
+
+ # Iterate over the actions, and either print the action's message, or
+ # run its code block, depending on whether why_run mode is active.
+ def converge!
+ @actions.each do |descriptions, block|
+ if !Chef::Config[:why_run]
+ block.call
+ end
+ events.resource_update_applied(@resource, @action, descriptions)
+ end
+ end
+ end
+
+ # == ResourceRequirements
+ # ResourceRequirements provides a framework for making assertions about
+ # the host system's state. It also provides a mechanism for making
+ # assumptions about what the system's state might have been when running
+ # in why run mode.
+ #
+ # For example, consider a recipe that consists of a package resource and
+ # a service resource. If the service's init script is installed by the
+ # package, and Chef is running in why run mode, then the service resource
+ # would fail when attempting to run `/etc/init.d/software-name status`.
+ # In order to provide a more useful approximation of what would happen in
+ # a real chef run, we want to instead assume that the service was created
+ # but isn't running. The logic would look like this:
+ #
+ # # Hypothetical service provider demonstrating why run assumption logic.
+ # # This isn't the actual API, it just shows the logic.
+ # class HypotheticalServiceProvider < Chef::Provider
+ #
+ # def load_current_resource
+ # # Make sure we have the init script available:
+ # if ::File.exist?("/etc/init.d/some-service"
+ # # If the init script exists, proceed as normal:
+ # status_cmd = shell_out("/etc/init.d/some-service status")
+ # if status_cmd.success?
+ # @current_resource.status(:running)
+ # else
+ # @current_resource.status(:stopped)
+ # end
+ # else
+ # if whyrun_mode?
+ # # If the init script is not available, and we're in why run mode,
+ # # assume that some previous action would've created it:
+ # log("warning: init script '/etc/init.d/some-service' is not available")
+ # log("warning: assuming that the init script would have been created, assuming the state of 'some-service' is 'stopped'")
+ # @current_resource.status(:stopped)
+ # else
+ # raise "expected init script /etc/init.d/some-service doesn't exist"
+ # end
+ # end
+ # end
+ #
+ # end
+ #
+ # In short, the code above does the following:
+ # * runs a test to determine if a requirement is met:
+ # `::File.exist?("/etc/init.d/some-service"`
+ # * raises an error if the requirement is not met, and we're not in why
+ # run mode.
+ # * if we *are* in why run mode, print a message explaining the
+ # situation, and run some code that makes an assumption about what the
+ # state of the system would be. In this case, we also skip the normal
+ # `load_current_resource` logic
+ # * when the requirement *is* met, we run the normal `load_current_resource`
+ # logic
+ #
+ # ResourceRequirements encapsulates the above logic in a more declarative API.
+ #
+ # === Examples
+ # Assertions and assumptions should be created through the WhyRun#assert
+ # method, which gets mixed in to providers. See that method's
+ # documentation for examples.
+ class ResourceRequirements
+
+ # Implements the logic for a single assertion/assumption. See the
+ # documentation for ResourceRequirements for full discussion.
+ class Assertion
+ class AssertionFailure < RuntimeError
+ end
+
+ def initialize
+ @block_action = false
+ @assertion_proc = nil
+ @failure_message = nil
+ @whyrun_message = nil
+ @resource_modifier = nil
+ @assertion_failed = false
+ @exception_type = AssertionFailure
+ end
+
+ # Defines the code block that determines if a requirement is met. The
+ # block should return a truthy value to indicate that the requirement
+ # is met, and a falsey value if the requirement is not met.
+ # # in a provider:
+ # assert(:some_action) do |a|
+ # # This provider requires the file /tmp/foo to exist:
+ # a.assertion { ::File.exist?("/tmp/foo") }
+ # end
+ def assertion(&assertion_proc)
+ @assertion_proc = assertion_proc
+ end
+
+ # Defines the failure message, and optionally the Exception class to
+ # use when a requirement is not met. It works like `raise`:
+ # # in a provider:
+ # assert(:some_action) do |a|
+ # # This example shows usage with 1 or 2 args by calling #failure_message twice.
+ # # In practice you should only call this once per Assertion.
+ #
+ # # Set the Exception class explicitly
+ # a.failure_message(Chef::Exceptions::MissingRequiredFile, "File /tmp/foo doesn't exist")
+ # # Fallback to the default error class (AssertionFailure)
+ # a.failure_message("File /tmp/foo" doesn't exist")
+ # end
+ def failure_message(*args)
+ case args.size
+ when 1
+ @failure_message = args[0]
+ when 2
+ @exception_type, @failure_message = args[0], args[1]
+ else
+ raise ArgumentError, "#{self.class}#failure_message takes 1 or 2 arguments, you gave #{args.inspect}"
+ end
+ end
+
+ # Defines a message and optionally provides a code block to execute
+ # when the requirement is not met and Chef is executing in why run
+ # mode
+ #
+ # If no failure_message is provided (above), then execution
+ # will be allowed to continue in both whyrun an dnon-whyrun
+ # mode
+ #
+ # With a service resource that requires /etc/init.d/service-name to exist:
+ # # in a provider
+ # assert(:start, :restart) do |a|
+ # a.assertion { ::File.exist?("/etc/init.d/service-name") }
+ # a.whyrun("Init script '/etc/init.d/service-name' doesn't exist, assuming a prior action would have created it.") do
+ # # blindly assume that the service exists but is stopped in why run mode:
+ # @new_resource.status(:stopped)
+ # end
+ # end
+ def whyrun(message, &resource_modifier)
+ @whyrun_message = message
+ @resource_modifier = resource_modifier
+ end
+
+ # Prevents associated actions from being invoked in whyrun mode.
+ # This will also stop further processing of assertions for a given action.
+ #
+ # An example from the template provider: if the source template doesn't exist
+ # we can't parse it in the action_create block of template - something that we do
+ # even in whyrun mode. Because the soruce template may have been created in an earlier
+ # step, we still want to keep going in whyrun mode.
+ #
+ # assert(:create, :create_if_missing) do |a|
+ # a.assertion { File::exists?(@new_resource.source) }
+ # a.whyrun "Template source file does not exist, assuming it would have been created."
+ # a.block_action!
+ # end
+ #
+ def block_action!
+ @block_action = true
+ end
+
+ def block_action?
+ @block_action
+ end
+
+ def assertion_failed?
+ @assertion_failed
+ end
+
+
+ # Runs the assertion/assumption logic. Will raise an Exception of the
+ # type specified in #failure_message (or AssertionFailure by default)
+ # if the requirement is not met and Chef is not running in why run
+ # mode. An exception will also be raised if running in why run mode
+ # and no why run message or block has been declared.
+ def run(action, events, resource)
+ if !@assertion_proc || !@assertion_proc.call
+ @assertion_failed = true
+ if Chef::Config[:why_run] && @whyrun_message
+ events.provider_requirement_failed(action, resource, @exception_type, @failure_message)
+ events.whyrun_assumption(action, resource, @whyrun_message) if @whyrun_message
+ @resource_modifier.call if @resource_modifier
+ else
+ if @failure_message
+ events.provider_requirement_failed(action, resource, @exception_type, @failure_message)
+ raise @exception_type, @failure_message
+ end
+ end
+ end
+ end
+ end
+
+ def initialize(resource, run_context)
+ @resource, @run_context = resource, run_context
+ @assertions = Hash.new {|h,k| h[k] = [] }
+ @blocked_actions = []
+ end
+
+ def events
+ @run_context.events
+ end
+
+ # Check to see if a given action is blocked by a failed assertion
+ #
+ # Takes the action name to be verified.
+ def action_blocked?(action)
+ @blocked_actions.include?(action)
+ end
+
+ # Define a new Assertion.
+ #
+ # Takes a list of action names for which the assertion should be made.
+ # ==== Examples:
+ # A File provider that requires the parent directory to exist:
+ #
+ # assert(:create, :create_if_missing) do |a|
+ # parent_dir = File.basename(@new_resource.path)
+ # a.assertion { ::File.directory?(parent_dir) }
+ # a.failure_message(Exceptions::ParentDirectoryDoesNotExist,
+ # "Can't create file #{@new_resource.path}: parent directory #{parent_dir} doesn't exist")
+ # a.why_run("assuming parent directory #{parent_dir} would have been previously created"
+ # end
+ #
+ # A service provider that requires the init script to exist:
+ #
+ # assert(:start, :restart) do |a|
+ # a.assertion { ::File.exist?(@new_resource.init_script) }
+ # a.failure_message(Exceptions::MissingInitScript,
+ # "Can't check status of #{@new_resource}: init script #{@new_resource.init_script} is missing")
+ # a.why_run("Assuming init script would have been created and service is stopped") do
+ # @current_resource.status(:stopped)
+ # end
+ # end
+ #
+ # A File provider that will error out if you don't have permissions do
+ # delete the file, *even in why run mode*:
+ #
+ # assert(:delete) do |a|
+ # a.assertion { ::File.writable?(@new_resource.path) }
+ # a.failure_message(Exceptions::InsufficientPrivileges,
+ # "You don't have sufficient privileges to delete #{@new_resource.path}")
+ # end
+ #
+ # A Template provider that will prevent action execution but continue the run in
+ # whyrun mode if the template source is not available.
+ # assert(:create, :create_if_missing) do |a|
+ # a.assertion { File::exist?(@new_resource.source) }
+ # a.failure_message Chef::Exceptions::TemplateError, "Template #{@new_resource.source} could not be found exist."
+ # a.whyrun "Template source #{@new_resource.source} does not exist. Assuming it would have been created."
+ # a.block_action!
+ # end
+ #
+ # assert(:delete) do |a|
+ # a.assertion { ::File.writable?(@new_resource.path) }
+ # a.failure_message(Exceptions::InsufficientPrivileges,
+ # "You don't have sufficient privileges to delete #{@new_resource.path}")
+ # end
+ def assert(*actions)
+ assertion = Assertion.new
+ yield assertion
+ actions.each {|action| @assertions[action] << assertion }
+ end
+
+ # Run the assertion and assumption logic.
+ def run(action)
+ @assertions[action.to_sym].each do |a|
+ a.run(action, events, @resource)
+ if a.assertion_failed? and a.block_action?
+ @blocked_actions << action
+ return
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/mixin/xml_escape.rb b/lib/chef/mixin/xml_escape.rb
new file mode 100644
index 0000000000..dac2f0c6af
--- /dev/null
+++ b/lib/chef/mixin/xml_escape.rb
@@ -0,0 +1,140 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# Copyright:: Copyright (c) 2005 Sam Ruby
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#--
+# Portions of this code are adapted from Sam Ruby's xchar.rb
+# http://intertwingly.net/stories/2005/09/28/xchar.rb
+#
+# Such code appears here under Sam's original MIT license, while portions of
+# this file are covered by the above Apache License. For a completely MIT
+# licensed version, please see Sam's original.
+#
+# Thanks, Sam!
+#
+# Copyright (c) 2005, Sam Ruby
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+require 'chef/log'
+
+begin
+ require 'fast_xs'
+rescue LoadError
+ Chef::Log.info "The fast_xs gem is not installed, slower pure ruby XML escaping will be used."
+end
+
+class Chef
+ module Mixin
+ module XMLEscape
+
+ module PureRuby
+ extend self
+
+ CP1252 = {
+ 128 => 8364, # euro sign
+ 130 => 8218, # single low-9 quotation mark
+ 131 => 402, # latin small letter f with hook
+ 132 => 8222, # double low-9 quotation mark
+ 133 => 8230, # horizontal ellipsis
+ 134 => 8224, # dagger
+ 135 => 8225, # double dagger
+ 136 => 710, # modifier letter circumflex accent
+ 137 => 8240, # per mille sign
+ 138 => 352, # latin capital letter s with caron
+ 139 => 8249, # single left-pointing angle quotation mark
+ 140 => 338, # latin capital ligature oe
+ 142 => 381, # latin capital letter z with caron
+ 145 => 8216, # left single quotation mark
+ 146 => 8217, # right single quotation mark
+ 147 => 8220, # left double quotation mark
+ 148 => 8221, # right double quotation mark
+ 149 => 8226, # bullet
+ 150 => 8211, # en dash
+ 151 => 8212, # em dash
+ 152 => 732, # small tilde
+ 153 => 8482, # trade mark sign
+ 154 => 353, # latin small letter s with caron
+ 155 => 8250, # single right-pointing angle quotation mark
+ 156 => 339, # latin small ligature oe
+ 158 => 382, # latin small letter z with caron
+ 159 => 376 # latin capital letter y with diaeresis
+ }
+
+ # http://www.w3.org/TR/REC-xml/#dt-chardata
+ PREDEFINED = {
+ 38 => '&amp;', # ampersand
+ 60 => '&lt;', # left angle bracket
+ 62 => '&gt;' # right angle bracket
+ }
+
+ # http://www.w3.org/TR/REC-xml/#charsets
+ VALID = [[0x9, 0xA, 0xD], (0x20..0xD7FF),
+ (0xE000..0xFFFD), (0x10000..0x10FFFF)]
+
+ def xml_escape(unescaped_str)
+ begin
+ unescaped_str.unpack("U*").map {|char| xml_escape_char!(char)}.join
+ rescue
+ unescaped_str.unpack("C*").map {|char| xml_escape_char!(char)}.join
+ end
+ end
+
+ private
+
+ def xml_escape_char!(char)
+ char = CP1252[char] || char
+ char = 42 unless VALID.detect {|range| range.include? char}
+ char = PREDEFINED[char] || (char<128 ? char.chr : "&##{char};")
+ end
+ end
+
+ module FastXS
+ extend self
+
+ def xml_escape(string)
+ string.fast_xs
+ end
+
+ end
+
+ if "strings".respond_to?(:fast_xs)
+ include FastXS
+ extend FastXS
+ else
+ include PureRuby
+ extend PureRuby
+ end
+ end
+ end
+end
diff --git a/lib/chef/mixins.rb b/lib/chef/mixins.rb
new file mode 100644
index 0000000000..557932c0e6
--- /dev/null
+++ b/lib/chef/mixins.rb
@@ -0,0 +1,14 @@
+require 'chef/mixin/shell_out'
+require 'chef/mixin/check_helper'
+require 'chef/mixin/checksum'
+require 'chef/mixin/command'
+require 'chef/mixin/convert_to_class_name'
+require 'chef/mixin/create_path'
+require 'chef/mixin/deep_merge'
+require 'chef/mixin/enforce_ownership_and_permissions'
+require 'chef/mixin/from_file'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/path_sanity'
+require 'chef/mixin/template'
+require 'chef/mixin/securable'
+require 'chef/mixin/xml_escape'
diff --git a/lib/chef/monkey_patches/dir.rb b/lib/chef/monkey_patches/dir.rb
new file mode 100644
index 0000000000..c86edcf013
--- /dev/null
+++ b/lib/chef/monkey_patches/dir.rb
@@ -0,0 +1,36 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 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.
+#
+
+if RUBY_VERSION < "1.8.6" || RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ class Dir
+ class << self
+ alias_method :glob_, :glob
+ # Adds a Dir.glob to Ruby 1.8.5, for compat
+ def glob(pattern, flags=0)
+ raise ArgumentError unless (
+ !pattern.nil? and (
+ pattern.is_a? Array and !pattern.empty?
+ ) or pattern.is_a? String
+ )
+ pattern.gsub!(/\\/, "/") if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ [pattern].flatten.inject([]) { |r, p| r + glob_(p, flags) }
+ end
+ alias_method :[], :glob
+ end
+ end
+end
diff --git a/lib/chef/monkey_patches/moneta.rb b/lib/chef/monkey_patches/moneta.rb
new file mode 100644
index 0000000000..1c2895db56
--- /dev/null
+++ b/lib/chef/monkey_patches/moneta.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# ensure data is written and read in binary mode
+# stops "dump format error for symbol(0x75)" errors
+module Moneta
+ class BasicFile
+
+ def store(key, value, options = {})
+ ensure_directory_created(::File.dirname(path(key)))
+ ::File.open(path(key), "wb") do |file|
+ if @expires
+ data = {:value => value}
+ if options[:expires_in]
+ data[:expires_at] = Time.now + options[:expires_in]
+ end
+ contents = Marshal.dump(data)
+ else
+ contents = Marshal.dump(value)
+ end
+ file.puts(contents)
+ end
+ end
+
+ def raw_get(key)
+ if ::File.respond_to?(:binread)
+ data = ::File.binread(path(key))
+ else
+ data = ::File.open(path(key),"rb") { |f| f.read }
+ end
+ Marshal.load(data)
+ end
+
+ end
+end
diff --git a/lib/chef/monkey_patches/net_http.rb b/lib/chef/monkey_patches/net_http.rb
new file mode 100644
index 0000000000..ad4ba957f6
--- /dev/null
+++ b/lib/chef/monkey_patches/net_http.rb
@@ -0,0 +1,22 @@
+
+# Module gets mixed in to Net::HTTP exception classes so we can attach our
+# RESTRequest object to them and get the request parameters back out later.
+module ChefNetHTTPExceptionExtensions
+ attr_accessor :chef_rest_request
+end
+
+require 'net/http'
+module Net
+ class HTTPError
+ include ChefNetHTTPExceptionExtensions
+ end
+ class HTTPRetriableError
+ include ChefNetHTTPExceptionExtensions
+ end
+ class HTTPServerException
+ include ChefNetHTTPExceptionExtensions
+ end
+ class HTTPFatalError
+ include ChefNetHTTPExceptionExtensions
+ end
+end
diff --git a/lib/chef/monkey_patches/numeric.rb b/lib/chef/monkey_patches/numeric.rb
new file mode 100644
index 0000000000..1f5ff14209
--- /dev/null
+++ b/lib/chef/monkey_patches/numeric.rb
@@ -0,0 +1,15 @@
+unless 0.respond_to?(:fdiv)
+ class Numeric
+ def fdiv(other)
+ to_f / other
+ end
+ end
+end
+
+# String elements referenced with [] <= 1.8.6 return a Fixnum. Cheat to allow
+# for the simpler "test"[2].ord construct
+class Numeric
+ def ord
+ return self
+ end
+end
diff --git a/lib/chef/monkey_patches/object.rb b/lib/chef/monkey_patches/object.rb
new file mode 100644
index 0000000000..017a4b7938
--- /dev/null
+++ b/lib/chef/monkey_patches/object.rb
@@ -0,0 +1,9 @@
+class Object
+ unless new.respond_to?(:tap)
+ def tap
+ yield self
+ return self
+ end
+ end
+end
+
diff --git a/lib/chef/monkey_patches/regexp.rb b/lib/chef/monkey_patches/regexp.rb
new file mode 100644
index 0000000000..9304209edf
--- /dev/null
+++ b/lib/chef/monkey_patches/regexp.rb
@@ -0,0 +1,34 @@
+# Copyright (c) 2009 Marc-Andre Lafortune
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+class Regexp
+ # Standard in Ruby 1.8.7+. See official documentation[http://www.ruby-doc.org/core-1.8.7/classes/Regexp.html]
+ class << self
+ unless (union(%w(a b)) rescue false)
+ alias :union_without_array_argument :union
+
+ def union(*arg)
+ return union_without_array_argument(*arg) unless arg.size == 1
+ union_without_array_argument(*arg.first)
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/lib/chef/monkey_patches/string.rb b/lib/chef/monkey_patches/string.rb
new file mode 100644
index 0000000000..c77c5c8816
--- /dev/null
+++ b/lib/chef/monkey_patches/string.rb
@@ -0,0 +1,49 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 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.
+#
+
+# == String (Patch)
+# On ruby 1.9, Strings are aware of multibyte characters, so +size+ and +length+
+# give the actual number of characters. In Chef::REST, we need the bytesize
+# so we can correctly set the Content-Length headers, but ruby 1.8.6 and lower
+# don't define String#bytesize. Monkey patching time!
+
+begin
+ require 'enumerator'
+rescue LoadError
+end
+
+class String
+ unless method_defined?(:bytesize)
+ alias :bytesize :size
+ end
+
+ unless method_defined?(:lines)
+ def lines
+ enum_for(:each)
+ end
+ end
+end
+
+# <= 1.8.6 needs some ord!
+class String
+ unless method_defined?(:ord)
+ def ord
+ self.unpack('C').first
+ end
+ end
+end
diff --git a/lib/chef/monkey_patches/tempfile.rb b/lib/chef/monkey_patches/tempfile.rb
new file mode 100644
index 0000000000..3135fb1a00
--- /dev/null
+++ b/lib/chef/monkey_patches/tempfile.rb
@@ -0,0 +1,64 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 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.
+#
+
+# == Tempfile (Patch)
+# Tempfile has a horrible bug where it causes an IOError: closed stream in its
+# finalizer, leading to intermittent application crashes with confusing stack
+# traces. Here we monkey patch the fix into place. You can track the bug on
+# ruby's redmine: http://redmine.ruby-lang.org/issues/show/3119
+#
+# The patch is slightly different for Ruby 1.8 and Ruby 1.9, both patches are
+# included here.
+class Tempfile # :nodoc:
+ # Tempfile has changes between 1.8.x and 1.9.x
+ # so we monkey patch separately
+ if RUBY_VERSION =~ /^1\.8/
+ def unlink
+ # keep this order for thread safeness
+ begin
+ File.unlink(@tmpname) if File.exist?(@tmpname)
+ @@cleanlist.delete(@tmpname)
+ @tmpname = nil
+ ObjectSpace.undefine_finalizer(self)
+ rescue Errno::EACCES
+ # may not be able to unlink on Windows; just ignore
+ end
+ end
+ alias delete unlink
+
+
+ # There is a patch for this, to be merged into 1.9 at some point.
+ # When that happens, we'll want to also check the RUBY_PATCHLEVEL
+ elsif RUBY_VERSION =~ /^1\.9/
+ def unlink
+ # keep this order for thread safeness
+ return unless @tmpname
+ begin
+ if File.exist?(@tmpname)
+ File.unlink(@tmpname)
+ end
+ # remove tmpname from remover
+ @data[0] = @data[2] = nil
+ @tmpname = nil
+ rescue Errno::EACCES
+ # may not be able to unlink on Windows; just ignore
+ end
+ end
+ alias delete unlink
+ end
+end
diff --git a/lib/chef/nil_argument.rb b/lib/chef/nil_argument.rb
new file mode 100644
index 0000000000..c7cbc1b39e
--- /dev/null
+++ b/lib/chef/nil_argument.rb
@@ -0,0 +1,3 @@
+class Chef
+ NIL_ARGUMENT = Object.new
+end
diff --git a/lib/chef/node.rb b/lib/chef/node.rb
new file mode 100644
index 0000000000..92a2374bce
--- /dev/null
+++ b/lib/chef/node.rb
@@ -0,0 +1,476 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2008-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'forwardable'
+require 'chef/config'
+require 'chef/nil_argument'
+require 'chef/mixin/check_helper'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/from_file'
+require 'chef/mixin/deep_merge'
+require 'chef/dsl/include_attribute'
+require 'chef/environment'
+require 'chef/rest'
+require 'chef/run_list'
+require 'chef/node/attribute'
+require 'chef/mash'
+require 'chef/json_compat'
+require 'chef/search/query'
+
+class Chef
+ class Node
+
+ extend Forwardable
+
+ def_delegators :attributes, :keys, :each_key, :each_value, :key?, :has_key?
+
+ attr_accessor :recipe_list, :run_state, :run_list
+
+ attr_accessor :run_context
+
+ include Chef::Mixin::FromFile
+ include Chef::DSL::IncludeAttribute
+
+ include Chef::Mixin::CheckHelper
+ include Chef::Mixin::ParamsValidate
+
+ # Create a new Chef::Node object.
+ def initialize
+ @name = nil
+
+ @chef_environment = '_default'
+ @run_list = Chef::RunList.new
+
+ @attributes = Chef::Node::Attribute.new({}, {}, {}, {})
+
+ @run_state = {}
+ end
+
+ # Used by DSL
+ def node
+ self
+ end
+
+ def chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ # Set the name of this Node, or return the current name.
+ def name(arg=nil)
+ if arg != nil
+ validate(
+ {:name => arg },
+ {:name => { :kind_of => String,
+ :cannot_be => :blank,
+ :regex => /^[\-[:alnum:]_:.]+$/}
+ })
+ @name = arg
+ else
+ @name
+ end
+ end
+
+ def chef_environment(arg=nil)
+ set_or_return(
+ :chef_environment,
+ arg,
+ { :regex => /^[\-[:alnum:]_]+$/, :kind_of => String }
+ )
+ end
+
+ def chef_environment=(environment)
+ chef_environment(environment)
+ end
+
+ alias :environment :chef_environment
+
+ def attributes
+ @attributes
+ end
+
+ alias :attribute :attributes
+ alias :construct_attributes :attributes
+
+ # Return an attribute of this node. Returns nil if the attribute is not found.
+ def [](attrib)
+ attributes[attrib]
+ end
+
+ # Set a normal attribute of this node, but auto-vivify any Mashes that
+ # might be missing
+ def normal
+ attributes.normal
+ end
+
+ alias_method :set, :normal
+
+ # Set a normal attribute of this node, auto-vivifying any mashes that are
+ # missing, but if the final value already exists, don't set it
+ def normal_unless
+ attributes.set_unless_value_present = true
+ attributes.normal
+ end
+ alias_method :set_unless, :normal_unless
+
+ # Set a default of this node, but auto-vivify any Mashes that might
+ # be missing
+ def default
+ attributes.default
+ end
+
+ # Set a default attribute of this node, auto-vivifying any mashes that are
+ # missing, but if the final value already exists, don't set it
+ def default_unless
+ attributes.set_unless_value_present = true
+ attributes.default
+ end
+
+ # Set an override attribute of this node, but auto-vivify any Mashes that
+ # might be missing
+ def override
+ attributes.override
+ end
+
+ # Set an override attribute of this node, auto-vivifying any mashes that
+ # are missing, but if the final value already exists, don't set it
+ def override_unless
+ attributes.set_unless_value_present = true
+ attributes.override
+ end
+
+
+ def override_attrs
+ attributes.override
+ end
+
+ def override_attrs=(new_values)
+ attributes.override = new_values
+ end
+
+ def default_attrs
+ attributes.default
+ end
+
+ def default_attrs=(new_values)
+ attributes.default = new_values
+ end
+
+ def normal_attrs
+ attributes.normal
+ end
+
+ def normal_attrs=(new_values)
+ attributes.normal = new_values
+ end
+
+ def automatic_attrs
+ attributes.automatic
+ end
+
+ def automatic_attrs=(new_values)
+ attributes.automatic = new_values
+ end
+
+ # Return true if this Node has a given attribute, false if not. Takes either a symbol or
+ # a string.
+ #
+ # Only works on the top level. Preferred way is to use the normal [] style
+ # lookup and call attribute?()
+ def attribute?(attrib)
+ attributes.attribute?(attrib)
+ end
+
+ # Yield each key of the top level to the block.
+ def each(&block)
+ attributes.each(&block)
+ end
+
+ # Iterates over each attribute, passing the attribute and value to the block.
+ def each_attribute(&block)
+ attributes.each_attribute(&block)
+ end
+
+ # Only works for attribute fetches, setting is no longer supported
+ def method_missing(symbol, *args)
+ attributes.send(symbol, *args)
+ end
+
+ # Returns true if this Node expects a given recipe, false if not.
+ #
+ # First, the run list is consulted to see whether the recipe is
+ # explicitly included. If it's not there, it looks in
+ # `node[:recipes]`, which is populated when the run_list is expanded
+ #
+ # NOTE: It's used by cookbook authors
+ def recipe?(recipe_name)
+ run_list.include?(recipe_name) || self[recipes].include?(recipe_name)
+ end
+
+ # Returns true if this Node expects a given role, false if not.
+ def role?(role_name)
+ run_list.include?("role[#{role_name}]")
+ end
+
+ # Returns an Array of roles and recipes, in the order they will be applied.
+ # If you call it with arguments, they will become the new list of roles and recipes.
+ def run_list(*args)
+ args.length > 0 ? @run_list.reset!(args) : @run_list
+ end
+
+ # Returns true if this Node expects a given role, false if not.
+ def run_list?(item)
+ run_list.detect { |r| r == item } ? true : false
+ end
+
+ # Consume data from ohai and Attributes provided as JSON on the command line.
+ def consume_external_attrs(ohai_data, json_cli_attrs)
+ Chef::Log.debug("Extracting run list from JSON attributes provided on command line")
+ consume_attributes(json_cli_attrs)
+
+ self.automatic_attrs = ohai_data
+
+ platform, version = Chef::Platform.find_platform_and_version(self)
+ Chef::Log.debug("Platform is #{platform} version #{version}")
+ self.automatic[:platform] = platform
+ self.automatic[:platform_version] = version
+ end
+
+ # Consumes the combined run_list and other attributes in +attrs+
+ def consume_attributes(attrs)
+ normal_attrs_to_merge = consume_run_list(attrs)
+ Chef::Log.debug("Applying attributes from json file")
+ self.normal_attrs = Chef::Mixin::DeepMerge.merge(normal_attrs,normal_attrs_to_merge)
+ self.tags # make sure they're defined
+ end
+
+ # Lazy initializer for tags attribute
+ def tags
+ normal[:tags] = [] unless attribute?(:tags)
+ normal[:tags]
+ end
+
+ # Extracts the run list from +attrs+ and applies it. Returns the remaining attributes
+ def consume_run_list(attrs)
+ attrs = attrs ? attrs.dup : {}
+ if new_run_list = attrs.delete("recipes") || attrs.delete("run_list")
+ if attrs.key?("recipes") || attrs.key?("run_list")
+ raise Chef::Exceptions::AmbiguousRunlistSpecification, "please set the node's run list using the 'run_list' attribute only."
+ end
+ Chef::Log.info("Setting the run_list to #{new_run_list.inspect} from JSON")
+ run_list(new_run_list)
+ end
+ attrs
+ end
+
+ # Clear defaults and overrides, so that any deleted attributes
+ # between runs are still gone.
+ def reset_defaults_and_overrides
+ self.default.clear
+ self.override.clear
+ end
+
+ # Expands the node's run list and sets the default and override
+ # attributes. Also applies stored attributes (from json provided
+ # on the command line)
+ #
+ # Returns the fully-expanded list of recipes, a RunListExpansion.
+ #
+ #--
+ # TODO: timh/cw, 5-14-2010: Should this method exist? Should we
+ # instead modify default_attrs and override_attrs whenever our
+ # run_list is mutated? Or perhaps do something smarter like
+ # on-demand generation of default_attrs and override_attrs,
+ # invalidated only when run_list is mutated?
+ def expand!(data_source = 'server')
+ expansion = run_list.expand(chef_environment, data_source)
+ raise Chef::Exceptions::MissingRole, expansion if expansion.errors?
+
+ self.tags # make sure they're defined
+
+ automatic_attrs[:recipes] = expansion.recipes
+ automatic_attrs[:roles] = expansion.roles
+
+ expansion
+ end
+
+ # Apply the default and overrides attributes from the expansion
+ # passed in, which came from roles.
+ def apply_expansion_attributes(expansion)
+ load_chef_environment_object = (chef_environment == "_default" ? nil : Chef::Environment.load(chef_environment))
+ environment_default_attrs = load_chef_environment_object.nil? ? {} : load_chef_environment_object.default_attributes
+ default_before_roles = Chef::Mixin::DeepMerge.merge(default_attrs, environment_default_attrs)
+ self.default_attrs = Chef::Mixin::DeepMerge.merge(default_before_roles, expansion.default_attrs)
+ environment_override_attrs = load_chef_environment_object.nil? ? {} : load_chef_environment_object.override_attributes
+ overrides_before_environments = Chef::Mixin::DeepMerge.merge(override_attrs, expansion.override_attrs)
+ self.override_attrs = Chef::Mixin::DeepMerge.merge(overrides_before_environments, environment_override_attrs)
+ end
+
+ # Transform the node to a Hash
+ def to_hash
+ index_hash = Hash.new
+ index_hash["chef_type"] = "node"
+ index_hash["name"] = name
+ index_hash["chef_environment"] = chef_environment
+ attribute.each do |key, value|
+ index_hash[key] = value
+ end
+ index_hash["recipe"] = run_list.recipe_names if run_list.recipe_names.length > 0
+ index_hash["role"] = run_list.role_names if run_list.role_names.length > 0
+ index_hash["run_list"] = run_list.run_list if run_list.run_list.length > 0
+ index_hash
+ end
+
+ def display_hash
+ display = {}
+ display["name"] = name
+ display["chef_environment"] = chef_environment
+ display["automatic"] = automatic_attrs
+ display["normal"] = normal_attrs
+ display["default"] = default_attrs
+ display["override"] = override_attrs
+ display["run_list"] = run_list.run_list
+ display
+ end
+
+ # Serialize this object as a hash
+ def to_json(*a)
+ result = {
+ "name" => name,
+ "chef_environment" => chef_environment,
+ 'json_class' => self.class.name,
+ "automatic" => attributes.automatic,
+ "normal" => attributes.normal,
+ "chef_type" => "node",
+ "default" => attributes.default,
+ "override" => attributes.override,
+ #Render correctly for run_list items so malformed json does not result
+ "run_list" => run_list.run_list.map { |item| item.to_s }
+ }
+ result.to_json(*a)
+ end
+
+ def update_from!(o)
+ run_list.reset!(o.run_list)
+ self.automatic_attrs = o.automatic_attrs
+ self.normal_attrs = o.normal_attrs
+ self.override_attrs = o.override_attrs
+ self.default_attrs = o.default_attrs
+ chef_environment(o.chef_environment)
+ self
+ end
+
+ # Create a Chef::Node from JSON
+ def self.json_create(o)
+ node = new
+ node.name(o["name"])
+ node.chef_environment(o["chef_environment"])
+ if o.has_key?("attributes")
+ node.normal_attrs = o["attributes"]
+ end
+ node.automatic_attrs = Mash.new(o["automatic"]) if o.has_key?("automatic")
+ node.normal_attrs = Mash.new(o["normal"]) if o.has_key?("normal")
+ node.default_attrs = Mash.new(o["default"]) if o.has_key?("default")
+ node.override_attrs = Mash.new(o["override"]) if o.has_key?("override")
+
+ if o.has_key?("run_list")
+ node.run_list.reset!(o["run_list"])
+ else
+ o["recipes"].each { |r| node.recipes << r }
+ end
+ node
+ end
+
+ def self.list_by_environment(environment, inflate=false)
+ if inflate
+ response = Hash.new
+ Chef::Search::Query.new.search(:node, "chef_environment:#{environment}") {|n| response[n.name] = n unless n.nil?}
+ response
+ else
+ Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("environments/#{environment}/nodes")
+ end
+ end
+
+ def self.list(inflate=false)
+ if inflate
+ response = Hash.new
+ Chef::Search::Query.new.search(:node) do |n|
+ response[n.name] = n unless n.nil?
+ end
+ response
+ else
+ Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("nodes")
+ end
+ end
+
+ def self.find_or_create(node_name)
+ load(node_name)
+ rescue Net::HTTPServerException => e
+ raise unless e.response.code == '404'
+ node = build(node_name)
+ node.create
+ end
+
+ def self.build(node_name)
+ node = new
+ node.name(node_name)
+ node.chef_environment(Chef::Config[:environment]) unless Chef::Config[:environment].nil? || Chef::Config[:environment].chop.empty?
+ node
+ end
+
+ # Load a node by name
+ def self.load(name)
+ Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("nodes/#{name}")
+ end
+
+ # Remove this node via the REST API
+ def destroy
+ chef_server_rest.delete_rest("nodes/#{name}")
+ end
+
+ # Save this node via the REST API
+ def save
+ # Try PUT. If the node doesn't yet exist, PUT will return 404,
+ # so then POST to create.
+ begin
+ if Chef::Config[:why_run]
+ Chef::Log.warn("In whyrun mode, so NOT performing node save.")
+ else
+ chef_server_rest.put_rest("nodes/#{name}", self)
+ end
+ rescue Net::HTTPServerException => e
+ raise e unless e.response.code == "404"
+ chef_server_rest.post_rest("nodes", self)
+ end
+ self
+ end
+
+ # Create the node via the REST API
+ def create
+ chef_server_rest.post_rest("nodes", self)
+ self
+ end
+
+ def to_s
+ "node[#{name}]"
+ end
+
+ end
+end
diff --git a/lib/chef/node/attribute.rb b/lib/chef/node/attribute.rb
new file mode 100644
index 0000000000..22075533b4
--- /dev/null
+++ b/lib/chef/node/attribute.rb
@@ -0,0 +1,254 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/node/immutable_collections'
+require 'chef/node/attribute_collections'
+require 'chef/mixin/deep_merge'
+require 'chef/log'
+
+class Chef
+ class Node
+
+ # == Attribute
+ # Attribute implements a nested key-value (Hash) and flat collection
+ # (Array) data structure supporting multiple levels of precedence, such
+ # that a given key may have multiple values internally, but will only
+ # return the highest precedence value when reading.
+ class Attribute < Mash
+
+ include Immutablize
+
+ include Enumerable
+
+ COMPONENTS = [:@default, :@normal, :@override, :@automatic].freeze
+ COMPONENT_ACCESSORS = {:default => :@default,
+ :normal => :@normal,
+ :override => :@override,
+ :automatic => :@automatic
+ }
+
+ attr_accessor :properties
+ attr_reader :serial_number
+
+ [:all?,
+ :any?,
+ :assoc,
+ :chunk,
+ :collect,
+ :collect_concat,
+ :compare_by_identity,
+ :compare_by_identity?,
+ :count,
+ :cycle,
+ :detect,
+ :drop,
+ :drop_while,
+ :each,
+ :each_cons,
+ :each_entry,
+ :each_key,
+ :each_pair,
+ :each_slice,
+ :each_value,
+ :each_with_index,
+ :each_with_object,
+ :empty?,
+ :entries,
+ :except,
+ :fetch,
+ :find,
+ :find_all,
+ :find_index,
+ :first,
+ :flat_map,
+ :flatten,
+ :grep,
+ :group_by,
+ :has_value?,
+ :include?,
+ :index,
+ :inject,
+ :invert,
+ :key,
+ :keys,
+ :length,
+ :map,
+ :max,
+ :max_by,
+ :merge,
+ :min,
+ :min_by,
+ :minmax,
+ :minmax_by,
+ :none?,
+ :one?,
+ :partition,
+ :rassoc,
+ :reduce,
+ :reject,
+ :reverse_each,
+ :select,
+ :size,
+ :slice_before,
+ :sort,
+ :sort_by,
+ :store,
+ :symbolize_keys,
+ :take,
+ :take_while,
+ :to_a,
+ :to_hash,
+ :to_set,
+ :value?,
+ :values,
+ :values_at,
+ :zip].each do |delegated_method|
+ class_eval(<<-METHOD_DEFN)
+ def #{delegated_method}(*args, &block)
+ merged_attributes.send(:#{delegated_method}, *args, &block)
+ end
+ METHOD_DEFN
+ end
+
+ def initialize(normal, default, override, automatic)
+ @serial_number = 0
+ @set_unless_present = false
+
+ @normal = VividMash.new(self, normal)
+ @default = VividMash.new(self, default)
+ @override = VividMash.new(self, override)
+ @automatic = VividMash.new(self, automatic)
+
+ @merged_attributes = nil
+ end
+
+ def set_unless_value_present=(setting)
+ @set_unless_present = setting
+ end
+
+ def reset_cache
+ @serial_number += 1
+ @merged_attributes = nil
+ end
+
+ def reset
+ @serial_number += 1
+ @merged_attributes = nil
+ end
+
+ def default
+ @default
+ end
+
+ def default=(new_data)
+ reset
+ @default = VividMash.new(self, new_data)
+ end
+
+ def normal
+ @normal
+ end
+
+ def normal=(new_data)
+ reset
+ @normal = VividMash.new(self, new_data)
+ end
+
+ def override
+ @override
+ end
+
+ def override=(new_data)
+ reset
+ @override = VividMash.new(self, new_data)
+ end
+
+ def automatic
+ @automatic
+ end
+
+ def automatic=(new_data)
+ reset
+ @automatic = VividMash.new(self, new_data)
+ end
+
+ def merged_attributes
+ @merged_attributes ||= begin
+ resolved_attrs = COMPONENTS.inject(Mash.new) do |merged, component_ivar|
+ component_value = instance_variable_get(component_ivar)
+ Chef::Mixin::DeepMerge.merge(merged, component_value)
+ end
+ immutablize(self, resolved_attrs)
+ end
+ end
+
+ def [](key)
+ merged_attributes[key]
+ end
+
+ def []=(key, value)
+ merged_attributes[key] = value
+ end
+
+ def has_key?(key)
+ COMPONENTS.any? do |component_ivar|
+ instance_variable_get(component_ivar).has_key?(key)
+ end
+ end
+
+ alias :attribute? :has_key?
+ alias :member? :has_key?
+ alias :include? :has_key?
+ alias :key? :has_key?
+
+ alias :each_attribute :each
+
+ def method_missing(symbol, *args)
+ if args.empty?
+ if key?(symbol)
+ self[symbol]
+ else
+ raise NoMethodError, "Undefined method or attribute `#{symbol}' on `node'"
+ end
+ elsif symbol.to_s =~ /=$/
+ key_to_set = symbol.to_s[/^(.+)=$/, 1]
+ self[key_to_set] = (args.length == 1 ? args[0] : args)
+ else
+ raise NoMethodError, "Undefined node attribute or method `#{symbol}' on `node'"
+ end
+ end
+
+ def inspect
+ "#<#{self.class} " << (COMPONENTS + [:@merged_attributes, :@properties]).map{|iv|
+ "#{iv}=#{instance_variable_get(iv).inspect}"
+ }.join(', ') << ">"
+ end
+
+ def set_unless?
+ @set_unless_present
+ end
+
+ def stale_subtree?(serial_number)
+ serial_number != @serial_number
+ end
+
+ end
+
+ end
+end
diff --git a/lib/chef/node/attribute_collections.rb b/lib/chef/node/attribute_collections.rb
new file mode 100644
index 0000000000..521687e8df
--- /dev/null
+++ b/lib/chef/node/attribute_collections.rb
@@ -0,0 +1,191 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Node
+
+ # == AttrArray
+ # AttrArray is identical to Array, except that it keeps a reference to the
+ # "root" (Chef::Node::Attribute) object, and will trigger a cache
+ # invalidation on that object when mutated.
+ class AttrArray < Array
+
+ MUTATOR_METHODS = [
+ :<<,
+ :[]=,
+ :clear,
+ :collect!,
+ :compact!,
+ :default=,
+ :default_proc=,
+ :delete,
+ :delete_at,
+ :delete_if,
+ :fill,
+ :flatten!,
+ :insert,
+ :keep_if,
+ :map!,
+ :merge!,
+ :pop,
+ :push,
+ :update,
+ :reject!,
+ :reverse!,
+ :replace,
+ :select!,
+ :shift,
+ :slice!,
+ :sort!,
+ :sort_by!,
+ :uniq!,
+ :unshift
+ ]
+
+ # For all of the methods that may mutate an Array, we override them to
+ # also invalidate the cached merged_attributes on the root
+ # Node::Attribute object.
+ MUTATOR_METHODS.each do |mutator|
+ class_eval(<<-METHOD_DEFN)
+ def #{mutator}(*args, &block)
+ root.reset_cache
+ super
+ end
+ METHOD_DEFN
+ end
+
+ attr_reader :root
+
+ def initialize(root, data)
+ @root = root
+ super(data)
+ end
+
+ end
+
+ # == VividMash
+ # VividMash is identical to a Mash, with a few exceptions:
+ # * It has a reference to the root Chef::Node::Attribute to which it
+ # belongs, and will trigger cache invalidation on that object when
+ # mutated.
+ # * It auto-vivifies, that is a reference to a missing element will result
+ # in the creation of a new VividMash for that key. (This only works when
+ # using the element reference method, `[]` -- other methods, such as
+ # #fetch, work as normal).
+ # * It supports a set_unless flag (via the root Attribute object) which
+ # allows `||=` style behavior (`||=` does not work with
+ # auto-vivification). This is only implemented for #[]=; methods such as
+ # #store work as normal.
+ # * attr_accessor style element set and get are supported via method_missing
+ class VividMash < Mash
+ attr_reader :root
+
+ # Methods that mutate a VividMash. Each of them is overridden so that it
+ # also invalidates the cached merged_attributes on the root Attribute
+ # object.
+ MUTATOR_METHODS = [
+ :clear,
+ :delete,
+ :delete_if,
+ :keep_if,
+ :merge!,
+ :update,
+ :reject!,
+ :replace,
+ :select!,
+ :shift
+ ]
+
+ # For all of the mutating methods on Mash, override them so that they
+ # also invalidate the cached `merged_attributes` on the root Attribute
+ # object.
+ MUTATOR_METHODS.each do |mutator|
+ class_eval(<<-METHOD_DEFN)
+ def #{mutator}(*args, &block)
+ root.reset_cache
+ super
+ end
+ METHOD_DEFN
+ end
+
+ def initialize(root, data={})
+ @root = root
+ super(data)
+ end
+
+ def [](key)
+ value = super
+ if !key?(key)
+ value = self.class.new(root)
+ self[key] = value
+ else
+ value
+ end
+ end
+
+ def []=(key, value)
+ if set_unless? && key?(key)
+ self[key]
+ else
+ root.reset_cache
+ super
+ end
+ end
+
+ alias :attribute? :has_key?
+
+ def method_missing(symbol, *args)
+ if args.empty?
+ self[symbol]
+ elsif symbol.to_s =~ /=$/
+ key_to_set = symbol.to_s[/^(.+)=$/, 1]
+ self[key_to_set] = (args.length == 1 ? args[0] : args)
+ else
+ raise NoMethodError, "Undefined node attribute or method `#{symbol}' on `node'. To set an attribute, use `#{symbol}=value' instead."
+ end
+ end
+
+ def set_unless?
+ @root.set_unless?
+ end
+
+ def convert_key(key)
+ super
+ end
+
+ # Mash uses #convert_value to mashify values on input.
+ # We override it here to convert hash or array values to VividMash or
+ # AttrArray for consistency and to ensure that the added parts of the
+ # attribute tree will have the correct cache invalidation behavior.
+ def convert_value(value)
+ case value
+ when VividMash
+ value
+ when Hash
+ VividMash.new(root, value)
+ when Array
+ AttrArray.new(root, value)
+ else
+ value
+ end
+ end
+
+ end
+
+ end
+end
diff --git a/lib/chef/node/immutable_collections.rb b/lib/chef/node/immutable_collections.rb
new file mode 100644
index 0000000000..33f722cd4a
--- /dev/null
+++ b/lib/chef/node/immutable_collections.rb
@@ -0,0 +1,387 @@
+
+class Chef
+ class Node
+
+ module Immutablize
+ def immutablize(root, value)
+ case value
+ when Hash
+ ImmutableMash.new(root, value)
+ when Array
+ ImmutableArray.new(root, value)
+ else
+ value
+ end
+ end
+ end
+
+ # == ImmutableArray
+ # ImmutableArray is used to implement Array collections when reading node
+ # attributes.
+ #
+ # ImmutableArray acts like an ordinary Array, except:
+ # * Methods that mutate the array are overridden to raise an error, making
+ # the collection more or less immutable.
+ # * Since this class stores values computed from a parent
+ # Chef::Node::Attribute's values, it overrides all reader methods to
+ # detect staleness and raise an error if accessed when stale.
+ class ImmutableArray < Array
+ include Immutablize
+
+ attr_reader :root
+
+ alias :internal_push :<<
+ private :internal_push
+
+ # A list of methods that mutate Array. Each of these is overridden to
+ # raise an error, making this instances of this class more or less
+ # immutable.
+ DISALLOWED_MUTATOR_METHODS = [
+ :<<,
+ :[]=,
+ :clear,
+ :collect!,
+ :compact!,
+ :default=,
+ :default_proc=,
+ :delete,
+ :delete_at,
+ :delete_if,
+ :fill,
+ :flatten!,
+ :insert,
+ :keep_if,
+ :map!,
+ :merge!,
+ :pop,
+ :push,
+ :update,
+ :reject!,
+ :reverse!,
+ :replace,
+ :select!,
+ :shift,
+ :slice!,
+ :sort!,
+ :sort_by!,
+ :uniq!,
+ :unshift
+ ]
+
+ # A list of methods that read values from the Array. Each of these is
+ # overridden to verify that the Chef::Node::Attribute object that this
+ # object belongs to has not been modified since the value was computed.
+ READER_METHODS =
+ [
+ :&,
+ :*,
+ :+,
+ :-,
+ :[],
+ :all?,
+ :any?,
+ :assoc,
+ :at,
+ :chunk,
+ :collect,
+ :collect_concat,
+ :combination,
+ :compact,
+ :concat,
+ :count,
+ :cycle,
+ :detect,
+ :drop,
+ :drop_while,
+ :each,
+ :each_cons,
+ :each_entry,
+ :each_index,
+ :each_slice,
+ :each_with_index,
+ :each_with_object,
+ :empty?,
+ :entries,
+ :fetch,
+ :find,
+ :find_all,
+ :find_index,
+ :first,
+ :flat_map,
+ :flatten,
+ :grep,
+ :group_by,
+ :include?,
+ :index,
+ :inject,
+ :join,
+ :last,
+ :length,
+ :map,
+ :max,
+ :max_by,
+ :member?,
+ :min,
+ :min_by,
+ :minmax,
+ :minmax_by,
+ :none?,
+ :one?,
+ :pack,
+ :partition,
+ :permutation,
+ :product,
+ :rassoc,
+ :reduce,
+ :reject,
+ :repeated_combination,
+ :repeated_permutation,
+ :reverse,
+ :reverse_each,
+ :rindex,
+ :rotate,
+ :sample,
+ :select,
+ :shelljoin,
+ :shuffle,
+ :size,
+ :slice,
+ :slice_before,
+ :sort,
+ :sort_by,
+ :take,
+ :take_while,
+ :to_a,
+ :to_ary,
+ :to_set,
+ :transpose,
+ :uniq,
+ :values_at,
+ :zip,
+ :|
+ ]
+
+ def initialize(root, array_data)
+ @root = root
+ @serial_number = root.serial_number
+ array_data.each do |value|
+ internal_push(immutablize(root, value))
+ end
+ end
+
+ # Redefine all of the methods that mutate a Hash to raise an error when called.
+ # This is the magic that makes this object "Immutable"
+ DISALLOWED_MUTATOR_METHODS.each do |mutator_method_name|
+ # Ruby 1.8 blocks can't have block arguments, so we must use string eval:
+ class_eval(<<-METHOD_DEFN)
+ def #{mutator_method_name}(*args, &block)
+ msg = "Node attributes are read-only when you do not specify which precedence level to set. " +
+ %Q(To set an attribute use code like `node.default["key"] = "value"')
+ raise Exceptions::ImmutableAttributeModification, msg
+ end
+ METHOD_DEFN
+ end
+
+ READER_METHODS.each do |reader|
+ class_eval(<<-METHOD_DEFN)
+ def #{reader}(*args, &block)
+ if root.stale_subtree?(@serial_number)
+ raise Exceptions::StaleAttributeRead,
+ "Node attributes have been modified since this value was read. Get an updated value by reading from node, e.g., `node[:key]`"
+ end
+ super
+ end
+ METHOD_DEFN
+ end
+
+ def dup
+ Array.new(self)
+ end
+ end
+
+ # == ImmutableMash
+ # ImmutableMash implements Hash/Dict behavior for reading values from node
+ # attributes.
+ #
+ # ImmutableMash acts like a Mash (Hash that is indifferent to String or
+ # Symbol keys), with some important exceptions:
+ # * Methods that mutate state are overridden to raise an error instead.
+ # * Methods that read from the collection are overriden so that they check
+ # if the Chef::Node::Attribute has been modified since an instance of
+ # this class was generated. An error is raised if the object detects that
+ # it is stale.
+ # * Values can be accessed in attr_reader-like fashion via method_missing.
+ class ImmutableMash < Mash
+
+ include Immutablize
+
+ attr_reader :root
+
+ alias :internal_set :[]=
+ private :internal_set
+
+ DISALLOWED_MUTATOR_METHODS = [
+ :[]=,
+ :clear,
+ :collect!,
+ :default=,
+ :default_proc=,
+ :delete,
+ :delete_if,
+ :keep_if,
+ :map!,
+ :merge!,
+ :update,
+ :reject!,
+ :replace,
+ :select!,
+ :shift
+ ]
+
+ READER_METHODS = [
+ :[],
+ :all?,
+ :any?,
+ :assoc,
+ :chunk,
+ :collect,
+ :collect_concat,
+ :count,
+ :cycle,
+ :detect,
+ :drop,
+ :drop_while,
+ :each,
+ :each_cons,
+ :each_entry,
+ :each_key,
+ :each_pair,
+ :each_slice,
+ :each_value,
+ :each_with_index,
+ :each_with_object,
+ :empty?,
+ :entries,
+ :except,
+ :fetch,
+ :find,
+ :find_all,
+ :find_index,
+ :first,
+ :flat_map,
+ :flatten,
+ :grep,
+ :group_by,
+ :has_key?,
+ :has_value?,
+ :include?,
+ :index,
+ :inject,
+ :invert,
+ :key,
+ :key?,
+ :keys,
+ :length,
+ :map,
+ :max,
+ :max_by,
+ :member?,
+ :merge,
+ :min,
+ :min_by,
+ :minmax,
+ :minmax_by,
+ :none?,
+ :one?,
+ :partition,
+ :rassoc,
+ :reduce,
+ :reject,
+ :reverse_each,
+ :select,
+ :size,
+ :slice_before,
+ :sort,
+ :sort_by,
+ :store,
+ :symbolize_keys,
+ :take,
+ :take_while,
+ :to_a,
+ :to_hash,
+ :to_set,
+ :value?,
+ :values,
+ :values_at,
+ :zip
+ ]
+
+ def initialize(root, mash_data)
+ @serial_number = root.serial_number
+ @root = root
+ mash_data.each do |key, value|
+ internal_set(key, immutablize(root, value))
+ end
+ end
+
+ alias :attribute? :has_key?
+
+ # Redefine all of the methods that mutate a Hash to raise an error when called.
+ # This is the magic that makes this object "Immutable"
+ DISALLOWED_MUTATOR_METHODS.each do |mutator_method_name|
+ # Ruby 1.8 blocks can't have block arguments, so we must use string eval:
+ class_eval(<<-METHOD_DEFN)
+ def #{mutator_method_name}(*args, &block)
+ msg = "Node attributes are read-only when you do not specify which precedence level to set. " +
+ %Q(To set an attribute use code like `node.default["key"] = "value"')
+ raise Exceptions::ImmutableAttributeModification, msg
+ end
+ METHOD_DEFN
+ end
+
+ READER_METHODS.each do |reader_method|
+ class_eval(<<-METHOD_DEFN)
+ def #{reader_method}(*args, &block)
+ if root.stale_subtree?(@serial_number)
+ raise Exceptions::StaleAttributeRead,
+ "Node attributes have been modified since this value was read. Get an updated value by reading from node, e.g., `node[:key]`"
+ end
+ super
+ end
+ METHOD_DEFN
+ end
+
+ def method_missing(symbol, *args)
+ if args.empty?
+ if key?(symbol)
+ self[symbol]
+ else
+ raise NoMethodError, "Undefined method or attribute `#{symbol}' on `node'"
+ end
+ # This will raise a ImmutableAttributeModification error:
+ elsif symbol.to_s =~ /=$/
+ key_to_set = symbol.to_s[/^(.+)=$/, 1]
+ self[key_to_set] = (args.length == 1 ? args[0] : args)
+ else
+ raise NoMethodError, "Undefined node attribute or method `#{symbol}' on `node'"
+ end
+ end
+
+ # Mash uses #convert_value to mashify values on input.
+ # Since we're handling this ourselves, override it to be a no-op
+ def convert_value(value)
+ value
+ end
+
+ # NOTE: #default and #default= are likely to be pretty confusing. For a
+ # regular ruby Hash, they control what value is returned for, e.g.,
+ # hash[:no_such_key] #=> hash.default
+ # Of course, 'default' has a specific meaning in Chef-land
+
+ def dup
+ Mash.new(self)
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/platform.rb b/lib/chef/platform.rb
new file mode 100644
index 0000000000..2cb6690da1
--- /dev/null
+++ b/lib/chef/platform.rb
@@ -0,0 +1,499 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'chef/log'
+require 'chef/mixin/params_validate'
+
+# Actually, this file depends on nearly every provider in chef, but actually
+# requiring them causes circular requires resulting in uninitialized constant
+# errors.
+require 'chef/provider'
+require 'chef/provider/log'
+require 'chef/provider/user'
+require 'chef/provider/group'
+require 'chef/provider/mount'
+require 'chef/provider/service'
+require 'chef/provider/package'
+
+
+class Chef
+ class Platform
+
+ class << self
+ attr_writer :platforms
+
+ def platforms
+ @platforms ||= {
+ :mac_os_x => {
+ :default => {
+ :package => Chef::Provider::Package::Macports,
+ :service => Chef::Provider::Service::Macosx,
+ :user => Chef::Provider::User::Dscl,
+ :group => Chef::Provider::Group::Dscl
+ }
+ },
+ :mac_os_x_server => {
+ :default => {
+ :package => Chef::Provider::Package::Macports,
+ :service => Chef::Provider::Service::Macosx,
+ :user => Chef::Provider::User::Dscl,
+ :group => Chef::Provider::Group::Dscl
+ }
+ },
+ :freebsd => {
+ :default => {
+ :group => Chef::Provider::Group::Pw,
+ :package => Chef::Provider::Package::Freebsd,
+ :service => Chef::Provider::Service::Freebsd,
+ :user => Chef::Provider::User::Pw,
+ :cron => Chef::Provider::Cron
+ }
+ },
+ :ubuntu => {
+ :default => {
+ :package => Chef::Provider::Package::Apt,
+ :service => Chef::Provider::Service::Debian,
+ :cron => Chef::Provider::Cron,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :linaro => {
+ :default => {
+ :package => Chef::Provider::Package::Apt,
+ :service => Chef::Provider::Service::Debian,
+ :cron => Chef::Provider::Cron,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :linuxmint => {
+ :default => {
+ :package => Chef::Provider::Package::Apt,
+ :service => Chef::Provider::Service::Upstart,
+ :cron => Chef::Provider::Cron,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :debian => {
+ :default => {
+ :package => Chef::Provider::Package::Apt,
+ :service => Chef::Provider::Service::Debian,
+ :cron => Chef::Provider::Cron,
+ :mdadm => Chef::Provider::Mdadm
+ },
+ "6.0" => {
+ :service => Chef::Provider::Service::Insserv
+ }
+ },
+ :xenserver => {
+ :default => {
+ :service => Chef::Provider::Service::Redhat,
+ :cron => Chef::Provider::Cron,
+ :package => Chef::Provider::Package::Yum,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :centos => {
+ :default => {
+ :service => Chef::Provider::Service::Redhat,
+ :cron => Chef::Provider::Cron,
+ :package => Chef::Provider::Package::Yum,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :amazon => {
+ :default => {
+ :service => Chef::Provider::Service::Redhat,
+ :cron => Chef::Provider::Cron,
+ :package => Chef::Provider::Package::Yum,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :scientific => {
+ :default => {
+ :service => Chef::Provider::Service::Redhat,
+ :cron => Chef::Provider::Cron,
+ :package => Chef::Provider::Package::Yum,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :fedora => {
+ :default => {
+ :service => Chef::Provider::Service::Redhat,
+ :cron => Chef::Provider::Cron,
+ :package => Chef::Provider::Package::Yum,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :suse => {
+ :default => {
+ :service => Chef::Provider::Service::Redhat,
+ :cron => Chef::Provider::Cron,
+ :package => Chef::Provider::Package::Zypper,
+ :group => Chef::Provider::Group::Suse
+ }
+ },
+ :oracle => {
+ :default => {
+ :service => Chef::Provider::Service::Redhat,
+ :cron => Chef::Provider::Cron,
+ :package => Chef::Provider::Package::Yum,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :redhat => {
+ :default => {
+ :service => Chef::Provider::Service::Redhat,
+ :cron => Chef::Provider::Cron,
+ :package => Chef::Provider::Package::Yum,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :gentoo => {
+ :default => {
+ :package => Chef::Provider::Package::Portage,
+ :service => Chef::Provider::Service::Gentoo,
+ :cron => Chef::Provider::Cron,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :arch => {
+ :default => {
+ :package => Chef::Provider::Package::Pacman,
+ :service => Chef::Provider::Service::Arch,
+ :cron => Chef::Provider::Cron,
+ :mdadm => Chef::Provider::Mdadm
+ }
+ },
+ :mswin => {
+ :default => {
+ :env => Chef::Provider::Env::Windows,
+ :service => Chef::Provider::Service::Windows,
+ :user => Chef::Provider::User::Windows,
+ :group => Chef::Provider::Group::Windows,
+ :mount => Chef::Provider::Mount::Windows
+ }
+ },
+ :mingw32 => {
+ :default => {
+ :env => Chef::Provider::Env::Windows,
+ :service => Chef::Provider::Service::Windows,
+ :user => Chef::Provider::User::Windows,
+ :group => Chef::Provider::Group::Windows,
+ :mount => Chef::Provider::Mount::Windows
+ }
+ },
+ :windows => {
+ :default => {
+ :env => Chef::Provider::Env::Windows,
+ :service => Chef::Provider::Service::Windows,
+ :user => Chef::Provider::User::Windows,
+ :group => Chef::Provider::Group::Windows,
+ :mount => Chef::Provider::Mount::Windows
+ }
+ },
+ :solaris => {},
+ :openindiana => {
+ :default => {
+ :service => Chef::Provider::Service::Solaris,
+ :package => Chef::Provider::Package::Ips,
+ :cron => Chef::Provider::Cron::Solaris,
+ :group => Chef::Provider::Group::Usermod
+ }
+ },
+ :opensolaris => {
+ :default => {
+ :service => Chef::Provider::Service::Solaris,
+ :package => Chef::Provider::Package::Ips,
+ :cron => Chef::Provider::Cron::Solaris,
+ :group => Chef::Provider::Group::Usermod
+ }
+ },
+ :nexentacore => {
+ :default => {
+ :service => Chef::Provider::Service::Solaris,
+ :package => Chef::Provider::Package::Solaris,
+ :cron => Chef::Provider::Cron::Solaris,
+ :group => Chef::Provider::Group::Usermod
+ }
+ },
+ :omnios => {
+ :default => {
+ :service => Chef::Provider::Service::Solaris,
+ :package => Chef::Provider::Package::Ips,
+ :cron => Chef::Provider::Cron::Solaris,
+ :group => Chef::Provider::Group::Usermod
+ }
+ },
+ :solaris2 => {
+ :default => {
+ :service => Chef::Provider::Service::Solaris,
+ :package => Chef::Provider::Package::Ips,
+ :cron => Chef::Provider::Cron::Solaris,
+ :group => Chef::Provider::Group::Usermod
+ },
+ "5.9" => {
+ :service => Chef::Provider::Service::Solaris,
+ :package => Chef::Provider::Package::Solaris,
+ :cron => Chef::Provider::Cron::Solaris,
+ :group => Chef::Provider::Group::Usermod
+ },
+ "5.10" => {
+ :service => Chef::Provider::Service::Solaris,
+ :package => Chef::Provider::Package::Solaris,
+ :cron => Chef::Provider::Cron::Solaris,
+ :group => Chef::Provider::Group::Usermod
+ }
+ },
+ :smartos => {
+ :default => {
+ :service => Chef::Provider::Service::Solaris,
+ :package => Chef::Provider::Package::SmartOS,
+ :cron => Chef::Provider::Cron::Solaris,
+ :group => Chef::Provider::Group::Usermod
+ }
+ },
+ :netbsd => {
+ :default => {
+ :service => Chef::Provider::Service::Freebsd,
+ :group => Chef::Provider::Group::Groupmod
+ }
+ },
+ :openbsd => {
+ :default => {
+ :group => Chef::Provider::Group::Usermod
+ }
+ },
+ :hpux => {
+ :default => {
+ :group => Chef::Provider::Group::Usermod
+ }
+ },
+ :aix => {
+ :default => {
+ :group => Chef::Provider::Group::Aix
+ }
+ },
+ :default => {
+ :file => Chef::Provider::File,
+ :directory => Chef::Provider::Directory,
+ :link => Chef::Provider::Link,
+ :template => Chef::Provider::Template,
+ :remote_directory => Chef::Provider::RemoteDirectory,
+ :execute => Chef::Provider::Execute,
+ :mount => Chef::Provider::Mount::Mount,
+ :script => Chef::Provider::Script,
+ :service => Chef::Provider::Service::Init,
+ :perl => Chef::Provider::Script,
+ :python => Chef::Provider::Script,
+ :ruby => Chef::Provider::Script,
+ :bash => Chef::Provider::Script,
+ :csh => Chef::Provider::Script,
+ :user => Chef::Provider::User::Useradd,
+ :group => Chef::Provider::Group::Gpasswd,
+ :http_request => Chef::Provider::HttpRequest,
+ :route => Chef::Provider::Route,
+ :ifconfig => Chef::Provider::Ifconfig,
+ :ruby_block => Chef::Provider::RubyBlock,
+ :erl_call => Chef::Provider::ErlCall,
+ :log => Chef::Provider::Log::ChefLog
+ }
+ }
+ end
+
+ include Chef::Mixin::ParamsValidate
+
+ def find(name, version)
+ provider_map = platforms[:default].clone
+
+ name_sym = name
+ if name.kind_of?(String)
+ name.downcase!
+ name.gsub!(/\s/, "_")
+ name_sym = name.to_sym
+ end
+
+ if platforms.has_key?(name_sym)
+ if platforms[name_sym].has_key?(version)
+ Chef::Log.debug("Platform #{name.to_s} version #{version} found")
+ if platforms[name_sym].has_key?(:default)
+ provider_map.merge!(platforms[name_sym][:default])
+ end
+ provider_map.merge!(platforms[name_sym][version])
+ elsif platforms[name_sym].has_key?(:default)
+ provider_map.merge!(platforms[name_sym][:default])
+ end
+ else
+ Chef::Log.debug("Platform #{name} not found, using all defaults. (Unsupported platform?)")
+ end
+ provider_map
+ end
+
+ def find_platform_and_version(node)
+ platform = nil
+ version = nil
+
+ if node[:platform]
+ platform = node[:platform]
+ elsif node.attribute?("os")
+ platform = node[:os]
+ end
+
+ raise ArgumentError, "Cannot find a platform for #{node}" unless platform
+
+ if node[:platform_version]
+ version = node[:platform_version]
+ elsif node[:os_version]
+ version = node[:os_version]
+ elsif node[:os_release]
+ version = node[:os_release]
+ end
+
+ raise ArgumentError, "Cannot find a version for #{node}" unless version
+
+ return platform, version
+ end
+
+ def provider_for_resource(resource, action=:nothing)
+ node = resource.run_context && resource.run_context.node
+ raise ArgumentError, "Cannot find the provider for a resource with no run context set" unless node
+ provider = find_provider_for_node(node, resource).new(resource, resource.run_context)
+ provider.action = action
+ provider
+ end
+
+ def provider_for_node(node, resource_type)
+ raise NotImplementedError, "#{self.class.name} no longer supports #provider_for_node"
+ find_provider_for_node(node, resource_type).new(node, resource_type)
+ end
+
+ def find_provider_for_node(node, resource_type)
+ platform, version = find_platform_and_version(node)
+ provider = find_provider(platform, version, resource_type)
+ end
+
+ def set(args)
+ validate(
+ args,
+ {
+ :platform => {
+ :kind_of => Symbol,
+ :required => false,
+ },
+ :version => {
+ :kind_of => String,
+ :required => false,
+ },
+ :resource => {
+ :kind_of => Symbol,
+ },
+ :provider => {
+ :kind_of => [ String, Symbol, Class ],
+ }
+ }
+ )
+ if args.has_key?(:platform)
+ if args.has_key?(:version)
+ if platforms.has_key?(args[:platform])
+ if platforms[args[:platform]].has_key?(args[:version])
+ platforms[args[:platform]][args[:version]][args[:resource].to_sym] = args[:provider]
+ else
+ platforms[args[:platform]][args[:version]] = {
+ args[:resource].to_sym => args[:provider]
+ }
+ end
+ else
+ platforms[args[:platform]] = {
+ args[:version] => {
+ args[:resource].to_sym => args[:provider]
+ }
+ }
+ end
+ else
+ if platforms.has_key?(args[:platform])
+ if platforms[args[:platform]].has_key?(:default)
+ platforms[args[:platform]][:default][args[:resource].to_sym] = args[:provider]
+ else
+ platforms[args[:platform]] = { :default => { args[:resource].to_sym => args[:provider] } }
+ end
+ else
+ platforms[args[:platform]] = {
+ :default => {
+ args[:resource].to_sym => args[:provider]
+ }
+ }
+ end
+ end
+ else
+ if platforms.has_key?(:default)
+ platforms[:default][args[:resource].to_sym] = args[:provider]
+ else
+ platforms[:default] = {
+ args[:resource].to_sym => args[:provider]
+ }
+ end
+ end
+ end
+
+ def find_provider(platform, version, resource_type)
+ pmap = Chef::Platform.find(platform, version)
+ provider_klass = explicit_provider(platform, version, resource_type) ||
+ platform_provider(platform, version, resource_type) ||
+ resource_matching_provider(platform, version, resource_type)
+
+ raise ArgumentError, "Cannot find a provider for #{resource_type} on #{platform} version #{version}" if provider_klass.nil?
+
+ provider_klass
+ end
+
+ def windows?
+ if RUBY_PLATFORM =~ /mswin|mingw|windows/
+ true
+ else
+ false
+ end
+ end
+
+ private
+
+ def explicit_provider(platform, version, resource_type)
+ resource_type.kind_of?(Chef::Resource) ? resource_type.provider : nil
+ end
+
+ def platform_provider(platform, version, resource_type)
+ pmap = Chef::Platform.find(platform, version)
+ rtkey = resource_type.kind_of?(Chef::Resource) ? resource_type.resource_name.to_sym : resource_type
+ pmap.has_key?(rtkey) ? pmap[rtkey] : nil
+ end
+
+ def resource_matching_provider(platform, version, resource_type)
+ if resource_type.kind_of?(Chef::Resource)
+ begin
+ Chef::Provider.const_get(resource_type.class.to_s.split('::').last)
+ rescue NameError
+ nil
+ end
+ else
+ nil
+ end
+ end
+
+ end
+
+ end
+end
diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb
new file mode 100644
index 0000000000..7bbcc3b915
--- /dev/null
+++ b/lib/chef/provider.rb
@@ -0,0 +1,233 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/from_file'
+require 'chef/mixin/convert_to_class_name'
+require 'chef/dsl/recipe'
+require 'chef/mixin/enforce_ownership_and_permissions'
+require 'chef/mixin/why_run'
+class Chef
+ class Provider
+ include Chef::DSL::Recipe
+ include Chef::Mixin::WhyRun
+ include Chef::Mixin::EnforceOwnershipAndPermissions
+
+ attr_accessor :new_resource
+ attr_accessor :current_resource
+ attr_accessor :run_context
+
+ #--
+ # TODO: this should be a reader, and the action should be passed in the
+ # constructor; however, many/most subclasses override the constructor so
+ # changing the arity would be a breaking change. Change this at the next
+ # break, e.g., Chef 11.
+ attr_accessor :action
+
+ def whyrun_supported?
+ false
+ end
+
+ def initialize(new_resource, run_context)
+ @new_resource = new_resource
+ @action = action
+ @current_resource = nil
+ @run_context = run_context
+ @converge_actions = nil
+ end
+
+ def whyrun_mode?
+ Chef::Config[:why_run]
+ end
+
+ def whyrun_supported?
+ false
+ end
+
+ def node
+ run_context && run_context.node
+ end
+
+ # Used by providers supporting embedded recipes
+ def resource_collection
+ run_context && run_context.resource_collection
+ end
+
+ def cookbook_name
+ new_resource.cookbook_name
+ end
+
+ def load_current_resource
+ raise Chef::Exceptions::Override, "You must override load_current_resource in #{self.to_s}"
+ end
+
+ def define_resource_requirements
+ end
+
+ def cleanup_after_converge
+ end
+
+ def action_nothing
+ Chef::Log.debug("Doing nothing for #{@new_resource.to_s}")
+ true
+ end
+
+ def events
+ run_context.events
+ end
+
+ def run_action(action=nil)
+ @action = action unless action.nil?
+
+ # TODO: it would be preferable to get the action to be executed in the
+ # constructor...
+
+ # user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode
+ if !whyrun_mode? || whyrun_supported?
+ load_current_resource
+ events.resource_current_state_loaded(@new_resource, @action, @current_resource)
+ elsif whyrun_mode? && !whyrun_supported?
+ events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource)
+ end
+
+ define_resource_requirements
+ process_resource_requirements
+
+ # user-defined providers including LWRPs may
+ # not include whyrun support - if they don't support it
+ # we can't execute any actions while we're running in
+ # whyrun mode. Instead we 'fake' whyrun by documenting that
+ # we can't execute the action.
+ # in non-whyrun mode, this will still cause the action to be
+ # executed normally.
+ if whyrun_supported? && !requirements.action_blocked?(@action)
+ send("action_#{@action}")
+ elsif whyrun_mode?
+ events.resource_bypassed(@new_resource, @action, self)
+ else
+ send("action_#{@action}")
+ end
+ converge
+
+ cleanup_after_converge
+ end
+
+ def process_resource_requirements
+ requirements.run(:all_actions) unless @action == :nothing
+ requirements.run(@action)
+ end
+
+ def converge
+ converge_actions.converge!
+ if converge_actions.empty? && !@new_resource.updated_by_last_action?
+ events.resource_up_to_date(@new_resource, @action)
+ else
+ events.resource_updated(@new_resource, @action)
+ new_resource.updated_by_last_action(true)
+ end
+ end
+
+ def requirements
+ @requirements ||= ResourceRequirements.new(@new_resource, run_context)
+ end
+
+ protected
+
+ def converge_actions
+ @converge_actions ||= ConvergeActions.new(@new_resource, run_context, @action)
+ end
+
+ def converge_by(descriptions, &block)
+ converge_actions.add_action(descriptions, &block)
+ end
+
+
+ def recipe_eval(&block)
+ # This block has new resource definitions within it, which
+ # essentially makes it an in-line Chef run. Save our current
+ # run_context and create one anew, so the new Chef run only
+ # executes the embedded resources.
+ #
+ # TODO: timh,cw: 2010-5-14: This means that the resources within
+ # this block cannot interact with resources outside, e.g.,
+ # manipulating notifies.
+
+ converge_by ("would evaluate block and run any associated actions") do
+ saved_run_context = @run_context
+ @run_context = @run_context.dup
+ @run_context.resource_collection = Chef::ResourceCollection.new
+ instance_eval(&block)
+ Chef::Runner.new(@run_context).converge
+ @run_context = saved_run_context
+ end
+ end
+
+ public
+
+ class << self
+ include Chef::Mixin::ConvertToClassName
+
+ def build_from_file(cookbook_name, filename, run_context)
+ pname = filename_to_qualified_string(cookbook_name, filename)
+
+ # Add log entry if we override an existing light-weight provider.
+ class_name = convert_to_class_name(pname)
+ overriding = Chef::Provider.const_defined?(class_name)
+ Chef::Log.info("#{class_name} light-weight provider already initialized -- overriding!") if overriding
+
+ new_provider_class = Class.new self do |cls|
+
+ include Chef::DSL::Recipe
+
+ # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore.
+ # They are not included by its replacment, Chef::DSL::Recipe, but
+ # they may be used in existing LWRPs.
+ include Chef::DSL::PlatformIntrospection
+ include Chef::DSL::DataQuery
+
+ def load_current_resource
+ # silence Chef::Exceptions::Override exception
+ end
+
+ class << cls
+ include Chef::Mixin::FromFile
+
+ # setup DSL's shortcut methods
+ def action(name, &block)
+ define_method("action_#{name.to_s}") do
+ instance_eval(&block)
+ end
+ end
+ end
+
+ # load provider definition from file
+ cls.class_from_file(filename)
+ end
+
+ # register new class as a Chef::Provider
+ pname = filename_to_qualified_string(cookbook_name, filename)
+ class_name = convert_to_class_name(pname)
+ Chef::Provider.const_set(class_name, new_provider_class)
+ Chef::Log.debug("Loaded contents of #{filename} into a provider named #{pname} defined in Chef::Provider::#{class_name}")
+
+ new_provider_class
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/provider/breakpoint.rb b/lib/chef/provider/breakpoint.rb
new file mode 100644
index 0000000000..224e2758eb
--- /dev/null
+++ b/lib/chef/provider/breakpoint.rb
@@ -0,0 +1,36 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Provider
+ class Breakpoint < Chef::Provider
+
+ def load_current_resource
+ end
+
+ def action_break
+ if defined?(Shell) && Shell.running?
+ run_context.resource_collection.iterator.pause
+ @new_resource.updated_by_last_action(true)
+ run_context.resource_collection.iterator
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/cookbook_file.rb b/lib/chef/provider/cookbook_file.rb
new file mode 100644
index 0000000000..431f3f2367
--- /dev/null
+++ b/lib/chef/provider/cookbook_file.rb
@@ -0,0 +1,84 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/file_access_control'
+require 'chef/provider/file'
+require 'tempfile'
+
+class Chef
+ class Provider
+ class CookbookFile < Chef::Provider::File
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::CookbookFile.new(@new_resource.name)
+ super
+ end
+
+ def action_create
+ if file_cache_location && content_stale?
+ description = []
+ description << "create a new cookbook_file #{@new_resource.path}"
+ description << diff_current(file_cache_location)
+ converge_by(description) do
+ Chef::Log.debug("#{@new_resource} has new contents")
+ backup_new_resource
+ deploy_tempfile do |tempfile|
+ Chef::Log.debug("#{@new_resource} staging #{file_cache_location} to #{tempfile.path}")
+ tempfile.close
+ FileUtils.cp(file_cache_location, tempfile.path)
+ # Since the @new_resource.path file will not be updated
+ # at the time of converge, we must use the tempfile
+ update_new_file_state(tempfile.path)
+ end
+ Chef::Log.info("#{@new_resource} created file #{@new_resource.path}")
+ end
+ else
+ set_all_access_controls
+ end
+ end
+
+ def file_cache_location
+ @file_cache_location ||= begin
+ cookbook = run_context.cookbook_collection[resource_cookbook]
+ cookbook.preferred_filename_on_disk_location(node, :files, @new_resource.source, @new_resource.path)
+ end
+ end
+
+ # Determine the cookbook to get the file from. If new resource sets an
+ # explicit cookbook, use it, otherwise fall back to the implicit cookbook
+ # i.e., the cookbook the resource was declared in.
+ def resource_cookbook
+ @new_resource.cookbook || @new_resource.cookbook_name
+ end
+
+ def backup_new_resource
+ if ::File.exists?(@new_resource.path)
+ backup @new_resource.path
+ end
+ end
+
+ def content_stale?
+ ( ! ::File.exist?(@new_resource.path)) || ( ! compare_content)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/cron.rb b/lib/chef/provider/cron.rb
new file mode 100644
index 0000000000..a7218fea5a
--- /dev/null
+++ b/lib/chef/provider/cron.rb
@@ -0,0 +1,214 @@
+#
+# Author:: Bryan McLellan (btm@loftninjas.org)
+# Copyright:: Copyright (c) 2009 Bryan McLellan
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/log'
+require 'chef/mixin/command'
+require 'chef/provider'
+
+class Chef
+ class Provider
+ class Cron < Chef::Provider
+ include Chef::Mixin::Command
+
+ CRON_PATTERN = /\A([-0-9*,\/]+)\s([-0-9*,\/]+)\s([-0-9*,\/]+)\s([-0-9*,\/]+|[a-zA-Z]{3})\s([-0-9*,\/]+|[a-zA-Z]{3})\s(.*)/
+ ENV_PATTERN = /\A(\S+)=(\S*)/
+
+ CRON_ATTRIBUTES = [:minute, :hour, :day, :month, :weekday, :command, :mailto, :path, :shell, :home, :environment]
+
+ def initialize(new_resource, run_context)
+ super(new_resource, run_context)
+ @cron_exists = false
+ @cron_empty = false
+ end
+ attr_accessor :cron_exists, :cron_empty
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ crontab_lines = []
+ @current_resource = Chef::Resource::Cron.new(@new_resource.name)
+ @current_resource.user(@new_resource.user)
+ if crontab = read_crontab
+ cron_found = false
+ crontab.each_line do |line|
+ case line.chomp
+ when "# Chef Name: #{@new_resource.name}"
+ Chef::Log.debug("Found cron '#{@new_resource.name}'")
+ cron_found = true
+ @cron_exists = true
+ next
+ when ENV_PATTERN
+ set_environment_var($1, $2) if cron_found
+ next
+ when CRON_PATTERN
+ if cron_found
+ @current_resource.minute($1)
+ @current_resource.hour($2)
+ @current_resource.day($3)
+ @current_resource.month($4)
+ @current_resource.weekday($5)
+ @current_resource.command($6)
+ cron_found=false
+ end
+ next
+ else
+ cron_found=false # We've got a Chef comment with no following crontab line
+ next
+ end
+ end
+ Chef::Log.debug("Cron '#{@new_resource.name}' not found") unless @cron_exists
+ else
+ Chef::Log.debug("Cron empty for '#{@new_resource.user}'")
+ @cron_empty = true
+ end
+
+ @current_resource
+ end
+
+ def cron_different?
+ CRON_ATTRIBUTES.any? do |cron_var|
+ !@new_resource.send(cron_var).nil? && @new_resource.send(cron_var) != @current_resource.send(cron_var)
+ end
+ end
+
+ def action_create
+ crontab = String.new
+ newcron = String.new
+ cron_found = false
+
+ newcron << "# Chef Name: #{new_resource.name}\n"
+ [ :mailto, :path, :shell, :home ].each do |v|
+ newcron << "#{v.to_s.upcase}=#{@new_resource.send(v)}\n" if @new_resource.send(v)
+ end
+ @new_resource.environment.each do |name, value|
+ newcron << "#{name}=#{value}\n"
+ end
+ newcron << "#{@new_resource.minute} #{@new_resource.hour} #{@new_resource.day} #{@new_resource.month} #{@new_resource.weekday} #{@new_resource.command}\n"
+
+ if @cron_exists
+ unless cron_different?
+ Chef::Log.debug("Skipping existing cron entry '#{@new_resource.name}'")
+ return
+ end
+ read_crontab.each_line do |line|
+ case line.chomp
+ when "# Chef Name: #{@new_resource.name}"
+ cron_found = true
+ next
+ when ENV_PATTERN
+ crontab << line unless cron_found
+ next
+ when CRON_PATTERN
+ if cron_found
+ cron_found = false
+ crontab << newcron
+ next
+ end
+ else
+ if cron_found # We've got a Chef comment with no following crontab line
+ crontab << newcron
+ cron_found = false
+ end
+ end
+ crontab << line
+ end
+
+ # Handle edge case where the Chef comment is the last line in the current crontab
+ crontab << newcron if cron_found
+
+ converge_by("update crontab entry for #{@new_resource}") do
+ write_crontab crontab
+ Chef::Log.info("#{@new_resource} updated crontab entry")
+ end
+
+ else
+ crontab = read_crontab unless @cron_empty
+ crontab << newcron
+
+ converge_by("add crontab entry for #{@new_resource}") do
+ write_crontab crontab
+ Chef::Log.info("#{@new_resource} added crontab entry")
+ end
+ end
+ end
+
+ def action_delete
+ if @cron_exists
+ crontab = String.new
+ cron_found = false
+ read_crontab.each_line do |line|
+ case line.chomp
+ when "# Chef Name: #{@new_resource.name}"
+ cron_found = true
+ next
+ when ENV_PATTERN
+ next if cron_found
+ when CRON_PATTERN
+ if cron_found
+ cron_found = false
+ next
+ end
+ else
+ # We've got a Chef comment with no following crontab line
+ cron_found = false
+ end
+ crontab << line
+ end
+ description = cron_found ? "remove #{@new_resource.name} from crontab" :
+ "save unmodified crontab"
+ converge_by(description) do
+ write_crontab crontab
+ Chef::Log.info("#{@new_resource} deleted crontab entry")
+ end
+ end
+ end
+
+ private
+
+ def set_environment_var(attr_name, attr_value)
+ if %w(MAILTO PATH SHELL HOME).include?(attr_name)
+ @current_resource.send(attr_name.downcase.to_sym, attr_value)
+ else
+ @current_resource.environment(@current_resource.environment.merge(attr_name => attr_value))
+ end
+ end
+
+ def read_crontab
+ crontab = nil
+ status = popen4("crontab -l -u #{@new_resource.user}") do |pid, stdin, stdout, stderr|
+ crontab = stdout.read
+ end
+ if status.exitstatus > 1
+ raise Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: #{status.exitstatus}"
+ end
+ crontab
+ end
+
+ def write_crontab(crontab)
+ status = popen4("crontab -u #{@new_resource.user} -", :waitlast => true) do |pid, stdin, stdout, stderr|
+ stdin.write crontab
+ end
+ if status.exitstatus > 0
+ raise Chef::Exceptions::Cron, "Error updating state of #{@new_resource.name}, exit: #{status.exitstatus}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/cron/solaris.rb b/lib/chef/provider/cron/solaris.rb
new file mode 100644
index 0000000000..e0811ba0ac
--- /dev/null
+++ b/lib/chef/provider/cron/solaris.rb
@@ -0,0 +1,56 @@
+#
+# Author:: Bryan McLellan (btm@loftninjas.org)
+# Author:: Toomas Pelberg (toomasp@gmx.net)
+# Copyright:: Copyright (c) 2009 Bryan McLellan
+# Copyright:: Copyright (c) 2010 Toomas Pelberg
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/log'
+require 'chef/provider'
+
+class Chef
+ class Provider
+ class Cron
+ class Solaris < Chef::Provider::Cron
+
+ private
+
+ def read_crontab
+ crontab = nil
+ status = popen4("crontab -l #{@new_resource.user}") do |pid, stdin, stdout, stderr|
+ crontab = stdout.read
+ end
+ if status.exitstatus > 1
+ raise Chef::Exceptions::Cron, "Error determining state of #{@new_resource.name}, exit: #{status.exitstatus}"
+ end
+ crontab
+ end
+
+ def write_crontab(crontab)
+ tempcron = Tempfile.new("chef-cron")
+ tempcron << crontab
+ tempcron.flush
+ tempcron.chmod(0644)
+ status = run_command(:command => "/usr/bin/crontab #{tempcron.path}",:user => @new_resource.user)
+ tempcron.close!
+ if status.exitstatus > 0
+ raise Chef::Exceptions::Cron, "Error updating state of #{@new_resource.name}, exit: #{status.exitstatus}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb
new file mode 100644
index 0000000000..60c626ab08
--- /dev/null
+++ b/lib/chef/provider/deploy.rb
@@ -0,0 +1,480 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "chef/mixin/command"
+require "chef/mixin/from_file"
+require "chef/provider/git"
+require "chef/provider/subversion"
+require 'chef/dsl/recipe'
+
+class Chef
+ class Provider
+ class Deploy < Chef::Provider
+
+ include Chef::DSL::Recipe
+ include Chef::Mixin::FromFile
+ include Chef::Mixin::Command
+
+ attr_reader :scm_provider, :release_path, :previous_release_path
+
+ def initialize(new_resource, run_context)
+ super(new_resource, run_context)
+
+ # will resolve to ither git or svn based on resource attributes ,
+ # and will create a resource corresponding to that provider
+ @scm_provider = new_resource.scm_provider.new(new_resource, run_context)
+
+ # @configuration is not used by Deploy, it is only for backwards compat with
+ # chef-deploy or capistrano hooks that might use it to get environment information
+ @configuration = @new_resource.to_hash
+ @configuration[:environment] = @configuration[:environment] && @configuration[:environment]["RAILS_ENV"]
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @scm_provider.load_current_resource
+ @release_path = @new_resource.deploy_to + "/releases/#{release_slug}"
+ end
+
+ def sudo(command,&block)
+ execute(command, &block)
+ end
+
+ def run(command, &block)
+ exec = execute(command, &block)
+ exec.user(@new_resource.user) if @new_resource.user
+ exec.group(@new_resource.group) if @new_resource.group
+ exec.cwd(release_path) unless exec.cwd
+ exec.environment(@new_resource.environment) unless exec.environment
+ converge_by("execute #{command}") do
+ exec
+ end
+ end
+
+ def define_resource_requirements
+ requirements.assert(:rollback) do |a|
+ a.assertion { all_releases[-2] }
+ a.failure_message(RuntimeError, "There is no release to rollback to!")
+ #There is no reason to assume 2 deployments in a single chef run, hence fails in whyrun.
+ end
+
+ [ @new_resource.before_migrate, @new_resource.before_symlink,
+ @new_resource.before_restart, @new_resource.after_restart ].each do |script|
+ requirements.assert(:deploy, :force_deploy) do |a|
+ callback_file = "#{release_path}/#{script}"
+ a.assertion do
+ if script && script.class == String
+ ::File.exist?(callback_file)
+ else
+ true
+ end
+ end
+ a.failure_message(RuntimeError, "Can't find your callback file #{callback_file}")
+ a.whyrun("Would assume callback file #{callback_file} included in release")
+ end
+ end
+
+ end
+
+ def action_deploy
+ save_release_state
+ if deployed?(release_path )
+ if current_release?(release_path )
+ Chef::Log.debug("#{@new_resource} is the latest version")
+ else
+ rollback_to release_path
+ end
+ else
+
+ with_rollback_on_error do
+ deploy
+ end
+ end
+ end
+
+ def action_force_deploy
+ if deployed?(release_path)
+ converge_by("delete deployed app at #{release_path} prior to force-deploy") do
+ Chef::Log.info("Already deployed app at #{release_path}, forcing.")
+ FileUtils.rm_rf(release_path)
+ Chef::Log.info("#{@new_resource} forcing deploy of already deployed app at #{release_path}")
+ end
+ end
+
+ # Alternatives:
+ # * Move release_path directory before deploy and move it back when error occurs
+ # * Rollback to previous commit
+ # * Do nothing - because deploy is force, it will be retried in short time
+ # Because last is simpliest, keep it
+ deploy
+ end
+
+ def action_rollback
+ rollback_to all_releases[-2]
+ end
+
+ def rollback_to(target_release_path)
+ @release_path = target_release_path
+
+ rp_index = all_releases.index(release_path)
+ releases_to_nuke = all_releases[(rp_index + 1)..-1]
+
+ rollback
+
+ releases_to_nuke.each do |i|
+ converge_by("roll back by removing release #{i}") do
+ Chef::Log.info "#{@new_resource} removing release: #{i}"
+ FileUtils.rm_rf i
+ end
+ release_deleted(i)
+ end
+ end
+
+ def deploy
+ verify_directories_exist
+ # CHEF-3435: We need to create the directories if they don't exist before calling the
+ # scm_provider because it expects them to be there in its own assertations
+ unless self.converge_actions.empty?
+ Chef::Log.info "#{@new_resource} running collected converge_actions before calling scm_provider"
+ self.converge_actions.converge!
+ end
+ update_cached_repo # no converge-by - scm provider will dothis
+ enforce_ownership
+ copy_cached_repo
+ install_gems
+ enforce_ownership
+ callback(:before_migrate, @new_resource.before_migrate)
+ migrate
+ callback(:before_symlink, @new_resource.before_symlink)
+ symlink
+ callback(:before_restart, @new_resource.before_restart)
+ restart
+ callback(:after_restart, @new_resource.after_restart)
+ cleanup!
+ Chef::Log.info "#{@new_resource} deployed to #{@new_resource.deploy_to}"
+ end
+
+ def rollback
+ Chef::Log.info "#{@new_resource} rolling back to previous release #{release_path}"
+ symlink
+ Chef::Log.info "#{@new_resource} restarting with previous release"
+ restart
+ end
+
+
+ def callback(what, callback_code=nil)
+ @collection = Chef::ResourceCollection.new
+ case callback_code
+ when Proc
+ Chef::Log.info "#{@new_resource} running callback #{what}"
+ recipe_eval(&callback_code)
+ when String
+ run_callback_from_file("#{release_path}/#{callback_code}")
+ when nil
+ run_callback_from_file("#{release_path}/deploy/#{what}.rb")
+ end
+ end
+
+ def migrate
+ run_symlinks_before_migrate
+
+ if @new_resource.migrate
+ enforce_ownership
+
+ environment = @new_resource.environment
+ env_info = environment && environment.map do |key_and_val|
+ "#{key_and_val.first}='#{key_and_val.last}'"
+ end.join(" ")
+
+ converge_by("execute migration command #{@new_resource.migration_command}") do
+ Chef::Log.info "#{@new_resource} migrating #{@new_resource.user} with environment #{env_info}"
+ run_command(run_options(:command => @new_resource.migration_command, :cwd=>release_path, :log_level => :info))
+ end
+ end
+ end
+
+ def symlink
+ purge_tempfiles_from_current_release
+ link_tempfiles_to_current_release
+ link_current_release_to_production
+ Chef::Log.info "#{@new_resource} updated symlinks"
+ end
+
+ def restart
+ if restart_cmd = @new_resource.restart_command
+ if restart_cmd.kind_of?(Proc)
+ Chef::Log.info("#{@new_resource} restarting app with embedded recipe")
+ recipe_eval(&restart_cmd)
+ else
+ converge_by("restart app using command #{@new_resource.restart_command}") do
+ Chef::Log.info("#{@new_resource} restarting app")
+ run_command(run_options(:command => @new_resource.restart_command, :cwd => @new_resource.current_path))
+ end
+ end
+ end
+ end
+
+ def cleanup!
+ chop = -1 - @new_resource.keep_releases
+ all_releases[0..chop].each do |old_release|
+ converge_by("remove old release #{old_release}") do
+ Chef::Log.info "#{@new_resource} removing old release #{old_release}"
+ FileUtils.rm_rf(old_release)
+ end
+ release_deleted(old_release)
+ end
+ end
+
+ def all_releases
+ Dir.glob(@new_resource.deploy_to + "/releases/*").sort
+ end
+
+ def update_cached_repo
+ if @new_resource.svn_force_export
+ # TODO assertion, non-recoverable - @scm_provider must be svn if force_export?
+ svn_force_export
+ else
+ run_scm_sync
+ end
+ end
+
+ def run_scm_sync
+ @scm_provider.run_action(:sync)
+ end
+
+ def svn_force_export
+ Chef::Log.info "#{@new_resource} exporting source repository"
+ @scm_provider.run_action(:force_export)
+ end
+
+ def copy_cached_repo
+ target_dir_path = @new_resource.deploy_to + "/releases"
+ converge_by("deploy from repo to #{@target_dir_path} ") do
+ FileUtils.mkdir_p(target_dir_path)
+ run_command(:command => "cp -RPp #{::File.join(@new_resource.destination, ".")} #{release_path}")
+ Chef::Log.info "#{@new_resource} copied the cached checkout to #{release_path}"
+ release_created(release_path)
+ end
+ end
+
+ def enforce_ownership
+ converge_by("force ownership of #{@new_resource.deploy_to} to #{@new_resource.group}:#{@new_resource.user}") do
+ FileUtils.chown_R(@new_resource.user, @new_resource.group, @new_resource.deploy_to)
+ Chef::Log.info("#{@new_resource} set user to #{@new_resource.user}") if @new_resource.user
+ Chef::Log.info("#{@new_resource} set group to #{@new_resource.group}") if @new_resource.group
+ end
+ end
+
+ def verify_directories_exist
+ create_dir_unless_exists(@new_resource.deploy_to)
+ create_dir_unless_exists(@new_resource.shared_path)
+ end
+
+ def link_current_release_to_production
+ converge_by(["remove existing link at #{@new_resource.current_path}",
+ "link release #{release_path} into production at #{@new_resource.current_path}"]) do
+ FileUtils.rm_f(@new_resource.current_path)
+ begin
+ FileUtils.ln_sf(release_path, @new_resource.current_path)
+ rescue => e
+ raise Chef::Exceptions::FileNotFound.new("Cannot symlink current release to production: #{e.message}")
+ end
+ Chef::Log.info "#{@new_resource} linked release #{release_path} into production at #{@new_resource.current_path}"
+ end
+ enforce_ownership
+ end
+
+ def run_symlinks_before_migrate
+ links_info = @new_resource.symlink_before_migrate.map { |src, dst| "#{src} => #{dst}" }.join(", ")
+ converge_by("make pre-migration symliinks: #{links_info}") do
+ @new_resource.symlink_before_migrate.each do |src, dest|
+ begin
+ FileUtils.ln_sf(@new_resource.shared_path + "/#{src}", release_path + "/#{dest}")
+ rescue => e
+ raise Chef::Exceptions::FileNotFound.new("Cannot symlink #{@new_resource.shared_path}/#{src} to #{release_path}/#{dest} before migrate: #{e.message}")
+ end
+ end
+ Chef::Log.info "#{@new_resource} made pre-migration symlinks"
+ end
+ end
+
+ def link_tempfiles_to_current_release
+ dirs_info = @new_resource.create_dirs_before_symlink.join(",")
+ @new_resource.create_dirs_before_symlink.each do |dir|
+ create_dir_unless_exists(release_path + "/#{dir}")
+ end
+ Chef::Log.info("#{@new_resource} created directories before symlinking: #{dirs_info}")
+
+ links_info = @new_resource.symlinks.map { |src, dst| "#{src} => #{dst}" }.join(", ")
+ converge_by("link shared paths into current release: #{links_info}") do
+ @new_resource.symlinks.each do |src, dest|
+ begin
+ FileUtils.ln_sf(::File.join(@new_resource.shared_path, src), ::File.join(release_path, dest))
+ rescue => e
+ raise Chef::Exceptions::FileNotFound.new("Cannot symlink shared data #{::File.join(@new_resource.shared_path, src)} to #{::File.join(release_path, dest)}: #{e.message}")
+ end
+ end
+ Chef::Log.info("#{@new_resource} linked shared paths into current release: #{links_info}")
+ end
+ run_symlinks_before_migrate
+ enforce_ownership
+ end
+
+ def create_dirs_before_symlink
+ end
+
+ def purge_tempfiles_from_current_release
+ log_info = @new_resource.purge_before_symlink.join(", ")
+ converge_by("purge directories in checkout #{log_info}") do
+ @new_resource.purge_before_symlink.each { |dir| FileUtils.rm_rf(release_path + "/#{dir}") }
+ Chef::Log.info("#{@new_resource} purged directories in checkout #{log_info}")
+ end
+ end
+
+ protected
+
+ # Internal callback, called after copy_cached_repo.
+ # Override if you need to keep state externally.
+ # Note that YOU are responsible for implementing whyrun-friendly behavior
+ # in any actions you take in this callback.
+ def release_created(release_path)
+ end
+
+ # Note that YOU are responsible for using appropriate whyrun nomenclature
+ # Override if you need to keep state externally.
+ # Note that YOU are responsible for implementing whyrun-friendly behavior
+ # in any actions you take in this callback.
+ def release_deleted(release_path)
+ end
+
+ def release_slug
+ raise Chef::Exceptions::Override, "You must override release_slug in #{self.to_s}"
+ end
+
+ def install_gems
+ gem_resource_collection_runner.converge
+ end
+
+ def gem_resource_collection_runner
+ gems_collection = Chef::ResourceCollection.new
+ gem_packages.each { |rbgem| gems_collection << rbgem }
+ gems_run_context = run_context.dup
+ gems_run_context.resource_collection = gems_collection
+ Chef::Runner.new(gems_run_context)
+ end
+
+ def gem_packages
+ return [] unless ::File.exist?("#{release_path}/gems.yml")
+ gems = YAML.load(IO.read("#{release_path}/gems.yml"))
+
+ gems.map do |g|
+ r = Chef::Resource::GemPackage.new(g[:name], run_context)
+ r.version g[:version]
+ r.action :install
+ r.source "http://gems.github.com"
+ r
+ end
+ end
+
+ def run_options(run_opts={})
+ run_opts[:user] = @new_resource.user if @new_resource.user
+ run_opts[:group] = @new_resource.group if @new_resource.group
+ run_opts[:environment] = @new_resource.environment if @new_resource.environment
+ run_opts[:log_tag] = @new_resource.to_s
+ run_opts[:log_level] ||= :debug
+ if run_opts[:log_level] == :info
+ if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info?
+ run_opts[:live_stream] = STDOUT
+ end
+ end
+ run_opts
+ end
+
+ def run_callback_from_file(callback_file)
+ Chef::Log.info "#{@new_resource} queueing checkdeploy hook #{callback_file}"
+ recipe_eval do
+ Dir.chdir(release_path) do
+ from_file(callback_file) if ::File.exist?(callback_file)
+ end
+ end
+ end
+
+ def create_dir_unless_exists(dir)
+ if ::File.directory?(dir)
+ Chef::Log.debug "#{@new_resource} not creating #{dir} because it already exists"
+ return false
+ end
+ converge_by("create new directory #{dir}") do
+ begin
+ FileUtils.mkdir_p(dir)
+ Chef::Log.debug "#{@new_resource} created directory #{dir}"
+ if @new_resource.user
+ FileUtils.chown(@new_resource.user, nil, dir)
+ Chef::Log.debug("#{@new_resource} set user to #{@new_resource.user} for #{dir}")
+ end
+ if @new_resource.group
+ FileUtils.chown(nil, @new_resource.group, dir)
+ Chef::Log.debug("#{@new_resource} set group to #{@new_resource.group} for #{dir}")
+ end
+ rescue => e
+ raise Chef::Exceptions::FileNotFound.new("Cannot create directory #{dir}: #{e.message}")
+ end
+ end
+ end
+
+ def with_rollback_on_error
+ yield
+ rescue ::Exception => e
+ if @new_resource.rollback_on_error
+ Chef::Log.warn "Error on deploying #{release_path}: #{e.message}"
+ failed_release = release_path
+
+ if previous_release_path
+ @release_path = previous_release_path
+ rollback
+ end
+ converge_by("remove failed deploy #{failed_release}") do
+ Chef::Log.info "Removing failed deploy #{failed_release}"
+ FileUtils.rm_rf failed_release
+ end
+ release_deleted(failed_release)
+ end
+
+ raise
+ end
+
+ def save_release_state
+ if ::File.exists?(@new_resource.current_path)
+ release = ::File.readlink(@new_resource.current_path)
+ @previous_release_path = release if ::File.exists?(release)
+ end
+ end
+
+ def deployed?(release)
+ all_releases.include?(release)
+ end
+
+ def current_release?(release)
+ @previous_release_path == release
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/deploy/revision.rb b/lib/chef/provider/deploy/revision.rb
new file mode 100644
index 0000000000..3728fc6a31
--- /dev/null
+++ b/lib/chef/provider/deploy/revision.rb
@@ -0,0 +1,80 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider'
+require 'chef/provider/deploy'
+require 'chef/json_compat'
+
+class Chef
+ class Provider
+ class Deploy
+ class Revision < Chef::Provider::Deploy
+
+ def all_releases
+ sorted_releases
+ end
+
+ protected
+
+ def release_created(release)
+ sorted_releases {|r| r.delete(release); r << release }
+ end
+
+ def release_deleted(release)
+ sorted_releases { |r| r.delete(release)}
+ end
+
+ def release_slug
+ scm_provider.revision_slug
+ end
+
+ private
+
+ def sorted_releases
+ cache = load_cache
+ if block_given?
+ yield cache
+ save_cache(cache)
+ end
+ cache
+ end
+
+ def sorted_releases_from_filesystem
+ Dir.glob(new_resource.deploy_to + "/releases/*").sort_by { |d| ::File.ctime(d) }
+ end
+
+ def load_cache
+ begin
+ Chef::JSONCompat.from_json(Chef::FileCache.load("revision-deploys/#{new_resource.name}"))
+ rescue Chef::Exceptions::FileNotFound
+ sorted_releases_from_filesystem
+ end
+ end
+
+ def save_cache(cache)
+ Chef::FileCache.store("revision-deploys/#{new_resource.name}", cache.to_json)
+ cache
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/deploy/timestamped.rb b/lib/chef/provider/deploy/timestamped.rb
new file mode 100644
index 0000000000..9c2d55b490
--- /dev/null
+++ b/lib/chef/provider/deploy/timestamped.rb
@@ -0,0 +1,32 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Provider
+ class Deploy
+ class Timestamped < Chef::Provider::Deploy
+
+ protected
+
+ def release_slug
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb
new file mode 100644
index 0000000000..0329aeb1ad
--- /dev/null
+++ b/lib/chef/provider/directory.rb
@@ -0,0 +1,128 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'chef/log'
+require 'chef/resource/directory'
+require 'chef/provider'
+require 'chef/provider/file'
+require 'fileutils'
+
+class Chef
+ class Provider
+ class Directory < Chef::Provider::File
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Directory.new(@new_resource.name)
+ @current_resource.path(@new_resource.path)
+ load_current_resource_attrs
+ setup_acl
+
+ @current_resource
+ end
+
+ def define_resource_requirements
+ # this must be evaluated before whyrun messages are printed
+ access_controls.requires_changes?
+
+ requirements.assert(:create) do |a|
+ # Make sure the parent dir exists, or else fail.
+ # for why run, print a message explaining the potential error.
+ parent_directory = ::File.dirname(@new_resource.path)
+ a.assertion { @new_resource.recursive || ::File.directory?(parent_directory) }
+ a.failure_message(Chef::Exceptions::EnclosingDirectoryDoesNotExist, "Parent directory #{parent_directory} does not exist, cannot create #{@new_resource.path}")
+ a.whyrun("Assuming directory #{parent_directory} would have been created")
+ end
+
+ requirements.assert(:create) do |a|
+ parent_directory = ::File.dirname(@new_resource.path)
+ a.assertion do
+ if @new_resource.recursive
+ # find the lowest-level directory in @new_resource.path that already exists
+ # make sure we have write permissions to that directory
+ is_parent_writable = lambda do |base_dir|
+ base_dir = ::File.dirname(base_dir)
+ if ::File.exist?(base_dir)
+ ::File.writable?(base_dir)
+ else
+ is_parent_writable.call(base_dir)
+ end
+ end
+ is_parent_writable.call(@new_resource.path)
+ else
+ # in why run mode & parent directory does not exist no permissions check is required
+ # If not in why run, permissions must be valid and we rely on prior assertion that dir exists
+ if !whyrun_mode? || ::File.exist?(parent_directory)
+ ::File.writable?(parent_directory)
+ else
+ true
+ end
+ end
+ end
+ a.failure_message(Chef::Exceptions::InsufficientPermissions,
+ "Cannot create #{@new_resource} at #{@new_resource.path} due to insufficient permissions")
+ end
+
+ requirements.assert(:delete) do |a|
+ a.assertion do
+ if ::File.exist?(@new_resource.path)
+ ::File.directory?(@new_resource.path) && ::File.writable?(@new_resource.path)
+ else
+ true
+ end
+ end
+ a.failure_message(RuntimeError, "Cannot delete #{@new_resource} at #{@new_resource.path}!")
+ # No why-run handling here:
+ # * if we don't have permissions, this is unlikely to be changed earlier in the run
+ # * if the target is a file (not a dir), there's no reasonable path by which this would have been changed
+ end
+ end
+
+ def action_create
+ unless ::File.exist?(@new_resource.path)
+ converge_by("create new directory #{@new_resource.path}") do
+ if @new_resource.recursive == true
+ ::FileUtils.mkdir_p(@new_resource.path)
+ else
+ ::Dir.mkdir(@new_resource.path)
+ end
+ Chef::Log.info("#{@new_resource} created directory #{@new_resource.path}")
+ end
+ end
+ set_all_access_controls
+ end
+
+ def action_delete
+ if ::File.exist?(@new_resource.path)
+ converge_by("delete existing directory #{@new_resource.path}") do
+ if @new_resource.recursive == true
+ FileUtils.rm_rf(@new_resource.path)
+ Chef::Log.info("#{@new_resource} deleted #{@new_resource.path} recursively")
+ else
+ ::Dir.delete(@new_resource.path)
+ Chef::Log.info("#{@new_resource} deleted #{@new_resource.path}")
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/env.rb b/lib/chef/provider/env.rb
new file mode 100644
index 0000000000..e857d74d68
--- /dev/null
+++ b/lib/chef/provider/env.rb
@@ -0,0 +1,152 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider'
+require 'chef/mixin/command'
+require 'chef/resource/env'
+
+class Chef
+ class Provider
+ class Env < Chef::Provider
+ include Chef::Mixin::Command
+ attr_accessor :key_exists
+
+ def initialize(new_resource, run_context)
+ super
+ @key_exists = true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Env.new(@new_resource.name)
+ @current_resource.key_name(@new_resource.key_name)
+
+ if env_key_exists(@new_resource.key_name)
+ @current_resource.value(env_value(@new_resource.key_name))
+ else
+ @key_exists = false
+ Chef::Log.debug("#{@new_resource} key does not exist")
+ end
+
+ @current_resource
+ end
+
+ def env_value(key_name)
+ raise Chef::Exceptions::Env, "#{self.to_s} provider does not implement env_value!"
+ end
+
+ def env_key_exists(key_name)
+ env_value(key_name) ? true : false
+ end
+
+ # Check to see if value needs any changes
+ #
+ # ==== Returns
+ # <true>:: If a change is required
+ # <false>:: If a change is not required
+ def compare_value
+ if @new_resource.delim
+ #e.g. check for existing value within PATH
+ not @current_resource.value.split(@new_resource.delim).any? do |val|
+ val == @new_resource.value
+ end
+ else
+ @new_resource.value != @current_resource.value
+ end
+ end
+
+ def action_create
+ if @key_exists
+ if compare_value
+ modify_env
+ Chef::Log.info("#{@new_resource} altered")
+ @new_resource.updated_by_last_action(true)
+ end
+ else
+ create_env
+ Chef::Log.info("#{@new_resource} created")
+ @new_resource.updated_by_last_action(true)
+ end
+ end
+
+ #e.g. delete a PATH element
+ #
+ # ==== Returns
+ # <true>:: If we handled the element case and caller should not delete the key
+ # <false>:: Caller should delete the key, either no :delim was specific or value was empty
+ # after we removed the element.
+ def delete_element
+ return false unless @new_resource.delim #no delim: delete the key
+ if compare_value
+ Chef::Log.debug("#{@new_resource} element '#{@new_resource.value}' does not exist")
+ return true #do not delete the key
+ else
+ new_value =
+ @current_resource.value.split(@new_resource.delim).select { |item|
+ item != @new_resource.value
+ }.join(@new_resource.delim)
+
+ if new_value.empty?
+ return false #nothing left here, delete the key
+ else
+ old_value = @new_resource.value(new_value)
+ create_env
+ Chef::Log.debug("#{@new_resource} deleted #{old_value} element")
+ @new_resource.updated_by_last_action(true)
+ return true #we removed the element and updated; do not delete the key
+ end
+ end
+ end
+
+ def action_delete
+ if @key_exists && !delete_element
+ delete_env
+ Chef::Log.info("#{@new_resource} deleted")
+ @new_resource.updated_by_last_action(true)
+ end
+ end
+
+ def action_modify
+ if @key_exists
+ if compare_value
+ modify_env
+ Chef::Log.info("#{@new_resource} modified")
+ @new_resource.updated_by_last_action(true)
+ end
+ else
+ raise Chef::Exceptions::Env, "Cannot modify #{@new_resource} - key does not exist!"
+ end
+ end
+
+ def create_env
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :#{@new_resource.action}"
+ end
+
+ def delete_env
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :delete"
+ end
+
+ def modify_env
+ if @new_resource.delim
+ #e.g. add to PATH
+ @new_resource.value << @new_resource.delim << @current_resource.value
+ end
+ create_env
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/env/windows.rb b/lib/chef/provider/env/windows.rb
new file mode 100644
index 0000000000..bf728b1fae
--- /dev/null
+++ b/lib/chef/provider/env/windows.rb
@@ -0,0 +1,75 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ require 'ruby-wmi'
+ require 'Win32API'
+end
+
+class Chef
+ class Provider
+ class Env
+ class Windows < Chef::Provider::Env
+
+ def create_env
+ obj = env_obj(@new_resource.key_name)
+ unless obj
+ obj = WIN32OLE.connect("winmgmts://").get("Win32_Environment").spawninstance_
+ obj.name = @new_resource.key_name
+ obj.username = "<System>"
+ end
+ obj.variablevalue = @new_resource.value
+ obj.put_
+ broadcast_env_change
+ end
+
+ def delete_env
+ obj = env_obj(@new_resource.key_name)
+ if obj
+ obj.delete_
+ broadcast_env_change
+ end
+ end
+
+ def env_value(key_name)
+ obj = env_obj(key_name)
+ return obj ? obj.variablevalue : nil
+ end
+
+ def env_obj(key_name)
+ WMI::Win32_Environment.find(:first,
+ :conditions => { :name => key_name })
+ end
+
+ #see: http://msdn.microsoft.com/en-us/library/ms682653%28VS.85%29.aspx
+ HWND_BROADCAST = 0xffff
+ WM_SETTINGCHANGE = 0x001A
+ SMTO_BLOCK = 0x0001
+ SMTO_ABORTIFHUNG = 0x0002
+ SMTO_NOTIMEOUTIFNOTHUNG = 0x0008
+
+ def broadcast_env_change
+ result = 0
+ flags = SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG
+ @send_message ||= Win32API.new('user32', 'SendMessageTimeout', 'LLLPLLP', 'L')
+ @send_message.call(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 'Environment', flags, 5000, result)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/erl_call.rb b/lib/chef/provider/erl_call.rb
new file mode 100644
index 0000000000..1ee1da500c
--- /dev/null
+++ b/lib/chef/provider/erl_call.rb
@@ -0,0 +1,106 @@
+#
+# Author:: Joe Williams (<joe@joetify.com>)
+# Copyright:: Copyright (c) 2009 Joe Williams
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/log'
+require 'chef/mixin/command'
+require 'chef/provider'
+
+class Chef
+ class Provider
+ class ErlCall < Chef::Provider
+ include Chef::Mixin::Command
+
+ def initialize(node, new_resource)
+ super(node, new_resource)
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ true
+ end
+
+ def action_run
+ case @new_resource.name_type
+ when "sname"
+ node = "-sname #{@new_resource.node_name}"
+ when "name"
+ node = "-name #{@new_resource.node_name}"
+ end
+
+ if @new_resource.cookie
+ cookie = "-c #{@new_resource.cookie}"
+ else
+ cookie = ""
+ end
+
+ if @new_resource.distributed
+ distributed = "-s"
+ else
+ distributed = ""
+ end
+
+ command = "erl_call -e #{distributed} #{node} #{cookie}"
+
+ converge_by("run erlang block") do
+ begin
+ pid, stdin, stdout, stderr = popen4(command, :waitlast => true)
+
+ Chef::Log.debug("#{@new_resource} running")
+ Chef::Log.debug("#{@new_resource} command: #{command}")
+ Chef::Log.debug("#{@new_resource} code: #{@new_resource.code}")
+
+ @new_resource.code.each_line { |line| stdin.puts(line.chomp) }
+
+ stdin.close
+
+ Chef::Log.debug("#{@new_resource} output: ")
+
+ stdout_output = ""
+ stdout.each_line { |line| stdout_output << line }
+ stdout.close
+
+ stderr_output = ""
+ stderr.each_line { |line| stderr_output << line }
+ stderr.close
+
+ # fail if stderr contains anything
+ if stderr_output.length > 0
+ raise Chef::Exceptions::ErlCall, stderr_output
+ end
+
+ # fail if the first 4 characters aren't "{ok,"
+ unless stdout_output[0..3].include?('{ok,')
+ raise Chef::Exceptions::ErlCall, stdout_output
+ end
+
+ @new_resource.updated_by_last_action(true)
+
+ Chef::Log.debug("#{@new_resource} #{stdout_output}")
+ Chef::Log.info("#{@new_resouce} ran successfully")
+ ensure
+ Process.wait(pid) if pid
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/execute.rb b/lib/chef/provider/execute.rb
new file mode 100644
index 0000000000..d6b2f91bab
--- /dev/null
+++ b/lib/chef/provider/execute.rb
@@ -0,0 +1,68 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/shell_out'
+require 'chef/log'
+require 'chef/provider'
+
+class Chef
+ class Provider
+ class Execute < Chef::Provider
+
+ include Chef::Mixin::ShellOut
+
+ def load_current_resource
+ true
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def action_run
+ opts = {}
+
+ if sentinel_file = @new_resource.creates
+ if ::File.exists?(sentinel_file)
+ Chef::Log.debug("#{@new_resource} sentinel file #{sentinel_file} exists - nothing to do")
+ return false
+ end
+ end
+
+ # original implementation did not specify a timeout, but ShellOut
+ # *always* times out. So, set a very long default timeout
+ opts[:timeout] = @new_resource.timeout || 3600
+ opts[:returns] = @new_resource.returns if @new_resource.returns
+ opts[:environment] = @new_resource.environment if @new_resource.environment
+ opts[:user] = @new_resource.user if @new_resource.user
+ opts[:group] = @new_resource.group if @new_resource.group
+ opts[:cwd] = @new_resource.cwd if @new_resource.cwd
+ opts[:umask] = @new_resource.umask if @new_resource.umask
+ opts[:log_level] = :info
+ opts[:log_tag] = @new_resource.to_s
+ if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info?
+ opts[:live_stream] = STDOUT
+ end
+ converge_by("execute #{@new_resource.command}") do
+ result = shell_out!(@new_resource.command, opts)
+ Chef::Log.info("#{@new_resource} ran successfully")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb
new file mode 100644
index 0000000000..659afc6517
--- /dev/null
+++ b/lib/chef/provider/file.rb
@@ -0,0 +1,338 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'chef/log'
+require 'chef/resource/file'
+require 'chef/mixin/checksum'
+require 'chef/provider'
+require 'etc'
+require 'fileutils'
+require 'chef/scan_access_control'
+require 'chef/mixin/shell_out'
+
+class Chef
+
+ class Provider
+ class File < Chef::Provider
+ include Chef::Mixin::Checksum
+ include Chef::Mixin::ShellOut
+
+ def negative_complement(big)
+ if big > 1073741823 # Fixnum max
+ big -= (2**32) # diminished radix wrap to negative
+ end
+ big
+ end
+
+ def octal_mode(mode)
+ ((mode.respond_to?(:oct) ? mode.oct : mode.to_i) & 007777)
+ end
+
+ private :negative_complement, :octal_mode
+
+ def diff_current_from_content(new_content)
+ result = nil
+ Tempfile.open("chef-diff") do |file|
+ file.write new_content
+ file.close
+ result = diff_current file.path
+ end
+ result
+ end
+
+ def is_binary?(path)
+ ::File.open(path) do |file|
+
+ buff = file.read(Chef::Config[:diff_filesize_threshold])
+ buff = "" if buff.nil?
+ return buff !~ /^[\r[:print:]]*$/
+ end
+ end
+
+
+ def diff_current(temp_path)
+ suppress_resource_reporting = false
+
+ return [ "(diff output suppressed by config)" ] if Chef::Config[:diff_disabled]
+ return [ "(no temp file with new content, diff output suppressed)" ] unless ::File.exists?(temp_path) # should never happen?
+
+ # solaris does not support diff -N, so create tempfile to diff against if we are creating a new file
+ target_path = if ::File.exists?(@current_resource.path)
+ @current_resource.path
+ else
+ suppress_resource_reporting = true # suppress big diffs going to resource reporting service
+ tempfile = Tempfile.new('chef-tempfile')
+ tempfile.path
+ end
+
+ diff_filesize_threshold = Chef::Config[:diff_filesize_threshold]
+ diff_output_threshold = Chef::Config[:diff_output_threshold]
+
+ if ::File.size(target_path) > diff_filesize_threshold || ::File.size(temp_path) > diff_filesize_threshold
+ return [ "(file sizes exceed #{diff_filesize_threshold} bytes, diff output suppressed)" ]
+ end
+
+ # MacOSX(BSD?) diff will *sometimes* happily spit out nasty binary diffs
+ return [ "(current file is binary, diff output suppressed)"] if is_binary?(target_path)
+ return [ "(new content is binary, diff output suppressed)"] if is_binary?(temp_path)
+
+ begin
+ # -u: Unified diff format
+ result = shell_out("diff -u #{target_path} #{temp_path}" )
+ rescue Exception => e
+ # Should *not* receive this, but in some circumstances it seems that
+ # an exception can be thrown even using shell_out instead of shell_out!
+ return [ "Could not determine diff. Error: #{e.message}" ]
+ end
+
+ # diff will set a non-zero return code even when there's
+ # valid stdout results, if it encounters something unexpected
+ # So as long as we have output, we'll show it.
+ if not result.stdout.empty?
+ if result.stdout.length > diff_output_threshold
+ [ "(long diff of over #{diff_output_threshold} characters, diff output suppressed)" ]
+ else
+ val = result.stdout.split("\n")
+ val.delete("\\ No newline at end of file")
+ @new_resource.diff(val.join("\\n")) unless suppress_resource_reporting
+ val
+ end
+ elsif not result.stderr.empty?
+ [ "Could not determine diff. Error: #{result.stderr}" ]
+ else
+ [ "(no diff)" ]
+ end
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ # Every child should be specifying their own constructor, so this
+ # should only be run in the file case.
+ @current_resource ||= Chef::Resource::File.new(@new_resource.name)
+ @new_resource.path.gsub!(/\\/, "/") # for Windows
+ @current_resource.path(@new_resource.path)
+ if !::File.directory?(@new_resource.path)
+ if ::File.exist?(@new_resource.path)
+ @current_resource.checksum(checksum(@new_resource.path))
+ end
+ end
+ load_current_resource_attrs
+ setup_acl
+
+ @current_resource
+ end
+
+ def load_current_resource_attrs
+ if ::File.exist?(@new_resource.path)
+ stat = ::File.stat(@new_resource.path)
+ @current_resource.owner(stat.uid)
+ @current_resource.mode(stat.mode & 07777)
+ @current_resource.group(stat.gid)
+
+ if @new_resource.group.nil?
+ @new_resource.group(@current_resource.group)
+ end
+ if @new_resource.owner.nil?
+ @new_resource.owner(@current_resource.owner)
+ end
+ if @new_resource.mode.nil?
+ @new_resource.mode(@current_resource.mode)
+ end
+ end
+ end
+
+ def setup_acl
+ @acl_scanner = ScanAccessControl.new(@new_resource, @current_resource)
+ @acl_scanner.set_all!
+ end
+
+ def define_resource_requirements
+ # this must be evaluated before whyrun messages are printed
+ access_controls.requires_changes?
+
+ requirements.assert(:create, :create_if_missing, :touch) do |a|
+ # Make sure the parent dir exists, or else fail.
+ # for why run, print a message explaining the potential error.
+ parent_directory = ::File.dirname(@new_resource.path)
+
+ a.assertion { ::File.directory?(parent_directory) }
+ a.failure_message(Chef::Exceptions::EnclosingDirectoryDoesNotExist, "Parent directory #{parent_directory} does not exist.")
+ a.whyrun("Assuming directory #{parent_directory} would have been created")
+ end
+
+ # Make sure the file is deletable if it exists. Otherwise, fail.
+ requirements.assert(:delete) do |a|
+ a.assertion do
+ if ::File.exists?(@new_resource.path)
+ ::File.writable?(@new_resource.path)
+ else
+ true
+ end
+ end
+ a.failure_message(Chef::Exceptions::InsufficientPermissions,"File #{@new_resource.path} exists but is not writable so it cannot be deleted")
+ end
+ end
+
+ # Compare the content of a file. Returns true if they are the same, false if they are not.
+ def compare_content
+ checksum(@current_resource.path) == new_resource_content_checksum
+ end
+
+ # Set the content of the file, assuming it is not set correctly already.
+ def set_content
+ unless compare_content
+ description = []
+ description << "update content in file #{@new_resource.path} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(new_resource_content_checksum)}"
+ description << diff_current_from_content(@new_resource.content)
+ converge_by(description) do
+ backup @new_resource.path if ::File.exists?(@new_resource.path)
+ ::File.open(@new_resource.path, "w") {|f| f.write @new_resource.content }
+ Chef::Log.info("#{@new_resource} contents updated")
+ end
+ end
+ end
+
+ # if you are using a tempfile before creating, you must
+ # override the default with the tempfile, since the
+ # file at @new_resource.path will not be updated on converge
+ def update_new_file_state(path=@new_resource.path)
+ stat = ::File.stat(path)
+ @new_resource.owner(stat.uid)
+ @new_resource.mode(stat.mode & 07777)
+ @new_resource.group(stat.gid)
+ if !::File.directory?(path)
+ @new_resource.checksum(checksum(path))
+ end
+ end
+
+ def action_create
+ if !::File.exists?(@new_resource.path)
+ description = []
+ desc = "create new file #{@new_resource.path}"
+ desc << " with content checksum #{short_cksum(new_resource_content_checksum)}" if new_resource.content
+ description << desc
+ description << diff_current_from_content(@new_resource.content)
+
+ converge_by(description) do
+ Chef::Log.info("entered create")
+ ::File.open(@new_resource.path, "w+") {|f| f.write @new_resource.content }
+ access_controls.set_all
+ Chef::Log.info("#{@new_resource} created file #{@new_resource.path}")
+ update_new_file_state
+ end
+ else
+ set_content unless @new_resource.content.nil?
+ set_all_access_controls
+ end
+ end
+
+ def set_all_access_controls
+ if access_controls.requires_changes?
+ converge_by(access_controls.describe_changes) do
+ access_controls.set_all
+ #Update file state with new access values
+ update_new_file_state
+ end
+ end
+ end
+
+ def action_create_if_missing
+ if ::File.exists?(@new_resource.path)
+ Chef::Log.debug("#{@new_resource} exists at #{@new_resource.path} taking no action.")
+ else
+ action_create
+ end
+ end
+
+ def action_delete
+ if ::File.exists?(@new_resource.path)
+ converge_by("delete file #{@new_resource.path}") do
+ backup unless ::File.symlink?(@new_resource.path)
+ ::File.delete(@new_resource.path)
+ Chef::Log.info("#{@new_resource} deleted file at #{@new_resource.path}")
+ end
+ end
+ end
+
+ def action_touch
+ action_create
+ converge_by("update utime on file #{@new_resource.path}") do
+ time = Time.now
+ ::File.utime(time, time, @new_resource.path)
+ Chef::Log.info("#{@new_resource} updated atime and mtime to #{time}")
+ end
+ end
+
+ def backup(file=nil)
+ file ||= @new_resource.path
+ if @new_resource.backup != false && @new_resource.backup > 0 && ::File.exist?(file)
+ time = Time.now
+ savetime = time.strftime("%Y%m%d%H%M%S")
+ backup_filename = "#{@new_resource.path}.chef-#{savetime}"
+ backup_filename = backup_filename.sub(/^([A-Za-z]:)/, "") #strip drive letter on Windows
+ # if :file_backup_path is nil, we fallback to the old behavior of
+ # keeping the backup in the same directory. We also need to to_s it
+ # so we don't get a type error around implicit to_str conversions.
+ prefix = Chef::Config[:file_backup_path].to_s
+ backup_path = ::File.join(prefix, backup_filename)
+ FileUtils.mkdir_p(::File.dirname(backup_path)) if Chef::Config[:file_backup_path]
+ FileUtils.cp(file, backup_path, :preserve => true)
+ Chef::Log.info("#{@new_resource} backed up to #{backup_path}")
+
+ # Clean up after the number of backups
+ slice_number = @new_resource.backup
+ backup_files = Dir[::File.join(prefix, ".#{@new_resource.path}.chef-*")].sort { |a,b| b <=> a }
+ if backup_files.length >= @new_resource.backup
+ remainder = backup_files.slice(slice_number..-1)
+ remainder.each do |backup_to_delete|
+ FileUtils.rm(backup_to_delete)
+ Chef::Log.info("#{@new_resource} removed backup at #{backup_to_delete}")
+ end
+ end
+ end
+ end
+
+ def deploy_tempfile
+ Tempfile.open(::File.basename(@new_resource.name)) do |tempfile|
+ yield tempfile
+
+ temp_res = Chef::Resource::CookbookFile.new(@new_resource.name)
+ temp_res.path(tempfile.path)
+ ac = Chef::FileAccessControl.new(temp_res, @new_resource, self)
+ ac.set_all!
+ FileUtils.mv(tempfile.path, @new_resource.path)
+ end
+ end
+
+ private
+
+ def short_cksum(checksum)
+ return "none" if checksum.nil?
+ checksum.slice(0,6)
+ end
+
+ def new_resource_content_checksum
+ @new_resource.content && Digest::SHA2.hexdigest(@new_resource.content)
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/git.rb b/lib/chef/provider/git.rb
new file mode 100644
index 0000000000..cc524a2fcd
--- /dev/null
+++ b/lib/chef/provider/git.rb
@@ -0,0 +1,260 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+require 'chef/log'
+require 'chef/provider'
+require 'chef/mixin/shell_out'
+require 'fileutils'
+require 'shellwords'
+
+class Chef
+ class Provider
+ class Git < Chef::Provider
+
+ include Chef::Mixin::ShellOut
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @resolved_reference = nil
+ @current_resource = Chef::Resource::Git.new(@new_resource.name)
+ if current_revision = find_current_revision
+ @current_resource.revision current_revision
+ end
+ end
+
+ def define_resource_requirements
+ # Parent directory of the target must exist.
+ requirements.assert(:checkout, :sync) do |a|
+ dirname = ::File.dirname(@new_resource.destination)
+ a.assertion { ::File.directory?(dirname) }
+ a.whyrun("Directory #{dirname} does not exist, this run will fail unless it has been previously created. Assuming it would have been created.")
+ a.failure_message(Chef::Exceptions::MissingParentDirectory,
+ "Cannot clone #{@new_resource} to #{@new_resource.destination}, the enclosing directory #{dirname} does not exist")
+ end
+
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { !(@new_resource.revision =~ /^origin\//) }
+ a.failure_message Chef::Exceptions::InvalidRemoteGitReference,
+ "Deploying remote branches is not supported. " +
+ "Specify the remote branch as a local branch for " +
+ "the git repository you're deploying from " +
+ "(ie: '#{@new_resource.revision.gsub('origin/', '')}' rather than '#{@new_resource.revision}')."
+ end
+
+ requirements.assert(:all_actions) do |a|
+ # this can't be recovered from in why-run mode, because nothing that
+ # we do in the course of a run is likely to create a valid target_revision
+ # if we can't resolve it up front.
+ a.assertion { target_revision != nil }
+ a.failure_message Chef::Exceptions::UnresolvableGitReference,
+ "Unable to parse SHA reference for '#{@new_resource.revision}' in repository '#{@new_resource.repository}'. " +
+ "Verify your (case-sensitive) repository URL and revision.\n" +
+ "`git ls-remote` output: #{@resolved_reference}"
+ end
+ end
+
+ def action_checkout
+ if target_dir_non_existent_or_empty?
+ clone
+ checkout
+ enable_submodules
+ add_remotes
+ else
+ Chef::Log.debug "#{@new_resource} checkout destination #{@new_resource.destination} already exists or is a non-empty directory"
+ end
+ end
+
+ def action_export
+ action_checkout
+ converge_by("complete the export by removing #{@new_resource.destination}.git after checkout") do
+ FileUtils.rm_rf(::File.join(@new_resource.destination,".git"))
+ end
+ end
+
+ def action_sync
+ if existing_git_clone?
+ current_rev = find_current_revision
+ Chef::Log.debug "#{@new_resource} current revision: #{current_rev} target revision: #{target_revision}"
+ unless current_revision_matches_target_revision?
+ fetch_updates
+ enable_submodules
+ Chef::Log.info "#{@new_resource} updated to revision #{target_revision}"
+ end
+ add_remotes
+ else
+ action_checkout
+ end
+ end
+
+
+ def existing_git_clone?
+ ::File.exist?(::File.join(@new_resource.destination, ".git"))
+ end
+
+ def target_dir_non_existent_or_empty?
+ !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == ['.','..']
+ end
+
+ def find_current_revision
+ Chef::Log.debug("#{@new_resource} finding current git revision")
+ if ::File.exist?(::File.join(cwd, ".git"))
+ # 128 is returned when we're not in a git repo. this is fine
+ result = shell_out!('git rev-parse HEAD', :cwd => cwd, :returns => [0,128]).stdout.strip
+ end
+ sha_hash?(result) ? result : nil
+ end
+
+ def add_remotes
+ if (@new_resource.additional_remotes.length > 0)
+ @new_resource.additional_remotes.each_pair do |remote_name, remote_url|
+ converge_by("add remote #{remote_name} from #{remote_url}") do
+ Chef::Log.info "#{@new_resource} adding git remote #{remote_name} = #{remote_url}"
+ command = "git remote add #{remote_name} #{remote_url}"
+ if shell_out(command, run_options(:cwd => @new_resource.destination, :log_level => :info)).exitstatus != 0
+ @new_resource.updated_by_last_action(true)
+ end
+ end
+ end
+ end
+ end
+
+ def clone
+ converge_by("clone from #{@new_resource.repository} into #{@new_resource.destination}") do
+ remote = @new_resource.remote
+
+ args = []
+ args << "-o #{remote}" unless remote == 'origin'
+ args << "--depth #{@new_resource.depth}" if @new_resource.depth
+
+ Chef::Log.info "#{@new_resource} cloning repo #{@new_resource.repository} to #{@new_resource.destination}"
+
+ clone_cmd = "git clone #{args.join(' ')} #{@new_resource.repository} #{Shellwords.escape @new_resource.destination}"
+ shell_out!(clone_cmd, run_options(:log_level => :info))
+ end
+ end
+
+ def checkout
+ sha_ref = target_revision
+ converge_by("checkout ref #{sha_ref} branch #{@new_resource.revision}") do
+ # checkout into a local branch rather than a detached HEAD
+ shell_out!("git checkout -b deploy #{sha_ref}", run_options(:cwd => @new_resource.destination))
+ Chef::Log.info "#{@new_resource} checked out branch: #{@new_resource.revision} reference: #{sha_ref}"
+ end
+ end
+
+ def enable_submodules
+ if @new_resource.enable_submodules
+ converge_by("enable git submodules for #{@new_resource}") do
+ Chef::Log.info "#{@new_resource} enabling git submodules"
+ # the --recursive flag means we require git 1.6.5+ now, see CHEF-1827
+ command = "git submodule update --init --recursive"
+ shell_out!(command, run_options(:cwd => @new_resource.destination, :log_level => :info))
+ end
+ end
+ end
+
+ def fetch_updates
+ setup_remote_tracking_branches if @new_resource.remote != 'origin'
+ converge_by("fetch updates for #{@new_resource.remote}") do
+ # since we're in a local branch already, just reset to specified revision rather than merge
+ fetch_command = "git fetch #{@new_resource.remote} && git fetch #{@new_resource.remote} --tags && git reset --hard #{target_revision}"
+ Chef::Log.debug "Fetching updates from #{new_resource.remote} and resetting to revision #{target_revision}"
+ shell_out!(fetch_command, run_options(:cwd => @new_resource.destination))
+ end
+ end
+
+ # Use git-config to setup a remote tracking branches. Could use
+ # git-remote but it complains when a remote of the same name already
+ # exists, git-config will just silenty overwrite the setting every
+ # time. This could cause wierd-ness in the remote cache if the url
+ # changes between calls, but as long as the repositories are all
+ # based from each other it should still work fine.
+ def setup_remote_tracking_branches
+ command = []
+ converge_by("set up remote tracking branches for #{@new_resource.repository} at #{@new_resource.remote}") do
+ Chef::Log.debug "#{@new_resource} configuring remote tracking branches for repository #{@new_resource.repository} "+
+ "at remote #{@new_resource.remote}"
+ command << "git config remote.#{@new_resource.remote}.url #{@new_resource.repository}"
+ command << "git config remote.#{@new_resource.remote}.fetch +refs/heads/*:refs/remotes/#{@new_resource.remote}/*"
+ shell_out!(command.join(" && "), run_options(:cwd => @new_resource.destination))
+ end
+ end
+
+ def current_revision_matches_target_revision?
+ (!@current_resource.revision.nil?) && (target_revision.strip.to_i(16) == @current_resource.revision.strip.to_i(16))
+ end
+
+ def target_revision
+ @target_revision ||= begin
+ if sha_hash?(@new_resource.revision)
+ @target_revision = @new_resource.revision
+ else
+ @target_revision = remote_resolve_reference
+ end
+ end
+ end
+
+ alias :revision_slug :target_revision
+
+ def remote_resolve_reference
+ Chef::Log.debug("#{@new_resource} resolving remote reference")
+ command = git('ls-remote', @new_resource.repository, @new_resource.revision)
+ @resolved_reference = shell_out!(command, run_options).stdout
+ if @resolved_reference =~ /^([0-9a-f]{40})\s+(\S+)/
+ $1
+ else
+ nil
+ end
+ end
+
+ private
+
+ def run_options(run_opts={})
+ run_opts[:user] = @new_resource.user if @new_resource.user
+ run_opts[:group] = @new_resource.group if @new_resource.group
+ run_opts[:environment] = {"GIT_SSH" => @new_resource.ssh_wrapper} if @new_resource.ssh_wrapper
+ run_opts[:log_tag] = @new_resource.to_s
+ run_opts[:log_level] ||= :debug
+ if run_opts[:log_level] == :info
+ if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.info?
+ run_opts[:live_stream] = STDOUT
+ end
+ end
+ run_opts
+ end
+
+ def cwd
+ @new_resource.destination
+ end
+
+ def git(*args)
+ ["git", *args].compact.join(" ")
+ end
+
+ def sha_hash?(string)
+ string =~ /^[0-9a-f]{40}$/
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/group.rb b/lib/chef/provider/group.rb
new file mode 100644
index 0000000000..81d7d7a400
--- /dev/null
+++ b/lib/chef/provider/group.rb
@@ -0,0 +1,159 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 OpsCode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider'
+require 'chef/mixin/command'
+require 'chef/resource/group'
+require 'etc'
+
+class Chef
+ class Provider
+ class Group < Chef::Provider
+ include Chef::Mixin::Command
+ attr_accessor :group_exists
+ attr_accessor :change_desc
+
+ def whyrun_supported?
+ true
+ end
+
+ def initialize(new_resource, run_context)
+ super
+ @group_exists = true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Group.new(@new_resource.name)
+ @current_resource.group_name(@new_resource.group_name)
+
+ group_info = nil
+ begin
+ group_info = Etc.getgrnam(@new_resource.group_name)
+ rescue ArgumentError => e
+ @group_exists = false
+ Chef::Log.debug("#{@new_resource} group does not exist")
+ end
+
+ if group_info
+ @new_resource.gid(group_info.gid) unless @new_resource.gid
+ @current_resource.gid(group_info.gid)
+ @current_resource.members(group_info.mem)
+ end
+
+ @current_resource
+ end
+
+ def define_resource_requirements
+ requirements.assert(:modify) do |a|
+ a.assertion { @group_exists }
+ a.failure_message(Chef::Exceptions::Group, "Cannot modify #{@new_resource} - group does not exist!")
+ a.whyrun("Group #{@new_resource} does not exist. Unless it would have been created earlier in this run, this attempt to modify it would fail.")
+ end
+ end
+
+ # Check to see if a group needs any changes. Populate
+ # @change_desc with a description of why a change must occur
+ #
+ # ==== Returns
+ # <true>:: If a change is required
+ # <false>:: If a change is not required
+ def compare_group
+ @change_desc = nil
+ if @new_resource.gid != @current_resource.gid
+ @change_desc = "change gid #{@current_resource.gid} to #{@new_resource.gid}"
+ return true
+ end
+
+ if(@new_resource.append)
+ missing_members = []
+ @new_resource.members.each do |member|
+ next if @current_resource.members.include?(member)
+ missing_members << member
+ end
+ if missing_members.length > 0
+ @change_desc = "add missing member(s): #{missing_members.join(", ")}"
+ return true
+ end
+ else
+ if @new_resource.members != @current_resource.members
+ @change_desc = "replace group members with new list of members"
+ return true
+ end
+ end
+ return false
+ end
+
+ def action_create
+ case @group_exists
+ when false
+ converge_by("create #{@new_resource}") do
+ create_group
+ Chef::Log.info("#{@new_resource} created")
+ end
+ else
+ if compare_group
+ converge_by(["alter group #{@new_resource}", @change_desc ]) do
+ manage_group
+ Chef::Log.info("#{@new_resource} altered")
+ end
+ end
+ end
+ end
+
+ def action_remove
+ if @group_exists
+ converge_by("remove group #{@new_resource}") do
+ remove_group
+ Chef::Log.info("#{@new_resource} removed")
+ end
+ end
+ end
+
+ def action_manage
+ if @group_exists && compare_group
+ converge_by(["manage group #{@new_resource}", @change_desc]) do
+ manage_group
+ Chef::Log.info("#{@new_resource} managed")
+ end
+ end
+ end
+
+ def action_modify
+ if compare_group
+ converge_by(["modify group #{@new_resource}", @change_desc]) do
+ manage_group
+ Chef::Log.info("#{@new_resource} modified")
+ end
+ end
+ end
+
+ def create_group
+ raise NotImplementedError, "subclasses of Chef::Provider::Group should define #create_group"
+ end
+
+ def manage_group
+ raise NotImplementedError, "subclasses of Chef::Provider::Group should define #manage_group"
+ end
+
+ def remove_group
+ raise NotImplementedError, "subclasses of Chef::Provider::Group should define #remove_group"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/group/aix.rb b/lib/chef/provider/group/aix.rb
new file mode 100644
index 0000000000..9dedef351a
--- /dev/null
+++ b/lib/chef/provider/group/aix.rb
@@ -0,0 +1,70 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/group/usermod'
+
+class Chef
+ class Provider
+ class Group
+ class Aix < Chef::Provider::Group::Usermod
+
+ def required_binaries
+ [ "/usr/bin/mkgroup",
+ "/usr/bin/chgroup",
+ "/usr/sbin/rmgroup" ]
+ end
+
+ def create_group
+ command = "mkgroup"
+ command << set_options << " #{@new_resource.group_name}"
+ run_command(:command => command)
+ modify_group_members
+ end
+
+ def manage_group
+ command = "chgroup"
+ options = set_options
+ #Usage: chgroup [-R load_module] "attr=value" ... group
+ if options.size > 0
+ command << options << " #{@new_resource.group_name}"
+ run_command(:command => command)
+ end
+ modify_group_members
+ end
+
+ def remove_group
+ run_command(:command => "rmgroup #{@new_resource.group_name}")
+ end
+
+ def set_options
+ opts = ""
+ { :gid => "id" }.sort { |a,b| a[0] <=> b[0] }.each do |field, option|
+ if @current_resource.send(field) != @new_resource.send(field)
+ if @new_resource.send(field)
+ Chef::Log.debug("#{@new_resource} setting #{field.to_s} to #{@new_resource.send(field)}")
+ opts << " '#{option}=#{@new_resource.send(field)}'"
+ end
+ end
+ end
+ opts
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/group/dscl.rb b/lib/chef/provider/group/dscl.rb
new file mode 100644
index 0000000000..a8ba32641c
--- /dev/null
+++ b/lib/chef/provider/group/dscl.rb
@@ -0,0 +1,129 @@
+#
+# Author:: Dreamcat4 (<dreamcat4@gmail.com>)
+# Copyright:: Copyright (c) 2009 OpsCode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Provider
+ class Group
+ class Dscl < Chef::Provider::Group
+
+ def dscl(*args)
+ host = "."
+ stdout_result = ""; stderr_result = ""; cmd = "dscl #{host} -#{args.join(' ')}"
+ status = popen4(cmd) do |pid, stdin, stdout, stderr|
+ stdout.each { |line| stdout_result << line }
+ stderr.each { |line| stderr_result << line }
+ end
+ return [cmd, status, stdout_result, stderr_result]
+ end
+
+ def safe_dscl(*args)
+ result = dscl(*args)
+ return "" if ( args.first =~ /^delete/ ) && ( result[1].exitstatus != 0 )
+ raise(Chef::Exceptions::Group,"dscl error: #{result.inspect}") unless result[1].exitstatus == 0
+ raise(Chef::Exceptions::Group,"dscl error: #{result.inspect}") if result[2] =~ /No such key: /
+ return result[2]
+ end
+
+ # This is handled in providers/group.rb by Etc.getgrnam()
+ # def group_exists?(group)
+ # groups = safe_dscl("list /Groups")
+ # !! ( groups =~ Regexp.new("\n#{group}\n") )
+ # end
+
+ # get a free GID greater than 200
+ def get_free_gid(search_limit=1000)
+ gid = nil; next_gid_guess = 200
+ groups_gids = safe_dscl("list /Groups gid")
+ while(next_gid_guess < search_limit + 200)
+ if groups_gids =~ Regexp.new("#{Regexp.escape(next_gid_guess.to_s)}\n")
+ next_gid_guess += 1
+ else
+ gid = next_gid_guess
+ break
+ end
+ end
+ return gid || raise("gid not found. Exhausted. Searched #{search_limit} times")
+ end
+
+ def gid_used?(gid)
+ return false unless gid
+ groups_gids = safe_dscl("list /Groups gid")
+ !! ( groups_gids =~ Regexp.new("#{Regexp.escape(gid.to_s)}\n") )
+ end
+
+ def set_gid
+ @new_resource.gid(get_free_gid) if [nil,""].include? @new_resource.gid
+ raise(Chef::Exceptions::Group,"gid is already in use") if gid_used?(@new_resource.gid)
+ safe_dscl("create /Groups/#{@new_resource.group_name} PrimaryGroupID #{@new_resource.gid}")
+ end
+
+ def set_members
+ unless @new_resource.append
+ Chef::Log.debug("#{@new_resource} removing group members #{@current_resource.members.join(' ')}") unless @current_resource.members.empty?
+ safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembers ''") # clear guid list
+ safe_dscl("create /Groups/#{@new_resource.group_name} GroupMembership ''") # clear user list
+ end
+ unless @new_resource.members.empty?
+ Chef::Log.debug("#{@new_resource} setting group members #{@new_resource.members.join(', ')}")
+ safe_dscl("append /Groups/#{@new_resource.group_name} GroupMembership #{@new_resource.members.join(' ')}")
+ end
+ end
+
+ def define_resource_requirements
+ super
+ requirements.assert(:all_actions) do |a|
+ a.assertion { ::File.exists?("/usr/bin/dscl") }
+ a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/bin/dscl for #{@new_resource.name}"
+ # No whyrun alternative: this component should be available in the base install of any given system that uses it
+ end
+ end
+
+ def load_current_resource
+ super
+ end
+
+ def create_group
+ dscl_create_group
+ set_gid
+ set_members
+ end
+
+ def manage_group
+ if @new_resource.group_name && (@current_resource.group_name != @new_resource.group_name)
+ dscl_create_group
+ end
+ if @new_resource.gid && (@current_resource.gid != @new_resource.gid)
+ set_gid
+ end
+ if @new_resource.members && (@current_resource.members != @new_resource.members)
+ set_members
+ end
+ end
+
+ def dscl_create_group
+ safe_dscl("create /Groups/#{@new_resource.group_name}")
+ safe_dscl("create /Groups/#{@new_resource.group_name} Password '*'")
+ end
+
+ def remove_group
+ safe_dscl("delete /Groups/#{@new_resource.group_name}")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/group/gpasswd.rb b/lib/chef/provider/group/gpasswd.rb
new file mode 100644
index 0000000000..7fb27a7777
--- /dev/null
+++ b/lib/chef/provider/group/gpasswd.rb
@@ -0,0 +1,65 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 OpsCode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/group/groupadd'
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Provider
+ class Group
+ class Gpasswd < Chef::Provider::Group::Groupadd
+
+ include Chef::Mixin::ShellOut
+
+ def load_current_resource
+ super
+ end
+
+ def define_resource_requirements
+ super
+ requirements.assert(:all_actions) do |a|
+ a.assertion { ::File.exists?("/usr/bin/gpasswd") }
+ a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/bin/gpasswd for #{@new_resource}"
+ # No whyrun alternative: this component should be available in the base install of any given system that uses it
+ end
+ end
+
+ def modify_group_members
+ if(@new_resource.append)
+ unless @new_resource.members.empty?
+ @new_resource.members.each do |member|
+ Chef::Log.debug("#{@new_resource} appending member #{member} to group #{@new_resource.group_name}")
+ shell_out!("gpasswd -a #{member} #{@new_resource.group_name}")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} not changing group members, the group has no members to add")
+ end
+ else
+ unless @new_resource.members.empty?
+ Chef::Log.debug("#{@new_resource} setting group members to #{@new_resource.members.join(', ')}")
+ shell_out!("gpasswd -M #{@new_resource.members.join(',')} #{@new_resource.group_name}")
+ else
+ Chef::Log.debug("#{@new_resource} setting group members to: none")
+ shell_out!("gpasswd -M \"\" #{@new_resource.group_name}")
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/group/groupadd.rb b/lib/chef/provider/group/groupadd.rb
new file mode 100644
index 0000000000..544fee4304
--- /dev/null
+++ b/lib/chef/provider/group/groupadd.rb
@@ -0,0 +1,96 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 OpsCode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Provider
+ class Group
+ class Groupadd < Chef::Provider::Group
+
+ def required_binaries
+ [ "/usr/sbin/groupadd",
+ "/usr/sbin/groupmod",
+ "/usr/sbin/groupdel" ]
+ end
+
+ def load_current_resource
+ super
+ end
+
+ def define_resource_requirements
+ super
+ required_binaries.each do |required_binary|
+ requirements.assert(:all_actions) do |a|
+ a.assertion { ::File.exists?(required_binary) }
+ a.failure_message Chef::Exceptions::Group, "Could not find binary #{required_binary} for #{@new_resource}"
+ # No whyrun alternative: this component should be available in the base install of any given system that uses it
+ end
+ end
+ end
+
+ # Create the group
+ def create_group
+ command = "groupadd"
+ command << set_options
+ command << groupadd_options
+ run_command(:command => command)
+ modify_group_members
+ end
+
+ # Manage the group when it already exists
+ def manage_group
+ command = "groupmod"
+ command << set_options
+ run_command(:command => command)
+ modify_group_members
+ end
+
+ # Remove the group
+ def remove_group
+ run_command(:command => "groupdel #{@new_resource.group_name}")
+ end
+
+ def modify_group_members
+ raise Chef::Exceptions::Group, "you must override modify_group_members in #{self.to_s}"
+ end
+ # Little bit of magic as per Adam's useradd provider to pull the assign the command line flags
+ #
+ # ==== Returns
+ # <string>:: A string containing the option and then the quoted value
+ def set_options
+ opts = ""
+ { :gid => "-g" }.sort { |a,b| a[0] <=> b[0] }.each do |field, option|
+ if @current_resource.send(field) != @new_resource.send(field)
+ if @new_resource.send(field)
+ opts << " #{option} '#{@new_resource.send(field)}'"
+ Chef::Log.debug("#{@new_resource} set #{field.to_s} to #{@new_resource.send(field)}")
+ end
+ end
+ end
+ opts << " #{@new_resource.group_name}"
+ end
+
+ def groupadd_options
+ opts = ''
+ opts << " -r" if @new_resource.system
+ opts
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/group/groupmod.rb b/lib/chef/provider/group/groupmod.rb
new file mode 100644
index 0000000000..10fc680d78
--- /dev/null
+++ b/lib/chef/provider/group/groupmod.rb
@@ -0,0 +1,120 @@
+#
+# Author:: Dan Crosta (<dcrosta@late.am>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Provider
+ class Group
+ class Groupmod < Chef::Provider::Group
+
+ include Chef::Mixin::ShellOut
+
+ def load_current_resource
+ super
+ [ "group", "user" ].each do |binary|
+ raise Chef::Exceptions::Group, "Could not find binary /usr/sbin/#{binary} for #{@new_resource}" unless ::File.exists?("/usr/sbin/#{binary}")
+ end
+ end
+
+ # Create the group
+ def create_group
+ command = "group add"
+ command << set_options
+ shell_out!(command)
+
+ add_group_members(@new_resource.members)
+ end
+
+ # Manage the group when it already exists
+ def manage_group
+ if @new_resource.append
+ to_add = @new_resource.members.dup
+ to_add.reject! { |user| @current_resource.members.include?(user) }
+
+ to_delete = Array.new
+
+ Chef::Log.debug("#{@new_resource} not changing group members, the group has no members to add") if to_add.empty?
+ else
+ to_add = @new_resource.members.dup
+ to_add.reject! { |user| @current_resource.members.include?(user) }
+
+ to_delete = @current_resource.members.dup
+ to_delete.reject! { |user| @new_resource.members.include?(user) }
+
+ Chef::Log.debug("#{@new_resource} setting group members to: none") if @new_resource.members.empty?
+ end
+
+ if to_delete.empty?
+ # If we are only adding new members to this group, then
+ # call add_group_members with only those users
+ add_group_members(to_add)
+ else
+ Chef::Log.debug("#{@new_resource} removing members #{to_delete.join(', ')}")
+
+ # This is tricky, but works: rename the existing group to
+ # "<name>_bak", create a new group with the same GID and
+ # "<name>", then set correct members on that group
+ rename = "group mod -n #{@new_resource.group_name}_bak #{@new_resource.group_name}"
+ shell_out!(rename)
+
+ create = "group add"
+ create << set_options(:overwrite_gid => true)
+ shell_out!(create)
+
+ # Ignore to_add here, since we're replacing the group we
+ # have to add all members who should be in the group.
+ add_group_members(@new_resource.members)
+
+ remove = "group del #{@new_resource.group_name}_bak"
+ shell_out!(remove)
+ end
+ end
+
+ # Remove the group
+ def remove_group
+ shell_out!("group del #{@new_resource.group_name}")
+ end
+
+ # Adds a list of usernames to the group using `user mod`
+ def add_group_members(members)
+ Chef::Log.debug("#{@new_resource} adding members #{members.join(', ')}") if !members.empty?
+ members.each do |user|
+ shell_out!("user mod -G #{@new_resource.group_name} #{user}")
+ end
+ end
+
+ # Little bit of magic as per Adam's useradd provider to pull and assign the command line flags
+ #
+ # ==== Returns
+ # <string>:: A string containing the option and then the quoted value
+ def set_options(overwrite_gid=false)
+ opts = ""
+ if overwrite_gid || @new_resource.gid && (@current_resource.gid != @new_resource.gid)
+ opts << " -g '#{@new_resource.gid}'"
+ end
+ if overwrite_gid
+ opts << " -o"
+ end
+ opts << " #{@new_resource.group_name}"
+ opts
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/group/pw.rb b/lib/chef/provider/group/pw.rb
new file mode 100644
index 0000000000..3bf67a515a
--- /dev/null
+++ b/lib/chef/provider/group/pw.rb
@@ -0,0 +1,93 @@
+#
+# Author:: Stephen Haynes (<sh@nomitor.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Provider
+ class Group
+ class Pw < Chef::Provider::Group
+
+ def load_current_resource
+ super
+ end
+
+ def define_resource_requirements
+ super
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { ::File.exists?("/usr/sbin/pw") }
+ a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/pw for #{@new_resource}"
+ # No whyrun alternative: this component should be available in the base install of any given system that uses it
+ end
+ end
+
+ # Create the group
+ def create_group
+ command = "pw groupadd"
+ command << set_options
+ command << set_members_option
+ run_command(:command => command)
+ end
+
+ # Manage the group when it already exists
+ def manage_group
+ command = "pw groupmod"
+ command << set_options
+ command << set_members_option
+ run_command(:command => command)
+ end
+
+ # Remove the group
+ def remove_group
+ run_command(:command => "pw groupdel #{@new_resource.group_name}")
+ end
+
+ # Little bit of magic as per Adam's useradd provider to pull and assign the command line flags
+ #
+ # ==== Returns
+ # <string>:: A string containing the option and then the quoted value
+ def set_options
+ opts = " #{@new_resource.group_name}"
+ if @new_resource.gid && (@current_resource.gid != @new_resource.gid)
+ Chef::Log.debug("#{@new_resource}: current gid (#{@current_resource.gid}) doesnt match target gid (#{@new_resource.gid}), changing it")
+ opts << " -g '#{@new_resource.gid}'"
+ end
+ opts
+ end
+
+ # Set the membership option depending on the current resource states
+ def set_members_option
+ opt = ""
+ unless @new_resource.members.empty?
+ opt << " -M #{@new_resource.members.join(',')}"
+ Chef::Log.debug("#{@new_resource} setting group members to #{@new_resource.members.join(', ')}")
+ else
+ # New member list is empty so we should delete any old group members
+ unless @current_resource.members.empty?
+ opt << " -d #{@current_resource.members.join(',')}"
+ Chef::Log.debug("#{@new_resource} removing group members #{@current_resource.members.join(', ')}")
+ else
+ Chef::Log.debug("#{@new_resource} not changing group members, the group has no members")
+ end
+ end
+ opt
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/group/suse.rb b/lib/chef/provider/group/suse.rb
new file mode 100644
index 0000000000..0b66c1f912
--- /dev/null
+++ b/lib/chef/provider/group/suse.rb
@@ -0,0 +1,60 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 OpsCode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/group/groupadd'
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Provider
+ class Group
+ class Suse < Chef::Provider::Group::Groupadd
+
+ include Chef::Mixin::ShellOut
+
+ def load_current_resource
+ super
+ end
+
+ def define_resource_requirements
+ super
+ requirements.assert(:all_actions) do |a|
+ a.assertion { ::File.exists?("/usr/sbin/groupmod") }
+ a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/groupmod for #{@new_resource.name}"
+ # No whyrun alternative: this component should be available in the base install of any given system that uses it
+ end
+ end
+
+ def modify_group_members
+ unless @new_resource.members.empty?
+ if(@new_resource.append)
+ @new_resource.members.each do |member|
+ Chef::Log.debug("#{@new_resource} appending member #{member} to group #{@new_resource.group_name}")
+ shell_out!("groupmod -A #{member} #{@new_resource.group_name}")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} setting group members to #{@new_resource.members.join(', ')}")
+ shell_out!("groupmod -A #{@new_resource.members.join(',')} #{@new_resource.group_name}")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} not changing group members, the group has no members")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/group/usermod.rb b/lib/chef/provider/group/usermod.rb
new file mode 100644
index 0000000000..f0a9282831
--- /dev/null
+++ b/lib/chef/provider/group/usermod.rb
@@ -0,0 +1,68 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 OpsCode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/group/groupadd'
+
+class Chef
+ class Provider
+ class Group
+ class Usermod < Chef::Provider::Group::Groupadd
+
+ def load_current_resource
+ super
+ end
+
+ def define_resource_requirements
+ super
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { ::File.exists?("/usr/sbin/usermod") }
+ a.failure_message Chef::Exceptions::Group, "Could not find binary /usr/sbin/usermod for #{@new_resource}"
+ # No whyrun alternative: this component should be available in the base install of any given system that uses it
+ end
+
+ requirements.assert(:modify, :create) do |a|
+ a.assertion { @new_resource.members.empty? || @new_resource.append }
+ a.failure_message Chef::Exceptions::Group, "setting group members directly is not supported by #{self.to_s}, must set append true in group"
+ # No whyrun alternative - this action is simply not supported.
+ end
+ end
+
+ def modify_group_members
+ case node[:platform]
+ when "openbsd", "netbsd", "aix", "solaris2"
+ append_flags = "-G"
+ when "solaris"
+ append_flags = "-a -G"
+ end
+
+ unless @new_resource.members.empty?
+ if(@new_resource.append)
+ @new_resource.members.each do |member|
+ Chef::Log.debug("#{@new_resource} appending member #{member} to group #{@new_resource.group_name}")
+ run_command(:command => "usermod #{append_flags} #{@new_resource.group_name} #{member}" )
+ end
+ end
+ else
+ Chef::Log.debug("#{@new_resource} not changing group members, the group has no members")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/group/windows.rb b/lib/chef/provider/group/windows.rb
new file mode 100644
index 0000000000..88280408cd
--- /dev/null
+++ b/lib/chef/provider/group/windows.rb
@@ -0,0 +1,79 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/user'
+if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ require 'chef/util/windows/net_group'
+end
+
+class Chef
+ class Provider
+ class Group
+ class Windows < Chef::Provider::Group
+
+ def initialize(new_resource,run_context)
+ super
+ @net_group = Chef::Util::Windows::NetGroup.new(@new_resource.name)
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Group.new(@new_resource.name)
+ @current_resource.group_name(@new_resource.group_name)
+
+ members = nil
+ begin
+ members = @net_group.local_get_members
+ rescue => e
+ @group_exists = false
+ Chef::Log.debug("#{@new_resource} group does not exist")
+ end
+
+ if members
+ @current_resource.members(members)
+ end
+
+ @current_resource
+ end
+
+ def create_group
+ @net_group.local_add
+ manage_group
+ end
+
+ def manage_group
+ if @new_resource.append
+ begin
+ #ERROR_MEMBER_IN_ALIAS if a member already exists in the group
+ @net_group.local_add_members(@new_resource.members)
+ rescue
+ members = @new_resource.members + @current_resource.members
+ @net_group.local_set_members(members.uniq)
+ end
+ else
+ @net_group.local_set_members(@new_resource.members)
+ end
+ end
+
+ def remove_group
+ @net_group.local_delete
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/http_request.rb b/lib/chef/provider/http_request.rb
new file mode 100644
index 0000000000..0ea5f8289f
--- /dev/null
+++ b/lib/chef/provider/http_request.rb
@@ -0,0 +1,136 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 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 'tempfile'
+
+class Chef
+ class Provider
+ class HttpRequest < Chef::Provider
+
+ attr_accessor :rest
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @rest = Chef::REST.new(@new_resource.url, nil, nil)
+ end
+
+ # Send a HEAD request to @new_resource.url, with ?message=@new_resource.message
+ def action_head
+ message = check_message(@new_resource.message)
+ # returns true from Chef::REST if returns 2XX (Net::HTTPSuccess)
+ modified = @rest.run_request(
+ :HEAD,
+ @rest.create_url("#{@new_resource.url}?message=#{message}"),
+ @new_resource.headers,
+ false,
+ 10,
+ false
+ )
+ Chef::Log.info("#{@new_resource} HEAD to #{@new_resource.url} successful")
+ Chef::Log.debug("#{@new_resource} HEAD request response: #{modified}")
+ # :head is usually used to trigger notifications, which converge_by now does
+ if modified
+ converge_by("#{@new_resource} HEAD to #{@new_resource.url} returned modified, trigger notifications") {}
+ end
+ end
+
+ # Send a GET request to @new_resource.url, with ?message=@new_resource.message
+ def action_get
+ converge_by("#{@new_resource} GET to #{@new_resource.url}") do
+
+ message = check_message(@new_resource.message)
+ body = @rest.run_request(
+ :GET,
+ @rest.create_url("#{@new_resource.url}?message=#{message}"),
+ @new_resource.headers,
+ false,
+ 10,
+ false
+ )
+ Chef::Log.info("#{@new_resource} GET to #{@new_resource.url} successful")
+ Chef::Log.debug("#{@new_resource} GET request response: #{body}")
+ end
+ end
+
+ # Send a PUT request to @new_resource.url, with the message as the payload
+ def action_put
+ converge_by("#{@new_resource} PUT to #{@new_resource.url}") do
+ message = check_message(@new_resource.message)
+ body = @rest.run_request(
+ :PUT,
+ @rest.create_url("#{@new_resource.url}"),
+ @new_resource.headers,
+ message,
+ 10,
+ false
+ )
+ Chef::Log.info("#{@new_resource} PUT to #{@new_resource.url} successful")
+ Chef::Log.debug("#{@new_resource} PUT request response: #{body}")
+ end
+ end
+
+ # Send a POST request to @new_resource.url, with the message as the payload
+ def action_post
+ converge_by("#{@new_resource} POST to #{@new_resource.url}") do
+ message = check_message(@new_resource.message)
+ body = @rest.run_request(
+ :POST,
+ @rest.create_url("#{@new_resource.url}"),
+ @new_resource.headers,
+ message,
+ 10,
+ false
+ )
+ Chef::Log.info("#{@new_resource} POST to #{@new_resource.url} message: #{message.inspect} successful")
+ Chef::Log.debug("#{@new_resource} POST request response: #{body}")
+ end
+ end
+
+ # Send a DELETE request to @new_resource.url
+ def action_delete
+ converge_by("#{@new_resource} DELETE to #{@new_resource.url}") do
+ body = @rest.run_request(
+ :DELETE,
+ @rest.create_url("#{@new_resource.url}"),
+ @new_resource.headers,
+ false,
+ 10,
+ false
+ )
+ @new_resource.updated_by_last_action(true)
+ Chef::Log.info("#{@new_resource} DELETE to #{@new_resource.url} successful")
+ Chef::Log.debug("#{@new_resource} DELETE request response: #{body}")
+ end
+ end
+
+ private
+
+ def check_message(message)
+ if message.kind_of?(Proc)
+ message.call
+ else
+ message
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/ifconfig.rb b/lib/chef/provider/ifconfig.rb
new file mode 100644
index 0000000000..86680b2229
--- /dev/null
+++ b/lib/chef/provider/ifconfig.rb
@@ -0,0 +1,214 @@
+#
+# Author:: Jason K. Jackson (jasonjackson@gmail.com)
+# Copyright:: Copyright (c) 2009 Jason K. Jackson
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/log'
+require 'chef/mixin/command'
+require 'chef/provider'
+require 'chef/exceptions'
+require 'erb'
+
+# Recipe example:
+#
+# int = {Hash with your network settings...}
+#
+# ifconfig int['ip'] do
+# ignore_failure true
+# device int['dev']
+# mask int['mask']
+# gateway int['gateway']
+# mtu int['mtu']
+# end
+
+class Chef
+ class Provider
+ class Ifconfig < Chef::Provider
+ include Chef::Mixin::Command
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Ifconfig.new(@new_resource.name)
+
+ @ifconfig_success = true
+ @interfaces = {}
+
+ @status = popen4("ifconfig") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+
+ if !line[0..9].strip.empty?
+ @int_name = line[0..9].strip
+ @interfaces[@int_name] = {"hwaddr" => (line =~ /(HWaddr)/ ? ($') : "nil").strip.chomp }
+ else
+ @interfaces[@int_name]["inet_addr"] = (line =~ /inet addr:(\S+)/ ? ($1) : "nil") if line =~ /inet addr:/
+ @interfaces[@int_name]["bcast"] = (line =~ /Bcast:(\S+)/ ? ($1) : "nil") if line =~ /Bcast:/
+ @interfaces[@int_name]["mask"] = (line =~ /Mask:(\S+)/ ? ($1) : "nil") if line =~ /Mask:/
+ @interfaces[@int_name]["mtu"] = (line =~ /MTU:(\S+)/ ? ($1) : "nil") if line =~ /MTU:/
+ @interfaces[@int_name]["metric"] = (line =~ /Metric:(\S+)/ ? ($1) : "nil") if line =~ /Metric:/
+ end
+
+ if @interfaces.has_key?(@new_resource.device)
+ @interface = @interfaces.fetch(@new_resource.device)
+
+ @current_resource.target(@new_resource.target)
+ @current_resource.device(@int_name)
+ @current_resource.inet_addr(@interface["inet_addr"])
+ @current_resource.hwaddr(@interface["hwaddr"])
+ @current_resource.bcast(@interface["bcast"])
+ @current_resource.mask(@interface["mask"])
+ @current_resource.mtu(@interface["mtu"])
+ @current_resource.metric(@interface["metric"])
+ end
+ end
+ end
+ @current_resource
+ end
+
+ def define_resource_requirements
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @status.exitstatus == 0 }
+ a.failure_message Chef::Exceptions::Ifconfig, "ifconfig failed - #{@status.inspect}!"
+ # no whyrun - if the base ifconfig used in load_current_resource fails
+ # there's no reasonable action that could have been taken in the course of
+ # a chef run to fix it.
+ end
+ end
+
+ def action_add
+ # check to see if load_current_resource found interface in ifconfig
+ unless @current_resource.inet_addr
+ unless @new_resource.device == "lo"
+ command = "ifconfig #{@new_resource.device} #{@new_resource.name}"
+ command << " netmask #{@new_resource.mask}" if @new_resource.mask
+ command << " metric #{@new_resource.metric}" if @new_resource.metric
+ command << " mtu #{@new_resource.mtu}" if @new_resource.mtu
+ end
+ converge_by ("run #{command} to add #{@new_resource}") do
+ run_command(
+ :command => command
+ )
+ Chef::Log.info("#{@new_resource} added")
+ end
+ end
+
+ # Write out the config files
+ generate_config
+ end
+
+ def action_enable
+ # check to see if load_current_resource found ifconfig
+ # enables, but does not manage config files
+ unless @current_resource.inet_addr
+ unless @new_resource.device == "lo"
+ command = "ifconfig #{@new_resource.device} #{@new_resource.name}"
+ command << " netmask #{@new_resource.mask}" if @new_resource.mask
+ command << " metric #{@new_resource.metric}" if @new_resource.metric
+ command << " mtu #{@new_resource.mtu}" if @new_resource.mtu
+ end
+
+ converge_by ("run #{command} to enable #{@new_resource}") do
+ run_command(
+ :command => command
+ )
+ Chef::Log.info("#{@new_resource} enabled")
+ end
+ end
+ end
+
+ def action_delete
+ # check to see if load_current_resource found the interface
+ if @current_resource.device
+ command = "ifconfig #{@new_resource.device} down"
+ converge_by ("run #{command} to delete #{@new_resource}") do
+ run_command(
+ :command => command
+ )
+ delete_config
+ Chef::Log.info("#{@new_resource} deleted")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} does not exist - nothing to do")
+ end
+ end
+
+ def action_disable
+ # check to see if load_current_resource found the interface
+ # disables, but leaves config files in place.
+ if @current_resource.device
+ command = "ifconfig #{@new_resource.device} down"
+ converge_by ("run #{command} to disable #{@new_resource}") do
+ run_command(
+ :command => command
+ )
+ Chef::Log.info("#{@new_resource} disabled")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} does not exist - nothing to do")
+ end
+ end
+
+ def generate_config
+ b = binding
+ case node[:platform]
+ when "centos","redhat","fedora"
+ content = %{
+<% if @new_resource.device %>DEVICE=<%= @new_resource.device %><% end %>
+<% if @new_resource.onboot %>ONBOOT=<%= @new_resource.onboot %><% end %>
+<% if @new_resource.bootproto %>BOOTPROTO=<%= @new_resource.bootproto %><% end %>
+<% if @new_resource.target %>IPADDR=<%= @new_resource.target %><% end %>
+<% if @new_resource.mask %>NETMASK=<%= @new_resource.mask %><% end %>
+<% if @new_resource.network %>NETWORK=<%= @new_resource.network %><% end %>
+<% if @new_resource.bcast %>BROADCAST=<%= @new_resource.bcast %><% end %>
+<% if @new_resource.onparent %>ONPARENT=<%= @new_resource.onparent %><% end %>
+ }
+ template = ::ERB.new(content)
+ network_file_name = "/etc/sysconfig/network-scripts/ifcfg-#{@new_resource.device}"
+ converge_by ("generate configuration file : #{network_file_name}") do
+ network_file = ::File.new(network_file_name, "w")
+ network_file.puts(template.result(b))
+ network_file.close
+ end
+ Chef::Log.info("#{@new_resource} created configuration file")
+ when "debian","ubuntu"
+ # template
+ when "slackware"
+ # template
+ end
+ end
+
+ def delete_config
+ require 'fileutils'
+ case node[:platform]
+ when "centos","redhat","fedora"
+ ifcfg_file = "/etc/sysconfig/network-scripts/ifcfg-#{@new_resource.device}"
+ if ::File.exist?(ifcfg_file)
+ converge_by ("delete the #{ifcfg_file}") do
+ FileUtils.rm_f(ifcfg_file, :verbose => false)
+ end
+ end
+ when "debian","ubuntu"
+ # delete configs
+ when "slackware"
+ # delete configs
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/link.rb b/lib/chef/provider/link.rb
new file mode 100644
index 0000000000..d6f333bd2f
--- /dev/null
+++ b/lib/chef/provider/link.rb
@@ -0,0 +1,130 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'chef/log'
+require 'chef/mixin/shell_out'
+require 'chef/mixin/file_class'
+require 'chef/resource/link'
+require 'chef/provider'
+require 'chef/scan_access_control'
+
+class Chef
+ class Provider
+ class Link < Chef::Provider
+ include Chef::Mixin::ShellOut
+ include Chef::Mixin::FileClass
+
+ def negative_complement(big)
+ if big > 1073741823 # Fixnum max
+ big -= (2**32) # diminished radix wrap to negative
+ end
+ big
+ end
+
+ private :negative_complement
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Link.new(@new_resource.name)
+ @current_resource.target_file(@new_resource.target_file)
+ if file_class.symlink?(@current_resource.target_file)
+ @current_resource.link_type(:symbolic)
+ @current_resource.to(
+ canonicalize(file_class.readlink(@current_resource.target_file))
+ )
+ else
+ @current_resource.link_type(:hard)
+ if ::File.exists?(@current_resource.target_file)
+ if ::File.exists?(@new_resource.to) &&
+ file_class.stat(@current_resource.target_file).ino ==
+ file_class.stat(@new_resource.to).ino
+ @current_resource.to(canonicalize(@new_resource.to))
+ else
+ @current_resource.to("")
+ end
+ end
+ end
+ ScanAccessControl.new(@new_resource, @current_resource).set_all!
+ @current_resource
+ end
+
+ def define_resource_requirements
+ requirements.assert(:delete) do |a|
+ a.assertion do
+ if @current_resource.to
+ @current_resource.link_type == @new_resource.link_type and
+ (@current_resource.link_type == :symbolic or @current_resource.to != '')
+ else
+ true
+ end
+ end
+ a.failure_message Chef::Exceptions::Link, "Cannot delete #{@new_resource} at #{@new_resource.target_file}! Not a #{@new_resource.link_type.to_s} link."
+ a.whyrun("Would assume the link at #{@new_resource.target_file} was previously created")
+ end
+ end
+
+ def canonicalize(path)
+ Chef::Platform.windows? ? path.gsub('/', '\\') : path
+ end
+
+ def action_create
+ if @current_resource.to != canonicalize(@new_resource.to) ||
+ @current_resource.link_type != @new_resource.link_type
+ if @current_resource.to # nil if target_file does not exist
+ converge_by("unlink existing file at #{@new_resource.target_file}") do
+ ::File.unlink(@new_resource.target_file)
+ end
+ end
+ if @new_resource.link_type == :symbolic
+ converge_by("create symlink at #{@new_resource.target_file} to #{@new_resource.to}") do
+ file_class.symlink(canonicalize(@new_resource.to),@new_resource.target_file)
+ Chef::Log.debug("#{@new_resource} created #{@new_resource.link_type} link from #{@new_resource.to} -> #{@new_resource.target_file}")
+ Chef::Log.info("#{@new_resource} created")
+ end
+ elsif @new_resource.link_type == :hard
+ converge_by("create hard link at #{@new_resource.target_file} to #{@new_resource.to}") do
+ file_class.link(@new_resource.to, @new_resource.target_file)
+ Chef::Log.debug("#{@new_resource} created #{@new_resource.link_type} link from #{@new_resource.to} -> #{@new_resource.target_file}")
+ Chef::Log.info("#{@new_resource} created")
+ end
+ end
+ end
+ if @new_resource.link_type == :symbolic
+ if access_controls.requires_changes?
+ converge_by(access_controls.describe_changes) do
+ access_controls.set_all
+ end
+ end
+ end
+ end
+
+ def action_delete
+ if @current_resource.to # Exists
+ converge_by ("delete link at #{@new_resource.target_file}") do
+ ::File.delete(@new_resource.target_file)
+ Chef::Log.info("#{@new_resource} deleted")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/log.rb b/lib/chef/provider/log.rb
new file mode 100644
index 0000000000..5d0417ebda
--- /dev/null
+++ b/lib/chef/provider/log.rb
@@ -0,0 +1,54 @@
+#
+# Author:: Cary Penniman (<cary@rightscale.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+
+ class Provider
+
+ class Log
+
+ # Chef log provider, allows logging to chef's logs from recipes
+ class ChefLog < Chef::Provider
+
+ # No concept of a 'current' resource for logs, this is a no-op
+ #
+ # === Return
+ # true:: Always return true
+ def load_current_resource
+ true
+ end
+
+ # Write the log to Chef's log
+ #
+ # === Return
+ # true:: Always return true
+ def action_write
+ Chef::Log.send(@new_resource.level, @new_resource.name)
+ @new_resource.updated_by_last_action(true)
+ end
+
+ end
+
+ end
+
+ end
+
+end
+
+
+
diff --git a/lib/chef/provider/mdadm.rb b/lib/chef/provider/mdadm.rb
new file mode 100644
index 0000000000..d93ff69c13
--- /dev/null
+++ b/lib/chef/provider/mdadm.rb
@@ -0,0 +1,92 @@
+#
+# Author:: Joe Williams (<joe@joetify.com>)
+# Copyright:: Copyright (c) 2009 Joe Williams
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/log'
+require 'chef/mixin/shell_out'
+require 'chef/provider'
+
+class Chef
+ class Provider
+ class Mdadm < Chef::Provider
+
+ include Chef::Mixin::ShellOut
+
+ def popen4
+ raise Exception, "deprecated"
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Mdadm.new(@new_resource.name)
+ @current_resource.raid_device(@new_resource.raid_device)
+ Chef::Log.debug("#{@new_resource} checking for software raid device #{@current_resource.raid_device}")
+
+ device_not_found = 4
+ mdadm = shell_out!("mdadm --detail --test #{@new_resource.raid_device}", :returns => [0,device_not_found])
+ exists = (mdadm.status == 0)
+ @current_resource.exists(exists)
+ end
+
+ def action_create
+ unless @current_resource.exists
+ converge_by("create RAID device #{new_resource.raid_device}") do
+ command = "yes | mdadm --create #{@new_resource.raid_device} --chunk=#{@new_resource.chunk} --level #{@new_resource.level}"
+ command << " --metadata=#{@new_resource.metadata}"
+ command << " --bitmap=#{@new_resource.bitmap}" if @new_resource.bitmap
+ command << " --raid-devices #{@new_resource.devices.length} #{@new_resource.devices.join(" ")}"
+ Chef::Log.debug("#{@new_resource} mdadm command: #{command}")
+ shell_out!(command)
+ Chef::Log.info("#{@new_resource} created raid device (#{@new_resource.raid_device})")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} raid device already exists, skipping create (#{@new_resource.raid_device})")
+ end
+ end
+
+ def action_assemble
+ unless @current_resource.exists
+ converge_by("assemble RAID device #{new_resource.raid_device}") do
+ command = "yes | mdadm --assemble #{@new_resource.raid_device} #{@new_resource.devices.join(" ")}"
+ Chef::Log.debug("#{@new_resource} mdadm command: #{command}")
+ shell_out!(command)
+ Chef::Log.info("#{@new_resource} assembled raid device (#{@new_resource.raid_device})")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} raid device already exists, skipping assemble (#{@new_resource.raid_device})")
+ end
+ end
+
+ def action_stop
+ if @current_resource.exists
+ converge_by("stop RAID device #{new_resource.raid_device}") do
+ command = "yes | mdadm --stop #{@new_resource.raid_device}"
+ Chef::Log.debug("#{@new_resource} mdadm command: #{command}")
+ shell_out!(command)
+ Chef::Log.info("#{@new_resource} stopped raid device (#{@new_resource.raid_device})")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} raid device doesn't exist (#{@new_resource.raid_device}) - not stopping")
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/mount.rb b/lib/chef/provider/mount.rb
new file mode 100644
index 0000000000..fe41997d39
--- /dev/null
+++ b/lib/chef/provider/mount.rb
@@ -0,0 +1,128 @@
+#
+# Author:: Joshua Timberman (<joshua@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/log'
+require 'chef/mixin/command'
+require 'chef/provider'
+
+class Chef
+ class Provider
+ class Mount < Chef::Provider
+
+ include Chef::Mixin::Command
+
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ true
+ end
+
+ def action_mount
+ unless @current_resource.mounted
+ converge_by("mount #{@current_resource.device} to #{@current_resource.mount_point}") do
+ status = mount_fs()
+ if status
+ Chef::Log.info("#{@new_resource} mounted")
+ end
+ end
+ else
+ Chef::Log.debug("#{@new_resource} is already mounted")
+ end
+ end
+
+ def action_umount
+ if @current_resource.mounted
+ converge_by("unmount #{@current_resource.device}") do
+ status = umount_fs()
+ if status
+ Chef::Log.info("#{@new_resource} unmounted")
+ end
+ end
+ else
+ Chef::Log.debug("#{@new_resource} is already unmounted")
+ end
+ end
+
+ def action_remount
+ unless @new_resource.supports[:remount]
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :remount"
+ else
+ if @current_resource.mounted
+ converge_by("remount #{@current_resource.device}") do
+ status = remount_fs()
+ if status
+ Chef::Log.info("#{@new_resource} remounted")
+ end
+ end
+ else
+ Chef::Log.debug("#{@new_resource} not mounted, nothing to remount")
+ end
+ end
+ end
+
+ def action_enable
+ unless @current_resource.enabled
+ converge_by("remount #{@current_resource.device}") do
+ status = enable_fs
+ if status
+ Chef::Log.info("#{@new_resource} enabled")
+ else
+ Chef::Log.debug("#{@new_resource} already enabled")
+ end
+ end
+ end
+ end
+
+ def action_disable
+ if @current_resource.enabled
+ converge_by("remount #{@current_resource.device}") do
+ status = disable_fs
+ if status
+ Chef::Log.info("#{@new_resource} disabled")
+ else
+ Chef::Log.debug("#{@new_resource} already disabled")
+ end
+ end
+ end
+ end
+
+ def mount_fs
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :mount"
+ end
+
+ def umount_fs
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :umount"
+ end
+
+ def remount_fs
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :remount"
+ end
+
+ def enable_fs
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable"
+ end
+
+ def disable_fs
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable"
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/mount/mount.rb b/lib/chef/provider/mount/mount.rb
new file mode 100644
index 0000000000..9a85a9058a
--- /dev/null
+++ b/lib/chef/provider/mount/mount.rb
@@ -0,0 +1,252 @@
+#
+# Author:: Joshua Timberman (<joshua@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/mount'
+require 'chef/log'
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Provider
+ class Mount
+ class Mount < Chef::Provider::Mount
+ include Chef::Mixin::ShellOut
+
+ def initialize(new_resource, run_context)
+ super
+ @real_device = nil
+ end
+ attr_accessor :real_device
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Mount.new(@new_resource.name)
+ @current_resource.mount_point(@new_resource.mount_point)
+ @current_resource.device(@new_resource.device)
+ mounted?
+ enabled?
+ end
+
+ def mountable?
+ # only check for existence of non-remote devices
+ if (device_should_exist? && !::File.exists?(device_real) )
+ raise Chef::Exceptions::Mount, "Device #{@new_resource.device} does not exist"
+ elsif( !::File.exists?(@new_resource.mount_point) )
+ raise Chef::Exceptions::Mount, "Mount point #{@new_resource.mount_point} does not exist"
+ end
+ return true
+ end
+
+ def enabled?
+ # Check to see if there is a entry in /etc/fstab. Last entry for a volume wins.
+ enabled = false
+ ::File.foreach("/etc/fstab") do |line|
+ case line
+ when /^[#\s]/
+ next
+ when /^#{device_fstab_regex}\s+#{Regexp.escape(@new_resource.mount_point)}\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/
+ enabled = true
+ @current_resource.fstype($1)
+ @current_resource.options($2)
+ @current_resource.dump($3.to_i)
+ @current_resource.pass($4.to_i)
+ Chef::Log.debug("Found mount #{device_fstab} to #{@new_resource.mount_point} in /etc/fstab")
+ next
+ when /^[\/\w]+\s+#{Regexp.escape(@new_resource.mount_point)}\s+/
+ enabled = false
+ Chef::Log.debug("Found conflicting mount point #{@new_resource.mount_point} in /etc/fstab")
+ end
+ end
+ @current_resource.enabled(enabled)
+ end
+
+ def mounted?
+ mounted = false
+ shell_out!("mount").stdout.each_line do |line|
+ case line
+ when /^#{device_mount_regex}\s+on\s+#{Regexp.escape(@new_resource.mount_point)}/
+ mounted = true
+ Chef::Log.debug("Special device #{device_logstring} mounted as #{@new_resource.mount_point}")
+ when /^([\/\w])+\son\s#{Regexp.escape(@new_resource.mount_point)}\s+/
+ mounted = false
+ Chef::Log.debug("Special device #{$~[1]} mounted as #{@new_resource.mount_point}")
+ end
+ end
+ @current_resource.mounted(mounted)
+ end
+
+ def mount_fs
+ unless @current_resource.mounted
+ mountable?
+ command = "mount -t #{@new_resource.fstype}"
+ command << " -o #{@new_resource.options.join(',')}" unless @new_resource.options.nil? || @new_resource.options.empty?
+ command << case @new_resource.device_type
+ when :device
+ " #{device_real}"
+ when :label
+ " -L #{@new_resource.device}"
+ when :uuid
+ " -U #{@new_resource.device}"
+ end
+ command << " #{@new_resource.mount_point}"
+ shell_out!(command)
+ Chef::Log.debug("#{@new_resource} is mounted at #{@new_resource.mount_point}")
+ else
+ Chef::Log.debug("#{@new_resource} is already mounted at #{@new_resource.mount_point}")
+ end
+ end
+
+ def umount_fs
+ if @current_resource.mounted
+ shell_out!("umount #{@new_resource.mount_point}")
+ Chef::Log.debug("#{@new_resource} is no longer mounted at #{@new_resource.mount_point}")
+ else
+ Chef::Log.debug("#{@new_resource} is not mounted at #{@new_resource.mount_point}")
+ end
+ end
+
+ def remount_fs
+ if @current_resource.mounted and @new_resource.supports[:remount]
+ shell_out!("mount -o remount #{@new_resource.mount_point}")
+ @new_resource.updated_by_last_action(true)
+ Chef::Log.debug("#{@new_resource} is remounted at #{@new_resource.mount_point}")
+ elsif @current_resource.mounted
+ umount_fs
+ sleep 1
+ mount_fs
+ else
+ Chef::Log.debug("#{@new_resource} is not mounted at #{@new_resource.mount_point} - nothing to do")
+ end
+ end
+
+ def enable_fs
+ if @current_resource.enabled && mount_options_unchanged?
+ Chef::Log.debug("#{@new_resource} is already enabled - nothing to do")
+ return nil
+ end
+
+ if @current_resource.enabled
+ # The current options don't match what we have, so
+ # disable, then enable.
+ disable_fs
+ end
+ ::File.open("/etc/fstab", "a") do |fstab|
+ fstab.puts("#{device_fstab} #{@new_resource.mount_point} #{@new_resource.fstype} #{@new_resource.options.nil? ? "defaults" : @new_resource.options.join(",")} #{@new_resource.dump} #{@new_resource.pass}")
+ Chef::Log.debug("#{@new_resource} is enabled at #{@new_resource.mount_point}")
+ end
+ end
+
+ def disable_fs
+ if @current_resource.enabled
+ contents = []
+
+ found = false
+ ::File.readlines("/etc/fstab").reverse_each do |line|
+ if !found && line =~ /^#{device_fstab_regex}\s+#{Regexp.escape(@new_resource.mount_point)}/
+ found = true
+ Chef::Log.debug("#{@new_resource} is removed from fstab")
+ next
+ else
+ contents << line
+ end
+ end
+
+ ::File.open("/etc/fstab", "w") do |fstab|
+ contents.reverse_each { |line| fstab.puts line}
+ end
+ else
+ Chef::Log.debug("#{@new_resource} is not enabled - nothing to do")
+ end
+ end
+
+ def network_device?
+ @new_resource.device =~ /:/ || @new_resource.device =~ /\/\//
+ end
+
+ def device_should_exist?
+ ( not network_device? ) &&
+ ( not %w[ tmpfs fuse ].include? @new_resource.fstype )
+ end
+
+ private
+
+ def device_fstab
+ case @new_resource.device_type
+ when :device
+ @new_resource.device
+ when :label
+ "LABEL=#{@new_resource.device}"
+ when :uuid
+ "UUID=#{@new_resource.device}"
+ end
+ end
+
+ def device_real
+ if @real_device == nil
+ if @new_resource.device_type == :device
+ @real_device = @new_resource.device
+ else
+ @real_device = ""
+ status = popen4("/sbin/findfs #{device_fstab}") do |pid, stdin, stdout, stderr|
+ device_line = stdout.first # stdout.first consumes
+ @real_device = device_line.chomp unless device_line.nil?
+ end
+ end
+ end
+ @real_device
+ end
+
+ def device_logstring
+ case @new_resource.device_type
+ when :device
+ "#{device_real}"
+ when :label
+ "#{device_real} with label #{@new_resource.device}"
+ when :uuid
+ "#{device_real} with uuid #{@new_resource.device}"
+ end
+ end
+
+ def device_mount_regex
+ if network_device?
+ # ignore trailing slash
+ Regexp.escape(device_real)+"/?"
+ elsif ::File.symlink?(device_real)
+ "(?:#{Regexp.escape(device_real)})|(?:#{Regexp.escape(::File.readlink(device_real))})"
+ else
+ Regexp.escape(device_real)
+ end
+ end
+
+ def device_fstab_regex
+ if @new_resource.device_type == :device
+ device_mount_regex
+ else
+ device_fstab
+ end
+ end
+
+ def mount_options_unchanged?
+ @current_resource.fstype == @new_resource.fstype and
+ @current_resource.options == @new_resource.options and
+ @current_resource.dump == @new_resource.dump and
+ @current_resource.pass == @new_resource.pass
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/mount/windows.rb b/lib/chef/provider/mount/windows.rb
new file mode 100644
index 0000000000..dced0d3596
--- /dev/null
+++ b/lib/chef/provider/mount/windows.rb
@@ -0,0 +1,81 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/mount'
+if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ require 'chef/util/windows/net_use'
+ require 'chef/util/windows/volume'
+end
+
+class Chef
+ class Provider
+ class Mount
+ class Windows < Chef::Provider::Mount
+
+ def is_volume(name)
+ name =~ /^\\\\\?\\Volume\{[\w-]+\}\\$/ ? true : false
+ end
+
+ def initialize(new_resource, run_context)
+ super
+ @mount = nil
+ end
+
+ def load_current_resource
+ if is_volume(@new_resource.device)
+ @mount = Chef::Util::Windows::Volume.new(@new_resource.name)
+ else #assume network drive
+ @mount = Chef::Util::Windows::NetUse.new(@new_resource.name)
+ end
+
+ @current_resource = Chef::Resource::Mount.new(@new_resource.name)
+ @current_resource.mount_point(@new_resource.mount_point)
+ Chef::Log.debug("Checking for mount point #{@current_resource.mount_point}")
+
+ begin
+ @current_resource.device(@mount.device)
+ Chef::Log.debug("#{@current_resource.device} mounted on #{@new_resource.mount_point}")
+ @current_resource.mounted(true)
+ rescue ArgumentError => e
+ @current_resource.mounted(false)
+ Chef::Log.debug("#{@new_resource.mount_point} is not mounted: #{e.message}")
+ end
+ end
+
+ def mount_fs
+ unless @current_resource.mounted
+ @mount.add(@new_resource.device)
+ Chef::Log.debug("#{@new_resource} is mounted at #{@new_resource.mount_point}")
+ else
+ Chef::Log.debug("#{@new_resource} is already mounted at #{@new_resource.mount_point}")
+ end
+ end
+
+ def umount_fs
+ if @current_resource.mounted
+ @mount.delete
+ Chef::Log.debug("#{@new_resource} is no longer mounted at #{@new_resource.mount_point}")
+ else
+ Chef::Log.debug("#{@new_resource} is not mounted at #{@new_resource.mount_point}")
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/ohai.rb b/lib/chef/provider/ohai.rb
new file mode 100644
index 0000000000..c686f67450
--- /dev/null
+++ b/lib/chef/provider/ohai.rb
@@ -0,0 +1,47 @@
+#
+# Author:: Michael Leianrtas (<mleinartas@gmail.com>)
+# Copyright:: Copyright (c) 2010 Michael Leinartas
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'ohai'
+
+class Chef
+ class Provider
+ class Ohai < Chef::Provider
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ true
+ end
+
+ def action_reload
+ converge_by("re-run ohai and merge results into node attributes") do
+ ohai = ::Ohai::System.new
+ if @new_resource.plugin
+ ohai.require_plugin @new_resource.plugin
+ else
+ ohai.all_plugins
+ end
+ node.automatic_attrs.merge! ohai.data
+ Chef::Log.info("#{@new_resource} reloaded")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb
new file mode 100644
index 0000000000..a28a6f93fb
--- /dev/null
+++ b/lib/chef/provider/package.rb
@@ -0,0 +1,229 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/command'
+require 'chef/log'
+require 'chef/file_cache'
+require 'chef/platform'
+
+class Chef
+ class Provider
+ class Package < Chef::Provider
+
+ include Chef::Mixin::Command
+
+ attr_accessor :candidate_version
+ def initialize(new_resource, run_context)
+ super
+ @candidate_version = nil
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ end
+
+ def define_resource_requirements
+ requirements.assert(:install) do |a|
+ a.assertion { ((@new_resource.version != nil) && !(target_version_already_installed?)) \
+ || !(@current_resource.version.nil? && candidate_version.nil?) }
+ a.failure_message(Chef::Exceptions::Package, "No version specified, and no candidate version available for #{@new_resource.package_name}")
+ a.whyrun("Assuming a repository that offers #{@new_resource.package_name} would have been configured")
+ end
+
+ requirements.assert(:upgrade) do |a|
+ # Can't upgrade what we don't have
+ a.assertion { !(@current_resource.version.nil? && candidate_version.nil?) }
+ a.failure_message(Chef::Exceptions::Package, "No candidate version available for #{@new_resource.package_name}")
+ a.whyrun("Assuming a repository that offers #{@new_resource.package_name} would have been configured")
+ end
+ end
+
+ def action_install
+ # If we specified a version, and it's not the current version, move to the specified version
+ if !@new_resource.version.nil? && !(target_version_already_installed?)
+ install_version = @new_resource.version
+ # If it's not installed at all, install it
+ elsif @current_resource.version.nil?
+ install_version = candidate_version
+ else
+ Chef::Log.debug("#{@new_resource} is already installed - nothing to do")
+ return
+ end
+
+ # We need to make sure we handle the preseed file
+ if @new_resource.response_file
+ if preseed_file = get_preseed_file(@new_resource.package_name, install_version)
+ converge_by("preseed package #{@new_resource.package_name}") do
+ preseed_package(preseed_file)
+ end
+ end
+ end
+ description = install_version ? "version #{install_version} of" : ""
+ converge_by("install #{description} package #{@new_resource.package_name}") do
+ @new_resource.version(install_version)
+ install_package(@new_resource.package_name, install_version)
+ end
+ end
+
+ def action_upgrade
+ if candidate_version.nil?
+ Chef::Log.debug("#{@new_resource} no candidate version - nothing to do")
+ elsif @current_resource.version == candidate_version
+ Chef::Log.debug("#{@new_resource} is at the latest version - nothing to do")
+ else
+ @new_resource.version(candidate_version)
+ orig_version = @current_resource.version || "uninstalled"
+ converge_by("upgrade package #{@new_resource.package_name} from #{orig_version} to #{candidate_version}") do
+ status = upgrade_package(@new_resource.package_name, candidate_version)
+ Chef::Log.info("#{@new_resource} upgraded from #{orig_version} to #{candidate_version}")
+ end
+ end
+ end
+
+ def action_remove
+ if removing_package?
+ description = @new_resource.version ? "version #{@new_resource.version} of " : ""
+ converge_by("remove #{description} package #{@current_resource.package_name}") do
+ remove_package(@current_resource.package_name, @new_resource.version)
+ Chef::Log.info("#{@new_resource} removed")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} package does not exist - nothing to do")
+ end
+ end
+
+ def removing_package?
+ if @current_resource.version.nil?
+ false # nothing to remove
+ elsif @new_resource.version.nil?
+ true # remove any version of a package
+ elsif @new_resource.version == @current_resource.version
+ true # remove the version we have
+ else
+ false # we don't have the version we want to remove
+ end
+ end
+
+ def action_purge
+ if removing_package?
+ description = @new_resource.version ? "version #{@new_resource.version} of" : ""
+ converge_by("purge #{description} package #{@current_resource.package_name}") do
+ purge_package(@current_resource.package_name, @new_resource.version)
+ Chef::Log.info("#{@new_resource} purged")
+ end
+ end
+ end
+
+ def action_reconfig
+ if @current_resource.version == nil then
+ Chef::Log.debug("#{@new_resource} is NOT installed - nothing to do")
+ return
+ end
+
+ unless @new_resource.response_file then
+ Chef::Log.debug("#{@new_resource} no response_file provided - nothing to do")
+ return
+ end
+
+ if preseed_file = get_preseed_file(@new_resource.package_name, @current_resource.version)
+ converge_by("reconfigure package #{@new_resource.package_name}") do
+ preseed_package(preseed_file)
+ status = reconfig_package(@new_resource.package_name, @current_resource.version)
+ Chef::Log.info("#{@new_resource} reconfigured")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} preseeding has not changed - nothing to do")
+ end
+ end
+
+ def install_package(name, version)
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :install"
+ end
+
+ def upgrade_package(name, version)
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :upgrade"
+ end
+
+ def remove_package(name, version)
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :remove"
+ end
+
+ def purge_package(name, version)
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :purge"
+ end
+
+ def preseed_package(file)
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support pre-seeding package install/upgrade instructions"
+ end
+
+ def reconfig_package(name, version)
+ raise( Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reconfig" )
+ end
+
+ def get_preseed_file(name, version)
+ resource = preseed_resource(name, version)
+ resource.run_action(:create)
+ Chef::Log.debug("#{@new_resource} fetched preseed file to #{resource.path}")
+
+ if resource.updated_by_last_action?
+ resource.path
+ else
+ false
+ end
+ end
+
+ def preseed_resource(name, version)
+ # A directory in our cache to store this cookbook's preseed files in
+ file_cache_dir = Chef::FileCache.create_cache_path("preseed/#{@new_resource.cookbook_name}")
+ # The full path where the preseed file will be stored
+ cache_seed_to = "#{file_cache_dir}/#{name}-#{version}.seed"
+
+ Chef::Log.debug("#{@new_resource} fetching preseed file to #{cache_seed_to}")
+
+ begin
+ remote_file = Chef::Resource::Template.new(cache_seed_to, run_context)
+ remote_file.cookbook_name = @new_resource.cookbook_name
+ remote_file.source(@new_resource.response_file)
+ remote_file.backup(false)
+ provider = Chef::Platform.provider_for_resource(remote_file, :create)
+ provider.template_location
+ rescue
+ Chef::Log.debug("#{@new_resource} fetching preseed file via Template resource failed, fallback to CookbookFile resource")
+ remote_file = Chef::Resource::CookbookFile.new(cache_seed_to, run_context)
+ remote_file.cookbook_name = @new_resource.cookbook_name
+ remote_file.source(@new_resource.response_file)
+ remote_file.backup(false)
+ end
+
+ remote_file
+ end
+
+ def expand_options(options)
+ options ? " #{options}" : ""
+ end
+
+ def target_version_already_installed?
+ @new_resource.version == @current_resource.version
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb
new file mode 100644
index 0000000000..e8939b494e
--- /dev/null
+++ b/lib/chef/provider/package/apt.rb
@@ -0,0 +1,147 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+require 'chef/mixin/shell_out'
+
+
+class Chef
+ class Provider
+ class Package
+ class Apt < Chef::Provider::Package
+
+ include Chef::Mixin::ShellOut
+ attr_accessor :is_virtual_package
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+ check_package_state(@new_resource.package_name)
+ @current_resource
+ end
+
+ def default_release_options
+ # Use apt::Default-Release option only if provider was explicitly defined
+ "-o APT::Default-Release=#{@new_resource.default_release}" if @new_resource.provider && @new_resource.default_release
+ end
+
+ def check_package_state(package)
+ Chef::Log.debug("#{@new_resource} checking package status for #{package}")
+ installed = false
+
+ shell_out!("apt-cache#{expand_options(default_release_options)} policy #{package}").stdout.each_line do |line|
+ case line
+ when /^\s{2}Installed: (.+)$/
+ installed_version = $1
+ if installed_version == '(none)'
+ Chef::Log.debug("#{@new_resource} current version is nil")
+ @current_resource.version(nil)
+ else
+ Chef::Log.debug("#{@new_resource} current version is #{installed_version}")
+ @current_resource.version(installed_version)
+ installed = true
+ end
+ when /^\s{2}Candidate: (.+)$/
+ candidate_version = $1
+ if candidate_version == '(none)'
+ # This may not be an appropriate assumption, but it shouldn't break anything that already worked -- btm
+ @is_virtual_package = true
+ showpkg = shell_out!("apt-cache showpkg #{package}").stdout
+ providers = Hash.new
+ showpkg.rpartition(/Reverse Provides:? #{$/}/)[2].each_line do |line|
+ provider, version = line.split
+ providers[provider] = version
+ end
+ # Check if the package providing this virtual package is installed
+ num_providers = providers.length
+ raise Chef::Exceptions::Package, "#{@new_resource.package_name} has no candidate in the apt-cache" if num_providers == 0
+ # apt will only install a virtual package if there is a single providing package
+ raise Chef::Exceptions::Package, "#{@new_resource.package_name} is a virtual package provided by #{num_providers} packages, you must explicitly select one to install" if num_providers > 1
+ # Check if the package providing this virtual package is installed
+ Chef::Log.info("#{@new_resource} is a virtual package, actually acting on package[#{providers.keys.first}]")
+ installed = check_package_state(providers.keys.first)
+ else
+ Chef::Log.debug("#{@new_resource} candidate version is #{$1}")
+ @candidate_version = $1
+ end
+ end
+ end
+
+ return installed
+ end
+
+ def install_package(name, version)
+ package_name = "#{name}=#{version}"
+ package_name = name if @is_virtual_package
+ run_command_with_systems_locale(
+ :command => "apt-get -q -y#{expand_options(default_release_options)}#{expand_options(@new_resource.options)} install #{package_name}",
+ :environment => {
+ "DEBIAN_FRONTEND" => "noninteractive"
+ }
+ )
+ end
+
+ def upgrade_package(name, version)
+ install_package(name, version)
+ end
+
+ def remove_package(name, version)
+ package_name = "#{name}"
+ run_command_with_systems_locale(
+ :command => "apt-get -q -y#{expand_options(@new_resource.options)} remove #{package_name}",
+ :environment => {
+ "DEBIAN_FRONTEND" => "noninteractive"
+ }
+ )
+ end
+
+ def purge_package(name, version)
+ run_command_with_systems_locale(
+ :command => "apt-get -q -y#{expand_options(@new_resource.options)} purge #{@new_resource.package_name}",
+ :environment => {
+ "DEBIAN_FRONTEND" => "noninteractive"
+ }
+ )
+ end
+
+ def preseed_package(preseed_file)
+ Chef::Log.info("#{@new_resource} pre-seeding package installation instructions")
+ run_command_with_systems_locale(
+ :command => "debconf-set-selections #{preseed_file}",
+ :environment => {
+ "DEBIAN_FRONTEND" => "noninteractive"
+ }
+ )
+ end
+
+ def reconfig_package(name, version)
+ Chef::Log.info("#{@new_resource} reconfiguring")
+ run_command_with_systems_locale(
+ :command => "dpkg-reconfigure #{name}",
+ :environment => {
+ "DEBIAN_FRONTEND" => "noninteractive"
+ }
+ )
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb
new file mode 100644
index 0000000000..795a7b308b
--- /dev/null
+++ b/lib/chef/provider/package/dpkg.rb
@@ -0,0 +1,128 @@
+#
+# Author:: Bryan McLellan (btm@loftninjas.org)
+# Copyright:: Copyright (c) 2009 Bryan McLellan
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+require 'chef/mixin/get_source_from_package'
+
+class Chef
+ class Provider
+ class Package
+ class Dpkg < Chef::Provider::Package::Apt
+ DPKG_INFO = /([a-z\d\-\+\.]+)\t([\w\d.~-]+)/
+ DPKG_INSTALLED = /^Status: install ok installed/
+ DPKG_VERSION = /^Version: (.+)$/
+
+ include Chef::Mixin::GetSourceFromPackage
+ def define_resource_requirements
+ super
+ requirements.assert(:install) do |a|
+ a.assertion{ not @new_resource.source.nil? }
+ a.failure_message Chef::Exceptions::Package, "Source for package #{@new_resource.name} required for action install"
+ end
+
+ # TODO this was originally written for any action in which .source is provided
+ # but would it make more sense to only look at source if the action is :install?
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @source_exists }
+ a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}"
+ a.whyrun "Assuming it would have been previously downloaded."
+ end
+ end
+
+ def load_current_resource
+ @source_exists = true
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+ @new_resource.version(nil)
+
+ if @new_resource.source
+ @source_exists = ::File.exists?(@new_resource.source)
+ if @source_exists
+ # Get information from the package if supplied
+ Chef::Log.debug("#{@new_resource} checking dpkg status")
+ status = popen4("dpkg-deb -W #{@new_resource.source}") do |pid, stdin, stdout, stderr|
+ stdout.each_line do |line|
+ if pkginfo = DPKG_INFO.match(line)
+ @current_resource.package_name(pkginfo[1])
+ @new_resource.version(pkginfo[2])
+ end
+ end
+ end
+ else
+ # Source provided but not valid means we can't safely do further processing
+ return
+ end
+
+ end
+
+ # Check to see if it is installed
+ package_installed = nil
+ Chef::Log.debug("#{@new_resource} checking install state")
+ status = popen4("dpkg -s #{@current_resource.package_name}") do |pid, stdin, stdout, stderr|
+ stdout.each_line do |line|
+ case line
+ when DPKG_INSTALLED
+ package_installed = true
+ when DPKG_VERSION
+ if package_installed
+ Chef::Log.debug("#{@new_resource} current version is #{$1}")
+ @current_resource.version($1)
+ end
+ end
+ end
+ end
+
+ unless status.exitstatus == 0 || status.exitstatus == 1
+ raise Chef::Exceptions::Package, "dpkg failed - #{status.inspect}!"
+ end
+
+ @current_resource
+ end
+
+ def install_package(name, version)
+ run_command_with_systems_locale(
+ :command => "dpkg -i#{expand_options(@new_resource.options)} #{@new_resource.source}",
+ :environment => {
+ "DEBIAN_FRONTEND" => "noninteractive"
+ }
+ )
+ end
+
+ def remove_package(name, version)
+ run_command_with_systems_locale(
+ :command => "dpkg -r#{expand_options(@new_resource.options)} #{@new_resource.package_name}",
+ :environment => {
+ "DEBIAN_FRONTEND" => "noninteractive"
+ }
+ )
+ end
+
+ def purge_package(name, version)
+ run_command_with_systems_locale(
+ :command => "dpkg -P#{expand_options(@new_resource.options)} #{@new_resource.package_name}",
+ :environment => {
+ "DEBIAN_FRONTEND" => "noninteractive"
+ }
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/easy_install.rb b/lib/chef/provider/package/easy_install.rb
new file mode 100644
index 0000000000..6c9dacc55d
--- /dev/null
+++ b/lib/chef/provider/package/easy_install.rb
@@ -0,0 +1,136 @@
+#
+# Author:: Joe Williams (<joe@joetify.com>)
+# Copyright:: Copyright (c) 2009 Joe Williams
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/package'
+require 'chef/mixin/command'
+require 'chef/mixin/shell_out'
+require 'chef/resource/package'
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Provider
+ class Package
+ class EasyInstall < Chef::Provider::Package
+
+ include Chef::Mixin::ShellOut
+
+ def install_check(name)
+ check = false
+
+ begin
+ # first check to see if we can import it
+ output = shell_out!("#{python_binary_path} -c \"import #{name}\"", :returns=>[0,1]).stderr
+ if output.include? "ImportError"
+ # then check to see if its on the path
+ output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout
+ if output.downcase.include? "#{name.downcase}"
+ check = true
+ end
+ else
+ check = true
+ end
+ rescue
+ # it's probably not installed
+ end
+
+ check
+ end
+
+ def easy_install_binary_path
+ path = @new_resource.easy_install_binary
+ path ? path : 'easy_install'
+ end
+
+ def python_binary_path
+ path = @new_resource.python_binary
+ path ? path : 'python'
+ end
+
+ def module_name
+ m = @new_resource.module_name
+ m ? m : @new_resource.name
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+ @current_resource.version(nil)
+
+ # get the currently installed version if installed
+ package_version = nil
+ if install_check(module_name)
+ begin
+ output = shell_out!("#{python_binary_path} -c \"import #{module_name}; print #{module_name}.__version__\"").stdout
+ package_version = output.strip
+ rescue
+ output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout
+
+ output_array = output.gsub(/[\[\]]/,'').split(/\s*,\s*/)
+ package_path = ""
+
+ output_array.each do |entry|
+ if entry.downcase.include?(@new_resource.package_name)
+ package_path = entry
+ end
+ end
+
+ package_path[/\S\S(.*)\/(.*)-(.*)-py(.*).egg\S/]
+ package_version = $3
+ end
+ end
+
+ if package_version == @new_resource.version
+ Chef::Log.debug("#{@new_resource} at version #{@new_resource.version}")
+ @current_resource.version(@new_resource.version)
+ else
+ Chef::Log.debug("#{@new_resource} at version #{package_version}")
+ @current_resource.version(package_version)
+ end
+
+ @current_resource
+ end
+
+ def candidate_version
+ return @candidate_version if @candidate_version
+
+ # do a dry run to get the latest version
+ result = shell_out!("#{easy_install_binary_path} -n #{@new_resource.package_name}", :returns=>[0,1])
+ @candidate_version = result.stdout[/(.*)Best match: (.*) (.*)$/, 3]
+ @candidate_version
+ end
+
+ def install_package(name, version)
+ run_command(:command => "#{easy_install_binary_path}#{expand_options(@new_resource.options)} \"#{name}==#{version}\"")
+ end
+
+ def upgrade_package(name, version)
+ install_package(name, version)
+ end
+
+ def remove_package(name, version)
+ run_command(:command => "#{easy_install_binary_path }#{expand_options(@new_resource.options)} -m #{name}")
+ end
+
+ def purge_package(name, version)
+ remove_package(name, version)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/freebsd.rb b/lib/chef/provider/package/freebsd.rb
new file mode 100644
index 0000000000..afdd0d812e
--- /dev/null
+++ b/lib/chef/provider/package/freebsd.rb
@@ -0,0 +1,149 @@
+#
+# Authors:: Bryan McLellan (btm@loftninjas.org)
+# Matthew Landauer (matthew@openaustralia.org)
+# Copyright:: Copyright (c) 2009 Bryan McLellan, Matthew Landauer
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/package'
+require 'chef/mixin/shell_out'
+require 'chef/resource/package'
+require 'chef/mixin/get_source_from_package'
+
+class Chef
+ class Provider
+ class Package
+ class Freebsd < Chef::Provider::Package
+ include Chef::Mixin::ShellOut
+
+ include Chef::Mixin::GetSourceFromPackage
+
+ def initialize(*args)
+ super
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ end
+
+ def current_installed_version
+ pkg_info = shell_out!("pkg_info -E \"#{package_name}*\"", :env => nil, :returns => [0,1])
+ pkg_info.stdout[/^#{package_name}-(.+)/, 1]
+ end
+
+ def port_path
+ case @new_resource.package_name
+ # When the package name starts with a '/' treat it as the full path to the ports directory
+ when /^\//
+ @new_resource.package_name
+ # Otherwise if the package name contains a '/' not at the start (like 'www/wordpress') treat as a relative
+ # path from /usr/ports
+ when /\//
+ "/usr/ports/#{@new_resource.package_name}"
+ # Otherwise look up the path to the ports directory using 'whereis'
+ else
+ whereis = shell_out!("whereis -s #{@new_resource.package_name}", :env => nil)
+ unless path = whereis.stdout[/^#{@new_resource.package_name}:\s+(.+)$/, 1]
+ raise Chef::Exceptions::Package, "Could not find port with the name #{@new_resource.package_name}"
+ end
+ path
+ end
+ end
+
+ def ports_makefile_variable_value(variable)
+ make_v = shell_out!("make -V #{variable}", :cwd => port_path, :env => nil, :returns => [0,1])
+ make_v.stdout.strip.split($\).first # $\ is the line separator, i.e., newline
+ end
+
+ def ports_candidate_version
+ ports_makefile_variable_value("PORTVERSION")
+ end
+
+ def file_candidate_version_path
+ Dir["#{@new_resource.source}/#{@current_resource.package_name}*"][-1].to_s
+ end
+
+ def file_candidate_version
+ file_candidate_version_path.split(/-/).last.split(/.tbz/).first
+ end
+
+ def load_current_resource
+ @current_resource.package_name(@new_resource.package_name)
+
+ @current_resource.version(current_installed_version)
+ Chef::Log.debug("#{@new_resource} current version is #{@current_resource.version}") if @current_resource.version
+
+ case @new_resource.source
+ when /^http/, /^ftp/
+ @candidate_version = "0.0.0"
+ when /^\//
+ @candidate_version = file_candidate_version
+ else
+ @candidate_version = ports_candidate_version
+ end
+
+ Chef::Log.debug("#{@new_resource} ports candidate version is #{@candidate_version}") if @candidate_version
+
+ @current_resource
+ end
+
+ def latest_link_name
+ ports_makefile_variable_value("LATEST_LINK")
+ end
+
+ # The name of the package (without the version number) as understood by pkg_add and pkg_info
+ def package_name
+ if ::File.exist?("/usr/ports/Makefile")
+ if ports_makefile_variable_value("PKGNAME") =~ /^(.+)-[^-]+$/
+ $1
+ else
+ raise Chef::Exceptions::Package, "Unexpected form for PKGNAME variable in #{port_path}/Makefile"
+ end
+ else
+ @new_resource.package_name
+ end
+ end
+
+ def install_package(name, version)
+ unless @current_resource.version
+ case @new_resource.source
+ when /^ports$/
+ shell_out!("make -DBATCH install", :timeout => 1200, :env => nil, :cwd => port_path).status
+ when /^http/, /^ftp/
+ if @new_resource.source =~ /\/$/
+ shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGESITE" => @new_resource.source, 'LC_ALL' => nil }).status
+ else
+ shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGEROOT" => @new_resource.source, 'LC_ALL' => nil }).status
+ end
+ Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}")
+ when /^\//
+ shell_out!("pkg_add #{file_candidate_version_path}", :env => { "PKG_PATH" => @new_resource.source , 'LC_ALL'=>nil}).status
+ Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}")
+ else
+ shell_out!("pkg_add -r #{latest_link_name}", :env => nil).status
+ end
+ end
+ end
+
+ def remove_package(name, version)
+ # a version is mandatory
+ if version
+ shell_out!("pkg_delete #{package_name}-#{version}", :env => nil).status
+ else
+ shell_out!("pkg_delete #{package_name}-#{@current_resource.version}", :env => nil).status
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/ips.rb b/lib/chef/provider/package/ips.rb
new file mode 100644
index 0000000000..5beb46a20a
--- /dev/null
+++ b/lib/chef/provider/package/ips.rb
@@ -0,0 +1,101 @@
+#
+# Author:: Jason J. W. Williams (<williamsjj@digitar.com>)
+# Author:: Stephen Nelson-Smith (<sns@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'open3'
+require 'chef/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Provider
+ class Package
+ class Ips < Chef::Provider::Package
+
+ include Chef::Mixin::ShellOut
+ attr_accessor :virtual
+
+ def define_resource_requirements
+ super
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { ! @candidate_version.nil? }
+ a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.package_name} not found"
+ a.whyrun "Assuming package #{@new_resource.package_name} would have been made available."
+ end
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.name)
+ check_package_state(@new_resource.package_name)
+ @current_resource
+ end
+
+ def check_package_state(package)
+ Chef::Log.debug("Checking package status for #{package}")
+ installed = false
+ depends = false
+
+ shell_out!("pkg info -r #{package}").stdout.each_line do |line|
+ case line
+ when /^\s+State: Installed/
+ installed = true
+ when /^\s+Version: (.*)/
+ @candidate_version = $1.split[0]
+ if installed
+ @current_resource.version($1)
+ else
+ @current_resource.version(nil)
+ end
+ end
+ end
+
+ return installed
+ end
+
+ def install_package(name, version)
+ package_name = "#{name}@#{version}"
+ normal_command = "pkg#{expand_options(@new_resource.options)} install -q #{package_name}"
+ if @new_resource.respond_to?(:accept_license) and @new_resource.accept_license
+ command = normal_command.gsub('-q', '-q --accept')
+ else
+ command = normal_command
+ end
+ begin
+ run_command_with_systems_locale(:command => command)
+ rescue
+ end
+ end
+
+ def upgrade_package(name, version)
+ install_package(name, version)
+ end
+
+ def remove_package(name, version)
+ package_name = "#{name}@#{version}"
+ run_command_with_systems_locale(
+ :command => "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}"
+ )
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/provider/package/macports.rb b/lib/chef/provider/package/macports.rb
new file mode 100644
index 0000000000..fd33788944
--- /dev/null
+++ b/lib/chef/provider/package/macports.rb
@@ -0,0 +1,105 @@
+class Chef
+ class Provider
+ class Package
+ class Macports < Chef::Provider::Package
+ def load_current_resource
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+
+ @current_resource.version(current_installed_version)
+ Chef::Log.debug("#{@new_resource} current version is #{@current_resource.version}") if @current_resource.version
+
+ @candidate_version = macports_candidate_version
+
+ if !@new_resource.version and !@candidate_version
+ raise Chef::Exceptions::Package, "Could not get a candidate version for this package -- #{@new_resource.name} does not seem to be a valid package!"
+ end
+
+ Chef::Log.debug("#{@new_resource} candidate version is #{@candidate_version}") if @candidate_version
+
+ @current_resource
+ end
+
+ def current_installed_version
+ command = "port installed #{@new_resource.package_name}"
+ output = get_response_from_command(command)
+
+ response = nil
+ output.each_line do |line|
+ match = line.match(/^.+ @([^\s]+) \(active\)$/)
+ response = match[1] if match
+ end
+ response
+ end
+
+ def macports_candidate_version
+ command = "port info --version #{@new_resource.package_name}"
+ output = get_response_from_command(command)
+
+ match = output.match(/^version: (.+)$/)
+
+ match ? match[1] : nil
+ end
+
+ def install_package(name, version)
+ unless @current_resource.version == version
+ command = "port#{expand_options(@new_resource.options)} install #{name}"
+ command << " @#{version}" if version and !version.empty?
+ run_command_with_systems_locale(
+ :command => command
+ )
+ end
+ end
+
+ def purge_package(name, version)
+ command = "port#{expand_options(@new_resource.options)} uninstall #{name}"
+ command << " @#{version}" if version and !version.empty?
+ run_command_with_systems_locale(
+ :command => command
+ )
+ end
+
+ def remove_package(name, version)
+ command = "port#{expand_options(@new_resource.options)} deactivate #{name}"
+ command << " @#{version}" if version and !version.empty?
+
+ run_command_with_systems_locale(
+ :command => command
+ )
+ end
+
+ def upgrade_package(name, version)
+ # Saving this to a variable -- weird rSpec behavior
+ # happens otherwise...
+ current_version = @current_resource.version
+
+ if current_version.nil? or current_version.empty?
+ # Macports doesn't like when you upgrade a package
+ # that hasn't been installed.
+ install_package(name, version)
+ elsif current_version != version
+ run_command_with_systems_locale(
+ :command => "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}"
+ )
+ end
+ end
+
+ private
+ def get_response_from_command(command)
+ output = nil
+ status = popen4(command) do |pid, stdin, stdout, stderr|
+ begin
+ output = stdout.read
+ rescue Exception
+ raise Chef::Exceptions::Package, "Could not read from STDOUT on command: #{command}"
+ end
+ end
+ unless status.exitstatus == 0 || status.exitstatus == 1
+ raise Chef::Exceptions::Package, "#{command} failed - #{status.insect}!"
+ end
+ output
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb
new file mode 100644
index 0000000000..f81486ae84
--- /dev/null
+++ b/lib/chef/provider/package/pacman.rb
@@ -0,0 +1,111 @@
+#
+# Author:: Jan Zimmek (<jan.zimmek@web.de>)
+# Copyright:: Copyright (c) 2010 Jan Zimmek
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+
+class Chef
+ class Provider
+ class Package
+ class Pacman < Chef::Provider::Package
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+
+ @current_resource.version(nil)
+
+ Chef::Log.debug("#{@new_resource} checking pacman for #{@new_resource.package_name}")
+ status = popen4("pacman -Qi #{@new_resource.package_name}") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ line.force_encoding(Encoding::UTF_8) if line.respond_to?(:force_encoding)
+ case line
+ when /^Version(\s?)*: (.+)$/
+ Chef::Log.debug("#{@new_resource} current version is #{$2}")
+ @current_resource.version($2)
+ end
+ end
+ end
+
+ unless status.exitstatus == 0 || status.exitstatus == 1
+ raise Chef::Exceptions::Package, "pacman failed - #{status.inspect}!"
+ end
+
+ @current_resource
+ end
+
+ def candidate_version
+ return @candidate_version if @candidate_version
+
+ repos = ["extra","core","community"]
+
+ if(::File.exists?("/etc/pacman.conf"))
+ pacman = ::File.read("/etc/pacman.conf")
+ repos = pacman.scan(/\[(.+)\]/).flatten
+ end
+
+ package_repos = repos.map {|r| Regexp.escape(r) }.join('|')
+
+ status = popen4("pacman -Ss #{@new_resource.package_name}") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ case line
+ when /^(#{package_repos})\/#{Regexp.escape(@new_resource.package_name)} (.+)$/
+ # $2 contains a string like "4.4.0-1 (kde kdenetwork)" or "3.10-4 (base)"
+ # simply split by space and use first token
+ @candidate_version = $2.split(" ").first
+ end
+ end
+ end
+
+ unless status.exitstatus == 0 || status.exitstatus == 1
+ raise Chef::Exceptions::Package, "pacman failed - #{status.inspect}!"
+ end
+
+ unless @candidate_version
+ raise Chef::Exceptions::Package, "pacman does not have a version of package #{@new_resource.package_name}"
+ end
+
+ @candidate_version
+
+ end
+
+ def install_package(name, version)
+ run_command_with_systems_locale(
+ :command => "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}"
+ )
+ end
+
+ def upgrade_package(name, version)
+ install_package(name, version)
+ end
+
+ def remove_package(name, version)
+ run_command_with_systems_locale(
+ :command => "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}"
+ )
+ end
+
+ def purge_package(name, version)
+ remove_package(name, version)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/portage.rb b/lib/chef/provider/package/portage.rb
new file mode 100644
index 0000000000..eb13e9855a
--- /dev/null
+++ b/lib/chef/provider/package/portage.rb
@@ -0,0 +1,138 @@
+#
+# Author:: Ezra Zygmuntowicz (<ezra@engineyard.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+
+class Chef
+ class Provider
+ class Package
+ class Portage < Chef::Provider::Package
+ PACKAGE_NAME_PATTERN = %r{(?:([^/]+)/)?([^/]+)}
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+
+ @current_resource.version(nil)
+
+ category, pkg = %r{^#{PACKAGE_NAME_PATTERN}$}.match(@new_resource.package_name)[1,2]
+
+ possibilities = Dir["/var/db/pkg/#{category || "*"}/#{pkg}-*"].map {|d| d.sub(%r{/var/db/pkg/}, "") }
+ versions = possibilities.map do |entry|
+ if(entry =~ %r{[^/]+/#{Regexp.escape(pkg)}\-(\d[\.\d]*((_(alpha|beta|pre|rc|p)\d*)*)?(-r\d+)?)})
+ [$&, $1]
+ end
+ end.compact
+
+ if versions.size > 1
+ atoms = versions.map {|v| v.first }.sort
+ categories = atoms.map {|v| v.split('/')[0] }.uniq
+ if !category && categories.size > 1
+ raise Chef::Exceptions::Package, "Multiple packages found for #{@new_resource.package_name}: #{atoms.join(" ")}. Specify a category."
+ end
+ elsif versions.size == 1
+ @current_resource.version(versions.first.last)
+ Chef::Log.debug("#{@new_resource} current version #{$1}")
+ end
+
+ @current_resource
+ end
+
+
+ def parse_emerge(package, txt)
+ availables = {}
+ package_without_category = package.split("/").last
+ found_package_name = nil
+
+ txt.each_line do |line|
+ if line =~ /\*\s+#{PACKAGE_NAME_PATTERN}/
+ found_package_name = $&.strip
+ if found_package_name == package || found_package_name.split("/").last == package_without_category
+ availables[found_package_name] = nil
+ end
+ end
+
+ if line =~ /Latest version available: (.*)/ && availables.has_key?(found_package_name)
+ availables[found_package_name] = $1.strip
+ end
+ end
+
+ if availables.size > 1
+ # shouldn't happen if a category is specified so just use `package`
+ raise Chef::Exceptions::Package, "Multiple emerge results found for #{package}: #{availables.keys.join(" ")}. Specify a category."
+ end
+
+ availables.values.first
+ end
+
+ def candidate_version
+ return @candidate_version if @candidate_version
+
+ status = popen4("emerge --color n --nospinner --search #{@new_resource.package_name.split('/').last}") do |pid, stdin, stdout, stderr|
+ available, installed = parse_emerge(@new_resource.package_name, stdout.read)
+ @candidate_version = available
+ end
+
+ unless status.exitstatus == 0
+ raise Chef::Exceptions::Package, "emerge --search failed - #{status.inspect}!"
+ end
+
+ @candidate_version
+
+ end
+
+
+ def install_package(name, version)
+ pkg = "=#{name}-#{version}"
+
+ if(version =~ /^\~(.+)/)
+ # If we start with a tilde
+ pkg = "~#{name}-#{$1}"
+ end
+
+ run_command_with_systems_locale(
+ :command => "emerge -g --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}"
+ )
+ end
+
+ def upgrade_package(name, version)
+ install_package(name, version)
+ end
+
+ def remove_package(name, version)
+ if(version)
+ pkg = "=#{@new_resource.package_name}-#{version}"
+ else
+ pkg = "#{@new_resource.package_name}"
+ end
+
+ run_command_with_systems_locale(
+ :command => "emerge --unmerge --color n --nospinner --quiet#{expand_options(@new_resource.options)} #{pkg}"
+ )
+ end
+
+ def purge_package(name, version)
+ remove_package(name, version)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb
new file mode 100644
index 0000000000..033ce8efb9
--- /dev/null
+++ b/lib/chef/provider/package/rpm.rb
@@ -0,0 +1,121 @@
+#
+# Author:: Joshua Timberman (<joshua@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+require 'chef/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+require 'chef/mixin/get_source_from_package'
+
+class Chef
+ class Provider
+ class Package
+ class Rpm < Chef::Provider::Package
+
+ include Chef::Mixin::GetSourceFromPackage
+
+ def define_resource_requirements
+ super
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @package_source_exists }
+ a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}"
+ a.whyrun "Assuming package #{@new_resource.name} would have been made available."
+ end
+ requirements.assert(:all_actions) do |a|
+ a.assertion { !@rpm_status.nil? && (@rpm_status.exitstatus == 0 || @rpm_status.exitstatus == 1) }
+ a.failure_message Chef::Exceptions::Package, "Unable to determine current version due to RPM failure. Detail: #{@rpm_status.inspect}"
+ a.whyrun "Assuming current version would have been determined for package#{@new_resource.name}."
+ end
+ end
+
+ def load_current_resource
+ @package_source_provided = true
+ @package_source_exists = true
+
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+ @new_resource.version(nil)
+
+ if @new_resource.source
+ unless ::File.exists?(@new_resource.source)
+ @package_source_exists = false
+ return
+ end
+
+ Chef::Log.debug("#{@new_resource} checking rpm status")
+ status = popen4("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ case line
+ when /([\w\d_.-]+)\s([\w\d_.-]+)/
+ @current_resource.package_name($1)
+ @new_resource.version($2)
+ end
+ end
+ end
+ else
+ if Array(@new_resource.action).include?(:install)
+ @package_source_exists = false
+ return
+ end
+ end
+
+ Chef::Log.debug("#{@new_resource} checking install state")
+ @rpm_status = popen4("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ case line
+ when /([\w\d_.-]+)\s([\w\d_.-]+)/
+ Chef::Log.debug("#{@new_resource} current version is #{$2}")
+ @current_resource.version($2)
+ end
+ end
+ end
+
+
+ @current_resource
+ end
+
+ def install_package(name, version)
+ unless @current_resource.version
+ run_command_with_systems_locale(
+ :command => "rpm #{@new_resource.options} -i #{@new_resource.source}"
+ )
+ else
+ run_command_with_systems_locale(
+ :command => "rpm #{@new_resource.options} -U #{@new_resource.source}"
+ )
+ end
+ end
+
+ alias_method :upgrade_package, :install_package
+
+ def remove_package(name, version)
+ if version
+ run_command_with_systems_locale(
+ :command => "rpm #{@new_resource.options} -e #{name}-#{version}"
+ )
+ else
+ run_command_with_systems_locale(
+ :command => "rpm #{@new_resource.options} -e #{name}"
+ )
+ end
+ end
+
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb
new file mode 100644
index 0000000000..e60d73ab62
--- /dev/null
+++ b/lib/chef/provider/package/rubygems.rb
@@ -0,0 +1,548 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+require 'chef/mixin/get_source_from_package'
+
+# Class methods on Gem are defined in rubygems
+require 'rubygems'
+# Ruby 1.9's gem_prelude can interact poorly with loading the full rubygems
+# explicitly like this. Make sure rubygems/specification is always last in this
+# list
+require 'rubygems/version'
+require 'rubygems/dependency'
+require 'rubygems/spec_fetcher'
+require 'rubygems/platform'
+require 'rubygems/format'
+require 'rubygems/dependency_installer'
+require 'rubygems/uninstaller'
+require 'rubygems/specification'
+
+class Chef
+ class Provider
+ class Package
+ class Rubygems < Chef::Provider::Package
+ class GemEnvironment
+ # HACK: trigger gem config load early. Otherwise it can get lazy
+ # loaded during operations where we've set Gem.sources to an
+ # alternate value and overwrite it with the defaults.
+ Gem.configuration
+
+ DEFAULT_UNINSTALLER_OPTS = {:ignore => true, :executables => true}
+
+ ##
+ # The paths where rubygems should search for installed gems.
+ # Implemented by subclasses.
+ def gem_paths
+ raise NotImplementedError
+ end
+
+ ##
+ # A rubygems source index containing the list of gemspecs for all
+ # available gems in the gem installation.
+ # Implemented by subclasses
+ # === Returns
+ # Gem::SourceIndex
+ def gem_source_index
+ raise NotImplementedError
+ end
+
+ ##
+ # A rubygems specification object containing the list of gemspecs for all
+ # available gems in the gem installation.
+ # Implemented by subclasses
+ # For rubygems >= 1.8.0
+ # === Returns
+ # Gem::Specification
+ def gem_specification
+ raise NotImplementedError
+ end
+
+ ##
+ # Lists the installed versions of +gem_name+, constrained by the
+ # version spec in +gem_dep+
+ # === Arguments
+ # Gem::Dependency +gem_dep+ is a Gem::Dependency object, its version
+ # specification constrains which gems are returned.
+ # === Returns
+ # [Gem::Specification] an array of Gem::Specification objects
+ def installed_versions(gem_dep)
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.8.0')
+ gem_specification.find_all_by_name(gem_dep.name, gem_dep.requirement)
+ else
+ gem_source_index.search(gem_dep)
+ end
+ end
+
+ ##
+ # Yields to the provided block with rubygems' source list set to the
+ # list provided. Always resets the list when the block returns or
+ # raises an exception.
+ def with_gem_sources(*sources)
+ sources.compact!
+ original_sources = Gem.sources
+ Gem.sources = sources unless sources.empty?
+ yield
+ ensure
+ Gem.sources = original_sources
+ end
+
+ ##
+ # Determines the candidate version for a gem from a .gem file on disk
+ # and checks if it matches the version contraints in +gem_dependency+
+ # === Returns
+ # Gem::Version a singular gem version object is returned if the gem
+ # is available
+ # nil returns nil if the gem on disk doesn't match the
+ # version constraints for +gem_dependency+
+ def candidate_version_from_file(gem_dependency, source)
+ spec = Gem::Format.from_file_by_path(source).spec
+ if spec.satisfies_requirement?(gem_dependency)
+ logger.debug {"#{@new_resource} found candidate gem version #{spec.version} from local gem package #{source}"}
+ spec.version
+ else
+ # This is probably going to end badly...
+ logger.warn { "#{@new_resource} gem package #{source} does not satisfy the requirements #{gem_dependency.to_s}" }
+ nil
+ end
+ end
+
+ ##
+ # Finds the newest version that satisfies the constraints of
+ # +gem_dependency+. The version is determined from the cache or a
+ # round-trip to the server as needed. The architecture and gem
+ # sources will be set before making the query.
+ # === Returns
+ # Gem::Version a singular gem version object is returned if the gem
+ # is available
+ # nil returns nil if the gem could not be found
+ def candidate_version_from_remote(gem_dependency, *sources)
+ raise NotImplementedError
+ end
+
+ ##
+ # Find the newest gem version available from Gem.sources that satisfies
+ # the constraints of +gem_dependency+
+ def find_newest_remote_version(gem_dependency, *sources)
+ # DependencyInstaller sorts the results such that the last one is
+ # always the one it considers best.
+ spec_with_source = dependency_installer.find_gems_with_sources(gem_dependency).last
+
+ spec = spec_with_source && spec_with_source[0]
+ version = spec && spec_with_source[0].version
+ if version
+ logger.debug { "#{@new_resource} found gem #{spec.name} version #{version} for platform #{spec.platform} from #{spec_with_source[1]}" }
+ version
+ else
+ source_list = sources.compact.empty? ? "[#{Gem.sources.join(', ')}]" : "[#{sources.join(', ')}]"
+ logger.warn { "#{@new_resource} failed to find gem #{gem_dependency} from #{source_list}" }
+ nil
+ end
+ end
+
+ ##
+ # Installs a gem via the rubygems ruby API.
+ # === Options
+ # :sources rubygems servers to use
+ # Other options are passed to Gem::DependencyInstaller.new
+ def install(gem_dependency, options={})
+ with_gem_sources(*options.delete(:sources)) do
+ with_correct_verbosity do
+ dependency_installer(options).install(gem_dependency)
+ end
+ end
+ end
+
+ ##
+ # Uninstall the gem +gem_name+ via the rubygems ruby API. If
+ # +gem_version+ is provided, only that version will be uninstalled.
+ # Otherwise, all versions are uninstalled.
+ # === Options
+ # Options are passed to Gem::Uninstaller.new
+ def uninstall(gem_name, gem_version=nil, opts={})
+ gem_version ? opts[:version] = gem_version : opts[:all] = true
+ with_correct_verbosity do
+ uninstaller(gem_name, opts).uninstall
+ end
+ end
+
+ ##
+ # Set rubygems' user interaction to ConsoleUI or SilentUI depending
+ # on our current debug level
+ def with_correct_verbosity
+ Gem::DefaultUserInteraction.ui = Chef::Log.debug? ? Gem::ConsoleUI.new : Gem::SilentUI.new
+ yield
+ end
+
+ def dependency_installer(opts={})
+ Gem::DependencyInstaller.new(opts)
+ end
+
+ def uninstaller(gem_name, opts={})
+ Gem::Uninstaller.new(gem_name, DEFAULT_UNINSTALLER_OPTS.merge(opts))
+ end
+
+ private
+
+ def logger
+ Chef::Log.logger
+ end
+
+ end
+
+ class CurrentGemEnvironment < GemEnvironment
+
+ def gem_paths
+ Gem.path
+ end
+
+ def gem_source_index
+ Gem.source_index
+ end
+
+ def gem_specification
+ Gem::Specification
+ end
+
+ def candidate_version_from_remote(gem_dependency, *sources)
+ with_gem_sources(*sources) do
+ find_newest_remote_version(gem_dependency, *sources)
+ end
+ end
+
+ end
+
+ class AlternateGemEnvironment < GemEnvironment
+ JRUBY_PLATFORM = /(:?universal|x86_64|x86)\-java\-[0-9\.]+/
+
+ def self.gempath_cache
+ @gempath_cache ||= {}
+ end
+
+ def self.platform_cache
+ @platform_cache ||= {}
+ end
+
+ include Chef::Mixin::ShellOut
+
+ attr_reader :gem_binary_location
+
+ def initialize(gem_binary_location)
+ @gem_binary_location = gem_binary_location
+ end
+
+ def gem_paths
+ if self.class.gempath_cache.key?(@gem_binary_location)
+ self.class.gempath_cache[@gem_binary_location]
+ else
+ # shellout! is a fork/exec which won't work on windows
+ shell_style_paths = shell_out!("#{@gem_binary_location} env gempath").stdout
+ # on windows, the path separator is (usually? always?) semicolon
+ paths = shell_style_paths.split(::File::PATH_SEPARATOR).map { |path| path.strip }
+ self.class.gempath_cache[@gem_binary_location] = paths
+ end
+ end
+
+ def gem_source_index
+ @source_index ||= Gem::SourceIndex.from_gems_in(*gem_paths.map { |p| p + '/specifications' })
+ end
+
+ def gem_specification
+ # Only once, dirs calls a reset
+ unless @specification
+ Gem::Specification.dirs = gem_paths
+ @specification = Gem::Specification
+ end
+ @specification
+ end
+
+ ##
+ # Attempt to detect the correct platform settings for the target gem
+ # environment.
+ #
+ # In practice, this only makes a difference if different versions are
+ # available depending on platform, and only if the target gem
+ # environment has a radically different platform (i.e., jruby), so we
+ # just try to detect jruby and fall back to the current platforms
+ # (Gem.platforms) if we don't detect it.
+ #
+ # === Returns
+ # [String|Gem::Platform] returns an array of Gem::Platform-compatible
+ # objects, i.e., Strings that are valid for Gem::Platform or actual
+ # Gem::Platform objects.
+ def gem_platforms
+ if self.class.platform_cache.key?(@gem_binary_location)
+ self.class.platform_cache[@gem_binary_location]
+ else
+ gem_environment = shell_out!("#{@gem_binary_location} env").stdout
+ if jruby = gem_environment[JRUBY_PLATFORM]
+ self.class.platform_cache[@gem_binary_location] = ['ruby', Gem::Platform.new(jruby)]
+ else
+ self.class.platform_cache[@gem_binary_location] = Gem.platforms
+ end
+ end
+ end
+
+ def with_gem_platforms(*alt_gem_platforms)
+ alt_gem_platforms.flatten!
+ original_gem_platforms = Gem.platforms
+ Gem.platforms = alt_gem_platforms
+ yield
+ ensure
+ Gem.platforms = original_gem_platforms
+ end
+
+ def candidate_version_from_remote(gem_dependency, *sources)
+ with_gem_sources(*sources) do
+ with_gem_platforms(*gem_platforms) do
+ find_newest_remote_version(gem_dependency, *sources)
+ end
+ end
+ end
+
+ end
+
+ include Chef::Mixin::ShellOut
+
+ attr_reader :gem_env
+ attr_reader :cleanup_gem_env
+
+ def logger
+ Chef::Log.logger
+ end
+
+ include Chef::Mixin::GetSourceFromPackage
+
+ def initialize(new_resource, run_context=nil)
+ super
+ @cleanup_gem_env = true
+ if new_resource.gem_binary
+ if new_resource.options && new_resource.options.kind_of?(Hash)
+ msg = "options cannot be given as a hash when using an explicit gem_binary\n"
+ msg << "in #{new_resource} from #{new_resource.source_line}"
+ raise ArgumentError, msg
+ end
+ @gem_env = AlternateGemEnvironment.new(new_resource.gem_binary)
+ Chef::Log.debug("#{@new_resource} using gem '#{new_resource.gem_binary}'")
+ elsif is_omnibus? && (!@new_resource.instance_of? Chef::Resource::ChefGem)
+ # Opscode Omnibus - The ruby that ships inside omnibus is only used for Chef
+ # Default to installing somewhere more functional
+ if new_resource.options && new_resource.options.kind_of?(Hash)
+ msg = "options should be a string instead of a hash\n"
+ msg << "in #{new_resource} from #{new_resource.source_line}"
+ raise ArgumentError, msg
+ end
+ gem_location = find_gem_by_path
+ @new_resource.gem_binary gem_location
+ @gem_env = AlternateGemEnvironment.new(gem_location)
+ Chef::Log.debug("#{@new_resource} using gem '#{gem_location}'")
+ else
+ @gem_env = CurrentGemEnvironment.new
+ @cleanup_gem_env = false
+ Chef::Log.debug("#{@new_resource} using gem from running ruby environment")
+ end
+ end
+
+ def is_omnibus?
+ if RbConfig::CONFIG['bindir'] =~ %r!/opt/(opscode|chef)/embedded/bin!
+ Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}")
+ # Omnibus installs to a static path because of linking on unix, find it.
+ true
+ elsif RbConfig::CONFIG['bindir'].sub(/^[\w]:/, '') == "/opscode/chef/embedded/bin"
+ Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}")
+ # windows, with the drive letter removed
+ true
+ else
+ false
+ end
+ end
+
+ def find_gem_by_path
+ Chef::Log.debug("#{@new_resource} searching for 'gem' binary in path: #{ENV['PATH']}")
+ separator = ::File::ALT_SEPARATOR ? ::File::ALT_SEPARATOR : ::File::SEPARATOR
+ path_to_first_gem = ENV['PATH'].split(::File::PATH_SEPARATOR).select { |path| ::File.exists?(path + separator + "gem") }.first
+ raise Chef::Exceptions::FileNotFound, "Unable to find 'gem' binary in path: #{ENV['PATH']}" if path_to_first_gem.nil?
+ path_to_first_gem + separator + "gem"
+ end
+
+ def gem_dependency
+ Gem::Dependency.new(@new_resource.package_name, @new_resource.version)
+ end
+
+ def source_is_remote?
+ return true if @new_resource.source.nil?
+ scheme = URI.parse(@new_resource.source).scheme
+ # URI.parse gets confused by MS Windows paths with forward slashes.
+ scheme = nil if scheme =~ /^[a-z]$/
+ %w{http https}.include?(scheme)
+ end
+
+ def current_version
+ #raise 'todo'
+ # If one or more matching versions are installed, the newest of them
+ # is the current version
+ if !matching_installed_versions.empty?
+ gemspec = matching_installed_versions.last
+ logger.debug { "#{@new_resource} found installed gem #{gemspec.name} version #{gemspec.version} matching #{gem_dependency}"}
+ gemspec
+ # If no version matching the requirements exists, the latest installed
+ # version is the current version.
+ elsif !all_installed_versions.empty?
+ gemspec = all_installed_versions.last
+ logger.debug { "#{@new_resource} newest installed version of gem #{gemspec.name} is #{gemspec.version}" }
+ gemspec
+ else
+ logger.debug { "#{@new_resource} no installed version found for #{gem_dependency.to_s}"}
+ nil
+ end
+ end
+
+ def matching_installed_versions
+ @matching_installed_versions ||= @gem_env.installed_versions(gem_dependency)
+ end
+
+ def all_installed_versions
+ @all_installed_versions ||= begin
+ @gem_env.installed_versions(Gem::Dependency.new(gem_dependency.name, '>= 0'))
+ end
+ end
+
+ def gem_sources
+ @new_resource.source ? Array(@new_resource.source) : nil
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Package::GemPackage.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+ if current_spec = current_version
+ @current_resource.version(current_spec.version.to_s)
+ end
+ @current_resource
+ end
+
+ def cleanup_after_converge
+ if @cleanup_gem_env
+ logger.debug { "#{@new_resource} resetting gem environment to default" }
+ Gem.clear_paths
+ end
+ end
+
+ def candidate_version
+ @candidate_version ||= begin
+ if target_version_already_installed?
+ nil
+ elsif source_is_remote?
+ @gem_env.candidate_version_from_remote(gem_dependency, *gem_sources).to_s
+ else
+ @gem_env.candidate_version_from_file(gem_dependency, @new_resource.source).to_s
+ end
+ end
+ end
+
+ def target_version_already_installed?
+ return false unless @current_resource && @current_resource.version
+ return false if @current_resource.version.nil?
+
+ Gem::Requirement.new(@new_resource.version).satisfied_by?(Gem::Version.new(@current_resource.version))
+ end
+
+ ##
+ # Installs the gem, using either the gems API or shelling out to `gem`
+ # according to the following criteria:
+ # 1. Use gems API (Gem::DependencyInstaller) by default
+ # 2. shell out to `gem install` when a String of options is given
+ # 3. use gems API with options if a hash of options is given
+ def install_package(name, version)
+ if source_is_remote? && @new_resource.gem_binary.nil?
+ if @new_resource.options.nil?
+ @gem_env.install(gem_dependency, :sources => gem_sources)
+ elsif @new_resource.options.kind_of?(Hash)
+ options = @new_resource.options
+ options[:sources] = gem_sources
+ @gem_env.install(gem_dependency, options)
+ else
+ install_via_gem_command(name, version)
+ end
+ elsif @new_resource.gem_binary.nil?
+ @gem_env.install(@new_resource.source)
+ else
+ install_via_gem_command(name,version)
+ end
+ true
+ end
+
+ def gem_binary_path
+ @new_resource.gem_binary || 'gem'
+ end
+
+ def install_via_gem_command(name, version)
+ if @new_resource.source =~ /\.gem$/i
+ name = @new_resource.source
+ else
+ src = @new_resource.source && " --source=#{@new_resource.source} --source=http://rubygems.org"
+ end
+ if version
+ shell_out!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env=>nil)
+ else
+ shell_out!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri #{src}#{opts}", :env=>nil)
+ end
+ end
+
+ def upgrade_package(name, version)
+ install_package(name, version)
+ end
+
+ def remove_package(name, version)
+ if @new_resource.gem_binary.nil?
+ if @new_resource.options.nil?
+ @gem_env.uninstall(name, version)
+ elsif @new_resource.options.kind_of?(Hash)
+ @gem_env.uninstall(name, version, @new_resource.options)
+ else
+ uninstall_via_gem_command(name, version)
+ end
+ else
+ uninstall_via_gem_command(name, version)
+ end
+ end
+
+ def uninstall_via_gem_command(name, version)
+ if version
+ shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env=>nil)
+ else
+ shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env=>nil)
+ end
+ end
+
+ def purge_package(name, version)
+ remove_package(name, version)
+ end
+
+ private
+
+ def opts
+ expand_options(@new_resource.options)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/smartos.rb b/lib/chef/provider/package/smartos.rb
new file mode 100644
index 0000000000..a3ef1e5e86
--- /dev/null
+++ b/lib/chef/provider/package/smartos.rb
@@ -0,0 +1,84 @@
+#
+# Authors:: Trevor O (trevoro@joyent.com)
+# Bryan McLellan (btm@loftninjas.org)
+# Matthew Landauer (matthew@openaustralia.org)
+# Copyright:: Copyright (c) 2009 Bryan McLellan, Matthew Landauer
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Notes
+#
+# * Supports installing using a local package name
+# * Otherwise reverts to installing from the pkgsrc repositories URL
+
+require 'chef/provider/package'
+require 'chef/mixin/shell_out'
+require 'chef/resource/package'
+require 'chef/mixin/get_source_from_package'
+
+class Chef
+ class Provider
+ class Package
+ class SmartOS < Chef::Provider::Package
+ include Chef::Mixin::ShellOut
+ attr_accessor :is_virtual_package
+
+
+ def load_current_resource
+ Chef::Log.debug("#{@new_resource} loading current resource")
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+ @current_resource.version(nil)
+ check_package_state(@new_resource.package_name)
+ @current_resource # modified by check_package_state
+ end
+
+ def check_package_state(name)
+ Chef::Log.debug("#{@new_resource} checking package #{name}")
+ # XXX
+ version = nil
+ info = shell_out!("pkg_info -E \"#{name}*\"", :env => nil, :returns => [0,1])
+
+ if info.stdout
+ version = info.stdout[/^#{@new_resource.package_name}-(.+)/, 1]
+ end
+
+ if !version
+ @current_resource.version(nil)
+ else
+ @current_resource.version(version)
+ end
+ end
+
+ def install_package(name, version)
+ Chef::Log.debug("#{@new_resource} installing package #{name}-#{version}")
+ package = "#{name}-#{version}"
+ out = shell_out!("pkgin -y install #{package}", :env => nil)
+ end
+
+ def upgrade_package(name, version)
+ Chef::Log.debug("#{@new_resource} upgrading package #{name}-#{version}")
+ install_package(name, version)
+ end
+
+ def remove_package(name, version)
+ Chef::Log.debug("#{@new_resource} removing package #{name}-#{version}")
+ package = "#{name}-#{version}"
+ out = shell_out!("pkgin -y remove #{package}", :env => nil)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/solaris.rb b/lib/chef/provider/package/solaris.rb
new file mode 100644
index 0000000000..f502a0dc96
--- /dev/null
+++ b/lib/chef/provider/package/solaris.rb
@@ -0,0 +1,139 @@
+#
+# Author:: Toomas Pelberg (<toomasp@gmx.net>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+require 'chef/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+require 'chef/mixin/get_source_from_package'
+
+class Chef
+ class Provider
+ class Package
+ class Solaris < Chef::Provider::Package
+
+ include Chef::Mixin::GetSourceFromPackage
+
+ # def initialize(*args)
+ # super
+ # @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ # end
+ def define_resource_requirements
+ super
+ requirements.assert(:install) do |a|
+ a.assertion { @new_resource.source }
+ a.failure_message Chef::Exceptions::Package, "Source for package #{@new_resource.name} required for action install"
+ end
+ requirements.assert(:all_actions) do |a|
+ a.assertion { !@new_resource.source || @package_source_found }
+ a.failure_message Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}"
+ a.whyrun "would assume #{@new_resource.source} would be have previously been made available"
+ end
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+ @new_resource.version(nil)
+
+ if @new_resource.source
+ @package_source_found = ::File.exists?(@new_resource.source)
+ if @package_source_found
+ Chef::Log.debug("#{@new_resource} checking pkg status")
+ status = popen4("pkginfo -l -d #{@new_resource.source} #{@new_resource.package_name}") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ case line
+ when /VERSION:\s+(.+)/
+ @new_resource.version($1)
+ end
+ end
+ end
+ end
+ end
+
+ Chef::Log.debug("#{@new_resource} checking install state")
+ status = popen4("pkginfo -l #{@current_resource.package_name}") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ case line
+ when /VERSION:\s+(.+)/
+ Chef::Log.debug("#{@new_resource} version #{$1} is already installed")
+ @current_resource.version($1)
+ end
+ end
+ end
+
+ unless status.exitstatus == 0 || status.exitstatus == 1
+ raise Chef::Exceptions::Package, "pkginfo failed - #{status.inspect}!"
+ end
+
+ unless @current_resource.version.nil?
+ @current_resource.version(nil)
+ end
+
+ @current_resource
+ end
+
+ def candidate_version
+ return @candidate_version if @candidate_version
+ status = popen4("pkginfo -l -d #{@new_resource.source} #{new_resource.package_name}") do |pid, stdin, stdout, stderr|
+ stdout.each_line do |line|
+ case line
+ when /VERSION:\s+(.+)/
+ @candidate_version = $1
+ @new_resource.version($1)
+ Chef::Log.debug("#{@new_resource} setting install candidate version to #{@candidate_version}")
+ end
+ end
+ end
+ unless status.exitstatus == 0
+ raise Chef::Exceptions::Package, "pkginfo -l -d #{@new_resource.source} - #{status.inspect}!"
+ end
+ @candidate_version
+ end
+
+ def install_package(name, version)
+ Chef::Log.debug("#{@new_resource} package install options: #{@new_resource.options}")
+ if @new_resource.options.nil?
+ run_command_with_systems_locale(
+ :command => "pkgadd -n -d #{@new_resource.source} all"
+ )
+ Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}")
+ else
+ run_command_with_systems_locale(
+ :command => "pkgadd -n#{expand_options(@new_resource.options)} -d #{@new_resource.source} all"
+ )
+ Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}")
+ end
+ end
+
+ def remove_package(name, version)
+ if @new_resource.options.nil?
+ run_command_with_systems_locale(
+ :command => "pkgrm -n #{name}"
+ )
+ Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}")
+ else
+ run_command_with_systems_locale(
+ :command => "pkgrm -n#{expand_options(@new_resource.options)} #{name}"
+ )
+ Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}")
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/yum-dump.py b/lib/chef/provider/package/yum-dump.py
new file mode 100644
index 0000000000..99136eceec
--- /dev/null
+++ b/lib/chef/provider/package/yum-dump.py
@@ -0,0 +1,287 @@
+#
+# Author:: Matthew Kent (<mkent@magoazul.com>)
+# Copyright:: Copyright (c) 2009, 2011 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.
+#
+
+# yum-dump.py
+# Inspired by yumhelper.py by David Lutterkort
+#
+# Produce a list of installed, available and re-installable packages using yum
+# and dump the results to stdout.
+#
+# yum-dump invokes yum similarly to the command line interface which makes it
+# subject to most of the configuration paramaters in yum.conf. yum-dump will
+# also load yum plugins in the same manor as yum - these can affect the output.
+#
+# Can be run as non root, but that won't update the cache.
+#
+# Intended to support yum 2.x and 3.x
+
+import os
+import sys
+import time
+import yum
+import re
+import errno
+
+from yum import Errors
+from optparse import OptionParser
+from distutils import version
+
+YUM_PID_FILE='/var/run/yum.pid'
+
+# Seconds to wait for exclusive access to yum
+LOCK_TIMEOUT = 10
+
+YUM_VER = version.StrictVersion(yum.__version__)
+YUM_MAJOR = YUM_VER.version[0]
+
+if YUM_MAJOR > 3 or YUM_MAJOR < 2:
+ print >> sys.stderr, "yum-dump Error: Can't match supported yum version" \
+ " (%s)" % yum.__version__
+ sys.exit(1)
+
+# Required for Provides output
+if YUM_MAJOR == 2:
+ import rpm
+ import rpmUtils.miscutils
+
+def setup(yb, options):
+ # Only want our output
+ #
+ if YUM_MAJOR == 3:
+ try:
+ if YUM_VER >= version.StrictVersion("3.2.22"):
+ yb.preconf.errorlevel=0
+ yb.preconf.debuglevel=0
+
+ # initialize the config
+ yb.conf
+ else:
+ yb.doConfigSetup(errorlevel=0, debuglevel=0)
+ except yum.Errors.ConfigError, e:
+ # supresses an ignored exception at exit
+ yb.preconf = None
+ print >> sys.stderr, "yum-dump Config Error: %s" % e
+ return 1
+ except ValueError, e:
+ yb.preconf = None
+ print >> sys.stderr, "yum-dump Options Error: %s" % e
+ return 1
+ elif YUM_MAJOR == 2:
+ yb.doConfigSetup()
+
+ def __log(a,b): pass
+
+ yb.log = __log
+ yb.errorlog = __log
+
+ # Give Chef every possible package version, it can decide what to do with them
+ if YUM_MAJOR == 3:
+ yb.conf.showdupesfromrepos = True
+ elif YUM_MAJOR == 2:
+ yb.conf.setConfigOption('showdupesfromrepos', True)
+
+ # Optionally run only on cached repositories, but non root must use the cache
+ if os.geteuid() != 0:
+ if YUM_MAJOR == 3:
+ yb.conf.cache = True
+ elif YUM_MAJOR == 2:
+ yb.conf.setConfigOption('cache', True)
+ else:
+ if YUM_MAJOR == 3:
+ yb.conf.cache = options.cache
+ elif YUM_MAJOR == 2:
+ yb.conf.setConfigOption('cache', options.cache)
+
+ return 0
+
+def dump_packages(yb, list, output_provides):
+ packages = {}
+
+ if YUM_MAJOR == 2:
+ yb.doTsSetup()
+ yb.doRepoSetup()
+ yb.doSackSetup()
+
+ db = yb.doPackageLists(list)
+
+ for pkg in db.installed:
+ pkg.type = 'i'
+ packages[str(pkg)] = pkg
+
+ if YUM_VER >= version.StrictVersion("3.2.21"):
+ for pkg in db.available:
+ pkg.type = 'a'
+ packages[str(pkg)] = pkg
+
+ # These are both installed and available
+ for pkg in db.reinstall_available:
+ pkg.type = 'r'
+ packages[str(pkg)] = pkg
+ else:
+ # Old style method - no reinstall list
+ for pkg in yb.pkgSack.returnPackages():
+
+ if str(pkg) in packages:
+ if packages[str(pkg)].type == "i":
+ packages[str(pkg)].type = 'r'
+ continue
+
+ pkg.type = 'a'
+ packages[str(pkg)] = pkg
+
+ unique_packages = packages.values()
+
+ unique_packages.sort(lambda x, y: cmp(x.name, y.name))
+
+ for pkg in unique_packages:
+ if output_provides == "all" or \
+ (output_provides == "installed" and (pkg.type == "i" or pkg.type == "r")):
+
+ # yum 2 doesn't have provides_print, implement it ourselves using methods
+ # based on requires gathering in packages.py
+ if YUM_MAJOR == 2:
+ provlist = []
+
+ # Installed and available are gathered in different ways
+ if pkg.type == 'i' or pkg.type == 'r':
+ names = pkg.hdr[rpm.RPMTAG_PROVIDENAME]
+ flags = pkg.hdr[rpm.RPMTAG_PROVIDEFLAGS]
+ ver = pkg.hdr[rpm.RPMTAG_PROVIDEVERSION]
+ if names is not None:
+ tmplst = zip(names, flags, ver)
+
+ for (n, f, v) in tmplst:
+ prov = rpmUtils.miscutils.formatRequire(n, v, f)
+ provlist.append(prov)
+ # This is slow :(
+ elif pkg.type == 'a':
+ for prcoTuple in pkg.returnPrco('provides'):
+ prcostr = pkg.prcoPrintable(prcoTuple)
+ provlist.append(prcostr)
+
+ provides = provlist
+ else:
+ provides = pkg.provides_print
+ else:
+ provides = "[]"
+
+ print '%s %s %s %s %s %s %s %s' % (
+ pkg.name,
+ pkg.epoch,
+ pkg.version,
+ pkg.release,
+ pkg.arch,
+ provides,
+ pkg.type,
+ pkg.repoid )
+
+ return 0
+
+def yum_dump(options):
+ lock_obtained = False
+
+ yb = yum.YumBase()
+
+ status = setup(yb, options)
+ if status != 0:
+ return status
+
+ if options.output_options:
+ print "[option installonlypkgs] %s" % " ".join(yb.conf.installonlypkgs)
+
+ # Non root can't handle locking on rhel/centos 4
+ if os.geteuid() != 0:
+ return dump_packages(yb, options.package_list, options.output_provides)
+
+ # Wrap the collection and output of packages in yum's global lock to prevent
+ # any inconsistencies.
+ try:
+ # Spin up to LOCK_TIMEOUT
+ countdown = LOCK_TIMEOUT
+ while True:
+ try:
+ yb.doLock(YUM_PID_FILE)
+ lock_obtained = True
+ except Errors.LockError, e:
+ time.sleep(1)
+ countdown -= 1
+ if countdown == 0:
+ print >> sys.stderr, "yum-dump Locking Error! Couldn't obtain an " \
+ "exclusive yum lock in %d seconds. Giving up." % LOCK_TIMEOUT
+ return 200
+ else:
+ break
+
+ return dump_packages(yb, options.package_list, options.output_provides)
+
+ # Ensure we clear the lock and cleanup any resources
+ finally:
+ try:
+ yb.closeRpmDB()
+ if lock_obtained == True:
+ yb.doUnlock(YUM_PID_FILE)
+ except Errors.LockError, e:
+ print >> sys.stderr, "yum-dump Unlock Error: %s" % e
+ return 200
+
+def main():
+ usage = "Usage: %prog [options]\n" + \
+ "Output a list of installed, available and re-installable packages via yum"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-C", "--cache",
+ action="store_true", dest="cache", default=False,
+ help="run entirely from cache, don't update cache")
+ parser.add_option("-o", "--options",
+ action="store_true", dest="output_options", default=False,
+ help="output select yum options useful to Chef")
+ parser.add_option("-p", "--installed-provides",
+ action="store_const", const="installed", dest="output_provides", default="none",
+ help="output Provides for installed packages, big/wide output")
+ parser.add_option("-P", "--all-provides",
+ action="store_const", const="all", dest="output_provides", default="none",
+ help="output Provides for all package, slow, big/wide output")
+ parser.add_option("-i", "--installed",
+ action="store_const", const="installed", dest="package_list", default="all",
+ help="output only installed packages")
+ parser.add_option("-a", "--available",
+ action="store_const", const="available", dest="package_list", default="all",
+ help="output only available and re-installable packages")
+
+ (options, args) = parser.parse_args()
+
+ try:
+ return yum_dump(options)
+
+ except yum.Errors.RepoError, e:
+ print >> sys.stderr, "yum-dump Repository Error: %s" % e
+ return 1
+
+ except yum.Errors.YumBaseError, e:
+ print >> sys.stderr, "yum-dump General Error: %s" % e
+ return 1
+
+try:
+ status = main()
+# Suppress a nasty broken pipe error when output is piped to utilities like 'head'
+except IOError, e:
+ if e.errno == errno.EPIPE:
+ sys.exit(1)
+ else:
+ raise
+
+sys.exit(status)
diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb
new file mode 100644
index 0000000000..9048048b83
--- /dev/null
+++ b/lib/chef/provider/package/yum.rb
@@ -0,0 +1,1214 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+require 'singleton'
+require 'chef/mixin/get_source_from_package'
+
+
+class Chef
+ class Provider
+ class Package
+ class Yum < Chef::Provider::Package
+
+ class RPMUtils
+ class << self
+
+ # RPM::Version version_parse equivalent
+ def version_parse(evr)
+ return if evr.nil?
+
+ epoch = nil
+ # assume this is a version
+ version = evr
+ release = nil
+
+ lead = 0
+ tail = evr.size
+
+ if evr =~ %r{^([\d]+):}
+ epoch = $1.to_i
+ lead = $1.length + 1
+ elsif evr[0].ord == ":".ord
+ epoch = 0
+ lead = 1
+ end
+
+ if evr =~ %r{:?.*-(.*)$}
+ release = $1
+ tail = evr.length - release.length - lead - 1
+
+ if release.empty?
+ release = nil
+ end
+ end
+
+ version = evr[lead,tail]
+ if version.empty?
+ version = nil
+ end
+
+ [ epoch, version, release ]
+ end
+
+ # verify
+ def isalnum(x)
+ isalpha(x) or isdigit(x)
+ end
+
+ def isalpha(x)
+ v = x.ord
+ (v >= 65 and v <= 90) or (v >= 97 and v <= 122)
+ end
+
+ def isdigit(x)
+ v = x.ord
+ v >= 48 and v <= 57
+ end
+
+ # based on the reference spec in lib/rpmvercmp.c in rpm 4.9.0
+ def rpmvercmp(x, y)
+ # easy! :)
+ return 0 if x == y
+
+ if x.nil?
+ x = ""
+ end
+
+ if y.nil?
+ y = ""
+ end
+
+ # not so easy :(
+ #
+ # takes 2 strings like
+ #
+ # x = "1.20.b18.el5"
+ # y = "1.20.b17.el5"
+ #
+ # breaks into purely alpha and numeric segments and compares them using
+ # some rules
+ #
+ # * 10 > 1
+ # * 1 > a
+ # * z > a
+ # * Z > A
+ # * z > Z
+ # * leading zeros are ignored
+ # * separators (periods, commas) are ignored
+ # * "1.20.b18.el5.extrastuff" > "1.20.b18.el5"
+
+ x_pos = 0 # overall string element reference position
+ x_pos_max = x.length - 1 # number of elements in string, starting from 0
+ x_seg_pos = 0 # segment string element reference position
+ x_comp = nil # segment to compare
+
+ y_pos = 0
+ y_seg_pos = 0
+ y_pos_max = y.length - 1
+ y_comp = nil
+
+ while (x_pos <= x_pos_max and y_pos <= y_pos_max)
+ # first we skip over anything non alphanumeric
+ while (x_pos <= x_pos_max) and (isalnum(x[x_pos]) == false)
+ x_pos += 1 # +1 over pos_max if end of string
+ end
+ while (y_pos <= y_pos_max) and (isalnum(y[y_pos]) == false)
+ y_pos += 1
+ end
+
+ # if we hit the end of either we are done matching segments
+ if (x_pos == x_pos_max + 1) or (y_pos == y_pos_max + 1)
+ break
+ end
+
+ # we are now at the start of a alpha or numeric segment
+ x_seg_pos = x_pos
+ y_seg_pos = y_pos
+
+ # grab segment so we can compare them
+ if isdigit(x[x_seg_pos].ord)
+ x_seg_is_num = true
+
+ # already know it's a digit
+ x_seg_pos += 1
+
+ # gather up our digits
+ while (x_seg_pos <= x_pos_max) and isdigit(x[x_seg_pos])
+ x_seg_pos += 1
+ end
+ # copy the segment but not the unmatched character that x_seg_pos will
+ # refer to
+ x_comp = x[x_pos,x_seg_pos - x_pos]
+
+ while (y_seg_pos <= y_pos_max) and isdigit(y[y_seg_pos])
+ y_seg_pos += 1
+ end
+ y_comp = y[y_pos,y_seg_pos - y_pos]
+ else
+ # we are comparing strings
+ x_seg_is_num = false
+
+ while (x_seg_pos <= x_pos_max) and isalpha(x[x_seg_pos])
+ x_seg_pos += 1
+ end
+ x_comp = x[x_pos,x_seg_pos - x_pos]
+
+ while (y_seg_pos <= y_pos_max) and isalpha(y[y_seg_pos])
+ y_seg_pos += 1
+ end
+ y_comp = y[y_pos,y_seg_pos - y_pos]
+ end
+
+ # if y_seg_pos didn't advance in the above loop it means the segments are
+ # different types
+ if y_pos == y_seg_pos
+ # numbers always win over letters
+ return x_seg_is_num ? 1 : -1
+ end
+
+ # move the ball forward before we mess with the segments
+ x_pos += x_comp.length # +1 over pos_max if end of string
+ y_pos += y_comp.length
+
+ # we are comparing numbers - simply convert them
+ if x_seg_is_num
+ x_comp = x_comp.to_i
+ y_comp = y_comp.to_i
+ end
+
+ # compares ints or strings
+ # don't return if equal - try the next segment
+ if x_comp > y_comp
+ return 1
+ elsif x_comp < y_comp
+ return -1
+ end
+
+ # if we've reached here than the segments are the same - try again
+ end
+
+ # we must have reached the end of one or both of the strings and they
+ # matched up until this point
+
+ # segments matched completely but the segment separators were different -
+ # rpm reference code treats these as equal.
+ if (x_pos == x_pos_max + 1) and (y_pos == y_pos_max + 1)
+ return 0
+ end
+
+ # the most unprocessed characters left wins
+ if (x_pos_max - x_pos) > (y_pos_max - y_pos)
+ return 1
+ else
+ return -1
+ end
+ end
+
+ end # self
+ end # RPMUtils
+
+ class RPMVersion
+ include Comparable
+
+ def initialize(*args)
+ if args.size == 1
+ @e, @v, @r = RPMUtils.version_parse(args[0])
+ elsif args.size == 3
+ @e = args[0].to_i
+ @v = args[1]
+ @r = args[2]
+ else
+ raise ArgumentError, "Expecting either 'epoch-version-release' or 'epoch, " +
+ "version, release'"
+ end
+ end
+ attr_reader :e, :v, :r
+ alias :epoch :e
+ alias :version :v
+ alias :release :r
+
+ def self.parse(*args)
+ self.new(*args)
+ end
+
+ def <=>(y)
+ compare_versions(y)
+ end
+
+ def compare(y)
+ compare_versions(y, false)
+ end
+
+ def partial_compare(y)
+ compare_versions(y, true)
+ end
+
+ # RPM::Version rpm_version_to_s equivalent
+ def to_s
+ if @r.nil?
+ @v
+ else
+ "#{@v}-#{@r}"
+ end
+ end
+
+ def evr
+ "#{@e}:#{@v}-#{@r}"
+ end
+
+ private
+
+ # Rough RPM::Version rpm_version_cmp equivalent - except much slower :)
+ #
+ # partial lets epoch and version segment equality be good enough to return equal, eg:
+ #
+ # 2:1.2-1 == 2:1.2
+ # 2:1.2-1 == 2:
+ #
+ def compare_versions(y, partial=false)
+ x = self
+
+ # compare epoch
+ if (x.e.nil? == false and x.e > 0) and y.e.nil?
+ return 1
+ elsif x.e.nil? and (y.e.nil? == false and y.e > 0)
+ return -1
+ elsif x.e.nil? == false and y.e.nil? == false
+ if x.e < y.e
+ return -1
+ elsif x.e > y.e
+ return 1
+ end
+ end
+
+ # compare version
+ if partial and (x.v.nil? or y.v.nil?)
+ return 0
+ elsif x.v.nil? == false and y.v.nil?
+ return 1
+ elsif x.v.nil? and y.v.nil? == false
+ return -1
+ elsif x.v.nil? == false and y.v.nil? == false
+ cmp = RPMUtils.rpmvercmp(x.v, y.v)
+ return cmp if cmp != 0
+ end
+
+ # compare release
+ if partial and (x.r.nil? or y.r.nil?)
+ return 0
+ elsif x.r.nil? == false and y.r.nil?
+ return 1
+ elsif x.r.nil? and y.r.nil? == false
+ return -1
+ elsif x.r.nil? == false and y.r.nil? == false
+ cmp = RPMUtils.rpmvercmp(x.r, y.r)
+ return cmp
+ end
+
+ return 0
+ end
+ end
+
+ class RPMPackage
+ include Comparable
+
+ def initialize(*args)
+ if args.size == 4
+ @n = args[0]
+ @version = RPMVersion.new(args[1])
+ @a = args[2]
+ @provides = args[3]
+ elsif args.size == 6
+ @n = args[0]
+ e = args[1].to_i
+ v = args[2]
+ r = args[3]
+ @version = RPMVersion.new(e,v,r)
+ @a = args[4]
+ @provides = args[5]
+ else
+ raise ArgumentError, "Expecting either 'name, epoch-version-release, arch, provides' " +
+ "or 'name, epoch, version, release, arch, provides'"
+ end
+
+ # We always have one, ourselves!
+ if @provides.empty?
+ @provides = [ RPMProvide.new(@n, @version.evr, :==) ]
+ end
+ end
+ attr_reader :n, :a, :version, :provides
+ alias :name :n
+ alias :arch :a
+
+ def <=>(y)
+ compare(y)
+ end
+
+ def compare(y)
+ x = self
+
+ # easy! :)
+ return 0 if x.nevra == y.nevra
+
+ # compare name
+ if x.n.nil? == false and y.n.nil?
+ return 1
+ elsif x.n.nil? and y.n.nil? == false
+ return -1
+ elsif x.n.nil? == false and y.n.nil? == false
+ if x.n < y.n
+ return -1
+ elsif x.n > y.n
+ return 1
+ end
+ end
+
+ # compare version
+ if x.version > y.version
+ return 1
+ elsif x.version < y.version
+ return -1
+ end
+
+ # compare arch
+ if x.a.nil? == false and y.a.nil?
+ return 1
+ elsif x.a.nil? and y.a.nil? == false
+ return -1
+ elsif x.a.nil? == false and y.a.nil? == false
+ if x.a < y.a
+ return -1
+ elsif x.a > y.a
+ return 1
+ end
+ end
+
+ return 0
+ end
+
+ def to_s
+ nevra
+ end
+
+ def nevra
+ "#{@n}-#{@version.evr}.#{@a}"
+ end
+ end
+
+ # Simple implementation from rpm and ruby-rpm reference code
+ class RPMDependency
+ def initialize(*args)
+ if args.size == 3
+ @name = args[0]
+ @version = RPMVersion.new(args[1])
+ # Our requirement to other dependencies
+ @flag = args[2] || :==
+ elsif args.size == 5
+ @name = args[0]
+ e = args[1].to_i
+ v = args[2]
+ r = args[3]
+ @version = RPMVersion.new(e,v,r)
+ @flag = args[4] || :==
+ else
+ raise ArgumentError, "Expecting either 'name, epoch-version-release, flag' or " +
+ "'name, epoch, version, release, flag'"
+ end
+ end
+ attr_reader :name, :version, :flag
+
+ # Parses 2 forms:
+ #
+ # "mtr >= 2:0.71-3.0"
+ # "mta"
+ def self.parse(string)
+ if string =~ %r{^(\S+)\s+(>|>=|=|==|<=|<)\s+(\S+)$}
+ name = $1
+ if $2 == "="
+ flag = :==
+ else
+ flag = :"#{$2}"
+ end
+ version = $3
+
+ return self.new(name, version, flag)
+ else
+ name = string
+ return self.new(name, nil, nil)
+ end
+ end
+
+ # Test if another RPMDependency satisfies our requirements
+ def satisfy?(y)
+ unless y.kind_of?(RPMDependency)
+ raise ArgumentError, "Expecting an RPMDependency object"
+ end
+
+ x = self
+
+ # Easy!
+ if x.name != y.name
+ return false
+ end
+
+ # Partial compare
+ #
+ # eg: x.version 2.3 == y.version 2.3-1
+ sense = x.version.partial_compare(y.version)
+
+ # Thanks to rpmdsCompare() rpmds.c
+ if sense < 0 and (x.flag == :> || x.flag == :>=) || (y.flag == :<= || y.flag == :<)
+ return true
+ elsif sense > 0 and (x.flag == :< || x.flag == :<=) || (y.flag == :>= || y.flag == :>)
+ return true
+ elsif sense == 0 and (
+ ((x.flag == :== or x.flag == :<= or x.flag == :>=) and (y.flag == :== or y.flag == :<= or y.flag == :>=)) or
+ (x.flag == :< and y.flag == :<) or
+ (x.flag == :> and y.flag == :>)
+ )
+ return true
+ end
+
+ return false
+ end
+ end
+
+ class RPMProvide < RPMDependency; end
+ class RPMRequire < RPMDependency; end
+
+ class RPMDbPackage < RPMPackage
+ # <rpm parts>, installed, available
+ def initialize(*args)
+ @repoid = args.pop
+ # state
+ @available = args.pop
+ @installed = args.pop
+ super(*args)
+ end
+ attr_reader :repoid, :available, :installed
+ end
+
+ # Simple storage for RPMPackage objects - keeps them unique and sorted
+ class RPMDb
+ def initialize
+ # package name => [ RPMPackage, RPMPackage ] of different versions
+ @rpms = Hash.new
+ # package nevra => RPMPackage for lookups
+ @index = Hash.new
+ # provide name (aka feature) => [RPMPackage, RPMPackage] each providing this feature
+ @provides = Hash.new
+ # RPMPackages listed as available
+ @available = Set.new
+ # RPMPackages listed as installed
+ @installed = Set.new
+ end
+
+ def [](package_name)
+ self.lookup(package_name)
+ end
+
+ # Lookup package_name and return a descending array of package objects
+ def lookup(package_name)
+ pkgs = @rpms[package_name]
+ if pkgs
+ return pkgs.sort.reverse
+ else
+ return nil
+ end
+ end
+
+ def lookup_provides(provide_name)
+ @provides[provide_name]
+ end
+
+ # Using the package name as a key, and nevra for an index, keep a unique list of packages.
+ # The available/installed state can be overwritten for existing packages.
+ def push(*args)
+ args.flatten.each do |new_rpm|
+ unless new_rpm.kind_of?(RPMDbPackage)
+ raise ArgumentError, "Expecting an RPMDbPackage object"
+ end
+
+ @rpms[new_rpm.n] ||= Array.new
+
+ # we may already have this one, like when the installed list is refreshed
+ idx = @index[new_rpm.nevra]
+ if idx
+ # grab the existing package if it's not
+ curr_rpm = idx
+ else
+ @rpms[new_rpm.n] << new_rpm
+
+ new_rpm.provides.each do |provide|
+ @provides[provide.name] ||= Array.new
+ @provides[provide.name] << new_rpm
+ end
+
+ curr_rpm = new_rpm
+ end
+
+ # Track the nevra -> RPMPackage association to avoid having to compare versions
+ # with @rpms[new_rpm.n] on the next round
+ @index[new_rpm.nevra] = curr_rpm
+
+ # these are overwritten for existing packages
+ if new_rpm.available
+ @available << curr_rpm
+ end
+ if new_rpm.installed
+ @installed << curr_rpm
+ end
+ end
+ end
+
+ def <<(*args)
+ self.push(args)
+ end
+
+ def clear
+ @rpms.clear
+ @index.clear
+ @provides.clear
+ clear_available
+ clear_installed
+ end
+
+ def clear_available
+ @available.clear
+ end
+
+ def clear_installed
+ @installed.clear
+ end
+
+ def size
+ @rpms.size
+ end
+ alias :length :size
+
+ def available_size
+ @available.size
+ end
+
+ def installed_size
+ @installed.size
+ end
+
+ def available?(package)
+ @available.include?(package)
+ end
+
+ def installed?(package)
+ @installed.include?(package)
+ end
+
+ def whatprovides(rpmdep)
+ unless rpmdep.kind_of?(RPMDependency)
+ raise ArgumentError, "Expecting an RPMDependency object"
+ end
+
+ what = []
+
+ packages = lookup_provides(rpmdep.name)
+ if packages
+ packages.each do |pkg|
+ pkg.provides.each do |provide|
+ if provide.satisfy?(rpmdep)
+ what << pkg
+ end
+ end
+ end
+ end
+
+ return what
+ end
+ end
+
+ # Cache for our installed and available packages, pulled in from yum-dump.py
+ class YumCache
+ include Chef::Mixin::Command
+ include Singleton
+
+ def initialize
+ @rpmdb = RPMDb.new
+
+ # Next time @rpmdb is accessed:
+ # :all - Trigger a run of "yum-dump.py --options --installed-provides", updates
+ # yum's cache and parses options from /etc/yum.conf. Pulls in Provides
+ # dependency data for installed packages only - this data is slow to
+ # gather.
+ # :provides - Same as :all but pulls in Provides data for available packages as well.
+ # Used as a last resort when we can't find a Provides match.
+ # :installed - Trigger a run of "yum-dump.py --installed", only reads the local rpm
+ # db. Used between client runs for a quick refresh.
+ # :none - Do nothing, a call to one of the reload methods is required.
+ @next_refresh = :all
+
+ @allow_multi_install = []
+
+ # these are for subsequent runs if we are on an interval
+ Chef::Client.when_run_starts do
+ YumCache.instance.reload
+ end
+ end
+
+ # Cache management
+ #
+
+ def refresh
+ case @next_refresh
+ when :none
+ return nil
+ when :installed
+ reset_installed
+ # fast
+ opts=" --installed"
+ when :all
+ reset
+ # medium
+ opts=" --options --installed-provides"
+ when :provides
+ reset
+ # slow!
+ opts=" --options --all-provides"
+ else
+ raise ArgumentError, "Unexpected value in next_refresh: #{@next_refresh}"
+ end
+
+ one_line = false
+ error = nil
+
+ helper = ::File.join(::File.dirname(__FILE__), 'yum-dump.py')
+
+ status = popen4("/usr/bin/python #{helper}#{opts}", :waitlast => true) do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ one_line = true
+
+ line.chomp!
+
+ if line =~ %r{\[option (.*)\] (.*)}
+ if $1 == "installonlypkgs"
+ @allow_multi_install = $2.split
+ else
+ raise Chef::Exceptions::Package, "Strange, unknown option line '#{line}' from yum-dump.py"
+ end
+ next
+ end
+
+ if line =~ %r{^(\S+) ([0-9]+) (\S+) (\S+) (\S+) \[(.*)\] ([i,a,r]) (\S+)$}
+ name = $1
+ epoch = $2
+ version = $3
+ release = $4
+ arch = $5
+ provides = parse_provides($6)
+ type = $7
+ repoid = $8
+ else
+ Chef::Log.warn("Problem parsing line '#{line}' from yum-dump.py! " +
+ "Please check your yum configuration.")
+ next
+ end
+
+ case type
+ when "i"
+ # if yum-dump was called with --installed this may not be true, but it's okay
+ # since we don't touch the @available Set in reload_installed
+ available = false
+ installed = true
+ when "a"
+ available = true
+ installed = false
+ when "r"
+ available = true
+ installed = true
+ end
+
+ pkg = RPMDbPackage.new(name, epoch, version, release, arch, provides, installed, available, repoid)
+ @rpmdb << pkg
+ end
+
+ error = stderr.readlines
+ end
+
+ if status.exitstatus != 0
+ raise Chef::Exceptions::Package, "Yum failed - #{status.inspect} - returns: #{error}"
+ else
+ unless one_line
+ Chef::Log.warn("Odd, no output from yum-dump.py. Please check " +
+ "your yum configuration.")
+ end
+ end
+
+ # A reload method must be called before the cache is altered
+ @next_refresh = :none
+ end
+
+ def reload
+ @next_refresh = :all
+ end
+
+ def reload_installed
+ @next_refresh = :installed
+ end
+
+ def reload_provides
+ @next_refresh = :provides
+ end
+
+ def reset
+ @rpmdb.clear
+ end
+
+ def reset_installed
+ @rpmdb.clear_installed
+ end
+
+ # Querying the cache
+ #
+
+ # Check for package by name or name+arch
+ def package_available?(package_name)
+ refresh
+
+ if @rpmdb.lookup(package_name)
+ return true
+ else
+ if package_name =~ %r{^(.*)\.(.*)$}
+ pkg_name = $1
+ pkg_arch = $2
+
+ if matches = @rpmdb.lookup(pkg_name)
+ matches.each do |m|
+ return true if m.arch == pkg_arch
+ end
+ end
+ end
+ end
+
+ return false
+ end
+
+ # Returns a array of packages satisfying an RPMDependency
+ def packages_from_require(rpmdep)
+ refresh
+ @rpmdb.whatprovides(rpmdep)
+ end
+
+ # Check if a package-version.arch is available to install
+ def version_available?(package_name, desired_version, arch=nil)
+ version(package_name, arch, true, false) do |v|
+ return true if desired_version == v
+ end
+
+ return false
+ end
+
+ # Return the source repository for a package-version.arch
+ def package_repository(package_name, desired_version, arch=nil)
+ package(package_name, arch, true, false) do |pkg|
+ return pkg.repoid if desired_version == pkg.version.to_s
+ end
+
+ return nil
+ end
+
+ # Return the latest available version for a package.arch
+ def available_version(package_name, arch=nil)
+ version(package_name, arch, true, false)
+ end
+ alias :candidate_version :available_version
+
+ # Return the currently installed version for a package.arch
+ def installed_version(package_name, arch=nil)
+ version(package_name, arch, false, true)
+ end
+
+ # Return an array of packages allowed to be installed multiple times, such as the kernel
+ def allow_multi_install
+ refresh
+ @allow_multi_install
+ end
+
+ private
+
+ def version(package_name, arch=nil, is_available=false, is_installed=false)
+ package(package_name, arch, is_available, is_installed) do |pkg|
+ if block_given?
+ yield pkg.version.to_s
+ else
+ # first match is latest version
+ return pkg.version.to_s
+ end
+ end
+
+ if block_given?
+ return self
+ else
+ return nil
+ end
+ end
+
+ def package(package_name, arch=nil, is_available=false, is_installed=false)
+ refresh
+ packages = @rpmdb[package_name]
+ if packages
+ packages.each do |pkg|
+ if is_available
+ next unless @rpmdb.available?(pkg)
+ end
+ if is_installed
+ next unless @rpmdb.installed?(pkg)
+ end
+ if arch
+ next unless pkg.arch == arch
+ end
+
+ if block_given?
+ yield pkg
+ else
+ # first match is latest version
+ return pkg
+ end
+ end
+ end
+
+ if block_given?
+ return self
+ else
+ return nil
+ end
+ end
+
+ # Parse provides from yum-dump.py output
+ def parse_provides(string)
+ ret = []
+ # ['atk = 1.12.2-1.fc6', 'libatk-1.0.so.0']
+ string.split(", ").each do |seg|
+ # 'atk = 1.12.2-1.fc6'
+ if seg =~ %r{^'(.*)'$}
+ ret << RPMProvide.parse($1)
+ end
+ end
+
+ return ret
+ end
+
+ end # YumCache
+
+ include Chef::Mixin::GetSourceFromPackage
+
+ def initialize(new_resource, run_context)
+ super
+
+ @yum = YumCache.instance
+ end
+
+ # Extra attributes
+ #
+
+ def arch
+ if @new_resource.respond_to?("arch")
+ @new_resource.arch
+ else
+ nil
+ end
+ end
+
+ def flush_cache
+ if @new_resource.respond_to?("flush_cache")
+ @new_resource.flush_cache
+ else
+ { :before => false, :after => false }
+ end
+ end
+
+ def allow_downgrade
+ if @new_resource.respond_to?("allow_downgrade")
+ @new_resource.allow_downgrade
+ else
+ false
+ end
+ end
+
+ # Helpers
+ #
+
+ def yum_arch
+ arch ? ".#{arch}" : nil
+ end
+
+ def yum_command(command)
+ status, stdout, stderr = output_of_command(command, {})
+
+ # This is fun: rpm can encounter errors in the %post/%postun scripts which aren't
+ # considered fatal - meaning the rpm is still successfully installed. These issue
+ # cause yum to emit a non fatal warning but still exit(1). As there's currently no
+ # way to suppress this behavior and an exit(1) will break a Chef run we make an
+ # effort to trap these and re-run the same install command - it will either fail a
+ # second time or succeed.
+ #
+ # A cleaner solution would have to be done in python and better hook into
+ # yum/rpm to handle exceptions as we see fit.
+ if status.exitstatus == 1
+ stdout.each_line do |l|
+ # rpm-4.4.2.3 lib/psm.c line 2182
+ if l =~ %r{^error: %(post|postun)\(.*\) scriptlet failed, exit status \d+$}
+ Chef::Log.warn("#{@new_resource} caught non-fatal scriptlet issue: \"#{l}\". Can't trust yum exit status " +
+ "so running install again to verify.")
+ status, stdout, stderr = output_of_command(command, {})
+ break
+ end
+ end
+ end
+
+ if status.exitstatus > 0
+ command_output = "STDOUT: #{stdout}"
+ command_output << "STDERR: #{stderr}"
+ handle_command_failures(status, command_output, {})
+ end
+ end
+
+ # Standard Provider methods for Parent
+ #
+
+ def load_current_resource
+ if flush_cache[:before]
+ @yum.reload
+ end
+
+ # At this point package_name could be:
+ #
+ # 1) a package name, eg: "foo"
+ # 2) a package name.arch, eg: "foo.i386"
+ # 3) or a dependency, eg: "foo >= 1.1"
+
+ # Check if we have name or name+arch which has a priority over a dependency
+ unless @yum.package_available?(@new_resource.package_name)
+ # If they aren't in the installed packages they could be a dependency
+ parse_dependency
+ end
+
+ # Don't overwrite an existing arch
+ unless arch
+ parse_arch
+ end
+
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+
+ if @new_resource.source
+ unless ::File.exists?(@new_resource.source)
+ raise Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}"
+ end
+
+ Chef::Log.debug("#{@new_resource} checking rpm status")
+ status = popen4("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ case line
+ when /([\w\d_.-]+)\s([\w\d_.-]+)/
+ @current_resource.package_name($1)
+ @new_resource.version($2)
+ end
+ end
+ end
+ end
+
+ if @new_resource.version
+ new_resource = "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch}"
+ else
+ new_resource = "#{@new_resource.package_name}#{yum_arch}"
+ end
+
+ Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}")
+
+ installed_version = @yum.installed_version(@new_resource.package_name, arch)
+ @current_resource.version(installed_version)
+
+ @candidate_version = @yum.candidate_version(@new_resource.package_name, arch)
+
+ Chef::Log.debug("#{@new_resource} installed version: #{installed_version || "(none)"} candidate version: " +
+ "#{@candidate_version || "(none)"}")
+
+ @current_resource
+ end
+
+ def install_package(name, version)
+ if @new_resource.source
+ yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}")
+ else
+ # Work around yum not exiting with an error if a package doesn't exist for CHEF-2062
+ if @yum.version_available?(name, version, arch)
+ method = "install"
+ log_method = "installing"
+
+ # More Yum fun:
+ #
+ # yum install of an old name+version will exit(1)
+ # yum install of an old name+version+arch will exit(0) for some reason
+ #
+ # Some packages can be installed multiple times like the kernel
+ unless @yum.allow_multi_install.include?(name)
+ if RPMVersion.parse(@current_resource.version) > RPMVersion.parse(version)
+ # Unless they want this...
+ if allow_downgrade
+ method = "downgrade"
+ log_method = "downgrading"
+ else
+ # we bail like yum when the package is older
+ raise Chef::Exceptions::Package, "Installed package #{name}-#{@current_resource.version} is newer " +
+ "than candidate package #{name}-#{version}"
+ end
+ end
+ end
+
+ repo = @yum.package_repository(name, version, arch)
+ Chef::Log.info("#{@new_resource} #{log_method} #{name}-#{version}#{yum_arch} from #{repo} repository")
+
+ yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{name}-#{version}#{yum_arch}")
+ else
+ raise Chef::Exceptions::Package, "Version #{version} of #{name} not found. Did you specify both version " +
+ "and release? (version-release, e.g. 1.84-10.fc6)"
+ end
+ end
+
+ if flush_cache[:after]
+ @yum.reload
+ else
+ @yum.reload_installed
+ end
+ end
+
+ # Keep upgrades from trying to install an older candidate version. Can happen when a new
+ # version is installed then removed from a repository, now the older available version
+ # shows up as a viable install candidate.
+ #
+ # Can be done in upgrade_package but an upgraded from->to log message slips out
+ #
+ # Hacky - better overall solution? Custom compare in Package provider?
+ def action_upgrade
+ # Could be uninstalled or have no candidate
+ if @current_resource.version.nil? || candidate_version.nil?
+ super
+ # Ensure the candidate is newer
+ elsif RPMVersion.parse(candidate_version) > RPMVersion.parse(@current_resource.version)
+ super
+ else
+ Chef::Log.debug("#{@new_resource} is at the latest version - nothing to do")
+ end
+ end
+
+ def upgrade_package(name, version)
+ install_package(name, version)
+ end
+
+ def remove_package(name, version)
+ if version
+ yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}-#{version}#{yum_arch}")
+ else
+ yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}#{yum_arch}")
+ end
+
+ if flush_cache[:after]
+ @yum.reload
+ else
+ @yum.reload_installed
+ end
+ end
+
+ def purge_package(name, version)
+ remove_package(name, version)
+ end
+
+ private
+
+ def parse_arch
+ # Allow for foo.x86_64 style package_name like yum uses in it's output
+ #
+ if @new_resource.package_name =~ %r{^(.*)\.(.*)$}
+ new_package_name = $1
+ new_arch = $2
+ # foo.i386 and foo.beta1 are both valid package names or expressions of an arch.
+ # Ensure we don't have an existing package matching package_name, then ensure we at
+ # least have a match for the new_package+new_arch before we overwrite. If neither
+ # then fall through to standard package handling.
+ if (@yum.installed_version(@new_resource.package_name).nil? and @yum.candidate_version(@new_resource.package_name).nil?) and
+ (@yum.installed_version(new_package_name, new_arch) or @yum.candidate_version(new_package_name, new_arch))
+ @new_resource.package_name(new_package_name)
+ @new_resource.arch(new_arch)
+ end
+ end
+ end
+
+ # If we don't have the package we could have been passed a 'whatprovides' feature
+ #
+ # eg: yum install "perl(Config)"
+ # yum install "mtr = 2:0.71-3.1"
+ # yum install "mtr > 2:0.71"
+ #
+ # We support resolving these out of the Provides data imported from yum-dump.py and
+ # matching them up with an actual package so the standard resource handling can apply.
+ #
+ # There is currently no support for filename matching.
+ def parse_dependency
+ # Transform the package_name into a requirement
+ yum_require = RPMRequire.parse(@new_resource.package_name)
+ # and gather all the packages that have a Provides feature satisfying the requirement.
+ # It could be multiple be we can only manage one
+ packages = @yum.packages_from_require(yum_require)
+
+ if packages.empty?
+ # Don't bother if we are just ensuring a package is removed - we don't need Provides data
+ actions = Array(@new_resource.action)
+ unless actions.size == 1 and (actions[0] == :remove || actions[0] == :purge)
+ Chef::Log.debug("#{@new_resource} couldn't match #{@new_resource.package_name} in " +
+ "installed Provides, loading available Provides - this may take a moment")
+ @yum.reload_provides
+ packages = @yum.packages_from_require(yum_require)
+ end
+ end
+
+ unless packages.empty?
+ new_package_name = packages.first.name
+ Chef::Log.debug("#{@new_resource} no package found for #{@new_resource.package_name} " +
+ "but matched Provides for #{new_package_name}")
+
+ # Ensure it's not the same package under a different architecture
+ unique_names = []
+ packages.each do |pkg|
+ unique_names << "#{pkg.name}-#{pkg.version.evr}"
+ end
+ unique_names.uniq!
+
+ if unique_names.size > 1
+ Chef::Log.warn("#{@new_resource} matched multiple Provides for #{@new_resource.package_name} " +
+ "but we can only use the first match: #{new_package_name}. Please use a more " +
+ "specific version.")
+ end
+
+ @new_resource.package_name(new_package_name)
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/zypper.rb b/lib/chef/provider/package/zypper.rb
new file mode 100644
index 0000000000..43727466e2
--- /dev/null
+++ b/lib/chef/provider/package/zypper.rb
@@ -0,0 +1,144 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/package'
+require 'chef/mixin/command'
+require 'chef/resource/package'
+require 'singleton'
+
+class Chef
+ class Provider
+ class Package
+ class Zypper < Chef::Provider::Package
+
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Package.new(@new_resource.name)
+ @current_resource.package_name(@new_resource.package_name)
+
+ is_installed=false
+ is_out_of_date=false
+ version=''
+ oud_version=''
+ Chef::Log.debug("#{@new_resource} checking zypper")
+ status = popen4("zypper info #{@new_resource.package_name}") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ case line
+ when /^Version: (.+)$/
+ version = $1
+ Chef::Log.debug("#{@new_resource} version #{$1}")
+ when /^Installed: Yes$/
+ is_installed=true
+ Chef::Log.debug("#{@new_resource} is installed")
+
+ when /^Installed: No$/
+ is_installed=false
+ Chef::Log.debug("#{@new_resource} is not installed")
+ when /^Status: out-of-date \(version (.+) installed\)$/
+ is_out_of_date=true
+ oud_version=$1
+ Chef::Log.debug("#{@new_resource} out of date version #{$1}")
+ end
+ end
+ end
+
+ if is_installed==false
+ @candidate_version=version
+ @current_resource.version(nil)
+ end
+
+ if is_installed==true
+ if is_out_of_date==true
+ @current_resource.version(oud_version)
+ @candidate_version=version
+ else
+ @current_resource.version(version)
+ @candidate_version=version
+ end
+ end
+
+ unless status.exitstatus == 0
+ raise Chef::Exceptions::Package, "zypper failed - #{status.inspect}!"
+ end
+
+ @current_resource
+ end
+
+ #Gets the zypper Version from command output (Returns Floating Point number)
+ def zypper_version()
+ `zypper -V 2>&1`.scan(/\d+/).join(".").to_f
+ end
+
+ def install_package(name, version)
+ if zypper_version < 1.0
+ run_command(
+ :command => "zypper install -y #{name}"
+ )
+ elsif version
+ run_command(
+ :command => "zypper -n --no-gpg-checks install -l #{name}=#{version}"
+ )
+ else
+ run_command(
+ :command => "zypper -n --no-gpg-checks install -l #{name}"
+ )
+ end
+ end
+
+ def upgrade_package(name, version)
+ if zypper_version < 1.0
+ run_command(
+ :command => "zypper install -y #{name}"
+ )
+ elsif version
+ run_command(
+ :command => "zypper -n --no-gpg-checks install -l #{name}=#{version}"
+ )
+ else
+ run_command(
+ :command => "zypper -n --no-gpg-checks install -l #{name}"
+ )
+ end
+ end
+
+ def remove_package(name, version)
+ if zypper_version < 1.0
+ run_command(
+ :command => "zypper remove -y #{name}"
+ )
+ elsif version
+ run_command(
+ :command => "zypper -n --no-gpg-checks remove #{name}=#{version}"
+ )
+ else
+ run_command(
+ :command => "zypper -n --no-gpg-checks remove #{name}"
+ )
+ end
+
+
+ end
+
+ def purge_package(name, version)
+ remove_package(name, version)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb
new file mode 100644
index 0000000000..9ccd7ea056
--- /dev/null
+++ b/lib/chef/provider/remote_directory.rb
@@ -0,0 +1,174 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/file'
+require 'chef/provider/directory'
+require 'chef/resource/directory'
+require 'chef/resource/remote_file'
+require 'chef/mixin/file_class'
+require 'chef/platform'
+require 'uri'
+require 'tempfile'
+require 'net/https'
+require 'set'
+
+class Chef
+ class Provider
+ class RemoteDirectory < Chef::Provider::Directory
+ include Chef::Mixin::FileClass
+
+ def action_create
+ super
+
+ files_to_purge = Set.new(
+ Dir.glob(::File.join(@new_resource.path, '**', '*'), ::File::FNM_DOTMATCH).select do |name|
+ name !~ /(?:^|#{Regexp.escape(::File::SEPARATOR)})\.\.?$/
+ end
+ )
+ files_to_transfer.each do |cookbook_file_relative_path|
+ create_cookbook_file(cookbook_file_relative_path)
+ # the file is removed from the purge list
+ files_to_purge.delete(::File.join(@new_resource.path, cookbook_file_relative_path))
+ # parent directories are also removed from the purge list
+ directories=::File.dirname(::File.join(@new_resource.path, cookbook_file_relative_path)).split(::File::SEPARATOR)
+ for i in 0..directories.length-1
+ files_to_purge.delete(::File.join(directories[0..i]))
+ end
+ end
+ purge_unmanaged_files(files_to_purge)
+ end
+
+ def action_create_if_missing
+ # if this action is called, ignore the existing overwrite flag
+ @new_resource.overwrite(false)
+ action_create
+ end
+
+ protected
+
+ def purge_unmanaged_files(unmanaged_files)
+ if @new_resource.purge
+ unmanaged_files.sort.reverse.each do |f|
+ # file_class comes from Chef::Mixin::FileClass
+ if ::File.directory?(f) && !Chef::Platform.windows? && !file_class.symlink?(f.dup)
+ # Linux treats directory symlinks as files
+ # Remove a directory as a directory when not on windows if it is not a symlink
+ purge_directory(f)
+ elsif ::File.directory?(f) && Chef::Platform.windows?
+ # Windows treats directory symlinks as directories so we delete them here
+ purge_directory(f)
+ else
+ converge_by("delete unmanaged file #{f}") do
+ ::File.delete(f)
+ Chef::Log.debug("#{@new_resource} deleted file #{f}")
+ end
+ end
+ end
+ end
+ end
+
+ def purge_directory(dir)
+ converge_by("delete unmanaged directory #{dir}") do
+ Dir::rmdir(dir)
+ Chef::Log.debug("#{@new_resource} removed directory #{dir}")
+ end
+ end
+
+ def files_to_transfer
+ cookbook = run_context.cookbook_collection[resource_cookbook]
+ files = cookbook.relative_filenames_in_preferred_directory(node, :files, @new_resource.source)
+ files.sort.reverse
+ end
+
+ def directory_root_in_cookbook_cache
+ @directory_root_in_cookbook_cache ||= begin
+ cookbook = run_context.cookbook_collection[resource_cookbook]
+ cookbook.preferred_filename_on_disk_location(node, :files, @new_resource.source, @new_resource.path)
+ end
+ end
+
+ # Determine the cookbook to get the file from. If new resource sets an
+ # explicit cookbook, use it, otherwise fall back to the implicit cookbook
+ # i.e., the cookbook the resource was declared in.
+ def resource_cookbook
+ @new_resource.cookbook || @new_resource.cookbook_name
+ end
+
+ def create_cookbook_file(cookbook_file_relative_path)
+ full_path = ::File.join(@new_resource.path, cookbook_file_relative_path)
+
+ ensure_directory_exists(::File.dirname(full_path))
+
+ file_to_fetch = cookbook_file_resource(full_path, cookbook_file_relative_path)
+ if @new_resource.overwrite
+ file_to_fetch.run_action(:create)
+ else
+ file_to_fetch.run_action(:create_if_missing)
+ end
+ @new_resource.updated_by_last_action(true) if file_to_fetch.updated?
+ end
+
+ def cookbook_file_resource(target_path, relative_source_path)
+ cookbook_file = Chef::Resource::CookbookFile.new(target_path, run_context)
+ cookbook_file.cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name
+ cookbook_file.source(::File.join(@new_resource.source, relative_source_path))
+ if Chef::Platform.windows? && @new_resource.files_rights
+ @new_resource.files_rights.each_pair do |permission, *args|
+ cookbook_file.rights(permission, *args)
+ end
+ end
+ cookbook_file.mode(@new_resource.files_mode) if @new_resource.files_mode
+ cookbook_file.group(@new_resource.files_group) if @new_resource.files_group
+ cookbook_file.owner(@new_resource.files_owner) if @new_resource.files_owner
+ cookbook_file.backup(@new_resource.files_backup) if @new_resource.files_backup
+
+ cookbook_file
+ end
+
+ def ensure_directory_exists(path)
+ unless ::File.directory?(path)
+ directory_to_create = resource_for_directory(path)
+ directory_to_create.run_action(:create)
+ @new_resource.updated_by_last_action(true) if directory_to_create.updated?
+ end
+ end
+
+ def resource_for_directory(path)
+ dir = Chef::Resource::Directory.new(path, run_context)
+ dir.cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name
+ if Chef::Platform.windows? && @new_resource.rights
+ # rights are only meant to be applied to the toppest-level directory;
+ # Windows will handle inheritance.
+ if path == @new_resource.path
+ @new_resource.rights.each do |rights| #rights is a hash
+ permissions = rights.delete(:permissions) #delete will return the value or nil if not found
+ principals = rights.delete(:principals)
+ dir.rights(permissions, principals, rights)
+ end
+ end
+ end
+ dir.mode(@new_resource.mode) if @new_resource.mode
+ dir.group(@new_resource.group)
+ dir.owner(@new_resource.owner)
+ dir.recursive(true)
+ dir
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/remote_file.rb b/lib/chef/provider/remote_file.rb
new file mode 100644
index 0000000000..90e367f558
--- /dev/null
+++ b/lib/chef/provider/remote_file.rb
@@ -0,0 +1,138 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/file'
+require 'chef/rest'
+require 'uri'
+require 'tempfile'
+require 'net/https'
+
+class Chef
+ class Provider
+ class RemoteFile < Chef::Provider::File
+
+ def load_current_resource
+ @current_resource = Chef::Resource::RemoteFile.new(@new_resource.name)
+ super
+ end
+
+ def action_create
+ Chef::Log.debug("#{@new_resource} checking for changes")
+
+ if current_resource_matches_target_checksum?
+ Chef::Log.debug("#{@new_resource} checksum matches target checksum (#{@new_resource.checksum}) - not updating")
+ else
+ sources = @new_resource.source
+ source = sources.shift
+ begin
+ rest = Chef::REST.new(source, nil, nil, http_client_opts(source))
+ raw_file = rest.streaming_request(rest.create_url(source), {})
+ rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Net::HTTPFatalError => e
+ Chef::Log.debug("#{@new_resource} cannot be downloaded from #{source}")
+ if source = sources.shift
+ Chef::Log.debug("#{@new_resource} trying to download from another mirror")
+ retry
+ else
+ raise e
+ end
+ end
+ if matches_current_checksum?(raw_file)
+ Chef::Log.debug "#{@new_resource} target and source checksums are the same - not updating"
+ else
+ description = []
+ description << "copy file downloaded from #{@new_resource.source} into #{@new_resource.path}"
+ description << diff_current(raw_file.path)
+ converge_by(description) do
+ backup_new_resource
+ FileUtils.cp raw_file.path, @new_resource.path
+ Chef::Log.info "#{@new_resource} updated"
+ raw_file.close!
+ end
+ # whyrun mode cleanup - the temp file will never be used,
+ # so close/unlink it here.
+ if whyrun_mode?
+ raw_file.close!
+ end
+ end
+ end
+ set_all_access_controls
+ end
+
+ def current_resource_matches_target_checksum?
+ @new_resource.checksum && @current_resource.checksum && @current_resource.checksum =~ /^#{Regexp.escape(@new_resource.checksum)}/
+ end
+
+ def matches_current_checksum?(candidate_file)
+ Chef::Log.debug "#{@new_resource} checking for file existence of #{@new_resource.path}"
+ if ::File.exists?(@new_resource.path)
+ Chef::Log.debug "#{@new_resource} file exists at #{@new_resource.path}"
+ @new_resource.checksum(checksum(candidate_file.path))
+ Chef::Log.debug "#{@new_resource} target checksum: #{@current_resource.checksum}"
+ Chef::Log.debug "#{@new_resource} source checksum: #{@new_resource.checksum}"
+
+ @new_resource.checksum == @current_resource.checksum
+ else
+ Chef::Log.debug "#{@new_resource} creating #{@new_resource.path}"
+ false
+ end
+ end
+
+ def backup_new_resource
+ if ::File.exists?(@new_resource.path)
+ Chef::Log.debug "#{@new_resource} checksum changed from #{@current_resource.checksum} to #{@new_resource.checksum}"
+ backup @new_resource.path
+ end
+ end
+
+ def source_file(source, current_checksum, &block)
+ if absolute_uri?(source)
+ fetch_from_uri(source, &block)
+ elsif !Chef::Config[:solo]
+ fetch_from_chef_server(source, current_checksum, &block)
+ else
+ fetch_from_local_cookbook(source, &block)
+ end
+ end
+
+ def http_client_opts(source)
+ opts={}
+ # CHEF-3140
+ # 1. If it's already compressed, trying to compress it more will
+ # probably be counter-productive.
+ # 2. Some servers are misconfigured so that you GET $URL/file.tgz but
+ # they respond with content type of tar and content encoding of gzip,
+ # which tricks Chef::REST into decompressing the response body. In this
+ # case you'd end up with a tar archive (no gzip) named, e.g., foo.tgz,
+ # which is not what you wanted.
+ if @new_resource.path =~ /gz$/ or source =~ /gz$/
+ opts[:disable_gzip] = true
+ end
+ opts
+ end
+
+ private
+
+ def absolute_uri?(source)
+ URI.parse(source).absolute?
+ rescue URI::InvalidURIError
+ false
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/resource_update.rb b/lib/chef/provider/resource_update.rb
new file mode 100644
index 0000000000..e2c6bffca4
--- /dev/null
+++ b/lib/chef/provider/resource_update.rb
@@ -0,0 +1,55 @@
+
+class Chef
+ class Provider
+
+ # {
+ # "run_id" : "1000",
+ # "resource" : {
+ # "type" : "file",
+ # "name" : "/etc/passwd",
+ # "start_time" : "2012-01-09T08:15:30-05:00",
+ # "end_time" : "2012-01-09T08:15:30-05:00",
+ # "status" : "modified",
+ # "initial_state" : "exists",
+ # "final_state" : "modified",
+ # "before" : {
+ # "group" : "root",
+ # "owner" : "root",
+ # "checksum" : "xyz"
+ # },
+ # "after" : {
+ # "group" : "root",
+ # "owner" : "root",
+ # "checksum" : "abc"
+ # },
+ # "delta" : "escaped delta goes here"
+ # },
+ # "event_data" : ""
+ # }
+
+ class ResourceUpdate
+
+ attr_accessor :type
+ attr_accessor :name
+ attr_accessor :duration #ms
+ attr_accessor :status
+ attr_accessor :initial_state
+ attr_accessor :final_state
+ attr_accessor :initial_properties
+ attr_accessor :final_properties
+ attr_accessor :event_data # e.g., a diff.
+
+ def initial_state_from_resource(resource)
+ @initial_properties = resource.to_hash
+ end
+
+ def updated_state_from_resource(resource)
+ @final_properties = resource.to_hash
+ end
+
+ end
+ end
+end
+
+
+
diff --git a/lib/chef/provider/route.rb b/lib/chef/provider/route.rb
new file mode 100644
index 0000000000..5aedcb99ec
--- /dev/null
+++ b/lib/chef/provider/route.rb
@@ -0,0 +1,223 @@
+#
+# Author:: Bryan McLellan (btm@loftninjas.org), Jesse Nelson (spheromak@gmail.com)
+# Copyright:: Copyright (c) 2009 Bryan McLellan
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/log'
+require 'chef/mixin/command'
+require 'chef/provider'
+require 'ipaddr'
+
+class Chef::Provider::Route < Chef::Provider
+ include Chef::Mixin::Command
+
+ attr_accessor :is_running
+
+ MASK = {'0.0.0.0' => '0',
+ '128.0.0.0' => '1',
+ '192.0.0.0' => '2',
+ '224.0.0.0' => '3',
+ '240.0.0.0' => '4',
+ '248.0.0.0' => '5',
+ '252.0.0.0' => '6',
+ '254.0.0.0' => '7',
+ '255.0.0.0' => '8',
+ '255.128.0.0' => '9',
+ '255.192.0.0' => '10',
+ '255.224.0.0' => '11',
+ '255.240.0.0' => '12',
+ '255.248.0.0' => '13',
+ '255.252.0.0' => '14',
+ '255.254.0.0' => '15',
+ '255.255.0.0' => '16',
+ '255.255.128.0' => '17',
+ '255.255.192.0' => '18',
+ '255.255.224.0' => '19',
+ '255.255.240.0' => '20',
+ '255.255.248.0' => '21',
+ '255.255.252.0' => '22',
+ '255.255.254.0' => '23',
+ '255.255.255.0' => '24',
+ '255.255.255.128' => '25',
+ '255.255.255.192' => '26',
+ '255.255.255.224' => '27',
+ '255.255.255.240' => '28',
+ '255.255.255.248' => '29',
+ '255.255.255.252' => '30',
+ '255.255.255.254' => '31',
+ '255.255.255.255' => '32' }
+
+ def hex2ip(hex_data)
+ # Cleanup hex data
+ hex_ip = hex_data.to_s.downcase.gsub(/[^0-9a-f]/, '')
+
+ # Check hex data format (IP is a 32bit integer, so should be 8 chars long)
+ return nil if hex_ip.length != hex_data.length || hex_ip.length != 8
+
+ # Extract octets from hex data
+ octets = hex_ip.scan(/../).reverse.collect { |octet| [octet].pack('H2').unpack("C").first }
+
+ # Validate IP
+ ip = octets.join('.')
+ begin
+ IPAddr.new(ip, Socket::AF_INET).to_s
+ rescue ArgumentError
+ Chef::Log.debug("Invalid IP address data: hex=#{hex_ip}, ip=#{ip}")
+ return nil
+ end
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ self.is_running = false
+
+ # cidr or quad dot mask
+ if @new_resource.netmask
+ new_ip = IPAddr.new("#{@new_resource.target}/#{@new_resource.netmask}")
+ else
+ new_ip = IPAddr.new(@new_resource.target)
+ end
+
+ # For linux, we use /proc/net/route file to read proc table info
+ if node[:os] == "linux"
+ route_file = ::File.open("/proc/net/route", "r")
+
+ # Read all routes
+ while (line = route_file.gets)
+ # Get all the fields for a route
+ iface,destination,gateway,flags,refcnt,use,metric,mask,mtu,window,irtt = line.split
+
+ # Convert hex-encoded values to quad-dotted notation (e.g. 0064A8C0 => 192.168.100.0)
+ destination = hex2ip(destination)
+ gateway = hex2ip(gateway)
+ mask = hex2ip(mask)
+
+ # Skip formatting lines (header, etc)
+ next unless destination && gateway && mask
+ Chef::Log.debug("#{@new_resource} system has route: dest=#{destination} mask=#{mask} gw=#{gateway}")
+
+ # check if what were trying to configure is already there
+ # use an ipaddr object with ip/mask this way we can have
+ # a new resource be in cidr format (i don't feel like
+ # expanding bitmask by hand.
+ #
+ running_ip = IPAddr.new("#{destination}/#{mask}")
+ Chef::Log.debug("#{@new_resource} new ip: #{new_ip.inspect} running ip: #{running_ip.inspect}")
+ self.is_running = true if running_ip == new_ip && gateway == @new_resource.gateway
+ end
+
+ route_file.close
+ end
+ end
+
+ def action_add
+ # check to see if load_current_resource found the route
+ if is_running
+ Chef::Log.debug("#{@new_resource} route already active - nothing to do")
+ else
+ command = generate_command(:add)
+ converge_by ("run #{ command } to add route") do
+ run_command( :command => command )
+ Chef::Log.info("#{@new_resource} added")
+ end
+ end
+
+ #for now we always write the file (ugly but its what it is)
+ generate_config
+ end
+
+ def action_delete
+ if is_running
+ command = generate_command(:delete)
+ converge_by ("run #{ command } to delete route ") do
+ run_command( :command => command )
+ Chef::Log.info("#{@new_resource} removed")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} route does not exist - nothing to do")
+ end
+ end
+
+ def generate_config
+ conf = Hash.new
+ case node[:platform]
+ when "centos", "redhat", "fedora"
+ # walk the collection
+ run_context.resource_collection.each do |resource|
+ if resource.is_a? Chef::Resource::Route
+ # default to eth0
+ if resource.device
+ dev = resource.device
+ else
+ dev = "eth0"
+ end
+
+ conf[dev] = String.new if conf[dev].nil?
+ if resource.action == :add
+ conf[dev] << config_file_contents(:add, :target => resource.target, :netmask => resource.netmask, :gateway => resource.gateway)
+ else
+ # need to do this for the case when the last route on an int
+ # is removed
+ conf[dev] << config_file_contents(:delete)
+ end
+ end
+ end
+ conf.each do |k, v|
+ network_file_name = "/etc/sysconfig/network-scripts/route-#{k}"
+ converge_by ("write route route.#{k}\n#{conf[k]} to #{ network_file_name }") do
+ network_file = ::File.new(network_file_name, "w")
+ network_file.puts(conf[k])
+ Chef::Log.debug("#{@new_resource} writing route.#{k}\n#{conf[k]}")
+ network_file.close
+ end
+ end
+ end
+ end
+
+ def generate_command(action)
+ common_route_items = ''
+ common_route_items << "/#{MASK[@new_resource.netmask.to_s]}" if @new_resource.netmask
+ common_route_items << " via #{@new_resource.gateway} " if @new_resource.gateway
+
+ case action
+ when :add
+ command = "ip route replace #{@new_resource.target}"
+ command << common_route_items
+ command << " dev #{@new_resource.device} " if @new_resource.device
+ when :delete
+ command = "ip route delete #{@new_resource.target}"
+ command << common_route_items
+ end
+
+ return command
+ end
+
+ def config_file_contents(action, options={})
+ content = ''
+ case action
+ when :add
+ content << "#{options[:target]}"
+ content << "/#{options[:netmask]}" if options[:netmask]
+ content << " via #{options[:gateway]}" if options[:gateway]
+ content << "\n"
+ end
+
+ return content
+ end
+end
diff --git a/lib/chef/provider/ruby_block.rb b/lib/chef/provider/ruby_block.rb
new file mode 100644
index 0000000000..16908b0eff
--- /dev/null
+++ b/lib/chef/provider/ruby_block.rb
@@ -0,0 +1,42 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 Provider
+ class RubyBlock < Chef::Provider
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ true
+ end
+
+ def action_run
+ converge_by("execute the ruby block #{@new_resource.name}") do
+ @new_resource.block.call
+ Chef::Log.info("#{@new_resource} called")
+ end
+ end
+
+ alias :action_create :action_run
+
+ end
+ end
+end
diff --git a/lib/chef/provider/script.rb b/lib/chef/provider/script.rb
new file mode 100644
index 0000000000..9e5a7d7fe1
--- /dev/null
+++ b/lib/chef/provider/script.rb
@@ -0,0 +1,57 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 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 'tempfile'
+require 'chef/provider/execute'
+
+class Chef
+ class Provider
+ class Script < Chef::Provider::Execute
+
+ def action_run
+ script_file.puts(@new_resource.code)
+ script_file.close
+
+ set_owner_and_group
+
+ @new_resource.command("\"#{@new_resource.interpreter}\" #{@new_resource.flags} \"#{script_file.path}\"")
+ super
+ converge_by(nil) do
+ # ensure script is unlinked at end of converge!
+ unlink_script_file
+ end
+ end
+
+ def set_owner_and_group
+ # FileUtils itself implements a no-op if +user+ or +group+ are nil
+ # You can prove this by running FileUtils.chown(nil,nil,'/tmp/file')
+ # as an unprivileged user.
+ FileUtils.chown(@new_resource.user, @new_resource.group, script_file.path)
+ end
+
+ def script_file
+ @script_file ||= Tempfile.open("chef-script")
+ end
+
+ def unlink_script_file
+ @script_file && @script_file.close!
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb
new file mode 100644
index 0000000000..decca7fd7c
--- /dev/null
+++ b/lib/chef/provider/service.rb
@@ -0,0 +1,158 @@
+#
+# Author:: AJ Christensen (<aj@hjksolutions.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/command'
+require 'chef/provider'
+
+class Chef
+ class Provider
+ class Service < Chef::Provider
+
+ include Chef::Mixin::Command
+
+ def initialize(new_resource, run_context)
+ super
+ @enabled = nil
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_new_resource_state
+ # If the user didn't specify a change in enabled state,
+ # it will be the same as the old resource
+ if ( @new_resource.enabled.nil? )
+ @new_resource.enabled(@current_resource.enabled)
+ end
+ if ( @new_resource.running.nil? )
+ @new_resource.running(@current_resource.running)
+ end
+ end
+
+ def shared_resource_requirements
+ end
+
+ def define_resource_requirements
+ requirements.assert(:reload) do |a|
+ a.assertion { @new_resource.supports[:reload] || @new_resource.reload_command }
+ a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload"
+ # if a service is not declared to support reload, that won't
+ # typically change during the course of a run - so no whyrun
+ # alternative here.
+ end
+ end
+
+ def action_enable
+ if @current_resource.enabled
+ Chef::Log.debug("#{@new_resource} already enabled - nothing to do")
+ else
+ converge_by("enable service #{@new_resource}") do
+ enable_service
+ Chef::Log.info("#{@new_resource} enabled")
+ end
+ end
+ load_new_resource_state
+ @new_resource.enabled(true)
+ end
+
+ def action_disable
+ if @current_resource.enabled
+ converge_by("disable service #{@new_resource}") do
+ disable_service
+ Chef::Log.info("#{@new_resource} disabled")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} already disabled - nothing to do")
+ end
+ load_new_resource_state
+ @new_resource.enabled(false)
+ end
+
+ def action_start
+ unless @current_resource.running
+ converge_by("start service #{@new_resource}") do
+ start_service
+ Chef::Log.info("#{@new_resource} started")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} already running - nothing to do")
+ end
+ load_new_resource_state
+ @new_resource.running(true)
+ end
+
+ def action_stop
+ if @current_resource.running
+ converge_by("stop service #{@new_resource}") do
+ stop_service
+ Chef::Log.info("#{@new_resource} stopped")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} already stopped - nothing to do")
+ end
+ load_new_resource_state
+ @new_resource.running(false)
+ end
+
+ def action_restart
+ converge_by("restart service #{@new_resource}") do
+ restart_service
+ Chef::Log.info("#{@new_resource} restarted")
+ end
+ load_new_resource_state
+ @new_resource.running(true)
+ end
+
+ def action_reload
+ if @current_resource.running
+ converge_by("disable service #{@new_resource}") do
+ reload_service
+ Chef::Log.info("#{@new_resource} reloaded")
+ end
+ end
+ load_new_resource_state
+ end
+
+ def enable_service
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable"
+ end
+
+ def disable_service
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable"
+ end
+
+ def start_service
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :start"
+ end
+
+ def stop_service
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :stop"
+ end
+
+ def restart_service
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :restart"
+ end
+
+ def reload_service
+ raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :restart"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/service/arch.rb b/lib/chef/provider/service/arch.rb
new file mode 100644
index 0000000000..8c8216c37f
--- /dev/null
+++ b/lib/chef/provider/service/arch.rb
@@ -0,0 +1,113 @@
+#
+# Author:: Jan Zimmek (<jan.zimmek@web.de>)
+# Copyright:: Copyright (c) 2010 Jan Zimmek
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/service/init'
+require 'chef/mixin/command'
+
+class Chef::Provider::Service::Arch < Chef::Provider::Service::Init
+
+ def initialize(new_resource, run_context)
+ super
+ @init_command = "/etc/rc.d/#{@new_resource.service_name}"
+ end
+
+ def load_current_resource
+ raise Chef::Exceptions::Service, "Could not find /etc/rc.conf" unless ::File.exists?("/etc/rc.conf")
+ raise Chef::Exceptions::Service, "No DAEMONS found in /etc/rc.conf" unless ::File.read("/etc/rc.conf").match(/DAEMONS=\((.*)\)/m)
+ super
+
+ @current_resource.enabled(daemons.include?(@current_resource.service_name))
+ @current_resource
+ end
+
+ # Get list of all daemons from the file '/etc/rc.conf'.
+ # Mutiple lines and background form are supported. Example:
+ # DAEMONS=(\
+ # foobar \
+ # @example \
+ # !net \
+ # )
+ def daemons
+ entries = []
+ if ::File.read("/etc/rc.conf").match(/DAEMONS=\((.*)\)/m)
+ entries += $1.gsub(/\\?[\r\n]/, ' ').gsub(/# *[^ ]+/,' ').split(' ') if $1.length > 0
+ end
+
+ yield(entries) if block_given?
+
+ entries
+ end
+
+ # FIXME: Multiple entries of DAEMONS will cause very bad results :)
+ def update_daemons(entries)
+ content = ::File.read("/etc/rc.conf").gsub(/DAEMONS=\((.*)\)/m, "DAEMONS=(#{entries.join(' ')})")
+ ::File.open("/etc/rc.conf", "w") do |f|
+ f.write(content)
+ end
+ end
+
+ def enable_service()
+ new_daemons = []
+ entries = daemons
+
+ if entries.include?(new_resource.service_name) or entries.include?("@#{new_resource.service_name}")
+ # exists and already enabled (or already enabled as a background service)
+ # new_daemons += entries
+ else
+ if entries.include?("!#{new_resource.service_name}")
+ # exists but disabled
+ entries.each do |daemon|
+ if daemon == "!#{new_resource.service_name}"
+ new_daemons << new_resource.service_name
+ else
+ new_daemons << daemon
+ end
+ end
+ else
+ # does not exist
+ new_daemons += entries
+ new_daemons << new_resource.service_name
+ end
+ update_daemons(new_daemons)
+ end
+ end
+
+ def disable_service()
+ new_daemons = []
+ entries = daemons
+
+ if entries.include?("!#{new_resource.service_name}")
+ # exists and disabled
+ # new_daemons += entries
+ else
+ if entries.include?(new_resource.service_name) or entries.include?("@#{new_resource.service_name}")
+ # exists but enabled (or enabled as a back-ground service)
+ # FIXME: Does arch support !@foobar ?
+ entries.each do |daemon|
+ if [new_resource.service_name, "@#{new_resource.service_name}"].include?(daemon)
+ new_daemons << "!#{new_resource.service_name}"
+ else
+ new_daemons << daemon
+ end
+ end
+ end
+ update_daemons(new_daemons)
+ end
+ end
+
+end
diff --git a/lib/chef/provider/service/debian.rb b/lib/chef/provider/service/debian.rb
new file mode 100644
index 0000000000..e2a0f60d91
--- /dev/null
+++ b/lib/chef/provider/service/debian.rb
@@ -0,0 +1,152 @@
+#
+# Author:: AJ Christensen (<aj@hjksolutions.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/service'
+require 'chef/provider/service/init'
+require 'chef/mixin/command'
+
+class Chef
+ class Provider
+ class Service
+ class Debian < Chef::Provider::Service::Init
+ UPDATE_RC_D_ENABLED_MATCHES = /\/rc[\dS].d\/S|not installed/i
+ UPDATE_RC_D_PRIORITIES = /\/rc([\dS]).d\/([SK])(\d\d)/i
+
+ def load_current_resource
+ super
+ @priority_success = true
+ @rcd_status = nil
+ @current_resource.priority(get_priority)
+ @current_resource.enabled(service_currently_enabled?(@current_resource.priority))
+ @current_resource
+ end
+
+ def define_resource_requirements
+ # do not call super here, inherit only shared_requirements
+ shared_resource_requirements
+ requirements.assert(:all_actions) do |a|
+ update_rcd = "/usr/sbin/update-rc.d"
+ a.assertion { ::File.exists? update_rcd }
+ a.failure_message Chef::Exceptions::Service, "#{update_rcd} does not exist!"
+ # no whyrun recovery - this is a base system component of debian
+ # distros and must be present
+ end
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @priority_success }
+ a.failure_message Chef::Exceptions::Service, "/usr/sbin/update-rc.d -n -f #{@current_resource.service_name} failed - #{@rcd_status.inspect}"
+ # This can happen if the service is not yet installed,so we'll fake it.
+ a.whyrun ["Unable to determine priority of service, assuming service would have been correctly installed earlier in the run.",
+ "Assigning temporary priorities to continue.",
+ "If this service is not properly installed prior to this point, this will fail."] do
+ temp_priorities = {"6"=>[:stop, "20"],
+ "0"=>[:stop, "20"],
+ "1"=>[:stop, "20"],
+ "2"=>[:start, "20"],
+ "3"=>[:start, "20"],
+ "4"=>[:start, "20"],
+ "5"=>[:start, "20"]}
+ @current_resource.priority(temp_priorities)
+ end
+ end
+ end
+
+ def get_priority
+ priority = {}
+
+ @rcd_status = popen4("/usr/sbin/update-rc.d -n -f #{@current_resource.service_name} remove") do |pid, stdin, stdout, stderr|
+
+ [stdout, stderr].each do |iop|
+ iop.each_line do |line|
+ if UPDATE_RC_D_PRIORITIES =~ line
+ # priority[runlevel] = [ S|K, priority ]
+ # S = Start, K = Kill
+ # debian runlevels: 0 Halt, 1 Singleuser, 2 Multiuser, 3-5 == 2, 6 Reboot
+ priority[$1] = [($2 == "S" ? :start : :stop), $3]
+ end
+ if line =~ UPDATE_RC_D_ENABLED_MATCHES
+ enabled = true
+ end
+ end
+ end
+ end
+
+ unless @rcd_status.exitstatus == 0
+ @priority_success = false
+ end
+ priority
+ end
+
+ def service_currently_enabled?(priority)
+ enabled = false
+ priority.each { |runlevel, arguments|
+ Chef::Log.debug("#{@new_resource} runlevel #{runlevel}, action #{arguments[0]}, priority #{arguments[1]}")
+ # if we are in a update-rc.d default startup runlevel && we start in this runlevel
+ if (2..5).include?(runlevel.to_i) && arguments[0] == :start
+ enabled = true
+ end
+ }
+
+ enabled
+ end
+
+ def enable_service()
+ if @new_resource.priority.is_a? Integer
+ run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove")
+ run_command(:command => "/usr/sbin/update-rc.d #{@new_resource.service_name} defaults #{@new_resource.priority} #{100 - @new_resource.priority}")
+ elsif @new_resource.priority.is_a? Hash
+ # we call the same command regardless of we're enabling or disabling
+ # users passing a Hash are responsible for setting their own start priorities
+ set_priority()
+ else # No priority, go with update-rc.d defaults
+ run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove")
+ run_command(:command => "/usr/sbin/update-rc.d #{@new_resource.service_name} defaults")
+ end
+
+ end
+
+ def disable_service()
+ if @new_resource.priority.is_a? Integer
+ # Stop processes in reverse order of start using '100 - start_priority'
+ run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove")
+ run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} stop #{100 - @new_resource.priority} 2 3 4 5 .")
+ elsif @new_resource.priority.is_a? Hash
+ # we call the same command regardless of we're enabling or disabling
+ # users passing a Hash are responsible for setting their own stop priorities
+ set_priority()
+ else
+ # no priority, using '100 - 20 (update-rc.d default)' to stop in reverse order of start
+ run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove")
+ run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} stop 80 2 3 4 5 .")
+ end
+ end
+
+ def set_priority()
+ args = ""
+ @new_resource.priority.each do |level, o|
+ action = o[0]
+ priority = o[1]
+ args += "#{action} #{priority} #{level} . "
+ end
+ run_command(:command => "/usr/sbin/update-rc.d -f #{@new_resource.service_name} remove")
+ run_command(:command => "/usr/sbin/update-rc.d #{@new_resource.service_name} #{args}")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb
new file mode 100644
index 0000000000..b875838ec2
--- /dev/null
+++ b/lib/chef/provider/service/freebsd.rb
@@ -0,0 +1,175 @@
+#
+# Author:: Bryan McLellan (btm@loftninjas.org)
+# Copyright:: Copyright (c) 2009 Bryan McLellan
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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'
+require 'chef/provider/service'
+require 'chef/mixin/command'
+
+class Chef
+ class Provider
+ class Service
+ class Freebsd < Chef::Provider::Service::Init
+
+ include Chef::Mixin::ShellOut
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Service.new(@new_resource.name)
+ @current_resource.service_name(@new_resource.service_name)
+ @rcd_script_found = true
+ @enabled_state_found = false
+ # Determine if we're talking about /etc/rc.d or /usr/local/etc/rc.d
+ if ::File.exists?("/etc/rc.d/#{current_resource.service_name}")
+ @init_command = "/etc/rc.d/#{current_resource.service_name}"
+ elsif ::File.exists?("/usr/local/etc/rc.d/#{current_resource.service_name}")
+ @init_command = "/usr/local/etc/rc.d/#{current_resource.service_name}"
+ else
+ @rcd_script_found = false
+ return
+ end
+ Chef::Log.debug("#{@current_resource} found at #{@init_command}")
+ determine_current_status!
+ # Default to disabled if the service doesn't currently exist
+ # at all
+ var_name = service_enable_variable_name
+ if ::File.exists?("/etc/rc.conf") && var_name
+ read_rc_conf.each do |line|
+ case line
+ when /#{Regexp.escape(var_name)}="(\w+)"/
+ @enabled_state_found = true
+ if $1 =~ /[Yy][Ee][Ss]/
+ @current_resource.enabled true
+ elsif $1 =~ /[Nn][Oo][Nn]?[Oo]?[Nn]?[Ee]?/
+ @current_resource.enabled false
+ end
+ end
+ end
+ end
+ unless @current_resource.enabled
+ Chef::Log.debug("#{@new_resource.name} enable/disable state unknown")
+ @current_resource.enabled false
+ end
+
+ @current_resource
+ end
+
+ def define_resource_requirements
+ shared_resource_requirements
+ requirements.assert(:start, :enable, :reload, :restart) do |a|
+ a.assertion { @rcd_script_found }
+ a.failure_message Chef::Exceptions::Service, "#{@new_resource}: unable to locate the rc.d script"
+ end
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @enabled_state_found }
+ # for consistentcy with original behavior, this will not fail in non-whyrun mode;
+ # rather it will silently set enabled state=>false
+ a.whyrun "Unable to determine enabled/disabled state, assuming this will be correct for an actual run. Assuming disabled."
+ end
+
+ requirements.assert(:start, :enable, :reload, :restart) do |a|
+ a.assertion { @rcd_script_found && service_enable_variable_name != nil }
+ a.failure_message Chef::Exceptions::Service, "Could not find the service name in #{@init_command} and rcvar"
+ # No recovery in whyrun mode - the init file is present but not correct.
+ end
+ end
+
+ def start_service
+ if @new_resource.start_command
+ super
+ else
+ shell_out!("#{@init_command} faststart")
+ end
+ end
+
+ def stop_service
+ if @new_resource.stop_command
+ super
+ else
+ shell_out!("#{@init_command} faststop")
+ end
+ end
+
+ def restart_service
+ if @new_resource.restart_command
+
+ super
+ elsif @new_resource.supports[:restart]
+ shell_out!("#{@init_command} fastrestart")
+ else
+ stop_service
+ sleep 1
+ start_service
+ end
+ end
+
+ def read_rc_conf
+ ::File.open("/etc/rc.conf", 'r') { |file| file.readlines }
+ end
+
+ def write_rc_conf(lines)
+ ::File.open("/etc/rc.conf", 'w') do |file|
+ lines.each { |line| file.puts(line) }
+ end
+ end
+
+ # The variable name used in /etc/rc.conf for enabling this service
+ def service_enable_variable_name
+ # Look for name="foo" in the shell script @init_command. Use this for determining the variable name in /etc/rc.conf
+ # corresponding to this service
+ # For example: to enable the service mysql-server with the init command /usr/local/etc/rc.d/mysql-server, you need
+ # to set mysql_enable="YES" in /etc/rc.conf$
+ if @rcd_script_found
+ ::File.open(@init_command) do |rcscript|
+ rcscript.each_line do |line|
+ if line =~ /^name="?(\w+)"?/
+ return $1 + "_enable"
+ end
+ end
+ end
+ # some scripts support multiple instances through symlinks such as openvpn.
+ # We should get the service name from rcvar.
+ Chef::Log.debug("name=\"service\" not found at #{@init_command}. falling back to rcvar")
+ sn = shell_out!("#{@init_command} rcvar").stdout[/(\w+_enable)=/, 1]
+ return sn
+ end
+ # Fallback allows us to keep running in whyrun mode when
+ # the script does not exist.
+ @new_resource.service_name
+ end
+
+ def set_service_enable(value)
+ lines = read_rc_conf
+ # Remove line that set the old value
+ lines.delete_if { |line| line =~ /#{Regexp.escape(service_enable_variable_name)}/ }
+ # And append the line that sets the new value at the end
+ lines << "#{service_enable_variable_name}=\"#{value}\""
+ write_rc_conf(lines)
+ end
+
+ def enable_service()
+ set_service_enable("YES") unless @current_resource.enabled
+ end
+
+ def disable_service()
+ set_service_enable("NO") if @current_resource.enabled
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/gentoo.rb b/lib/chef/provider/service/gentoo.rb
new file mode 100644
index 0000000000..45b5a21f9b
--- /dev/null
+++ b/lib/chef/provider/service/gentoo.rb
@@ -0,0 +1,67 @@
+#
+# Author:: Lee Jensen (<ljensen@engineyard.com>)
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/service'
+require 'chef/mixin/command'
+
+class Chef::Provider::Service::Gentoo < Chef::Provider::Service::Init
+ def load_current_resource
+
+ @new_resource.supports[:status] = true
+ @new_resource.supports[:restart] = true
+ @found_script = false
+ super
+
+ @current_resource.enabled(
+ Dir.glob("/etc/runlevels/**/#{@current_resource.service_name}").any? do |file|
+ @found_script = true
+ exists = ::File.exists? file
+ readable = ::File.readable? file
+ Chef::Log.debug "#{@new_resource} exists: #{exists}, readable: #{readable}"
+ exists and readable
+ end
+ )
+ Chef::Log.debug "#{@new_resource} enabled: #{@current_resource.enabled}"
+
+ @current_resource
+ end
+
+ def define_resource_requirements
+ requirements.assert(:all_actions) do |a|
+ a.assertion { ::File.exists?("/sbin/rc-update") }
+ a.failure_message Chef::Exceptions::Service, "/sbin/rc-update does not exist"
+ # no whyrun recovery -t his is a core component whose presence is
+ # unlikely to be affected by what we do in the course of a chef run
+ end
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @found_script }
+ # No failure, just informational output from whyrun
+ a.whyrun "Could not find service #{@new_resource.service_name} under any runlevel"
+ end
+ end
+
+ def enable_service()
+ run_command(:command => "/sbin/rc-update add #{@new_resource.service_name} default")
+ end
+
+ def disable_service()
+ run_command(:command => "/sbin/rc-update del #{@new_resource.service_name} default")
+ end
+end
diff --git a/lib/chef/provider/service/init.rb b/lib/chef/provider/service/init.rb
new file mode 100644
index 0000000000..ab843d764d
--- /dev/null
+++ b/lib/chef/provider/service/init.rb
@@ -0,0 +1,87 @@
+#
+# Author:: AJ Christensen (<aj@hjksolutions.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/shell_out'
+require 'chef/provider/service'
+require 'chef/provider/service/simple'
+require 'chef/mixin/command'
+
+class Chef
+ class Provider
+ class Service
+ class Init < Chef::Provider::Service::Simple
+
+ include Chef::Mixin::ShellOut
+
+ def initialize(new_resource, run_context)
+ super
+ @init_command = "/etc/init.d/#{@new_resource.service_name}"
+ end
+
+ def define_resource_requirements
+ # do not call super here, inherit only shared_requirements
+ shared_resource_requirements
+ requirements.assert(:start, :stop, :restart, :reload) do |a|
+ a.assertion { ::File.exist?(@init_command) }
+ a.failure_message(Chef::Exceptions::Service, "#{@init_command} does not exist!")
+ a.whyrun("Init script '#{@init_command}' doesn't exist, assuming a prior action would have created it.") do
+ # blindly assume that the service exists but is stopped in why run mode:
+ @status_load_success = false
+ end
+ end
+ end
+
+ def start_service
+ if @new_resource.start_command
+ super
+ else
+ shell_out!("#{@init_command} start")
+ end
+ end
+
+ def stop_service
+ if @new_resource.stop_command
+ super
+ else
+ shell_out!("#{@init_command} stop")
+ end
+ end
+
+ def restart_service
+ if @new_resource.restart_command
+ super
+ elsif @new_resource.supports[:restart]
+ shell_out!("#{@init_command} restart")
+ else
+ stop_service
+ sleep 1
+ start_service
+ end
+ end
+
+ def reload_service
+ if @new_resource.reload_command
+ super
+ elsif @new_resource.supports[:reload]
+ shell_out!("#{@init_command} reload")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/insserv.rb b/lib/chef/provider/service/insserv.rb
new file mode 100644
index 0000000000..32152376ee
--- /dev/null
+++ b/lib/chef/provider/service/insserv.rb
@@ -0,0 +1,52 @@
+#
+# Author:: Bryan McLellan <btm@loftninjas.org>
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/service'
+require 'chef/provider/service/init'
+require 'chef/mixin/command'
+
+class Chef
+ class Provider
+ class Service
+ class Insserv < Chef::Provider::Service::Init
+
+ def load_current_resource
+ super
+
+ # Look for a /etc/rc.*/SnnSERVICE link to signifiy that the service would be started in a runlevel
+ if Dir.glob("/etc/rc**/S*#{@current_resource.service_name}").empty?
+ @current_resource.enabled false
+ else
+ @current_resource.enabled true
+ end
+
+ @current_resource
+ end
+
+ def enable_service()
+ run_command(:command => "/sbin/insserv -r -f #{@new_resource.service_name}")
+ run_command(:command => "/sbin/insserv -d -f #{@new_resource.service_name}")
+ end
+
+ def disable_service()
+ run_command(:command => "/sbin/insserv -r -f #{@new_resource.service_name}")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/invokercd.rb b/lib/chef/provider/service/invokercd.rb
new file mode 100644
index 0000000000..69a17bb4fb
--- /dev/null
+++ b/lib/chef/provider/service/invokercd.rb
@@ -0,0 +1,35 @@
+#
+# Author:: AJ Christensen (<aj@hjksolutions.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/service'
+require 'chef/provider/service/init'
+require 'chef/mixin/command'
+
+class Chef
+ class Provider
+ class Service
+ class Invokercd < Chef::Provider::Service::Init
+
+ def initialize(new_resource, run_context)
+ super
+ @init_command = "/usr/sbin/invoke-rc.d #{@new_resource.service_name}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb
new file mode 100644
index 0000000000..72c02779c6
--- /dev/null
+++ b/lib/chef/provider/service/macosx.rb
@@ -0,0 +1,144 @@
+#
+# Author:: Igor Afonov <afonov@gmail.com>
+# Copyright:: Copyright (c) 2011 Igor Afonov
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/service'
+
+class Chef
+ class Provider
+ class Service
+ class Macosx < Chef::Provider::Service::Simple
+ include Chef::Mixin::ShellOut
+
+ PLIST_DIRS = %w{~/Library/LaunchAgents
+ /Library/LaunchAgents
+ /Library/LaunchDaemons
+ /System/Library/LaunchAgents
+ /System/Library/LaunchDaemons }
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Service.new(@new_resource.name)
+ @current_resource.service_name(@new_resource.service_name)
+ @plist_size = 0
+ @plist = find_service_plist
+ set_service_status
+
+ @current_resource
+ end
+
+ def define_resource_requirements
+ #super
+ requirements.assert(:enable) do |a|
+ a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable"
+ end
+
+ requirements.assert(:disable) do |a|
+ a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable"
+ end
+
+ requirements.assert(:reload) do |a|
+ a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload"
+ end
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @plist_size < 2 }
+ a.failure_message Chef::Exceptions::Service, "Several plist files match service name. Please use full service name."
+ end
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @plist_size > 0 }
+ # No failrue here in original code - so we also will not
+ # fail. Instead warn that the service is potentially missing
+ a.whyrun "Assuming that the service would have been previously installed and is currently disabled." do
+ @current_resource.enabled(false)
+ @current_resource.running(false)
+ end
+ end
+
+ end
+
+ def start_service
+ if @current_resource.running
+ Chef::Log.debug("#{@new_resource} already running, not starting")
+ else
+ if @new_resource.start_command
+ super
+ else
+ shell_out!("launchctl load -w '#{@plist}'", :user => @owner_uid, :group => @owner_gid)
+ end
+ end
+ end
+
+ def stop_service
+ unless @current_resource.running
+ Chef::Log.debug("#{@new_resource} not running, not stopping")
+ else
+ if @new_resource.stop_command
+ super
+ else
+ shell_out!("launchctl unload '#{@plist}'", :user => @owner_uid, :group => @owner_gid)
+ end
+ end
+ end
+
+ def restart_service
+ if @new_resource.restart_command
+ super
+ else
+ stop_service
+ sleep 1
+ start_service
+ end
+ end
+
+
+ def set_service_status
+ return if @plist == nil
+
+ @current_resource.enabled(!@plist.nil?)
+
+ if @current_resource.enabled
+ @owner_uid = ::File.stat(@plist).uid
+ @owner_gid = ::File.stat(@plist).gid
+
+ shell_out!("launchctl list", :user => @owner_uid, :group => @owner_gid).stdout.each_line do |line|
+ case line
+ when /(\d+|-)\s+(?:\d+|-)\s+(.*\.?)#{@current_resource.service_name}/
+ pid = $1
+ @current_resource.running(!pid.to_i.zero?)
+ end
+ end
+ else
+ @current_resource.running(false)
+ end
+ end
+
+ private
+
+ def find_service_plist
+ plists = PLIST_DIRS.inject([]) do |results, dir|
+ entries = Dir.glob("#{::File.expand_path(dir)}/*#{@current_resource.service_name}*.plist")
+ entries.any? ? results << entries : results
+ end
+ plists.flatten!
+ @plist_size = plists.size
+ plists.first
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/redhat.rb b/lib/chef/provider/service/redhat.rb
new file mode 100644
index 0000000000..629e4ee0c3
--- /dev/null
+++ b/lib/chef/provider/service/redhat.rb
@@ -0,0 +1,77 @@
+#
+# Author:: AJ Christensen (<aj@hjksolutions.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/service'
+require 'chef/provider/service/init'
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Provider
+ class Service
+ class Redhat < Chef::Provider::Service::Init
+ include Chef::Mixin::ShellOut
+
+ CHKCONFIG_ON = /\d:on/
+ CHKCONFIG_MISSING = /No such/
+
+ def initialize(new_resource, run_context)
+ super
+ @init_command = "/sbin/service #{@new_resource.service_name}"
+ @new_resource.supports[:status] = true
+ @service_missing = false
+ end
+
+ def define_resource_requirements
+ shared_resource_requirements
+
+ requirements.assert(:all_actions) do |a|
+ chkconfig_file = "/sbin/chkconfig"
+ a.assertion { ::File.exists? chkconfig_file }
+ a.failure_message Chef::Exceptions::Service, "#{chkconfig_file} does not exist!"
+ end
+
+ requirements.assert(:start, :enable, :reload, :restart) do |a|
+ a.assertion { !@service_missing }
+ a.failure_message Chef::Exceptions::Service, "#{@new_resource}: unable to locate the init.d script!"
+ a.whyrun "Assuming service would be disabled. The init script is not presently installed."
+ end
+ end
+
+ def load_current_resource
+ super
+
+ if ::File.exists?("/sbin/chkconfig")
+ chkconfig = shell_out!("/sbin/chkconfig --list #{@current_resource.service_name}", :returns => [0,1])
+ @current_resource.enabled(!!(chkconfig.stdout =~ CHKCONFIG_ON))
+ @service_missing = !!(chkconfig.stderr =~ CHKCONFIG_MISSING)
+ end
+
+ @current_resource
+ end
+
+ def enable_service()
+ shell_out! "/sbin/chkconfig #{@new_resource.service_name} on"
+ end
+
+ def disable_service()
+ shell_out! "/sbin/chkconfig #{@new_resource.service_name} off"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/simple.rb b/lib/chef/provider/service/simple.rb
new file mode 100644
index 0000000000..670c62d480
--- /dev/null
+++ b/lib/chef/provider/service/simple.rb
@@ -0,0 +1,172 @@
+#
+# Author:: Mathieu Sauve-Frankel <msf@kisoku.net>
+# Copyright:: Copyright (c) 2009 Mathieu Sauve-Frankel
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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'
+require 'chef/provider/service'
+require 'chef/mixin/command'
+
+class Chef
+ class Provider
+ class Service
+ class Simple < Chef::Provider::Service
+
+ include Chef::Mixin::ShellOut
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Service.new(@new_resource.name)
+ @current_resource.service_name(@new_resource.service_name)
+
+ @status_load_success = true
+ @ps_command_failed = false
+
+ determine_current_status!
+
+ @current_resource
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def shared_resource_requirements
+ super
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @status_load_success }
+ a.whyrun ["Service status not available. Assuming a prior action would have installed the service.", "Assuming status of not running."]
+ end
+ end
+
+ def define_resource_requirements
+ # FIXME? need reload from service.rb
+ shared_resource_requirements
+ requirements.assert(:start) do |a|
+ a.assertion { @new_resource.start_command }
+ a.failure_message Chef::Exceptions::Service, "#{self.to_s} requires that start_command be set"
+ end
+ requirements.assert(:stop) do |a|
+ a.assertion { @new_resource.stop_command }
+ a.failure_message Chef::Exceptions::Service, "#{self.to_s} requires that stop_command be set"
+ end
+
+ requirements.assert(:restart) do |a|
+ a.assertion { @new_resource.restart_command || ( @new_resource.start_command && @new_resource.stop_command ) }
+ a.failure_message Chef::Exceptions::Service, "#{self.to_s} requires a restart_command or both start_command and stop_command be set in order to perform a restart"
+ end
+
+ requirements.assert(:reload) do |a|
+ a.assertion { @new_resource.reload_command }
+ a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} requires a reload_command be set in order to perform a reload"
+ end
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @new_resource.status_command or @new_resource.supports[:status] or
+ (!ps_cmd.nil? and !ps_cmd.empty?) }
+ a.failure_message Chef::Exceptions::Service, "#{@new_resource} could not determine how to inspect the process table, please set this node's 'command.ps' attribute"
+ end
+ requirements.assert(:all_actions) do |a|
+ a.assertion { !@ps_command_failed }
+ a.failure_message Chef::Exceptions::Service, "Command #{ps_cmd} failed to execute, cannot determine service current status"
+ end
+ end
+
+ def start_service
+ shell_out!(@new_resource.start_command)
+ end
+
+ def stop_service
+ shell_out!(@new_resource.stop_command)
+ end
+
+ def restart_service
+ if @new_resource.restart_command
+ shell_out!(@new_resource.restart_command)
+ else
+ stop_service
+ sleep 1
+ start_service
+ end
+ end
+
+ def reload_service
+ shell_out!(@new_resource.reload_command)
+ end
+
+ protected
+ def determine_current_status!
+ if @new_resource.status_command
+ Chef::Log.debug("#{@new_resource} you have specified a status command, running..")
+
+ begin
+ if shell_out(@new_resource.status_command).exitstatus == 0
+ @current_resource.running true
+ Chef::Log.debug("#{@new_resource} is running")
+ end
+ rescue Mixlib::ShellOut::ShellCommandFailed, SystemCallError
+ # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed.
+ # Temporarily catching different types of exceptions here until we get Shellout fixed.
+ # TODO: Remove the line before one we get the ShellOut fix.
+ @status_load_success = false
+ @current_resource.running false
+ nil
+ end
+
+ elsif @new_resource.supports[:status]
+ Chef::Log.debug("#{@new_resource} supports status, running")
+ begin
+ if shell_out("#{@init_command} status").exitstatus == 0
+ @current_resource.running true
+ Chef::Log.debug("#{@new_resource} is running")
+ end
+ # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed.
+ # Temporarily catching different types of exceptions here until we get Shellout fixed.
+ # TODO: Remove the line before one we get the ShellOut fix.
+ rescue Mixlib::ShellOut::ShellCommandFailed, SystemCallError
+ @status_load_success = false
+ @current_resource.running false
+ nil
+ end
+ else
+ Chef::Log.debug "#{@new_resource} falling back to process table inspection"
+ r = Regexp.new(@new_resource.pattern)
+ Chef::Log.debug "#{@new_resource} attempting to match '#{@new_resource.pattern}' (#{r.inspect}) against process list"
+ begin
+ shell_out!(ps_cmd).stdout.each_line do |line|
+ if r.match(line)
+ @current_resource.running true
+ break
+ end
+ end
+
+ @current_resource.running false unless @current_resource.running
+ Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}"
+ # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed.
+ # Temporarily catching different types of exceptions here until we get Shellout fixed.
+ # TODO: Remove the line before one we get the ShellOut fix.
+ rescue Mixlib::ShellOut::ShellCommandFailed, SystemCallError
+ @ps_command_failed = true
+ end
+ end
+ end
+
+ def ps_cmd
+ @run_context.node[:command] && @run_context.node[:command][:ps]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/solaris.rb b/lib/chef/provider/service/solaris.rb
new file mode 100644
index 0000000000..8e131590e8
--- /dev/null
+++ b/lib/chef/provider/service/solaris.rb
@@ -0,0 +1,86 @@
+#
+# Author:: Toomas Pelberg (<toomasp@gmx.net>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/service'
+require 'chef/mixin/command'
+
+class Chef
+ class Provider
+ class Service
+ class Solaris < Chef::Provider::Service
+
+ def initialize(new_resource, run_context=nil)
+ super
+ @init_command = "/usr/sbin/svcadm"
+ @status_command = "/bin/svcs -l"
+ end
+
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Service.new(@new_resource.name)
+ @current_resource.service_name(@new_resource.service_name)
+ unless ::File.exists? "/bin/svcs"
+ raise Chef::Exceptions::Service, "/bin/svcs does not exist!"
+ end
+ @status = service_status.enabled
+ @current_resource
+ end
+
+ def enable_service
+ run_command(:command => "#{@init_command} enable #{@new_resource.service_name}")
+ return service_status.enabled
+ end
+
+ def disable_service
+ run_command(:command => "#{@init_command} disable #{@new_resource.service_name}")
+ return service_status.enabled
+ end
+
+ alias_method :stop_service, :disable_service
+ alias_method :start_service, :enable_service
+
+ def reload_service
+ run_command(:command => "#{@init_command} refresh #{@new_resource.service_name}")
+ end
+
+ def restart_service
+ disable_service
+ return enable_service
+ end
+
+ def service_status
+ status = popen4("#{@status_command} #{@current_resource.service_name}") do |pid, stdin, stdout, stderr|
+ stdout.each do |line|
+ case line
+ when /state\s+online/
+ @current_resource.enabled(true)
+ @current_resource.running(true)
+ end
+ end
+ end
+ unless @current_resource.enabled
+ @current_resource.enabled(false)
+ @current_resource.running(false)
+ end
+ @current_resource
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/systemd.rb b/lib/chef/provider/service/systemd.rb
new file mode 100644
index 0000000000..59b4fe1564
--- /dev/null
+++ b/lib/chef/provider/service/systemd.rb
@@ -0,0 +1,115 @@
+#
+# Author:: Stephen Haynes (<sh@nomitor.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/service'
+require 'chef/provider/service/simple'
+require 'chef/mixin/command'
+
+class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple
+ def load_current_resource
+ @current_resource = Chef::Resource::Service.new(@new_resource.name)
+ @current_resource.service_name(@new_resource.service_name)
+ @status_check_success = true
+
+ if @new_resource.status_command
+ Chef::Log.debug("#{@new_resource} you have specified a status command, running..")
+
+ begin
+ if run_command_with_systems_locale(:command => @new_resource.status_command) == 0
+ @current_resource.running(true)
+ end
+ rescue Chef::Exceptions::Exec
+ @status_check_success = false
+ @current_resource.running(false)
+ @current_resource.enabled(false)
+ nil
+ end
+ else
+ @current_resource.running(is_active?)
+ end
+
+ @current_resource.enabled(is_enabled?)
+ @current_resource
+ end
+
+ def define_resource_requirements
+ shared_resource_requirements
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @status_check_success }
+ # We won't stop in any case, but in whyrun warn and tell what we're doing.
+ a.whyrun ["Failed to determine status of #{@new_resource}, using command #{@new_resource.status_command}.",
+ "Assuming service would have been installed and is disabled"]
+ end
+ end
+
+ def start_service
+ if @current_resource.running
+ Chef::Log.debug("#{@new_resource} already running, not starting")
+ else
+ if @new_resource.start_command
+ super
+ else
+ run_command_with_systems_locale(:command => "/bin/systemctl start #{@new_resource.service_name}")
+ end
+ end
+ end
+
+ def stop_service
+ unless @current_resource.running
+ Chef::Log.debug("#{@new_resource} not running, not stopping")
+ else
+ if @new_resource.stop_command
+ super
+ else
+ run_command_with_systems_locale(:command => "/bin/systemctl stop #{@new_resource.service_name}")
+ end
+ end
+ end
+
+ def restart_service
+ if @new_resource.restart_command
+ super
+ else
+ run_command_with_systems_locale(:command => "/bin/systemctl restart #{@new_resource.service_name}")
+ end
+ end
+
+ def reload_service
+ if @new_resource.reload_command
+ super
+ else
+ run_command_with_systems_locale(:command => "/bin/systemctl reload #{@new_resource.service_name}")
+ end
+ end
+
+ def enable_service
+ run_command_with_systems_locale(:command => "/bin/systemctl enable #{@new_resource.service_name}")
+ end
+
+ def disable_service
+ run_command_with_systems_locale(:command => "/bin/systemctl disable #{@new_resource.service_name}")
+ end
+
+ def is_active?
+ run_command_with_systems_locale({:command => "/bin/systemctl is-active #{@new_resource.service_name}", :ignore_failure => true}) == 0
+ end
+
+ def is_enabled?
+ run_command_with_systems_locale({:command => "/bin/systemctl is-enabled #{@new_resource.service_name}", :ignore_failure => true}) == 0
+ end
+end
diff --git a/lib/chef/provider/service/upstart.rb b/lib/chef/provider/service/upstart.rb
new file mode 100644
index 0000000000..763a2aa92b
--- /dev/null
+++ b/lib/chef/provider/service/upstart.rb
@@ -0,0 +1,232 @@
+#
+# Author:: Bryan McLellan <btm@loftninjas.org>
+# Copyright:: Copyright (c) 2010 Bryan McLellan
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/service'
+require 'chef/provider/service/simple'
+require 'chef/mixin/command'
+require 'chef/util/file_edit'
+
+class Chef
+ class Provider
+ class Service
+ class Upstart < Chef::Provider::Service::Simple
+ UPSTART_STATE_FORMAT = /\w+ \(?(\w+)\)?[\/ ](\w+)/
+
+ # Upstart does more than start or stop a service, creating multiple 'states' [1] that a service can be in.
+ # In chef, when we ask a service to start, we expect it to have started before performing the next step
+ # since we have top down dependencies. Which is to say we may follow witha resource next that requires
+ # that service to be running. According to [2] we can trust that sending a 'goal' such as start will not
+ # return until that 'goal' is reached, or some error has occured.
+ #
+ # [1] http://upstart.ubuntu.com/wiki/JobStates
+ # [2] http://www.netsplit.com/2008/04/27/upstart-05-events/
+
+ def initialize(new_resource, run_context)
+ # TODO: re-evaluate if this is needed after integrating cookbook fix
+ raise ArgumentError, "run_context cannot be nil" unless run_context
+ super
+
+ run_context.node
+
+ @job = @new_resource.service_name
+
+ if @new_resource.parameters
+ @new_resource.parameters.each do |key, value|
+ @job << " #{key}=#{value}"
+ end
+ end
+
+ platform, version = Chef::Platform.find_platform_and_version(run_context.node)
+ if platform == "ubuntu" && (8.04..9.04).include?(version.to_f)
+ @upstart_job_dir = "/etc/event.d"
+ @upstart_conf_suffix = ""
+ else
+ @upstart_job_dir = "/etc/init"
+ @upstart_conf_suffix = ".conf"
+ end
+
+ @command_success = true # new_resource.status_command= false, means upstart used
+ @config_file_found = true
+ @upstart_command_success = true
+ end
+
+ def define_resource_requirements
+ # Do not call super, only call shared requirements
+ shared_resource_requirements
+ requirements.assert(:all_actions) do |a|
+ if !@command_success
+ whyrun_msg = @new_resource.status_command ? "Provided status command #{@new_resource.status_command} failed." :
+ "Could not determine upstart state for service"
+ end
+ a.assertion { @command_success }
+ # no failure here, just document the assumptions made.
+ a.whyrun "#{whyrun_msg} Assuming service installed and not running."
+ end
+
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @config_file_found }
+ # no failure here, just document the assumptions made.
+ a.whyrun "Could not find #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}. Assuming service is disabled."
+ end
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Service.new(@new_resource.name)
+ @current_resource.service_name(@new_resource.service_name)
+
+ # Get running/stopped state
+ # We do not support searching for a service via ps when using upstart since status is a native
+ # upstart function. We will however support status_command in case someone wants to do something special.
+ if @new_resource.status_command
+ Chef::Log.debug("#{@new_resource} you have specified a status command, running..")
+
+ begin
+ if run_command_with_systems_locale(:command => @new_resource.status_command) == 0
+ @current_resource.running true
+ end
+ rescue Chef::Exceptions::Exec
+ @command_success = false
+ @current_resource.running false
+ nil
+ end
+ else
+ begin
+ if upstart_state == "running"
+ @current_resource.running true
+ else
+ @current_resource.running false
+ end
+ rescue Chef::Exceptions::Exec
+ @command_success = false
+ @current_resource.running false
+ nil
+ end
+ end
+ # Get enabled/disabled state by reading job configuration file
+ if ::File.exists?("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}")
+ Chef::Log.debug("#{@new_resource} found #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}")
+ ::File.open("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}",'r') do |file|
+ while line = file.gets
+ case line
+ when /^start on/
+ Chef::Log.debug("#{@new_resource} enabled: #{line.chomp}")
+ @current_resource.enabled true
+ break
+ when /^#start on/
+ Chef::Log.debug("#{@new_resource} disabled: #{line.chomp}")
+ @current_resource.enabled false
+ break
+ end
+ end
+ end
+ else
+ @config_file_found = false
+ Chef::Log.debug("#{@new_resource} did not find #{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}")
+ @current_resource.enabled false
+ end
+
+ @current_resource
+ end
+
+ def start_service
+ # Calling start on a service that is already started will return 1
+ # Our 'goal' when we call start is to ensure the service is started
+ if @current_resource.running
+ Chef::Log.debug("#{@new_resource} already running, not starting")
+ else
+ if @new_resource.start_command
+ super
+ else
+ run_command_with_systems_locale(:command => "/sbin/start #{@job}")
+ end
+ end
+ end
+
+ def stop_service
+ # Calling stop on a service that is already stopped will return 1
+ # Our 'goal' when we call stop is to ensure the service is stopped
+ unless @current_resource.running
+ Chef::Log.debug("#{@new_resource} not running, not stopping")
+ else
+ if @new_resource.stop_command
+ super
+ else
+ run_command_with_systems_locale(:command => "/sbin/stop #{@job}")
+ end
+ end
+ end
+
+ def restart_service
+ if @new_resource.restart_command
+ super
+ # Upstart always provides restart functionality so we don't need to mimic it with stop/sleep/start.
+ # Older versions of upstart would fail on restart if the service was currently stopped, check for that. LP:430883
+ else @new_resource.supports[:restart]
+ if @current_resource.running
+ run_command_with_systems_locale(:command => "/sbin/restart #{@job}")
+ else
+ start_service
+ end
+ end
+ end
+
+ def reload_service
+ if @new_resource.reload_command
+ super
+ else
+ # upstart >= 0.6.3-4 supports reload (HUP)
+ run_command_with_systems_locale(:command => "/sbin/reload #{@job}")
+ end
+ end
+
+ # https://bugs.launchpad.net/upstart/+bug/94065
+
+ def enable_service
+ Chef::Log.debug("#{@new_resource} upstart lacks inherent support for enabling services, editing job config file")
+ conf = Chef::Util::FileEdit.new("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}")
+ conf.search_file_replace(/^#start on/, "start on")
+ conf.write_file
+ end
+
+ def disable_service
+ Chef::Log.debug("#{@new_resource} upstart lacks inherent support for disabling services, editing job config file")
+ conf = Chef::Util::FileEdit.new("#{@upstart_job_dir}/#{@new_resource.service_name}#{@upstart_conf_suffix}")
+ conf.search_file_replace(/^start on/, "#start on")
+ conf.write_file
+ end
+
+ def upstart_state
+ command = "/sbin/status #{@job}"
+ status = popen4(command) do |pid, stdin, stdout, stderr|
+ stdout.each_line do |line|
+ # rsyslog stop/waiting
+ # service goal/state
+ # OR
+ # rsyslog (stop) waiting
+ # service (goal) state
+ line =~ UPSTART_STATE_FORMAT
+ data = Regexp.last_match
+ return data[2]
+ end
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb
new file mode 100644
index 0000000000..ba51e53bed
--- /dev/null
+++ b/lib/chef/provider/service/windows.rb
@@ -0,0 +1,163 @@
+#
+# Author:: Nuo Yan <nuo@opscode.com>
+# Author:: Bryan McLellan <btm@loftninjas.org>
+# Author:: Seth Chisamore <schisamo@opscode.com>
+# Copyright:: Copyright (c) 2010-2011 Opscode, Inc
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/shell_out'
+require 'chef/provider/service/simple'
+if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ require 'win32/service'
+end
+
+class Chef::Provider::Service::Windows < Chef::Provider::Service
+
+ include Chef::Mixin::ShellOut
+
+ RUNNING = 'running'
+ STOPPED = 'stopped'
+ AUTO_START = 'auto start'
+ DISABLED = 'disabled'
+
+ def whyrun_supported?
+ false
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Service.new(@new_resource.name)
+ @current_resource.service_name(@new_resource.service_name)
+ @current_resource.running(current_state == RUNNING)
+ Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}"
+ @current_resource.enabled(start_type == AUTO_START)
+ Chef::Log.debug "#{@new_resource} enabled: #{@current_resource.enabled}"
+ @current_resource
+ end
+
+ def start_service
+ if Win32::Service.exists?(@new_resource.service_name)
+ if current_state == RUNNING
+ Chef::Log.debug "#{@new_resource} already started - nothing to do"
+ else
+ if @new_resource.start_command
+ Chef::Log.debug "#{@new_resource} starting service using the given start_command"
+ shell_out!(@new_resource.start_command)
+ else
+ spawn_command_thread do
+ Win32::Service.start(@new_resource.service_name)
+ wait_for_state(RUNNING)
+ end
+ end
+ @new_resource.updated_by_last_action(true)
+ end
+ else
+ Chef::Log.debug "#{@new_resource} does not exist - nothing to do"
+ end
+ end
+
+ def stop_service
+ if Win32::Service.exists?(@new_resource.service_name)
+ if current_state == RUNNING
+ if @new_resource.stop_command
+ Chef::Log.debug "#{@new_resource} stopping service using the given stop_command"
+ shell_out!(@new_resource.stop_command)
+ else
+ spawn_command_thread do
+ Win32::Service.stop(@new_resource.service_name)
+ wait_for_state(STOPPED)
+ end
+ end
+ @new_resource.updated_by_last_action(true)
+ else
+ Chef::Log.debug "#{@new_resource} already stopped - nothing to do"
+ end
+ else
+ Chef::Log.debug "#{@new_resource} does not exist - nothing to do"
+ end
+ end
+
+ def restart_service
+ if Win32::Service.exists?(@new_resource.service_name)
+ if @new_resource.restart_command
+ Chef::Log.debug "#{@new_resource} restarting service using the given restart_command"
+ shell_out!(@new_resource.restart_command)
+ else
+ stop_service
+ start_service
+ end
+ @new_resource.updated_by_last_action(true)
+ else
+ Chef::Log.debug "#{@new_resource} does not exist - nothing to do"
+ end
+ end
+
+ def enable_service
+ if Win32::Service.exists?(@new_resource.service_name)
+ if start_type == AUTO_START
+ Chef::Log.debug "#{@new_resource} already enabled - nothing to do"
+ else
+ Win32::Service.configure(
+ :service_name => @new_resource.service_name,
+ :start_type => Win32::Service::AUTO_START
+ )
+ @new_resource.updated_by_last_action(true)
+ end
+ else
+ Chef::Log.debug "#{@new_resource} does not exist - nothing to do"
+ end
+ end
+
+ def disable_service
+ if Win32::Service.exists?(@new_resource.service_name)
+ if start_type == AUTO_START
+ Win32::Service.configure(
+ :service_name => @new_resource.service_name,
+ :start_type => Win32::Service::DISABLED
+ )
+ @new_resource.updated_by_last_action(true)
+ else
+ Chef::Log.debug "#{@new_resource} already disabled - nothing to do"
+ end
+ else
+ Chef::Log.debug "#{@new_resource} does not exist - nothing to do"
+ end
+ end
+
+ private
+ def current_state
+ Win32::Service.status(@new_resource.service_name).current_state
+ end
+
+ def start_type
+ Win32::Service.config_info(@new_resource.service_name).start_type
+ end
+
+ # Helper method that waits for a status to change its state since state
+ # changes aren't usually instantaneous.
+ def wait_for_state(desired_state)
+ sleep 1 until current_state == desired_state
+ end
+
+ # There ain't no party like a thread party...
+ def spawn_command_thread
+ worker = Thread.new do
+ yield
+ end
+ Timeout.timeout(60) do
+ worker.join
+ end
+ end
+end
diff --git a/lib/chef/provider/subversion.rb b/lib/chef/provider/subversion.rb
new file mode 100644
index 0000000000..e1f87b4dd8
--- /dev/null
+++ b/lib/chef/provider/subversion.rb
@@ -0,0 +1,214 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2008 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.
+#
+
+
+#TODO subversion and git should both extend from a base SCM provider.
+
+require 'chef/log'
+require 'chef/provider'
+require 'chef/mixin/command'
+require 'fileutils'
+
+class Chef
+ class Provider
+ class Subversion < Chef::Provider
+
+ SVN_INFO_PATTERN = /^([\w\s]+): (.+)$/
+
+ include Chef::Mixin::Command
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Subversion.new(@new_resource.name)
+
+ unless [:export, :force_export].include?(Array(@new_resource.action).first)
+ if current_revision = find_current_revision
+ @current_resource.revision current_revision
+ end
+ end
+ end
+
+ def define_resource_requirements
+ requirements.assert(:all_actions) do |a|
+ # Make sure the parent dir exists, or else fail.
+ # for why run, print a message explaining the potential error.
+ parent_directory = ::File.dirname(@new_resource.destination)
+ a.assertion { ::File.directory?(parent_directory) }
+ a.failure_message(Chef::Exceptions::MissingParentDirectory,
+ "Cannot clone #{@new_resource} to #{@new_resource.destination}, the enclosing directory #{parent_directory} does not exist")
+ a.whyrun("Directory #{parent_directory} does not exist, assuming it would have been created")
+ end
+ end
+
+ def action_checkout
+ if target_dir_non_existent_or_empty?
+ converge_by("perform checkout of #{@new_resource.repository} into #{@new_resource.destination}") do
+ run_command(run_options(:command => checkout_command))
+ end
+ else
+ Chef::Log.debug "#{@new_resource} checkout destination #{@new_resource.destination} already exists or is a non-empty directory - nothing to do"
+ end
+ end
+
+ def action_export
+ if target_dir_non_existent_or_empty?
+ action_force_export
+ else
+ Chef::Log.debug "#{@new_resource} export destination #{@new_resource.destination} already exists or is a non-empty directory - nothing to do"
+ end
+ end
+
+ def action_force_export
+ converge_by("export #{@new_resource.repository} into #{@new_resource.destination}") do
+ run_command(run_options(:command => export_command))
+ end
+ end
+
+ def action_sync
+ assert_target_directory_valid!
+ if ::File.exist?(::File.join(@new_resource.destination, ".svn"))
+ current_rev = find_current_revision
+ Chef::Log.debug "#{@new_resource} current revision: #{current_rev} target revision: #{revision_int}"
+ unless current_revision_matches_target_revision?
+ converge_by("sync #{@new_resource.destination} from #{@new_resource.repository}") do
+ run_command(run_options(:command => sync_command))
+ Chef::Log.info "#{@new_resource} updated to revision: #{revision_int}"
+ end
+ end
+ else
+ action_checkout
+ end
+ end
+
+ def sync_command
+ c = scm :update, @new_resource.svn_arguments, verbose, authentication, "-r#{revision_int}", @new_resource.destination
+ Chef::Log.debug "#{@new_resource} updated working copy #{@new_resource.destination} to revision #{@new_resource.revision}"
+ c
+ end
+
+ def checkout_command
+ c = scm :checkout, @new_resource.svn_arguments, verbose, authentication,
+ "-r#{revision_int}", @new_resource.repository, @new_resource.destination
+ Chef::Log.info "#{@new_resource} checked out #{@new_resource.repository} at revision #{@new_resource.revision} to #{@new_resource.destination}"
+ c
+ end
+
+ def export_command
+ args = ["--force"]
+ args << @new_resource.svn_arguments << verbose << authentication <<
+ "-r#{revision_int}" << @new_resource.repository << @new_resource.destination
+ c = scm :export, *args
+ Chef::Log.info "#{@new_resource} exported #{@new_resource.repository} at revision #{@new_resource.revision} to #{@new_resource.destination}"
+ c
+ end
+
+ # If the specified revision isn't an integer ("HEAD" for example), look
+ # up the revision id by asking the server
+ # If the specified revision is an integer, trust it.
+ def revision_int
+ @revision_int ||= begin
+ if @new_resource.revision =~ /^\d+$/
+ @new_resource.revision
+ else
+ command = scm(:info, @new_resource.repository, @new_resource.svn_info_args, authentication, "-r#{@new_resource.revision}")
+ status, svn_info, error_message = output_of_command(command, run_options)
+ handle_command_failures(status, "STDOUT: #{svn_info}\nSTDERR: #{error_message}")
+ extract_revision_info(svn_info)
+ end
+ end
+ end
+
+ alias :revision_slug :revision_int
+
+ def find_current_revision
+ return nil unless ::File.exist?(::File.join(@new_resource.destination, ".svn"))
+ command = scm(:info)
+ status, svn_info, error_message = output_of_command(command, run_options(:cwd => cwd))
+
+ unless [0,1].include?(status.exitstatus)
+ handle_command_failures(status, "STDOUT: #{svn_info}\nSTDERR: #{error_message}")
+ end
+ extract_revision_info(svn_info)
+ end
+
+ def current_revision_matches_target_revision?
+ (!@current_resource.revision.nil?) && (revision_int.strip.to_i == @current_resource.revision.strip.to_i)
+ end
+
+ def run_options(run_opts={})
+ run_opts[:user] = @new_resource.user if @new_resource.user
+ run_opts[:group] = @new_resource.group if @new_resource.group
+ run_opts
+ end
+
+ private
+
+ def cwd
+ @new_resource.destination
+ end
+
+ def verbose
+ "-q"
+ end
+
+ def extract_revision_info(svn_info)
+ repo_attrs = svn_info.lines.inject({}) do |attrs, line|
+ if line =~ SVN_INFO_PATTERN
+ property, value = $1, $2
+ attrs[property] = value
+ end
+ attrs
+ end
+ rev = (repo_attrs['Last Changed Rev'] || repo_attrs['Revision'])
+ raise "Could not parse `svn info` data: #{svn_info}" if repo_attrs.empty?
+ Chef::Log.debug "#{@new_resource} resolved revision #{@new_resource.revision} to #{rev}"
+ rev
+ end
+
+ # If a username is configured for the SCM, return the command-line
+ # switches for that. Note that we don't need to return the password
+ # switch, since Capistrano will check for that prompt in the output
+ # and will respond appropriately.
+ def authentication
+ return "" unless @new_resource.svn_username
+ result = "--username #{@new_resource.svn_username} "
+ result << "--password #{@new_resource.svn_password} "
+ result
+ end
+
+ def scm(*args)
+ ['svn', *args].compact.join(" ")
+ end
+
+
+ def target_dir_non_existent_or_empty?
+ !::File.exist?(@new_resource.destination) || Dir.entries(@new_resource.destination).sort == ['.','..']
+ end
+ def assert_target_directory_valid!
+ target_parent_directory = ::File.dirname(@new_resource.destination)
+ unless ::File.directory?(target_parent_directory)
+ msg = "Cannot clone #{@new_resource} to #{@new_resource.destination}, the enclosing directory #{target_parent_directory} does not exist"
+ raise Chef::Exceptions::MissingParentDirectory, msg
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/template.rb b/lib/chef/provider/template.rb
new file mode 100644
index 0000000000..c937b9d980
--- /dev/null
+++ b/lib/chef/provider/template.rb
@@ -0,0 +1,117 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/file'
+require 'chef/mixin/template'
+require 'chef/mixin/checksum'
+require 'chef/file_access_control'
+
+class Chef
+ class Provider
+
+ class Template < Chef::Provider::File
+
+ include Chef::Mixin::Checksum
+ include Chef::Mixin::Template
+
+ def load_current_resource
+ @current_resource = Chef::Resource::Template.new(@new_resource.name)
+ super
+ end
+
+ def define_resource_requirements
+ super
+
+ requirements.assert(:create, :create_if_missing) do |a|
+ a.assertion { ::File::exist?(template_location) }
+ a.failure_message "Template source #{template_location} could not be found."
+ a.whyrun "Template source #{template_location} does not exist. Assuming it would have been created."
+ a.block_action!
+ end
+ end
+
+ def action_create
+ render_with_context(template_location) do |rendered_template|
+ rendered(rendered_template)
+ update = ::File.exist?(@new_resource.path)
+ if update && content_matches?
+ Chef::Log.debug("#{@new_resource} content has not changed.")
+ set_all_access_controls
+ else
+ description = []
+ action_message = update ? "update #{@current_resource} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(@new_resource.checksum)}" :
+ "create #{@new_resource}"
+ description << action_message
+ description << diff_current(rendered_template.path)
+ converge_by(description) do
+ backup
+ FileUtils.mv(rendered_template.path, @new_resource.path)
+ Chef::Log.info("#{@new_resource} updated content")
+ access_controls.set_all!
+ stat = ::File.stat(@new_resource.path)
+
+ # template depends on the checksum not changing, and updates it
+ # itself later in the code, so we cannot set it here, as we do with
+ # all other < File child provider classes
+ @new_resource.owner(stat.uid)
+ @new_resource.mode(stat.mode & 07777)
+ @new_resource.group(stat.gid)
+ end
+ end
+ end
+ end
+
+
+ def template_location
+ @template_file_cache_location ||= begin
+ if @new_resource.local
+ @new_resource.source
+ else
+ cookbook = run_context.cookbook_collection[resource_cookbook]
+ cookbook.preferred_filename_on_disk_location(node, :templates, @new_resource.source)
+ end
+ end
+ end
+
+ def resource_cookbook
+ @new_resource.cookbook || @new_resource.cookbook_name
+ end
+
+ def rendered(rendered_template)
+ @new_resource.checksum(checksum(rendered_template.path))
+ Chef::Log.debug("Current content's checksum: #{@current_resource.checksum}")
+ Chef::Log.debug("Rendered content's checksum: #{@new_resource.checksum}")
+ end
+
+ def content_matches?
+ @current_resource.checksum == @new_resource.checksum
+ end
+
+ private
+
+ def render_with_context(template_location, &block)
+ context = {}
+ context.merge!(@new_resource.variables)
+ context[:node] = node
+ render_template(IO.read(template_location), context, &block)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb
new file mode 100644
index 0000000000..e73c9de57e
--- /dev/null
+++ b/lib/chef/provider/user.rb
@@ -0,0 +1,207 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider'
+require 'chef/mixin/command'
+require 'chef/resource/user'
+require 'etc'
+
+class Chef
+ class Provider
+ class User < Chef::Provider
+
+ include Chef::Mixin::Command
+
+ attr_accessor :user_exists, :locked
+
+ def initialize(new_resource, run_context)
+ super
+ @user_exists = true
+ @locked = nil
+ @shadow_lib_ok = true
+ @group_name_resolved = true
+ end
+
+ def convert_group_name
+ if @new_resource.gid.is_a? String
+ @new_resource.gid(Etc.getgrnam(@new_resource.gid).gid)
+ end
+ rescue ArgumentError => e
+ @group_name_resolved = false
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::User.new(@new_resource.name)
+ @current_resource.username(@new_resource.username)
+
+ begin
+ user_info = Etc.getpwnam(@new_resource.username)
+ rescue ArgumentError => e
+ @user_exists = false
+ Chef::Log.debug("#{@new_resource} user does not exist")
+ user_info = nil
+ end
+
+ if user_info
+ @current_resource.uid(user_info.uid)
+ @current_resource.gid(user_info.gid)
+ @current_resource.comment(user_info.gecos)
+ @current_resource.home(user_info.dir)
+ @current_resource.shell(user_info.shell)
+ @current_resource.password(user_info.passwd)
+
+ if @new_resource.password && @current_resource.password == 'x'
+ begin
+ require 'shadow'
+ rescue LoadError
+ @shadow_lib_ok = false
+ else
+ shadow_info = Shadow::Passwd.getspnam(@new_resource.username)
+ @current_resource.password(shadow_info.sp_pwdp)
+ end
+ end
+
+ if @new_resource.gid
+ convert_group_name
+ end
+ end
+
+ @current_resource
+ end
+
+ def define_resource_requirements
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @group_name_resolved }
+ a.failure_message Chef::Exceptions::User, "Couldn't lookup integer GID for group name #{@new_resource.gid}"
+ a.whyrun "group name #{@new_resource.gid} does not exist. This will cause group assignment to fail. Assuming this group will have been created previously."
+ end
+ requirements.assert(:all_actions) do |a|
+ a.assertion { @shadow_lib_ok }
+ a.failure_message Chef::Exceptions::MissingLibrary, "You must have ruby-shadow installed for password support!"
+ a.whyrun "ruby-shadow is not installed. Attempts to set user password will cause failure. Assuming that this gem will have been previously installed." +
+ "Note that user update converge may report false-positive on the basis of mismatched password. "
+ end
+ requirements.assert(:modify, :lock, :unlock) do |a|
+ a.assertion { @user_exists }
+ a.failure_message(Chef::Exceptions::User, "Cannot modify user #{@new_resource} - does not exist!")
+ a.whyrun("Assuming user #{@new_resource} would have been created")
+ end
+ end
+
+ # Check to see if the user needs any changes
+ #
+ # === Returns
+ # <true>:: If a change is required
+ # <false>:: If the users are identical
+ def compare_user
+ [ :uid, :gid, :comment, :home, :shell, :password ].any? do |user_attrib|
+ !@new_resource.send(user_attrib).nil? && @new_resource.send(user_attrib) != @current_resource.send(user_attrib)
+ end
+ end
+
+ def action_create
+
+ if !@user_exists
+ converge_by("create user #{@new_resource}") do
+ create_user
+ Chef::Log.info("#{@new_resource} created")
+ end
+ elsif compare_user
+ converge_by("alter user #{@new_resource}") do
+ manage_user
+ Chef::Log.info("#{@new_resource} altered")
+ end
+ end
+ end
+
+ def action_remove
+ if @user_exists
+ converge_by("remove user #{@new_resource}") do
+ remove_user
+ Chef::Log.info("#{@new_resource} removed")
+ end
+ end
+ end
+
+ def remove_user
+ raise NotImplementedError
+ end
+
+ def action_manage
+ if @user_exists && compare_user
+ converge_by("manage user #{@new_resource}") do
+ manage_user
+ Chef::Log.info("#{@new_resource} managed")
+ end
+ end
+ end
+
+ def manage_user
+ raise NotImplementedError
+ end
+
+ def action_modify
+ if compare_user
+ converge_by("modify user #{@new_resource}") do
+ manage_user
+ Chef::Log.info("#{@new_resource} modified")
+ end
+ end
+ end
+
+ def action_lock
+ if check_lock() == false
+ converge_by("lock the user #{@new_resource}") do
+ lock_user
+ Chef::Log.info("#{@new_resource} locked")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} already locked - nothing to do")
+ end
+ end
+
+ def check_lock
+ raise NotImplementedError
+ end
+
+ def lock_user
+ raise NotImplementedError
+ end
+
+ def action_unlock
+ if check_lock() == true
+ converge_by("unlock user #{@new_resource}") do
+ unlock_user
+ Chef::Log.info("#{@new_resource} unlocked")
+ end
+ else
+ Chef::Log.debug("#{@new_resource} already unlocked - nothing to do")
+ end
+ end
+
+ def unlock_user
+ raise NotImplementedError
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/user/dscl.rb b/lib/chef/provider/user/dscl.rb
new file mode 100644
index 0000000000..94e8420c43
--- /dev/null
+++ b/lib/chef/provider/user/dscl.rb
@@ -0,0 +1,288 @@
+#
+# Author:: Dreamcat4 (<dreamcat4@gmail.com>)
+# Copyright:: Copyright (c) 2009 OpsCode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/shell_out'
+require 'chef/provider/user'
+require 'openssl'
+
+class Chef
+ class Provider
+ class User
+ class Dscl < Chef::Provider::User
+ include Chef::Mixin::ShellOut
+
+ NFS_HOME_DIRECTORY = %r{^NFSHomeDirectory: (.*)$}
+ AUTHENTICATION_AUTHORITY = %r{^AuthenticationAuthority: (.*)$}
+
+ def dscl(*args)
+ shell_out("dscl . -#{args.join(' ')}")
+ end
+
+ def safe_dscl(*args)
+ result = dscl(*args)
+ return "" if ( args.first =~ /^delete/ ) && ( result.exitstatus != 0 )
+ raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") unless result.exitstatus == 0
+ raise(Chef::Exceptions::DsclCommandFailed,"dscl error: #{result.inspect}") if result.stdout =~ /No such key: /
+ return result.stdout
+ end
+
+ # This is handled in providers/group.rb by Etc.getgrnam()
+ # def user_exists?(user)
+ # users = safe_dscl("list /Users")
+ # !! ( users =~ Regexp.new("\n#{user}\n") )
+ # end
+
+ # get a free UID greater than 200
+ def get_free_uid(search_limit=1000)
+ uid = nil; next_uid_guess = 200
+ users_uids = safe_dscl("list /Users uid")
+ while(next_uid_guess < search_limit + 200)
+ if users_uids =~ Regexp.new("#{Regexp.escape(next_uid_guess.to_s)}\n")
+ next_uid_guess += 1
+ else
+ uid = next_uid_guess
+ break
+ end
+ end
+ return uid || raise("uid not found. Exhausted. Searched #{search_limit} times")
+ end
+
+ def uid_used?(uid)
+ return false unless uid
+ users_uids = safe_dscl("list /Users uid")
+ !! ( users_uids =~ Regexp.new("#{Regexp.escape(uid.to_s)}\n") )
+ end
+
+ def set_uid
+ @new_resource.uid(get_free_uid) if (@new_resource.uid.nil? || @new_resource.uid == '')
+ if uid_used?(@new_resource.uid)
+ raise(Chef::Exceptions::RequestedUIDUnavailable, "uid #{@new_resource.uid} is already in use")
+ end
+ safe_dscl("create /Users/#{@new_resource.username} UniqueID #{@new_resource.uid}")
+ end
+
+ def modify_home
+ return safe_dscl("delete /Users/#{@new_resource.username} NFSHomeDirectory") if (@new_resource.home.nil? || @new_resource.home.empty?)
+ if @new_resource.supports[:manage_home]
+ validate_home_dir_specification!
+
+ if (@current_resource.home == @new_resource.home) && !new_home_exists?
+ ditto_home
+ elsif !current_home_exists? && !new_home_exists?
+ ditto_home
+ elsif current_home_exists?
+ move_home
+ end
+ end
+ safe_dscl("create /Users/#{@new_resource.username} NFSHomeDirectory '#{@new_resource.home}'")
+ end
+
+ def osx_shadow_hash?(string)
+ return !! ( string =~ /^[[:xdigit:]]{1240}$/ )
+ end
+
+ def osx_salted_sha1?(string)
+ return !! ( string =~ /^[[:xdigit:]]{48}$/ )
+ end
+
+ def guid
+ safe_dscl("read /Users/#{@new_resource.username} GeneratedUID").gsub(/GeneratedUID: /,"").strip
+ end
+
+ def shadow_hash_set?
+ user_data = safe_dscl("read /Users/#{@new_resource.username}")
+ if user_data =~ /AuthenticationAuthority: / && user_data =~ /ShadowHash/
+ true
+ else
+ false
+ end
+ end
+
+ def modify_password
+ if @new_resource.password
+ shadow_hash = nil
+
+ Chef::Log.debug("#{new_resource} updating password")
+ if osx_shadow_hash?(@new_resource.password)
+ shadow_hash = @new_resource.password.upcase
+ else
+ if osx_salted_sha1?(@new_resource.password)
+ salted_sha1 = @new_resource.password.upcase
+ else
+ hex_salt = ""
+ OpenSSL::Random.random_bytes(10).each_byte { |b| hex_salt << b.to_i.to_s(16) }
+ hex_salt = hex_salt.slice(0...8)
+ salt = [hex_salt].pack("H*")
+ sha1 = ::OpenSSL::Digest::SHA1.hexdigest(salt+@new_resource.password)
+ salted_sha1 = (hex_salt+sha1).upcase
+ end
+ shadow_hash = String.new("00000000"*155)
+ shadow_hash[168] = salted_sha1
+ end
+
+ ::File.open("/var/db/shadow/hash/#{guid}",'w',0600) do |output|
+ output.puts shadow_hash
+ end
+
+ unless shadow_hash_set?
+ safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';ShadowHash;'")
+ end
+ end
+ end
+
+ def load_current_resource
+ super
+ raise Chef::Exceptions::User, "Could not find binary /usr/bin/dscl for #{@new_resource}" unless ::File.exists?("/usr/bin/dscl")
+ end
+
+ def create_user
+ dscl_create_user
+ dscl_create_comment
+ set_uid
+ dscl_set_gid
+ modify_home
+ dscl_set_shell
+ modify_password
+ end
+
+ def manage_user
+ dscl_create_user if diverged?(:username)
+ dscl_create_comment if diverged?(:comment)
+ set_uid if diverged?(:uid)
+ dscl_set_gid if diverged?(:gid)
+ modify_home if diverged?(:home)
+ dscl_set_shell if diverged?(:shell)
+ modify_password if diverged?(:password)
+ end
+
+ def dscl_create_user
+ safe_dscl("create /Users/#{@new_resource.username}")
+ end
+
+ def dscl_create_comment
+ safe_dscl("create /Users/#{@new_resource.username} RealName '#{@new_resource.comment}'")
+ end
+
+ def dscl_set_gid
+ unless @new_resource.gid && @new_resource.gid.to_s.match(/^\d+$/)
+ begin
+ possible_gid = safe_dscl("read /Groups/#{@new_resource.gid} PrimaryGroupID").split(" ").last
+ rescue Chef::Exceptions::DsclCommandFailed => e
+ raise Chef::Exceptions::GroupIDNotFound.new("Group not found for #{@new_resource.gid} when creating user #{@new_resource.username}")
+ end
+ @new_resource.gid(possible_gid) if possible_gid && possible_gid.match(/^\d+$/)
+ end
+ safe_dscl("create /Users/#{@new_resource.username} PrimaryGroupID '#{@new_resource.gid}'")
+ end
+
+ def dscl_set_shell
+ if @new_resource.password || ::File.exists?("#{@new_resource.shell}")
+ safe_dscl("create /Users/#{@new_resource.username} UserShell '#{@new_resource.shell}'")
+ else
+ safe_dscl("create /Users/#{@new_resource.username} UserShell '/usr/bin/false'")
+ end
+ end
+
+ def remove_user
+ if @new_resource.supports[:manage_home]
+ user_info = safe_dscl("read /Users/#{@new_resource.username}")
+ if nfs_home_match = user_info.match(NFS_HOME_DIRECTORY)
+ #nfs_home = safe_dscl("read /Users/#{@new_resource.username} NFSHomeDirectory")
+ #nfs_home.gsub!(/NFSHomeDirectory: /,"").gsub!(/\n$/,"")
+ nfs_home = nfs_home_match[1]
+ FileUtils.rm_rf(nfs_home)
+ end
+ end
+ # remove the user from its groups
+ groups = []
+ Etc.group do |group|
+ groups << group.name if group.mem.include?(@new_resource.username)
+ end
+ groups.each do |group_name|
+ safe_dscl("delete /Groups/#{group_name} GroupMembership '#{@new_resource.username}'")
+ end
+ # remove user account
+ safe_dscl("delete /Users/#{@new_resource.username}")
+ end
+
+ def locked?
+ user_info = safe_dscl("read /Users/#{@new_resource.username}")
+ if auth_authority_md = AUTHENTICATION_AUTHORITY.match(user_info)
+ !!(auth_authority_md[1] =~ /DisabledUser/ )
+ else
+ false
+ end
+ end
+
+ def check_lock
+ return @locked = locked?
+ end
+
+ def lock_user
+ safe_dscl("append /Users/#{@new_resource.username} AuthenticationAuthority ';DisabledUser;'")
+ end
+
+ def unlock_user
+ auth_info = safe_dscl("read /Users/#{@new_resource.username} AuthenticationAuthority")
+ auth_string = auth_info.gsub(/AuthenticationAuthority: /,"").gsub(/;DisabledUser;/,"").strip#.gsub!(/[; ]*$/,"")
+ safe_dscl("create /Users/#{@new_resource.username} AuthenticationAuthority '#{auth_string}'")
+ end
+
+ def validate_home_dir_specification!
+ unless @new_resource.home =~ /^\//
+ raise(Chef::Exceptions::InvalidHomeDirectory,"invalid path spec for User: '#{@new_resource.username}', home directory: '#{@new_resource.home}'")
+ end
+ end
+
+ def current_home_exists?
+ ::File.exist?("#{@current_resource.home}")
+ end
+
+ def new_home_exists?
+ ::File.exist?("#{@new_resource.home}")
+ end
+
+ def ditto_home
+ skel = "/System/Library/User Template/English.lproj"
+ raise(Chef::Exceptions::User,"can't find skel at: #{skel}") unless ::File.exists?(skel)
+ shell_out! "ditto '#{skel}' '#{@new_resource.home}'"
+ ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home)
+ end
+
+ def move_home
+ Chef::Log.debug("#{@new_resource} moving #{self} home from #{@current_resource.home} to #{@new_resource.home}")
+
+ src = @current_resource.home
+ FileUtils.mkdir_p(@new_resource.home)
+ files = ::Dir.glob("#{src}/*", ::File::FNM_DOTMATCH) - ["#{src}/.","#{src}/.."]
+ ::FileUtils.mv(files,@new_resource.home, :force => true)
+ ::FileUtils.rmdir(src)
+ ::FileUtils.chown_R(@new_resource.username,@new_resource.gid.to_s,@new_resource.home)
+ end
+
+ def diverged?(parameter)
+ parameter_updated?(parameter) && (not @new_resource.send(parameter).nil?)
+ end
+
+ def parameter_updated?(parameter)
+ not (@new_resource.send(parameter) == @current_resource.send(parameter))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/user/pw.rb b/lib/chef/provider/user/pw.rb
new file mode 100644
index 0000000000..4f6393da89
--- /dev/null
+++ b/lib/chef/provider/user/pw.rb
@@ -0,0 +1,113 @@
+#
+# Author:: Stephen Haynes (<sh@nomitor.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/user'
+
+class Chef
+ class Provider
+ class User
+ class Pw < Chef::Provider::User
+
+ def load_current_resource
+ super
+ raise Chef::Exceptions::User, "Could not find binary /usr/sbin/pw for #{@new_resource}" unless ::File.exists?("/usr/sbin/pw")
+ end
+
+ def create_user
+ command = "pw useradd"
+ command << set_options
+ run_command(:command => command)
+ modify_password
+ end
+
+ def manage_user
+ command = "pw usermod"
+ command << set_options
+ run_command(:command => command)
+ modify_password
+ end
+
+ def remove_user
+ command = "pw userdel #{@new_resource.username}"
+ command << " -r" if @new_resource.supports[:manage_home]
+ run_command(:command => command)
+ end
+
+ def check_lock
+ case @current_resource.password
+ when /^\*LOCKED\*/
+ @locked = true
+ else
+ @locked = false
+ end
+ @locked
+ end
+
+ def lock_user
+ run_command(:command => "pw lock #{@new_resource.username}")
+ end
+
+ def unlock_user
+ run_command(:command => "pw unlock #{@new_resource.username}")
+ end
+
+ def set_options
+ opts = " #{@new_resource.username}"
+
+ field_list = {
+ 'comment' => "-c",
+ 'home' => "-d",
+ 'gid' => "-g",
+ 'uid' => "-u",
+ 'shell' => "-s"
+ }
+ field_list.sort{ |a,b| a[0] <=> b[0] }.each do |field, option|
+ field_symbol = field.to_sym
+ if @current_resource.send(field_symbol) != @new_resource.send(field_symbol)
+ if @new_resource.send(field_symbol)
+ Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field_symbol)}")
+ opts << " #{option} '#{@new_resource.send(field_symbol)}'"
+ end
+ end
+ end
+ if @new_resource.supports[:manage_home]
+ Chef::Log.debug("#{@new_resource} is managing the users home directory")
+ opts << " -m"
+ end
+ opts
+ end
+
+ def modify_password
+ if @current_resource.password != @new_resource.password
+ Chef::Log.debug("#{new_resource} updating password")
+ command = "pw usermod #{@new_resource.username} -H 0"
+ status = popen4(command, :waitlast => true) do |pid, stdin, stdout, stderr|
+ stdin.puts "#{@new_resource.password}"
+ end
+
+ unless status.exitstatus == 0
+ raise Chef::Exceptions::User, "pw failed - #{status.inspect}!"
+ end
+ else
+ Chef::Log.debug("#{new_resource} no change needed to password")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/user/useradd.rb b/lib/chef/provider/user/useradd.rb
new file mode 100644
index 0000000000..489632f722
--- /dev/null
+++ b/lib/chef/provider/user/useradd.rb
@@ -0,0 +1,144 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 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 'pathname'
+require 'chef/provider/user'
+
+class Chef
+ class Provider
+ class User
+ class Useradd < Chef::Provider::User
+ UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:password, "-p"], [:shell, "-s"], [:uid, "-u"]]
+
+ def create_user
+ command = compile_command("useradd") do |useradd|
+ useradd << universal_options
+ useradd << useradd_options
+ end
+ run_command(:command => command)
+ end
+
+ def manage_user
+ command = compile_command("usermod") do |u|
+ u << universal_options
+ end
+ run_command(:command => command)
+ end
+
+ def remove_user
+ command = "userdel"
+ command << " -r" if managing_home_dir?
+ command << " #{@new_resource.username}"
+ run_command(:command => command)
+ end
+
+ def check_lock
+ status = popen4("passwd -S #{@new_resource.username}") do |pid, stdin, stdout, stderr|
+ status_line = stdout.gets.split(' ')
+ case status_line[1]
+ when /^P/
+ @locked = false
+ when /^N/
+ @locked = false
+ when /^L/
+ @locked = true
+ end
+ end
+
+ unless status.exitstatus == 0
+ raise_lock_error = false
+ # we can get an exit code of 1 even when it's successful on rhel/centos (redhat bug 578534)
+ if status.exitstatus == 1 && ['redhat', 'centos'].include?(node[:platform])
+ passwd_version_status = popen4('rpm -q passwd') do |pid, stdin, stdout, stderr|
+ passwd_version = stdout.gets.chomp
+
+ unless passwd_version == 'passwd-0.73-1'
+ raise_lock_error = true
+ end
+ end
+ else
+ raise_lock_error = true
+ end
+
+ raise Chef::Exceptions::User, "Cannot determine if #{@new_resource} is locked!" if raise_lock_error
+ end
+
+ @locked
+ end
+
+ def lock_user
+ run_command(:command => "usermod -L #{@new_resource.username}")
+ end
+
+ def unlock_user
+ run_command(:command => "usermod -U #{@new_resource.username}")
+ end
+
+ def compile_command(base_command)
+ yield base_command
+ base_command << " #{@new_resource.username}"
+ base_command
+ end
+
+ def universal_options
+ opts = ''
+
+ UNIVERSAL_OPTIONS.each do |field, option|
+ if @current_resource.send(field) != @new_resource.send(field)
+ if @new_resource.send(field)
+ Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field)}")
+ opts << " #{option} '#{@new_resource.send(field)}'"
+ end
+ end
+ end
+ if updating_home?
+ if managing_home_dir?
+ Chef::Log.debug("#{@new_resource} managing the users home directory")
+ opts << " -m -d '#{@new_resource.home}'"
+ else
+ Chef::Log.debug("#{@new_resource} setting home to #{@new_resource.home}")
+ opts << " -d '#{@new_resource.home}'"
+ end
+ end
+ opts << " -o" if @new_resource.non_unique || @new_resource.supports[:non_unique]
+ opts
+ end
+
+ def useradd_options
+ opts = ''
+ opts << " -r" if @new_resource.system
+ opts
+ end
+
+ def updating_home?
+ # will return false if paths are equivalent
+ # Pathname#cleanpath does a better job than ::File::expand_path (on both unix and windows)
+ # ::File.expand_path("///tmp") == ::File.expand_path("/tmp") => false
+ # ::File.expand_path("\\tmp") => "C:/tmp"
+ return true if @current_resource.home.nil? && @new_resource.home
+ @new_resource.home and Pathname.new(@current_resource.home).cleanpath != Pathname.new(@new_resource.home).cleanpath
+ end
+
+ def managing_home_dir?
+ @new_resource.manage_home || @new_resource.supports[:manage_home]
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/user/windows.rb b/lib/chef/provider/user/windows.rb
new file mode 100644
index 0000000000..6bbb2a088c
--- /dev/null
+++ b/lib/chef/provider/user/windows.rb
@@ -0,0 +1,124 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/provider/user'
+if RUBY_PLATFORM =~ /mswin|mingw32|windows/
+ require 'chef/util/windows/net_user'
+end
+
+class Chef
+ class Provider
+ class User
+ class Windows < Chef::Provider::User
+
+ def initialize(new_resource,run_context)
+ super
+ @net_user = Chef::Util::Windows::NetUser.new(@new_resource.name)
+ end
+
+ def load_current_resource
+ @current_resource = Chef::Resource::User.new(@new_resource.name)
+ @current_resource.username(@new_resource.username)
+ user_info = nil
+ begin
+ user_info = @net_user.get_info
+ rescue
+ @user_exists = false
+ Chef::Log.debug("#{@new_resource} does not exist")
+ end
+
+ if user_info
+ @current_resource.uid(user_info[:user_id])
+ @current_resource.gid(user_info[:primary_group_id])
+ @current_resource.comment(user_info[:full_name])
+ @current_resource.home(user_info[:home_dir])
+ @current_resource.shell(user_info[:script_path])
+ end
+
+ @current_resource
+ end
+
+ # Check to see if the user needs any changes
+ #
+ # === Returns
+ # <true>:: If a change is required
+ # <false>:: If the users are identical
+ def compare_user
+ unless @net_user.validate_credentials(@new_resource.password)
+ Chef::Log.debug("#{@new_resource} password has changed")
+ return true
+ end
+ [ :uid, :gid, :comment, :home, :shell ].any? do |user_attrib|
+ !@new_resource.send(user_attrib).nil? && @new_resource.send(user_attrib) != @current_resource.send(user_attrib)
+ end
+ end
+
+ def create_user
+ @net_user.add(set_options)
+ end
+
+ def manage_user
+ @net_user.update(set_options)
+ end
+
+ def remove_user
+ @net_user.delete
+ end
+
+ def check_lock
+ @net_user.check_enabled
+ end
+
+ def lock_user
+ @net_user.disable_account
+ end
+
+ def unlock_user
+ @net_user.enable_account
+ end
+
+ def set_options
+ opts = {:name => @new_resource.username}
+
+ field_list = {
+ 'comment' => 'full_name',
+ 'home' => 'home_dir',
+ 'gid' => 'primary_group_id',
+ 'uid' => 'user_id',
+ 'shell' => 'script_path',
+ 'password' => 'password'
+ }
+
+ field_list.sort{ |a,b| a[0] <=> b[0] }.each do |field, option|
+ field_symbol = field.to_sym
+ if @current_resource.send(field_symbol) != @new_resource.send(field_symbol)
+ if @new_resource.send(field_symbol)
+ unless field_symbol == :password
+ Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field_symbol)}")
+ end
+ opts[option.to_sym] = @new_resource.send(field_symbol)
+ end
+ end
+ end
+ opts
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb
new file mode 100644
index 0000000000..36281abf6a
--- /dev/null
+++ b/lib/chef/providers.rb
@@ -0,0 +1,100 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/breakpoint'
+require 'chef/provider/cookbook_file'
+require 'chef/provider/cron'
+require 'chef/provider/cron/solaris'
+require 'chef/provider/deploy'
+require 'chef/provider/directory'
+require 'chef/provider/env'
+require 'chef/provider/erl_call'
+require 'chef/provider/execute'
+require 'chef/provider/file'
+require 'chef/provider/git'
+require 'chef/provider/group'
+require 'chef/provider/http_request'
+require 'chef/provider/ifconfig'
+require 'chef/provider/link'
+require 'chef/provider/log'
+require 'chef/provider/ohai'
+require 'chef/provider/mdadm'
+require 'chef/provider/mount'
+require 'chef/provider/package'
+require 'chef/provider/remote_directory'
+require 'chef/provider/remote_file'
+require 'chef/provider/route'
+require 'chef/provider/ruby_block'
+require 'chef/provider/script'
+require 'chef/provider/service'
+require 'chef/provider/subversion'
+require 'chef/provider/template'
+require 'chef/provider/user'
+
+require 'chef/provider/env/windows'
+
+require 'chef/provider/package/apt'
+require 'chef/provider/package/dpkg'
+require 'chef/provider/package/easy_install'
+require 'chef/provider/package/freebsd'
+require 'chef/provider/package/ips'
+require 'chef/provider/package/macports'
+require 'chef/provider/package/pacman'
+require 'chef/provider/package/portage'
+require 'chef/provider/package/rpm'
+require 'chef/provider/package/rubygems'
+require 'chef/provider/package/yum'
+require 'chef/provider/package/zypper'
+require 'chef/provider/package/solaris'
+require 'chef/provider/package/smartos'
+
+require 'chef/provider/service/arch'
+require 'chef/provider/service/debian'
+require 'chef/provider/service/freebsd'
+require 'chef/provider/service/gentoo'
+require 'chef/provider/service/init'
+require 'chef/provider/service/insserv'
+require 'chef/provider/service/invokercd'
+require 'chef/provider/service/redhat'
+require 'chef/provider/service/simple'
+require 'chef/provider/service/systemd'
+require 'chef/provider/service/upstart'
+require 'chef/provider/service/windows'
+require 'chef/provider/service/solaris'
+require 'chef/provider/service/macosx'
+
+require 'chef/provider/user/dscl'
+require 'chef/provider/user/pw'
+require 'chef/provider/user/useradd'
+require 'chef/provider/user/windows'
+
+require 'chef/provider/group/aix'
+require 'chef/provider/group/dscl'
+require 'chef/provider/group/gpasswd'
+require 'chef/provider/group/groupadd'
+require 'chef/provider/group/groupmod'
+require 'chef/provider/group/pw'
+require 'chef/provider/group/suse'
+require 'chef/provider/group/usermod'
+require 'chef/provider/group/windows'
+
+require 'chef/provider/mount/mount'
+require 'chef/provider/mount/windows'
+
+require 'chef/provider/deploy/revision'
+require 'chef/provider/deploy/timestamped'
diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb
new file mode 100644
index 0000000000..aca35db049
--- /dev/null
+++ b/lib/chef/recipe.rb
@@ -0,0 +1,133 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+require 'chef/dsl/recipe'
+require 'chef/dsl/data_query'
+require 'chef/dsl/platform_introspection'
+require 'chef/dsl/include_recipe'
+
+require 'chef/mixin/from_file'
+
+require 'chef/mixin/deprecation'
+
+class Chef
+ # == Chef::Recipe
+ # A Recipe object is the context in which Chef recipes are evaluated.
+ class Recipe
+
+ include Chef::DSL::DataQuery
+ include Chef::DSL::PlatformIntrospection
+ include Chef::DSL::IncludeRecipe
+ include Chef::DSL::Recipe
+
+ include Chef::Mixin::FromFile
+ include Chef::Mixin::Deprecation
+
+ attr_accessor :cookbook_name, :recipe_name, :recipe, :params, :run_context
+
+ # Parses a potentially fully-qualified recipe name into its
+ # cookbook name and recipe short name.
+ #
+ # For example:
+ # "aws::elastic_ip" returns [:aws, "elastic_ip"]
+ # "aws" returns [:aws, "default"]
+ #--
+ # TODO: Duplicates functionality of RunListItem
+ def self.parse_recipe_name(recipe_name)
+ rmatch = recipe_name.match(/(.+?)::(.+)/)
+ if rmatch
+ [ rmatch[1].to_sym, rmatch[2] ]
+ else
+ [ recipe_name.to_sym, "default" ]
+ end
+ end
+
+ def initialize(cookbook_name, recipe_name, run_context)
+ @cookbook_name = cookbook_name
+ @recipe_name = recipe_name
+ @run_context = run_context
+ # TODO: 5/19/2010 cw/tim: determine whether this can be removed
+ @params = Hash.new
+ @node = deprecated_ivar(run_context.node, :node, :warn)
+ end
+
+ # Used in DSL mixins
+ def node
+ run_context.node
+ end
+
+ # Used by the DSL to look up resources when executing in the context of a
+ # recipe.
+ def resources(*args)
+ run_context.resource_collection.find(*args)
+ end
+
+ # Sets a tag, or list of tags, for this node. Syntactic sugar for
+ # run_context.node[:tags].
+ #
+ # With no arguments, returns the list of tags.
+ #
+ # === Parameters
+ # tags<Array>:: A list of tags to add - can be a single string
+ #
+ # === Returns
+ # tags<Array>:: The contents of run_context.node[:tags]
+ def tag(*tags)
+ if tags.length > 0
+ tags.each do |tag|
+ tag = tag.to_s
+ run_context.node.normal[:tags] << tag unless run_context.node[:tags].include?(tag)
+ end
+ run_context.node[:tags]
+ else
+ run_context.node[:tags]
+ end
+ end
+
+ # Returns true if the node is tagged with *all* of the supplied +tags+.
+ #
+ # === Parameters
+ # tags<Array>:: A list of tags
+ #
+ # === Returns
+ # true<TrueClass>:: If all the parameters are present
+ # false<FalseClass>:: If any of the parameters are missing
+ def tagged?(*tags)
+ tags.each do |tag|
+ return false unless run_context.node[:tags].include?(tag)
+ end
+ true
+ end
+
+ # Removes the list of tags from the node.
+ #
+ # === Parameters
+ # tags<Array>:: A list of tags
+ #
+ # === Returns
+ # tags<Array>:: The current list of run_context.node[:tags]
+ def untag(*tags)
+ tags.each do |tag|
+ run_context.node.normal[:tags].delete(tag)
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/reserved_names.rb b/lib/chef/reserved_names.rb
new file mode 100644
index 0000000000..029cddac9a
--- /dev/null
+++ b/lib/chef/reserved_names.rb
@@ -0,0 +1,9 @@
+class Chef
+
+ # This module exists to hide conflicting constant names from the DSL.
+ # Hopefully we'll have a better/prettier/more sustainable solution in the
+ # future, but for now this will fix a regression introduced in Chef 0.10.10
+ # (conflict with the Win32 namespace)
+ module ReservedNames
+ end
+end
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
new file mode 100644
index 0000000000..9a1b983360
--- /dev/null
+++ b/lib/chef/resource.rb
@@ -0,0 +1,853 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/params_validate'
+require 'chef/mixin/check_helper'
+require 'chef/dsl/platform_introspection'
+require 'chef/mixin/convert_to_class_name'
+require 'chef/resource/conditional'
+require 'chef/resource_collection'
+require 'chef/resource_platform_map'
+require 'chef/node'
+
+require 'chef/mixin/deprecation'
+
+class Chef
+ class Resource
+ class Notification < Struct.new(:resource, :action, :notifying_resource)
+
+ def duplicates?(other_notification)
+ unless other_notification.respond_to?(:resource) && other_notification.respond_to?(:action)
+ msg = "only duck-types of Chef::Resource::Notification can be checked for duplication "\
+ "you gave #{other_notification.inspect}"
+ raise ArgumentError, msg
+ end
+ other_notification.resource == resource && other_notification.action == action
+ end
+
+ # If resource and/or notifying_resource is not a resource object, this will look them up in the resource collection
+ # and fix the references from strings to actual Resource objects.
+ def resolve_resource_reference(resource_collection)
+ return resource if resource.kind_of?(Chef::Resource) && notifying_resource.kind_of?(Chef::Resource)
+
+ if not(resource.kind_of?(Chef::Resource))
+ fix_resource_reference(resource_collection)
+ end
+
+ if not(notifying_resource.kind_of?(Chef::Resource))
+ fix_notifier_reference(resource_collection)
+ end
+ end
+
+ # This will look up the resource if it is not a Resource Object. It will complain if it finds multiple
+ # resources, can't find a resource, or gets invalid syntax.
+ def fix_resource_reference(resource_collection)
+ matching_resource = resource_collection.find(resource)
+ if Array(matching_resource).size > 1
+ msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple resources, "\
+ "but can only notify one resource. Notifying resource was defined on #{notifying_resource.source_line}"
+ raise Chef::Exceptions::InvalidResourceReference, msg
+ end
+ self.resource = matching_resource
+
+ rescue Chef::Exceptions::ResourceNotFound => e
+ err = Chef::Exceptions::ResourceNotFound.new(<<-FAIL)
+resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
+but #{resource} cannot be found in the resource collection. #{notifying_resource} is defined in \
+#{notifying_resource.source_line}
+FAIL
+ err.set_backtrace(e.backtrace)
+ raise err
+ rescue Chef::Exceptions::InvalidResourceSpecification => e
+ err = Chef::Exceptions::InvalidResourceSpecification.new(<<-F)
+Resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
+but #{resource.inspect} is not valid syntax to look up a resource in the resource collection. Notification \
+is defined near #{notifying_resource.source_line}
+F
+ err.set_backtrace(e.backtrace)
+ raise err
+ end
+
+ # This will look up the notifying_resource if it is not a Resource Object. It will complain if it finds multiple
+ # resources, can't find a resource, or gets invalid syntax.
+ def fix_notifier_reference(resource_collection)
+ matching_notifier = resource_collection.find(notifying_resource)
+ if Array(matching_notifier).size > 1
+ msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple notifying "\
+ "resources, but can only originate from one resource. Destination resource was defined "\
+ "on #{resource.source_line}"
+ raise Chef::Exceptions::InvalidResourceReference, msg
+ end
+ self.notifying_resource = matching_notifier
+
+ rescue Chef::Exceptions::ResourceNotFound => e
+ err = Chef::Exceptions::ResourceNotFound.new(<<-FAIL)
+Resource #{resource} is configured to receive notifications from #{notifying_resource} with action #{action}, \
+but #{notifying_resource} cannot be found in the resource collection. #{resource} is defined in \
+#{resource.source_line}
+FAIL
+ err.set_backtrace(e.backtrace)
+ raise err
+ rescue Chef::Exceptions::InvalidResourceSpecification => e
+ err = Chef::Exceptions::InvalidResourceSpecification.new(<<-F)
+Resource #{resource} is configured to receive notifications from #{notifying_resource} with action #{action}, \
+but #{notifying_resource.inspect} is not valid syntax to look up a resource in the resource collection. Notification \
+is defined near #{resource.source_line}
+F
+ err.set_backtrace(e.backtrace)
+ raise err
+ end
+
+ end
+
+ FORBIDDEN_IVARS = [:@run_context, :@node, :@not_if, :@only_if, :@enclosing_provider]
+ HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@name, :@node, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider]
+
+ include Chef::Mixin::CheckHelper
+ include Chef::Mixin::ParamsValidate
+ include Chef::DSL::PlatformIntrospection
+ include Chef::Mixin::ConvertToClassName
+ include Chef::Mixin::Deprecation
+
+
+ # Set or return the list of "state attributes" implemented by the Resource
+ # subclass. State attributes are attributes that describe the desired state
+ # of the system, such as file permissions or ownership. In general, state
+ # attributes are attributes that could be populated by examining the state
+ # of the system (e.g., File.stat can tell you the permissions on an
+ # existing file). Contrarily, attributes that are not "state attributes"
+ # usually modify the way Chef itself behaves, for example by providing
+ # additional options for a package manager to use when installing a
+ # package.
+ #
+ # This list is used by the Chef client auditing system to extract
+ # information from resources to describe changes made to the system.
+ def self.state_attrs(*attr_names)
+ @state_attrs ||= []
+ @state_attrs = attr_names unless attr_names.empty?
+
+ # Return *all* state_attrs that this class has, including inherited ones
+ if superclass.respond_to?(:state_attrs)
+ superclass.state_attrs + @state_attrs
+ else
+ @state_attrs
+ end
+ end
+
+ # Set or return the "identity attribute" for this resource class. This is
+ # generally going to be the "name attribute" for this resource. In other
+ # words, the resource type plus this attribute uniquely identify a given
+ # bit of state that chef manages. For a File resource, this would be the
+ # path, for a package resource, it will be the package name. This will show
+ # up in chef-client's audit records as a searchable field.
+ def self.identity_attr(attr_name=nil)
+ @identity_attr ||= nil
+ @identity_attr = attr_name if attr_name
+
+ # If this class doesn't have an identity attr, we'll defer to the superclass:
+ if @identity_attr || !superclass.respond_to?(:identity_attr)
+ @identity_attr
+ else
+ superclass.identity_attr
+ end
+ end
+
+ def self.dsl_name
+ convert_to_snake_case(name, 'Chef::Resource')
+ end
+
+ attr_accessor :params
+ attr_accessor :provider
+ attr_accessor :allowed_actions
+ attr_accessor :run_context
+ attr_accessor :cookbook_name
+ attr_accessor :recipe_name
+ attr_accessor :enclosing_provider
+ attr_accessor :source_line
+ attr_accessor :retries
+ attr_accessor :retry_delay
+
+ attr_reader :updated
+
+ attr_reader :resource_name
+ attr_reader :not_if_args
+ attr_reader :only_if_args
+
+ attr_reader :elapsed_time
+
+ # Each notify entry is a resource/action pair, modeled as an
+ # Struct with a #resource and #action member
+
+ def initialize(name, run_context=nil)
+ @name = name
+ @run_context = run_context
+ @noop = nil
+ @before = nil
+ @params = Hash.new
+ @provider = nil
+ @allowed_actions = [ :nothing ]
+ @action = :nothing
+ @updated = false
+ @updated_by_last_action = false
+ @supports = {}
+ @ignore_failure = false
+ @retries = 0
+ @retry_delay = 2
+ @not_if = []
+ @only_if = []
+ @source_line = nil
+ @elapsed_time = 0
+
+ @node = run_context ? deprecated_ivar(run_context.node, :node, :warn) : nil
+ end
+
+ # Returns a Hash of attribute => value for the state attributes declared in
+ # the resource's class definition.
+ def state
+ self.class.state_attrs.inject({}) do |state_attrs, attr_name|
+ state_attrs[attr_name] = send(attr_name)
+ state_attrs
+ end
+ end
+
+ # Returns the value of the identity attribute, if declared. Falls back to
+ # #name if no identity attribute is declared.
+ def identity
+ if identity_attr = self.class.identity_attr
+ send(identity_attr)
+ else
+ name
+ end
+ end
+
+
+ def updated=(true_or_false)
+ Chef::Log.warn("Chef::Resource#updated=(true|false) is deprecated. Please call #updated_by_last_action(true|false) instead.")
+ Chef::Log.warn("Called from:")
+ caller[0..3].each {|line| Chef::Log.warn(line)}
+ updated_by_last_action(true_or_false)
+ @updated = true_or_false
+ end
+
+ def node
+ run_context && run_context.node
+ end
+
+ # If an unknown method is invoked, determine whether the enclosing Provider's
+ # lexical scope can fulfill the request. E.g. This happens when the Resource's
+ # block invokes new_resource.
+ def method_missing(method_symbol, *args, &block)
+ if enclosing_provider && enclosing_provider.respond_to?(method_symbol)
+ enclosing_provider.send(method_symbol, *args, &block)
+ else
+ raise NoMethodError, "undefined method `#{method_symbol.to_s}' for #{self.class.to_s}"
+ end
+ end
+
+ def load_prior_resource
+ begin
+ prior_resource = run_context.resource_collection.lookup(self.to_s)
+ Chef::Log.debug("Setting #{self.to_s} to the state of the prior #{self.to_s}")
+ prior_resource.instance_variables.each do |iv|
+ unless iv.to_sym == :@source_line || iv.to_sym == :@action
+ self.instance_variable_set(iv, prior_resource.instance_variable_get(iv))
+ end
+ end
+ true
+ rescue Chef::Exceptions::ResourceNotFound
+ true
+ end
+ end
+
+ def supports(args={})
+ if args.any?
+ @supports = args
+ else
+ @supports
+ end
+ end
+
+ def provider(arg=nil)
+ klass = if arg.kind_of?(String) || arg.kind_of?(Symbol)
+ lookup_provider_constant(arg)
+ else
+ arg
+ end
+ set_or_return(
+ :provider,
+ klass,
+ :kind_of => [ Class ]
+ )
+ end
+
+ def action(arg=nil)
+ if arg
+ action_list = arg.kind_of?(Array) ? arg : [ arg ]
+ action_list = action_list.collect { |a| a.to_sym }
+ action_list.each do |action|
+ validate(
+ {
+ :action => action,
+ },
+ {
+ :action => { :kind_of => Symbol, :equal_to => @allowed_actions },
+ }
+ )
+ end
+ @action = action_list
+ else
+ @action
+ end
+ end
+
+ def name(name=nil)
+ set_if_args(@name, name) do
+ raise ArgumentError, "name must be a string!" unless name.kind_of?(String)
+ @name = name
+ end
+ end
+
+ def noop(tf=nil)
+ set_if_args(@noop, tf) do
+ raise ArgumentError, "noop must be true or false!" unless tf == true || tf == false
+ @noop = tf
+ end
+ end
+
+ def ignore_failure(arg=nil)
+ set_or_return(
+ :ignore_failure,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def retries(arg=nil)
+ set_or_return(
+ :retries,
+ arg,
+ :kind_of => Integer
+ )
+ end
+
+ def retry_delay(arg=nil)
+ set_or_return(
+ :retry_delay,
+ arg,
+ :kind_of => Integer
+ )
+ end
+
+ def epic_fail(arg=nil)
+ ignore_failure(arg)
+ end
+
+ # Sets up a notification from this resource to the resource specified by +resource_spec+.
+ def notifies(action, resource_spec, timing=:delayed)
+ # when using old-style resources(:template => "/foo.txt") style, you
+ # could end up with multiple resources.
+ resources = [ resource_spec ].flatten
+ resources.each do |resource|
+ case timing.to_s
+ when 'delayed'
+ notifies_delayed(action, resource)
+ when 'immediate', 'immediately'
+ notifies_immediately(action, resource)
+ else
+ raise ArgumentError, "invalid timing: #{timing} for notifies(#{action}, #{resources.inspect}, #{timing}) resource #{self} "\
+ "Valid timings are: :delayed, :immediate, :immediately"
+ end
+ end
+
+ true
+ end
+
+ # Iterates over all immediate and delayed notifications, calling
+ # resolve_resource_reference on each in turn, causing them to
+ # resolve lazy/forward references.
+ def resolve_notification_references
+ run_context.immediate_notifications(self).each { |n| n.resolve_resource_reference(run_context.resource_collection) }
+ run_context.delayed_notifications(self).each {|n| n.resolve_resource_reference(run_context.resource_collection) }
+ end
+
+ def notifies_immediately(action, resource_spec)
+ run_context.notifies_immediately(Notification.new(resource_spec, action, self))
+ end
+
+ def notifies_delayed(action, resource_spec)
+ run_context.notifies_delayed(Notification.new(resource_spec, action, self))
+ end
+
+ def immediate_notifications
+ run_context.immediate_notifications(self)
+ end
+
+ def delayed_notifications
+ run_context.delayed_notifications(self)
+ end
+
+ def resources(*args)
+ run_context.resource_collection.find(*args)
+ end
+
+ def subscribes(action, resources, timing=:delayed)
+ resources = [resources].flatten
+ resources.each do |resource|
+ if resource.is_a?(String)
+ resource = Chef::Resource.new(resource, run_context)
+ end
+ if resource.run_context.nil?
+ resource.run_context = run_context
+ end
+ resource.notifies(action, self, timing)
+ end
+ true
+ end
+
+ def is(*args)
+ if args.size == 1
+ args.first
+ else
+ return *args
+ end
+ end
+
+ def to_s
+ "#{@resource_name}[#{@name}]"
+ end
+
+ def to_text
+ ivars = instance_variables.map { |ivar| ivar.to_sym } - HIDDEN_IVARS
+ text = "# Declared in #{@source_line}\n\n"
+ text << self.class.dsl_name + "(\"#{name}\") do\n"
+ ivars.each do |ivar|
+ if (value = instance_variable_get(ivar)) && !(value.respond_to?(:empty?) && value.empty?)
+ value_string = value.respond_to?(:to_text) ? value.to_text : value.inspect
+ text << " #{ivar.to_s.sub(/^@/,'')} #{value_string}\n"
+ end
+ end
+ [@not_if, @only_if].flatten.each do |conditional|
+ text << " #{conditional.to_text}\n"
+ end
+ text << "end\n"
+ end
+
+ def inspect
+ ivars = instance_variables.map { |ivar| ivar.to_sym } - FORBIDDEN_IVARS
+ ivars.inject("<#{to_s}") do |str, ivar|
+ str << " #{ivar}: #{instance_variable_get(ivar).inspect}"
+ end << ">"
+ end
+
+ # as_json does most of the to_json heavy lifted. It exists here in case activesupport
+ # is loaded. activesupport will call as_json and skip over to_json. This ensure
+ # json is encoded as expected
+ def as_json(*a)
+ safe_ivars = instance_variables.map { |ivar| ivar.to_sym } - FORBIDDEN_IVARS
+ instance_vars = Hash.new
+ safe_ivars.each do |iv|
+ instance_vars[iv.to_s.sub(/^@/, '')] = instance_variable_get(iv)
+ end
+ {
+ 'json_class' => self.class.name,
+ 'instance_vars' => instance_vars
+ }
+ end
+
+ # Serialize this object as a hash
+ def to_json(*a)
+ results = as_json
+ results.to_json(*a)
+ end
+
+ def to_hash
+ safe_ivars = instance_variables.map { |ivar| ivar.to_sym } - FORBIDDEN_IVARS
+ instance_vars = Hash.new
+ safe_ivars.each do |iv|
+ key = iv.to_s.sub(/^@/,'').to_sym
+ instance_vars[key] = instance_variable_get(iv)
+ end
+ instance_vars
+ end
+
+ # If command is a block, returns true if the block returns true, false if it returns false.
+ # ("Only run this resource if the block is true")
+ #
+ # If the command is not a block, executes the command. If it returns any status other than
+ # 0, it returns false (clearly, a 0 status code is true)
+ #
+ # === Parameters
+ # command<String>:: A a string to execute.
+ # opts<Hash>:: Options control the execution of the command
+ # block<Proc>:: A ruby block to run. Ignored if a command is given.
+ #
+ # === Evaluation
+ # * evaluates to true if the block is true, or if the command returns 0
+ # * evaluates to false if the block is false, or if the command returns a non-zero exit code.
+ def only_if(command=nil, opts={}, &block)
+ if command || block_given?
+ @only_if << Conditional.only_if(command, opts, &block)
+ end
+ @only_if
+ end
+
+ # If command is a block, returns false if the block returns true, true if it returns false.
+ # ("Do not run this resource if the block is true")
+ #
+ # If the command is not a block, executes the command. If it returns a 0 exitstatus, returns false.
+ # ("Do not run this resource if the command returns 0")
+ #
+ # === Parameters
+ # command<String>:: A a string to execute.
+ # opts<Hash>:: Options control the execution of the command
+ # block<Proc>:: A ruby block to run. Ignored if a command is given.
+ #
+ # === Evaluation
+ # * evaluates to true if the block is false, or if the command returns a non-zero exit status.
+ # * evaluates to false if the block is true, or if the command returns a 0 exit status.
+ def not_if(command=nil, opts={}, &block)
+ if command || block_given?
+ @not_if << Conditional.not_if(command, opts, &block)
+ end
+ @not_if
+ end
+
+ def defined_at
+ if cookbook_name && recipe_name && source_line
+ "#{cookbook_name}::#{recipe_name} line #{source_line.split(':')[1]}"
+ elsif source_line
+ file, line_no = source_line.split(':')
+ "#{file} line #{line_no}"
+ else
+ "dynamically defined"
+ end
+ end
+
+ def cookbook_version
+ if cookbook_name
+ run_context.cookbook_collection[cookbook_name]
+ end
+ end
+
+ def events
+ run_context.events
+ end
+
+ def run_action(action, notification_type=nil, notifying_resource=nil)
+ # reset state in case of multiple actions on the same resource.
+ @elapsed_time = 0
+ start_time = Time.now
+ events.resource_action_start(self, action, notification_type, notifying_resource)
+ # Try to resolve lazy/forward references in notifications again to handle
+ # the case where the resource was defined lazily (ie. in a ruby_block)
+ resolve_notification_references
+ validate_action(action)
+
+ if Chef::Config[:verbose_logging] || Chef::Log.level == :debug
+ # This can be noisy
+ Chef::Log.info("Processing #{self} action #{action} (#{defined_at})")
+ end
+
+ # ensure that we don't leave @updated_by_last_action set to true
+ # on accident
+ updated_by_last_action(false)
+
+ begin
+ return if should_skip?(action)
+ provider_for_action(action).run_action
+ rescue Exception => e
+ if ignore_failure
+ Chef::Log.error("#{self} (#{defined_at}) had an error: #{e.message}; ignore_failure is set, continuing")
+ events.resource_failed(self, action, e)
+ elsif retries > 0
+ events.resource_failed_retriable(self, action, retries, e)
+ @retries -= 1
+ Chef::Log.info("Retrying execution of #{self}, #{retries} attempt(s) left")
+ sleep retry_delay
+ retry
+ else
+ events.resource_failed(self, action, e)
+ raise customize_exception(e)
+ end
+ ensure
+ @elapsed_time = Time.now - start_time
+ events.resource_completed(self)
+ end
+ end
+
+ def validate_action(action)
+ raise ArgumentError, "nil is not a valid action for resource #{self}" if action.nil?
+ end
+
+ def provider_for_action(action)
+ # leverage new platform => short_name => resource
+ # which requires explicitly setting provider in
+ # resource class
+ if self.provider
+ provider = self.provider.new(self, self.run_context)
+ provider.action = action
+ provider
+ else # fall back to old provider resolution
+ Chef::Platform.provider_for_resource(self, action)
+ end
+ end
+
+ def customize_exception(e)
+ new_exception = e.exception("#{self} (#{defined_at}) had an error: #{e.class.name}: #{e.message}")
+ new_exception.set_backtrace(e.backtrace)
+ new_exception
+ end
+ # Evaluates not_if and only_if conditionals. Returns a falsey value if any
+ # of the conditionals indicate that this resource should be skipped, i.e.,
+ # if an only_if evaluates to false or a not_if evaluates to true.
+ #
+ # If this resource should be skipped, returns the first conditional that
+ # "fails" its check. Subsequent conditionals are not evaluated, so in
+ # general it's not a good idea to rely on side effects from not_if or
+ # only_if commands/blocks being evaluated.
+ def should_skip?(action)
+ conditionals = only_if + not_if
+ return false if conditionals.empty?
+
+ conditionals.find do |conditional|
+ if conditional.continue?
+ false
+ else
+ events.resource_skipped(self, action, conditional)
+ Chef::Log.debug("Skipping #{self} due to #{conditional.description}")
+ true
+ end
+ end
+ end
+
+ def updated_by_last_action(true_or_false)
+ @updated ||= true_or_false
+ @updated_by_last_action = true_or_false
+ end
+
+ def updated_by_last_action?
+ @updated_by_last_action
+ end
+
+ def updated?
+ updated
+ end
+
+ def self.json_create(o)
+ resource = self.new(o["instance_vars"]["@name"])
+ o["instance_vars"].each do |k,v|
+ resource.instance_variable_set("@#{k}".to_sym, v)
+ end
+ resource
+ end
+
+ # Hook to allow a resource to run specific code after creation
+ def after_created
+ nil
+ end
+
+ extend Chef::Mixin::ConvertToClassName
+
+ def self.attribute(attr_name, validation_opts={})
+ # This atrocity is the only way to support 1.8 and 1.9 at the same time
+ # When you're ready to drop 1.8 support, do this:
+ # define_method attr_name.to_sym do |arg=nil|
+ # etc.
+ shim_method=<<-SHIM
+ def #{attr_name}(arg=nil)
+ _set_or_return_#{attr_name}(arg)
+ end
+ SHIM
+ class_eval(shim_method)
+
+ define_method("_set_or_return_#{attr_name.to_s}".to_sym) do |arg|
+ set_or_return(attr_name.to_sym, arg, validation_opts)
+ end
+ end
+
+ def self.build_from_file(cookbook_name, filename, run_context)
+ rname = filename_to_qualified_string(cookbook_name, filename)
+
+ # Add log entry if we override an existing light-weight resource.
+ class_name = convert_to_class_name(rname)
+ overriding = Chef::Resource.const_defined?(class_name)
+ Chef::Log.info("#{class_name} light-weight resource already initialized -- overriding!") if overriding
+
+ new_resource_class = Class.new self do |cls|
+
+ # default initialize method that ensures that when initialize is finally
+ # wrapped (see below), super is called in the event that the resource
+ # definer does not implement initialize
+ def initialize(name, run_context)
+ super(name, run_context)
+ end
+
+ @actions_to_create = []
+
+ class << cls
+ include Chef::Mixin::FromFile
+
+ attr_accessor :run_context
+ attr_reader :action_to_set_default
+
+ def node
+ self.run_context.node
+ end
+
+ def actions_to_create
+ @actions_to_create
+ end
+
+ define_method(:default_action) do |action_name|
+ actions_to_create.push(action_name)
+ @action_to_set_default = action_name
+ end
+
+ define_method(:actions) do |*action_names|
+ actions_to_create.push(*action_names)
+ end
+ end
+
+ # set the run context in the class instance variable
+ cls.run_context = run_context
+
+ # load resource definition from file
+ cls.class_from_file(filename)
+
+ # create a new constructor that wraps the old one and adds the actions
+ # specified in the DSL
+ old_init = instance_method(:initialize)
+
+ define_method(:initialize) do |name, *optional_args|
+ args_run_context = optional_args.shift
+ @resource_name = rname.to_sym
+ old_init.bind(self).call(name, args_run_context)
+ @action = self.class.action_to_set_default || @action
+ allowed_actions.push(self.class.actions_to_create).flatten!
+ end
+ end
+
+ # register new class as a Chef::Resource
+ class_name = convert_to_class_name(rname)
+ Chef::Resource.const_set(class_name, new_resource_class)
+ Chef::Log.debug("Loaded contents of #{filename} into a resource named #{rname} defined in Chef::Resource::#{class_name}")
+
+ new_resource_class
+ end
+
+ # Resources that want providers namespaced somewhere other than
+ # Chef::Provider can set the namespace with +provider_base+
+ # Ex:
+ # class MyResource < Chef::Resource
+ # provider_base Chef::Provider::Deploy
+ # # ...other stuff
+ # end
+ def self.provider_base(arg=nil)
+ @provider_base ||= arg
+ @provider_base ||= Chef::Provider
+ end
+
+ def self.platform_map
+ @@platform_map ||= PlatformMap.new
+ end
+
+ # Maps a short_name (and optionally a platform and version) to a
+ # Chef::Resource. This allows finer grained per platform resource
+ # attributes and the end of overloaded resource definitions
+ # (I'm looking at you Chef::Resource::Package)
+ # Ex:
+ # class WindowsFile < Chef::Resource
+ # provides :file, :on_platforms => ["windows"]
+ # # ...other stuff
+ # end
+ #
+ # TODO: 2011-11-02 schisamo - platform_version support
+ def self.provides(short_name, opts={})
+ short_name_sym = short_name
+ if short_name.kind_of?(String)
+ short_name.downcase!
+ short_name.gsub!(/\s/, "_")
+ short_name_sym = short_name.to_sym
+ end
+ if opts.has_key?(:on_platforms)
+ platforms = [opts[:on_platforms]].flatten
+ platforms.each do |p|
+ p = :default if :all == p.to_sym
+ platform_map.set(
+ :platform => p.to_sym,
+ :short_name => short_name_sym,
+ :resource => self
+ )
+ end
+ else
+ platform_map.set(
+ :short_name => short_name_sym,
+ :resource => self
+ )
+ end
+ end
+
+ # Returns a resource based on a short_name anda platform and version.
+ #
+ #
+ # ==== Parameters
+ # short_name<Symbol>:: short_name of the resource (ie :directory)
+ # platform<Symbol,String>:: platform name
+ # version<String>:: platform version
+ #
+ # === Returns
+ # <Chef::Resource>:: returns the proper Chef::Resource class
+ def self.resource_for_platform(short_name, platform=nil, version=nil)
+ platform_map.get(short_name, platform, version)
+ end
+
+ # Returns a resource based on a short_name and a node's
+ # platform and version.
+ #
+ # ==== Parameters
+ # short_name<Symbol>:: short_name of the resource (ie :directory)
+ # node<Chef::Node>:: Node object to look up platform and version in
+ #
+ # === Returns
+ # <Chef::Resource>:: returns the proper Chef::Resource class
+ def self.resource_for_node(short_name, node)
+ begin
+ platform, version = Chef::Platform.find_platform_and_version(node)
+ rescue ArgumentError
+ end
+ resource = resource_for_platform(short_name, platform, version)
+ resource
+ end
+
+ private
+
+ def lookup_provider_constant(name)
+ begin
+ self.class.provider_base.const_get(convert_to_class_name(name.to_s))
+ rescue NameError => e
+ if e.to_s =~ /#{Regexp.escape(self.class.provider_base.to_s)}/
+ raise ArgumentError, "No provider found to match '#{name}'"
+ else
+ raise e
+ end
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/resource/apt_package.rb b/lib/chef/resource/apt_package.rb
new file mode 100644
index 0000000000..524abbb370
--- /dev/null
+++ b/lib/chef/resource/apt_package.rb
@@ -0,0 +1,43 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+require 'chef/provider/package/apt'
+
+class Chef
+ class Resource
+ class AptPackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :apt_package
+ @provider = Chef::Provider::Package::Apt
+ @default_release = nil
+ end
+
+ def default_release(arg=nil)
+ set_or_return(
+ :default_release,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/bash.rb b/lib/chef/resource/bash.rb
new file mode 100644
index 0000000000..374bca9e11
--- /dev/null
+++ b/lib/chef/resource/bash.rb
@@ -0,0 +1,33 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/script'
+
+class Chef
+ class Resource
+ class Bash < Chef::Resource::Script
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :bash
+ @interpreter = "bash"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/breakpoint.rb b/lib/chef/resource/breakpoint.rb
new file mode 100644
index 0000000000..34aeae6b47
--- /dev/null
+++ b/lib/chef/resource/breakpoint.rb
@@ -0,0 +1,35 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+require 'chef/resource'
+
+class Chef
+ class Resource
+ class Breakpoint < Chef::Resource
+
+ def initialize(action="break", *args)
+ @name = caller.first
+ super(@name, *args)
+ @action = "break"
+ @allowed_actions << :break
+ @provider = Chef::Provider::Breakpoint
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/chef_gem.rb b/lib/chef/resource/chef_gem.rb
new file mode 100644
index 0000000000..d8de0edc00
--- /dev/null
+++ b/lib/chef/resource/chef_gem.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Bryan McLellan <btm@loftninjas.org>
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+require 'chef/resource/gem_package'
+
+class Chef
+ class Resource
+ class ChefGem < Chef::Resource::Package::GemPackage
+
+ provides :chef_gem, :on_platforms => :all
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :chef_gem
+ @provider = Chef::Provider::Package::Rubygems
+ end
+
+ # The chef_gem resources is for installing gems to the current gem environment only for use by Chef cookbooks.
+ def gem_binary(arg=nil)
+ if arg
+ raise ArgumentError, "The chef_gem resource is restricted to the current gem environment, use gem_package to install to other environments."
+ end
+
+ nil
+ end
+
+ def after_created
+ # Chef::Resource.run_action: Caveat: this skips Chef::Runner.run_action, where notifications are handled
+ # Action could be an array of symbols, but probably won't (think install + enable for a package)
+ Array(@action).each do |action|
+ self.run_action(action)
+ end
+ Gem.clear_paths
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/conditional.rb b/lib/chef/resource/conditional.rb
new file mode 100644
index 0000000000..3f892af827
--- /dev/null
+++ b/lib/chef/resource/conditional.rb
@@ -0,0 +1,101 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Resource
+ class Conditional
+ include Chef::Mixin::ShellOut
+
+ # We only create these via the `not_if` or `only_if` constructors, and
+ # not the default constructor
+ class << self
+ private :new
+ end
+
+ def self.not_if(command=nil, command_opts={}, &block)
+ new(:not_if, command, command_opts, &block)
+ end
+
+ def self.only_if(command=nil, command_opts={}, &block)
+ new(:only_if, command, command_opts, &block)
+ end
+
+ attr_reader :positivity
+ attr_reader :command
+ attr_reader :command_opts
+ attr_reader :block
+
+ def initialize(positivity, command=nil, command_opts={}, &block)
+ @positivity = positivity
+ case command
+ when String
+ @command, @command_opts = command, command_opts
+ @block = nil
+ when nil
+ raise ArgumentError, "only_if/not_if requires either a command or a block" unless block_given?
+ @command, @command_opts = nil, nil
+ @block = block
+ else
+ raise ArgumentError, "Invalid only_if/not_if command: #{command.inspect} (#{command.class})"
+ end
+ end
+
+ def continue?
+ case @positivity
+ when :only_if
+ evaluate
+ when :not_if
+ !evaluate
+ else
+ raise "Cannot evaluate resource conditional of type #{@positivity}"
+ end
+ end
+
+ def evaluate
+ @command ? evaluate_command : evaluate_block
+ end
+
+ def evaluate_command
+ shell_out(@command, @command_opts).status.success?
+ rescue Chef::Exceptions::CommandTimeout
+ Chef::Log.warn "Command '#{@command}' timed out"
+ false
+ end
+
+ def evaluate_block
+ @block.call
+ end
+
+ def description
+ cmd_or_block = @command ? "command `#{@command}`" : "ruby block"
+ "#{@positivity} #{cmd_or_block}"
+ end
+
+ def to_text
+ if @command
+ "#{positivity} \"#{@command}\""
+ else
+ "#{@positivity} { #code block }"
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/cookbook_file.rb b/lib/chef/resource/cookbook_file.rb
new file mode 100644
index 0000000000..de758aef71
--- /dev/null
+++ b/lib/chef/resource/cookbook_file.rb
@@ -0,0 +1,52 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/file'
+require 'chef/provider/cookbook_file'
+require 'chef/mixin/securable'
+
+class Chef
+ class Resource
+ class CookbookFile < Chef::Resource::File
+ include Chef::Mixin::Securable
+
+ provides :cookbook_file, :on_platforms => :all
+
+ def initialize(name, run_context=nil)
+ super
+ @provider = Chef::Provider::CookbookFile
+ @resource_name = :cookbook_file
+ @action = "create"
+ @source = ::File.basename(name)
+ @cookbook = nil
+ @provider = Chef::Provider::CookbookFile
+ end
+
+ def source(source_filename=nil)
+ set_or_return(:source, source_filename, :kind_of => String)
+ end
+
+ def cookbook(cookbook_name=nil)
+ set_or_return(:cookbook, cookbook_name, :kind_of => String)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/cron.rb b/lib/chef/resource/cron.rb
new file mode 100644
index 0000000000..5f858cec81
--- /dev/null
+++ b/lib/chef/resource/cron.rb
@@ -0,0 +1,202 @@
+#
+# Author:: Bryan McLellan (btm@loftninjas.org)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2009 Bryan McLellan
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/resource'
+
+class Chef
+ class Resource
+ class Cron < Chef::Resource
+
+ identity_attr :command
+
+ state_attrs :minute, :hour, :day, :month, :weekday, :user
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :cron
+ @action = :create
+ @allowed_actions.push(:create, :delete)
+ @minute = "*"
+ @hour = "*"
+ @day = "*"
+ @month = "*"
+ @weekday = "*"
+ @command = nil
+ @user = "root"
+ @mailto = nil
+ @path = nil
+ @shell = nil
+ @home = nil
+ @environment = {}
+ end
+
+ def minute(arg=nil)
+ if arg.is_a?(Integer)
+ converted_arg = arg.to_s
+ else
+ converted_arg = arg
+ end
+ begin
+ if integerize(arg) > 59 then raise RangeError end
+ rescue ArgumentError
+ end
+ set_or_return(
+ :minute,
+ converted_arg,
+ :kind_of => String
+ )
+ end
+
+ def hour(arg=nil)
+ if arg.is_a?(Integer)
+ converted_arg = arg.to_s
+ else
+ converted_arg = arg
+ end
+ begin
+ if integerize(arg) > 23 then raise RangeError end
+ rescue ArgumentError
+ end
+ set_or_return(
+ :hour,
+ converted_arg,
+ :kind_of => String
+ )
+ end
+
+ def day(arg=nil)
+ if arg.is_a?(Integer)
+ converted_arg = arg.to_s
+ else
+ converted_arg = arg
+ end
+ begin
+ if integerize(arg) > 31 then raise RangeError end
+ rescue ArgumentError
+ end
+ set_or_return(
+ :day,
+ converted_arg,
+ :kind_of => String
+ )
+ end
+
+ def month(arg=nil)
+ if arg.is_a?(Integer)
+ converted_arg = arg.to_s
+ else
+ converted_arg = arg
+ end
+ begin
+ if integerize(arg) > 12 then raise RangeError end
+ rescue ArgumentError
+ end
+ set_or_return(
+ :month,
+ converted_arg,
+ :kind_of => String
+ )
+ end
+
+ def weekday(arg=nil)
+ if arg.is_a?(Integer)
+ converted_arg = arg.to_s
+ else
+ converted_arg = arg
+ end
+ begin
+ if integerize(arg) > 7 then raise RangeError end
+ rescue ArgumentError
+ end
+ set_or_return(
+ :weekday,
+ converted_arg,
+ :kind_of => String
+ )
+ end
+
+ def mailto(arg=nil)
+ set_or_return(
+ :mailto,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def path(arg=nil)
+ set_or_return(
+ :path,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def home(arg=nil)
+ set_or_return(
+ :home,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def shell(arg=nil)
+ set_or_return(
+ :shell,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def command(arg=nil)
+ set_or_return(
+ :command,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def user(arg=nil)
+ set_or_return(
+ :user,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def environment(arg=nil)
+ set_or_return(
+ :environment,
+ arg,
+ :kind_of => Hash
+ )
+ end
+
+ private
+
+ # On Ruby 1.8, Kernel#Integer will happily do this for you. On 1.9, no.
+ def integerize(integerish)
+ Integer(integerish)
+ rescue TypeError
+ 0
+ end
+ end
+ end
+end
+
+
diff --git a/lib/chef/resource/csh.rb b/lib/chef/resource/csh.rb
new file mode 100644
index 0000000000..6e871e8605
--- /dev/null
+++ b/lib/chef/resource/csh.rb
@@ -0,0 +1,33 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/script'
+
+class Chef
+ class Resource
+ class Csh < Chef::Resource::Script
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :csh
+ @interpreter = "csh"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/deploy.rb b/lib/chef/resource/deploy.rb
new file mode 100644
index 0000000000..8b614028bf
--- /dev/null
+++ b/lib/chef/resource/deploy.rb
@@ -0,0 +1,403 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 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.
+#
+
+# EX:
+# deploy "/my/deploy/dir" do
+# repo "git@github.com/whoami/project"
+# revision "abc123" # or "HEAD" or "TAG_for_1.0" or (subversion) "1234"
+# user "deploy_ninja"
+# enable_submodules true
+# migrate true
+# migration_command "rake db:migrate"
+# environment "RAILS_ENV" => "production", "OTHER_ENV" => "foo"
+# shallow_clone true
+# action :deploy # or :rollback
+# restart_command "touch tmp/restart.txt"
+# git_ssh_wrapper "wrap-ssh4git.sh"
+# scm_provider Chef::Provider::Git # is the default, for svn: Chef::Provider::Subversion
+# svn_username "whoami"
+# svn_password "supersecret"
+# end
+
+require "chef/resource/scm"
+
+class Chef
+ class Resource
+
+ # Deploy: Deploy apps from a source control repository.
+ #
+ # Callbacks:
+ # Callbacks can be a block or a string. If given a block, the code
+ # is evaluated as an embedded recipe, and run at the specified
+ # point in the deploy process. If given a string, the string is taken as
+ # a path to a callback file/recipe. Paths are evaluated relative to the
+ # release directory. Callback files can contain chef code (resources, etc.)
+ #
+ class Deploy < Chef::Resource
+
+ provider_base Chef::Provider::Deploy
+
+ identity_attr :repository
+
+ state_attrs :deploy_to, :revision
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :deploy
+ @deploy_to = name
+ @environment = nil
+ @repository_cache = 'cached-copy'
+ @copy_exclude = []
+ @purge_before_symlink = %w{log tmp/pids public/system}
+ @create_dirs_before_symlink = %w{tmp public config}
+ @symlink_before_migrate = {"config/database.yml" => "config/database.yml"}
+ @symlinks = {"system" => "public/system", "pids" => "tmp/pids", "log" => "log"}
+ @revision = 'HEAD'
+ @action = :deploy
+ @migrate = false
+ @rollback_on_error = false
+ @remote = "origin"
+ @enable_submodules = false
+ @shallow_clone = false
+ @scm_provider = Chef::Provider::Git
+ @svn_force_export = false
+ @provider = Chef::Provider::Deploy::Timestamped
+ @allowed_actions.push(:force_deploy, :deploy, :rollback)
+ @additional_remotes = Hash[]
+ @keep_releases = 5
+ end
+
+ # where the checked out/cloned code goes
+ def destination
+ @destination ||= shared_path + "/#{@repository_cache}"
+ end
+
+ # where shared stuff goes, i.e., logs, tmp, etc. goes here
+ def shared_path
+ @shared_path ||= @deploy_to + "/shared"
+ end
+
+ # where the deployed version of your code goes
+ def current_path
+ @current_path ||= @deploy_to + "/current"
+ end
+
+ def depth
+ @shallow_clone ? "5" : nil
+ end
+
+ # note: deploy_to is your application "meta-root."
+ def deploy_to(arg=nil)
+ set_or_return(
+ :deploy_to,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def repo(arg=nil)
+ set_or_return(
+ :repo,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+ alias :repository :repo
+
+ def remote(arg=nil)
+ set_or_return(
+ :remote,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def role(arg=nil)
+ set_or_return(
+ :role,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def restart_command(arg=nil, &block)
+ arg ||= block
+ set_or_return(
+ :restart_command,
+ arg,
+ :kind_of => [ String, Proc ]
+ )
+ end
+ alias :restart :restart_command
+
+ def migrate(arg=nil)
+ set_or_return(
+ :migrate,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def migration_command(arg=nil)
+ set_or_return(
+ :migration_command,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def rollback_on_error(arg=nil)
+ set_or_return(
+ :rollback_on_error,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def user(arg=nil)
+ set_or_return(
+ :user,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def group(arg=nil)
+ set_or_return(
+ :group,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def enable_submodules(arg=nil)
+ set_or_return(
+ :enable_submodules,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def shallow_clone(arg=nil)
+ set_or_return(
+ :shallow_clone,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def repository_cache(arg=nil)
+ set_or_return(
+ :repository_cache,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def copy_exclude(arg=nil)
+ set_or_return(
+ :copy_exclude,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def revision(arg=nil)
+ set_or_return(
+ :revision,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+ alias :branch :revision
+
+ def git_ssh_wrapper(arg=nil)
+ set_or_return(
+ :git_ssh_wrapper,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+ alias :ssh_wrapper :git_ssh_wrapper
+
+ def svn_username(arg=nil)
+ set_or_return(
+ :svn_username,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def svn_password(arg=nil)
+ set_or_return(
+ :svn_password,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def svn_arguments(arg=nil)
+ set_or_return(
+ :svn_arguments,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def svn_info_args(arg=nil)
+ set_or_return(
+ :svn_arguments,
+ arg,
+ :kind_of => [ String ])
+ end
+
+ def scm_provider(arg=nil)
+ klass = if arg.kind_of?(String) || arg.kind_of?(Symbol)
+ lookup_provider_constant(arg)
+ else
+ arg
+ end
+ set_or_return(
+ :scm_provider,
+ klass,
+ :kind_of => [ Class ]
+ )
+ end
+
+ def svn_force_export(arg=nil)
+ set_or_return(
+ :svn_force_export,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def environment(arg=nil)
+ if arg.is_a?(String)
+ Chef::Log.debug "Setting RAILS_ENV, RACK_ENV, and MERB_ENV to `#{arg}'"
+ Chef::Log.warn "[DEPRECATED] please modify your deploy recipe or attributes to set the environment using a hash"
+ arg = {"RAILS_ENV"=>arg,"MERB_ENV"=>arg,"RACK_ENV"=>arg}
+ end
+ set_or_return(
+ :environment,
+ arg,
+ :kind_of => [ Hash ]
+ )
+ end
+
+ # The number of old release directories to keep around after cleanup
+ def keep_releases(arg=nil)
+ [set_or_return(
+ :keep_releases,
+ arg,
+ :kind_of => [ Integer ]), 1].max
+ end
+
+ # An array of paths, relative to your app's root, to be purged from a
+ # SCM clone/checkout before symlinking. Use this to get rid of files and
+ # directories you want to be shared between releases.
+ # Default: ["log", "tmp/pids", "public/system"]
+ def purge_before_symlink(arg=nil)
+ set_or_return(
+ :purge_before_symlink,
+ arg,
+ :kind_of => Array
+ )
+ end
+
+ # An array of paths, relative to your app's root, where you expect dirs to
+ # exist before symlinking. This runs after #purge_before_symlink, so you
+ # can use this to recreate dirs that you had previously purged.
+ # For example, if you plan to use a shared directory for pids, and you
+ # want it to be located in $APP_ROOT/tmp/pids, you could purge tmp,
+ # then specify tmp here so that the tmp directory will exist when you
+ # symlink the pids directory in to the current release.
+ # Default: ["tmp", "public", "config"]
+ def create_dirs_before_symlink(arg=nil)
+ set_or_return(
+ :create_dirs_before_symlink,
+ arg,
+ :kind_of => Array
+ )
+ end
+
+ # A Hash of shared/dir/path => release/dir/path. This attribute determines
+ # which files and dirs in the shared directory get symlinked to the current
+ # release directory, and where they go. If you have a directory
+ # $shared/pids that you would like to symlink as $current_release/tmp/pids
+ # you specify it as "pids" => "tmp/pids"
+ # Default {"system" => "public/system", "pids" => "tmp/pids", "log" => "log"}
+ def symlinks(arg=nil)
+ set_or_return(
+ :symlinks,
+ arg,
+ :kind_of => Hash
+ )
+ end
+
+ # A Hash of shared/dir/path => release/dir/path. This attribute determines
+ # which files in the shared directory get symlinked to the current release
+ # directory and where they go. Unlike map_shared_files, these are symlinked
+ # *before* any migration is run.
+ # For a rails/merb app, this is used to link in a known good database.yml
+ # (with the production db password) before running migrate.
+ # Default {"config/database.yml" => "config/database.yml"}
+ def symlink_before_migrate(arg=nil)
+ set_or_return(
+ :symlink_before_migrate,
+ arg,
+ :kind_of => Hash
+ )
+ end
+
+ # Callback fires before migration is run.
+ def before_migrate(arg=nil, &block)
+ arg ||= block
+ set_or_return(:before_migrate, arg, :kind_of => [Proc, String])
+ end
+
+ # Callback fires before symlinking
+ def before_symlink(arg=nil, &block)
+ arg ||= block
+ set_or_return(:before_symlink, arg, :kind_of => [Proc, String])
+ end
+
+ # Callback fires before restart
+ def before_restart(arg=nil, &block)
+ arg ||= block
+ set_or_return(:before_restart, arg, :kind_of => [Proc, String])
+ end
+
+ # Callback fires after restart
+ def after_restart(arg=nil, &block)
+ arg ||= block
+ set_or_return(:after_restart, arg, :kind_of => [Proc, String])
+ end
+
+ def additional_remotes(arg=nil)
+ set_or_return(
+ :additional_remotes,
+ arg,
+ :kind_of => Hash
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/deploy_revision.rb b/lib/chef/resource/deploy_revision.rb
new file mode 100644
index 0000000000..55a3e38130
--- /dev/null
+++ b/lib/chef/resource/deploy_revision.rb
@@ -0,0 +1,40 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Resource
+
+ # Convenience class for using the deploy resource with the revision
+ # deployment strategy (provider)
+ class DeployRevision < Chef::Resource::Deploy
+ def initialize(*args, &block)
+ super
+ @resource_name = :deploy_revision
+ @provider = Chef::Provider::Deploy::Revision
+ end
+ end
+
+ class DeployBranch < Chef::Resource::DeployRevision
+ def initialize(*args, &block)
+ super
+ @resource_name = :deploy_branch
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/resource/directory.rb b/lib/chef/resource/directory.rb
new file mode 100644
index 0000000000..a5d5ea7366
--- /dev/null
+++ b/lib/chef/resource/directory.rb
@@ -0,0 +1,65 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+require 'chef/provider/directory'
+require 'chef/mixin/securable'
+
+class Chef
+ class Resource
+ class Directory < Chef::Resource
+
+ identity_attr :path
+
+ state_attrs :group, :mode, :owner
+
+ include Chef::Mixin::Securable
+
+ provides :directory, :on_platforms => :all
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :directory
+ @path = name
+ @action = :create
+ @recursive = false
+ @allowed_actions.push(:create, :delete)
+ @provider = Chef::Provider::Directory
+ end
+
+ def recursive(arg=nil)
+ set_or_return(
+ :recursive,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def path(arg=nil)
+ set_or_return(
+ :path,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/dpkg_package.rb b/lib/chef/resource/dpkg_package.rb
new file mode 100644
index 0000000000..02886e8649
--- /dev/null
+++ b/lib/chef/resource/dpkg_package.rb
@@ -0,0 +1,34 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+require 'chef/provider/package/dpkg'
+
+class Chef
+ class Resource
+ class DpkgPackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :dpkg_package
+ @provider = Chef::Provider::Package::Dpkg
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/easy_install_package.rb b/lib/chef/resource/easy_install_package.rb
new file mode 100644
index 0000000000..10e80bdd3b
--- /dev/null
+++ b/lib/chef/resource/easy_install_package.rb
@@ -0,0 +1,57 @@
+#
+# Author:: Joe Williams (<joe@joetify.com>)
+# Copyright:: Copyright (c) 2009 Joe Williams
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/resource/package'
+
+class Chef
+ class Resource
+ class EasyInstallPackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :easy_install_package
+ @provider = Chef::Provider::Package::EasyInstall
+ end
+
+ def easy_install_binary(arg=nil)
+ set_or_return(
+ :easy_install_binary,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def python_binary(arg=nil)
+ set_or_return(
+ :python_install_binary,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def module_name(arg=nil)
+ set_or_return(
+ :module_name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/env.rb b/lib/chef/resource/env.rb
new file mode 100644
index 0000000000..4b5fe6cc09
--- /dev/null
+++ b/lib/chef/resource/env.rb
@@ -0,0 +1,63 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 Resource
+ class Env < Chef::Resource
+
+ identity_attr :key_name
+
+ state_attrs :value
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :env
+ @key_name = name
+ @value = nil
+ @action = :create
+ @delim = nil
+ @allowed_actions.push(:create, :delete, :modify)
+ end
+
+ def key_name(arg=nil)
+ set_or_return(
+ :key_name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def value(arg=nil)
+ set_or_return(
+ :value,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def delim(arg=nil)
+ set_or_return(
+ :delim,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/erl_call.rb b/lib/chef/resource/erl_call.rb
new file mode 100644
index 0000000000..e0e38926bb
--- /dev/null
+++ b/lib/chef/resource/erl_call.rb
@@ -0,0 +1,86 @@
+#
+# Author:: Joe Williams (<joe@joetify.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2009 Joe Williams
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/resource'
+
+class Chef
+ class Resource
+ class ErlCall < Chef::Resource
+
+ # erl_call : http://erlang.org/doc/man/erl_call.html
+
+ identity_attr :code
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :erl_call
+
+ @code = "q()." # your erlang code goes here
+ @cookie = nil # cookie of the erlang node
+ @distributed = false # if you want to have a distributed erlang node
+ @name_type = "sname" # type of erlang hostname name or sname
+ @node_name = "chef@localhost" # the erlang node hostname
+
+ @action = "run"
+ @allowed_actions.push(:run)
+ end
+
+ def code(arg=nil)
+ set_or_return(
+ :code,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def cookie(arg=nil)
+ set_or_return(
+ :cookie,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def distributed(arg=nil)
+ set_or_return(
+ :distributed,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def name_type(arg=nil)
+ set_or_return(
+ :name_type,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def node_name(arg=nil)
+ set_or_return(
+ :node_name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/execute.rb b/lib/chef/resource/execute.rb
new file mode 100644
index 0000000000..6c07bf9352
--- /dev/null
+++ b/lib/chef/resource/execute.rb
@@ -0,0 +1,132 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+
+class Chef
+ class Resource
+ class Execute < Chef::Resource
+
+ identity_attr :command
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :execute
+ @command = name
+ @backup = 5
+ @action = "run"
+ @creates = nil
+ @cwd = nil
+ @environment = nil
+ @group = nil
+ @path = nil
+ @returns = 0
+ @timeout = nil
+ @user = nil
+ @allowed_actions.push(:run)
+ @umask = nil
+ end
+
+ def umask(arg=nil)
+ set_or_return(
+ :umask,
+ arg,
+ :kind_of => [ String, Integer ]
+ )
+ end
+
+ def command(arg=nil)
+ set_or_return(
+ :command,
+ arg,
+ :kind_of => [ String, Array ]
+ )
+ end
+
+ def creates(arg=nil)
+ set_or_return(
+ :creates,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def cwd(arg=nil)
+ set_or_return(
+ :cwd,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def environment(arg=nil)
+ set_or_return(
+ :environment,
+ arg,
+ :kind_of => [ Hash ]
+ )
+ end
+
+ alias :env :environment
+
+ def group(arg=nil)
+ set_or_return(
+ :group,
+ arg,
+ :kind_of => [ String, Integer ]
+ )
+ end
+
+ def path(arg=nil)
+ set_or_return(
+ :path,
+ arg,
+ :kind_of => [ Array ]
+ )
+ end
+
+ def returns(arg=nil)
+ set_or_return(
+ :returns,
+ arg,
+ :kind_of => [ Integer, Array ]
+ )
+ end
+
+ def timeout(arg=nil)
+ set_or_return(
+ :timeout,
+ arg,
+ :kind_of => [ Integer ]
+ )
+ end
+
+ def user(arg=nil)
+ set_or_return(
+ :user,
+ arg,
+ :kind_of => [ String, Integer ]
+ )
+ end
+
+
+
+ end
+ end
+end
diff --git a/lib/chef/resource/file.rb b/lib/chef/resource/file.rb
new file mode 100644
index 0000000000..0b92f3332d
--- /dev/null
+++ b/lib/chef/resource/file.rb
@@ -0,0 +1,96 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+require 'chef/platform'
+require 'chef/provider/file'
+require 'chef/mixin/securable'
+
+class Chef
+ class Resource
+ class File < Chef::Resource
+ include Chef::Mixin::Securable
+
+ identity_attr :path
+
+ if Platform.windows?
+ # Use Windows rights instead of standard *nix permissions
+ state_attrs :checksum, :rights, :deny_rights
+ else
+ state_attrs :checksum, :owner, :group, :mode
+ end
+
+ provides :file, :on_platforms => :all
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :file
+ @path = name
+ @backup = 5
+ @action = "create"
+ @allowed_actions.push(:create, :delete, :touch, :create_if_missing)
+ @provider = Chef::Provider::File
+ @diff = nil
+ end
+
+
+ def content(arg=nil)
+ set_or_return(
+ :content,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def backup(arg=nil)
+ set_or_return(
+ :backup,
+ arg,
+ :kind_of => [ Integer, FalseClass ]
+ )
+ end
+
+ def checksum(arg=nil)
+ set_or_return(
+ :checksum,
+ arg,
+ :regex => /^[a-zA-Z0-9]{64}$/
+ )
+ end
+
+ def path(arg=nil)
+ set_or_return(
+ :path,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def diff(arg=nil)
+ set_or_return(
+ :diff,
+ arg,
+ :kind_of => String
+ )
+ end
+
+
+ end
+ end
+end
diff --git a/lib/chef/resource/freebsd_package.rb b/lib/chef/resource/freebsd_package.rb
new file mode 100644
index 0000000000..9a9a84900e
--- /dev/null
+++ b/lib/chef/resource/freebsd_package.rb
@@ -0,0 +1,35 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+require 'chef/provider/package/freebsd'
+
+class Chef
+ class Resource
+ class FreebsdPackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :freebsd_package
+ @provider = Chef::Provider::Package::Freebsd
+ end
+
+ end
+ end
+end
+
diff --git a/lib/chef/resource/gem_package.rb b/lib/chef/resource/gem_package.rb
new file mode 100644
index 0000000000..6def7b6653
--- /dev/null
+++ b/lib/chef/resource/gem_package.rb
@@ -0,0 +1,53 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+
+class Chef
+ class Resource
+ class GemPackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :gem_package
+ @provider = Chef::Provider::Package::Rubygems
+ end
+
+ def source(arg=nil)
+ set_or_return(:source, arg, :kind_of => [ String, Array ])
+ end
+
+ # Sets a custom gem_binary to run for gem commands.
+ def gem_binary(gem_cmd=nil)
+ set_or_return(:gem_binary,gem_cmd,:kind_of => [ String ])
+ end
+
+ ##
+ # Options for the gem install, either a Hash or a String. When a hash is
+ # given, the options are passed to Gem::DependencyInstaller.new, and the
+ # gem will be installed via the gems API. When a String is given, the gem
+ # will be installed by shelling out to the gem command. Using a Hash of
+ # options with an explicit gem_binary will result in undefined behavior.
+ def options(opts=nil)
+ set_or_return(:options,opts,:kind_of => [String,Hash])
+ end
+
+
+ end
+ end
+end
diff --git a/lib/chef/resource/git.rb b/lib/chef/resource/git.rb
new file mode 100644
index 0000000000..774bb24f24
--- /dev/null
+++ b/lib/chef/resource/git.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "chef/resource/scm"
+
+class Chef
+ class Resource
+ class Git < Chef::Resource::Scm
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :git
+ @provider = Chef::Provider::Git
+ @additional_remotes = Hash[]
+ end
+
+ def additional_remotes(arg=nil)
+ set_or_return(
+ :additional_remotes,
+ arg,
+ :kind_of => Hash
+ )
+ end
+
+ alias :branch :revision
+ alias :reference :revision
+
+ alias :repo :repository
+ end
+ end
+end
diff --git a/lib/chef/resource/group.rb b/lib/chef/resource/group.rb
new file mode 100644
index 0000000000..76f3a779ae
--- /dev/null
+++ b/lib/chef/resource/group.rb
@@ -0,0 +1,83 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 OpsCode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Resource
+ class Group < Chef::Resource
+
+ identity_attr :group_name
+
+ state_attrs :members
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :group
+ @group_name = name
+ @gid = nil
+ @members = []
+ @action = :create
+ @append = false
+ @allowed_actions.push(:create, :remove, :modify, :manage)
+ end
+
+ def group_name(arg=nil)
+ set_or_return(
+ :group_name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def gid(arg=nil)
+ set_or_return(
+ :gid,
+ arg,
+ :kind_of => [ String, Integer ]
+ )
+ end
+
+ def members(arg=nil)
+ converted_members = arg.is_a?(String) ? [].push(arg) : arg
+ set_or_return(
+ :members,
+ converted_members,
+ :kind_of => [ Array ]
+ )
+ end
+
+ alias_method :users, :members
+
+ def append(arg=nil)
+ set_or_return(
+ :append,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def system(arg=nil)
+ set_or_return(
+ :system,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/http_request.rb b/lib/chef/resource/http_request.rb
new file mode 100644
index 0000000000..d31ff0b1c9
--- /dev/null
+++ b/lib/chef/resource/http_request.rb
@@ -0,0 +1,64 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+
+class Chef
+ class Resource
+ class HttpRequest < Chef::Resource
+
+ identity_attr :url
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :http_request
+ @message = name
+ @url = nil
+ @action = :get
+ @headers = {}
+ @allowed_actions.push(:get, :put, :post, :delete, :head, :options)
+ end
+
+ def url(args=nil)
+ set_or_return(
+ :url,
+ args,
+ :kind_of => String
+ )
+ end
+
+ def message(args=nil)
+ set_or_return(
+ :message,
+ args,
+ :kind_of => Object
+ )
+ end
+
+ def headers(args=nil)
+ set_or_return(
+ :headers,
+ args,
+ :kind_of => Hash
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/ifconfig.rb b/lib/chef/resource/ifconfig.rb
new file mode 100644
index 0000000000..daa8a572a0
--- /dev/null
+++ b/lib/chef/resource/ifconfig.rb
@@ -0,0 +1,149 @@
+#
+# Author:: Jason K. Jackson (jasonjackson@gmail.com)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2009 Jason K. Jackson
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/resource'
+
+class Chef
+ class Resource
+ class Ifconfig < Chef::Resource
+
+ identity_attr :device
+
+ state_attrs :inet_addr, :mask
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :ifconfig
+ @target = name
+ @action = :add
+ @allowed_actions.push(:add, :delete, :enable, :disable)
+ @hwaddr = nil
+ @mask = nil
+ @inet_addr = nil
+ @bcast = nil
+ @mtu = nil
+ @metric = nil
+ @device = nil
+ @onboot = nil
+ @network = nil
+ @bootproto = nil
+ @onparent = nil
+ end
+
+ def target(arg=nil)
+ set_or_return(
+ :target,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def device(arg=nil)
+ set_or_return(
+ :device,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def hwaddr(arg=nil)
+ set_or_return(
+ :hwaddr,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def inet_addr(arg=nil)
+ set_or_return(
+ :inet_addr,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def bcast(arg=nil)
+ set_or_return(
+ :bcast,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def mask(arg=nil)
+ set_or_return(
+ :mask,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def mtu(arg=nil)
+ set_or_return(
+ :mtu,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def metric(arg=nil)
+ set_or_return(
+ :metric,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def onboot(arg=nil)
+ set_or_return(
+ :onboot,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def network(arg=nil)
+ set_or_return(
+ :network,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def bootproto(arg=nil)
+ set_or_return(
+ :bootproto,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def onparent(arg=nil)
+ set_or_return(
+ :onparent,
+ arg,
+ :kind_of => String
+ )
+ end
+ end
+
+ end
+end
+
+
diff --git a/lib/chef/resource/ips_package.rb b/lib/chef/resource/ips_package.rb
new file mode 100644
index 0000000000..f82e0877df
--- /dev/null
+++ b/lib/chef/resource/ips_package.rb
@@ -0,0 +1,42 @@
+#
+# Author:: Jason Williams (<williamsjj@digitar.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+require 'chef/provider/package/ips'
+
+class Chef
+ class Resource
+ class IpsPackage < ::Chef::Resource::Package
+ def initialize(name, run_context = nil)
+ super(name, run_context)
+ @resource_name = :ips_package
+ @provider = Chef::Provider::Package::Ips
+ @allowed_actions = [ :install, :remove, :upgrade ]
+ @accept_license = false
+ end
+
+ def accept_license(arg=nil)
+ set_or_return(
+ :purge,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/link.rb b/lib/chef/resource/link.rb
new file mode 100644
index 0000000000..a0fab0428d
--- /dev/null
+++ b/lib/chef/resource/link.rb
@@ -0,0 +1,92 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+require 'chef/mixin/securable'
+
+class Chef
+ class Resource
+ class Link < Chef::Resource
+ include Chef::Mixin::Securable
+
+ provides :link, :on_platform => :all
+
+ identity_attr :target_file
+
+ state_attrs :to, :owner, :group
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :link
+ @to = nil
+ @action = :create
+ @link_type = :symbolic
+ @target_file = name
+ @allowed_actions.push(:create, :delete)
+ @provider = Chef::Provider::Link
+ end
+
+ def to(arg=nil)
+ set_or_return(
+ :to,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def target_file(arg=nil)
+ set_or_return(
+ :target_file,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def link_type(arg=nil)
+ real_arg = arg.kind_of?(String) ? arg.to_sym : arg
+ set_or_return(
+ :link_type,
+ real_arg,
+ :equal_to => [ :symbolic, :hard ]
+ )
+ end
+
+ def group(arg=nil)
+ set_or_return(
+ :group,
+ arg,
+ :regex => Chef::Config[:group_valid_regex]
+ )
+ end
+
+ def owner(arg=nil)
+ set_or_return(
+ :owner,
+ arg,
+ :regex => Chef::Config[:user_valid_regex]
+ )
+ end
+
+ # make link quack like a file (XXX: not for public consumption)
+ def path
+ @target_file
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/log.rb b/lib/chef/resource/log.rb
new file mode 100644
index 0000000000..d1b6b5af0b
--- /dev/null
+++ b/lib/chef/resource/log.rb
@@ -0,0 +1,65 @@
+#
+# Author:: Cary Penniman (<cary@rightscale.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+class Chef
+ class Resource
+ class Log < Chef::Resource
+
+ identity_attr :name
+
+ # Sends a string from a recipe to a log provider
+ #
+ # log "some string to log" do
+ # level :info # (default) also supports :warn, :debug, and :error
+ # end
+ #
+ # === Example
+ # log "your string to log"
+ #
+ # or
+ #
+ # log "a debug string" { level :debug }
+ #
+
+ # Initialize log resource with a name as the string to log
+ #
+ # === Parameters
+ # name<String>:: Message to log
+ # collection<Array>:: Collection of included recipes
+ # node<Chef::Node>:: Node where resource will be used
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :log
+ @level = :info
+ @action = :write
+ end
+
+ # <Symbol> Log level, one of :debug, :info, :warn, :error or :fatal
+ def level(arg=nil)
+ set_or_return(
+ :level,
+ arg,
+ :equal_to => [ :debug, :info, :warn, :error, :fatal ]
+ )
+ end
+
+ end
+ end
+end
+
+
diff --git a/lib/chef/resource/macports_package.rb b/lib/chef/resource/macports_package.rb
new file mode 100644
index 0000000000..911d3c19cb
--- /dev/null
+++ b/lib/chef/resource/macports_package.rb
@@ -0,0 +1,29 @@
+#
+# Author:: David Balatero (<dbalatero@gmail.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Resource
+ class MacportsPackage < Chef::Resource::Package
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :macports_package
+ @provider = Chef::Provider::Package::Macports
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/mdadm.rb b/lib/chef/resource/mdadm.rb
new file mode 100644
index 0000000000..46a85b2475
--- /dev/null
+++ b/lib/chef/resource/mdadm.rb
@@ -0,0 +1,105 @@
+#
+# Author:: Joe Williams (<joe@joetify.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2009 Joe Williams
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/resource'
+
+class Chef
+ class Resource
+ class Mdadm < Chef::Resource
+
+ identity_attr :raid_device
+
+ state_attrs :devices, :level, :chunk
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :mdadm
+
+ @chunk = 16
+ @devices = []
+ @exists = false
+ @level = 1
+ @metadata = "0.90"
+ @bitmap = nil
+ @raid_device = name
+
+ @action = :create
+ @allowed_actions.push(:create, :assemble, :stop)
+ end
+
+ def chunk(arg=nil)
+ set_or_return(
+ :chunk,
+ arg,
+ :kind_of => [ Integer ]
+ )
+ end
+
+ def devices(arg=nil)
+ set_or_return(
+ :devices,
+ arg,
+ :kind_of => [ Array ]
+ )
+ end
+
+ def exists(arg=nil)
+ set_or_return(
+ :exists,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def level(arg=nil)
+ set_or_return(
+ :level,
+ arg,
+ :kind_of => [ Integer ]
+ )
+ end
+
+ def metadata(arg=nil)
+ set_or_return(
+ :metadata,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def bitmap(arg=nil)
+ set_or_return(
+ :bitmap,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def raid_device(arg=nil)
+ set_or_return(
+ :raid_device,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+
+ end
+ end
+end
diff --git a/lib/chef/resource/mount.rb b/lib/chef/resource/mount.rb
new file mode 100644
index 0000000000..8c32bdd280
--- /dev/null
+++ b/lib/chef/resource/mount.rb
@@ -0,0 +1,139 @@
+#
+# Author:: Joshua Timberman (<joshua@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+
+class Chef
+ class Resource
+ class Mount < Chef::Resource
+
+ identity_attr :device
+
+ state_attrs :mount_point, :device_type, :fstype
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :mount
+ @mount_point = name
+ @device = nil
+ @device_type = :device
+ @fstype = "auto"
+ @options = ["defaults"]
+ @dump = 0
+ @pass = 2
+ @mounted = false
+ @enabled = false
+ @action = :mount
+ @supports = { :remount => false }
+ @allowed_actions.push(:mount, :umount, :remount, :enable, :disable)
+ end
+
+ def mount_point(arg=nil)
+ set_or_return(
+ :mount_point,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def device(arg=nil)
+ set_or_return(
+ :device,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def device_type(arg=nil)
+ real_arg = arg.kind_of?(String) ? arg.to_sym : arg
+ set_or_return(
+ :device_type,
+ real_arg,
+ :equal_to => [ :device, :label, :uuid ]
+ )
+ end
+
+ def fstype(arg=nil)
+ set_or_return(
+ :fstype,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def options(arg=nil)
+ if arg.is_a?(String)
+ converted_arg = arg.gsub(/,/, ' ').split(/ /)
+ else
+ converted_arg = arg
+ end
+ set_or_return(
+ :options,
+ converted_arg,
+ :kind_of => [ Array ]
+ )
+ end
+
+ def dump(arg=nil)
+ set_or_return(
+ :dump,
+ arg,
+ :kind_of => [ Integer, FalseClass ]
+ )
+ end
+
+ def pass(arg=nil)
+ set_or_return(
+ :pass,
+ arg,
+ :kind_of => [ Integer, FalseClass ]
+ )
+ end
+
+ def mounted(arg=nil)
+ set_or_return(
+ :mounted,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def enabled(arg=nil)
+ set_or_return(
+ :enabled,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def supports(args={})
+ if args.is_a? Array
+ args.each { |arg| @supports[arg] = true }
+ elsif args.any?
+ @supports = args
+ else
+ @supports
+ end
+ end
+
+ end
+ end
+end
+
+
diff --git a/lib/chef/resource/ohai.rb b/lib/chef/resource/ohai.rb
new file mode 100644
index 0000000000..48e55e9f01
--- /dev/null
+++ b/lib/chef/resource/ohai.rb
@@ -0,0 +1,54 @@
+#
+# Author:: Michael Leinartas (<mleinartas@gmail.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2010 Michael Leinartas
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 Resource
+ class Ohai < Chef::Resource
+
+ identity_attr :name
+
+ state_attrs :plugin
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :ohai
+ @name = name
+ @allowed_actions.push(:reload)
+ @action = :reload
+ @plugin = nil
+ end
+
+ def plugin(arg=nil)
+ set_or_return(
+ :plugin,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def name(arg=nil)
+ set_or_return(
+ :name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/package.rb b/lib/chef/resource/package.rb
new file mode 100644
index 0000000000..eaad3e2e58
--- /dev/null
+++ b/lib/chef/resource/package.rb
@@ -0,0 +1,84 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+
+class Chef
+ class Resource
+ class Package < Chef::Resource
+
+ identity_attr :package_name
+
+ state_attrs :version, :options
+
+ def initialize(name, run_context=nil)
+ super
+ @action = :install
+ @allowed_actions.push(:install, :upgrade, :remove, :purge, :reconfig)
+ @candidate_version = nil
+ @options = nil
+ @package_name = name
+ @resource_name = :package
+ @response_file = nil
+ @source = nil
+ @version = nil
+ end
+
+ def package_name(arg=nil)
+ set_or_return(
+ :package_name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def version(arg=nil)
+ set_or_return(
+ :version,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def response_file(arg=nil)
+ set_or_return(
+ :response_file,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def source(arg=nil)
+ set_or_return(
+ :source,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def options(arg=nil)
+ set_or_return(
+ :options,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/pacman_package.rb b/lib/chef/resource/pacman_package.rb
new file mode 100644
index 0000000000..d66c93be66
--- /dev/null
+++ b/lib/chef/resource/pacman_package.rb
@@ -0,0 +1,33 @@
+#
+# Author:: Jan Zimmek (<jan.zimmek@web.de>)
+# Copyright:: Copyright (c) 2010 Jan Zimmek
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/resource/package'
+
+class Chef
+ class Resource
+ class PacmanPackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :pacman_package
+ @provider = Chef::Provider::Package::Pacman
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/perl.rb b/lib/chef/resource/perl.rb
new file mode 100644
index 0000000000..d3cf696cbb
--- /dev/null
+++ b/lib/chef/resource/perl.rb
@@ -0,0 +1,33 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/script'
+
+class Chef
+ class Resource
+ class Perl < Chef::Resource::Script
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :perl
+ @interpreter = "perl"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/portage_package.rb b/lib/chef/resource/portage_package.rb
new file mode 100644
index 0000000000..fc72381482
--- /dev/null
+++ b/lib/chef/resource/portage_package.rb
@@ -0,0 +1,33 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+
+class Chef
+ class Resource
+ class PortagePackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :portage_package
+ @provider = Chef::Provider::Package::Portage
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/python.rb b/lib/chef/resource/python.rb
new file mode 100644
index 0000000000..85a5348d27
--- /dev/null
+++ b/lib/chef/resource/python.rb
@@ -0,0 +1,33 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/script'
+
+class Chef
+ class Resource
+ class Python < Chef::Resource::Script
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :python
+ @interpreter = "python"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/remote_directory.rb b/lib/chef/resource/remote_directory.rb
new file mode 100644
index 0000000000..490e3c6ba7
--- /dev/null
+++ b/lib/chef/resource/remote_directory.rb
@@ -0,0 +1,125 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/directory'
+require 'chef/provider/remote_directory'
+require 'chef/mixin/securable'
+
+class Chef
+ class Resource
+ class RemoteDirectory < Chef::Resource::Directory
+ include Chef::Mixin::Securable
+
+ provides :remote_directory, :on_platforms => :all
+
+ identity_attr :path
+
+ state_attrs :files_owner, :files_group, :files_mode
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :remote_directory
+ @path = name
+ @source = ::File.basename(name)
+ @delete = false
+ @action = :create
+ @recursive = true
+ @purge = false
+ @files_backup = 5
+ @files_owner = nil
+ @files_group = nil
+ @files_mode = 0644 unless Chef::Platform.windows?
+ @overwrite = true
+ @allowed_actions.push(:create, :create_if_missing, :delete)
+ @cookbook = nil
+ @provider = Chef::Provider::RemoteDirectory
+ end
+
+ def source(args=nil)
+ set_or_return(
+ :source,
+ args,
+ :kind_of => String
+ )
+ end
+
+ def files_backup(arg=nil)
+ set_or_return(
+ :files_backup,
+ arg,
+ :kind_of => [ Integer, FalseClass ]
+ )
+ end
+
+ def purge(arg=nil)
+ set_or_return(
+ :purge,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def files_group(arg=nil)
+ set_or_return(
+ :files_group,
+ arg,
+ :regex => Chef::Config[:group_valid_regex]
+ )
+ end
+
+ if Chef::Platform.windows?
+ # create a second instance of the 'rights' attribute
+ Chef::Mixin::Securable.rights_attribute(:files_rights)
+ end
+
+ def files_mode(arg=nil)
+ set_or_return(
+ :files_mode,
+ arg,
+ :regex => /^\d{3,4}$/
+ )
+ end
+
+ def files_owner(arg=nil)
+ set_or_return(
+ :files_owner,
+ arg,
+ :regex => Chef::Config[:user_valid_regex]
+ )
+ end
+
+ def overwrite(arg=nil)
+ set_or_return(
+ :overwrite,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def cookbook(args=nil)
+ set_or_return(
+ :cookbook,
+ args,
+ :kind_of => String
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/remote_file.rb b/lib/chef/resource/remote_file.rb
new file mode 100644
index 0000000000..2798cba3f2
--- /dev/null
+++ b/lib/chef/resource/remote_file.rb
@@ -0,0 +1,81 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/file'
+require 'chef/provider/remote_file'
+require 'chef/mixin/securable'
+
+class Chef
+ class Resource
+ class RemoteFile < Chef::Resource::File
+ include Chef::Mixin::Securable
+
+ provides :remote_file, :on_platforms => :all
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :remote_file
+ @action = "create"
+ @source = nil
+ @provider = Chef::Provider::RemoteFile
+ end
+
+ def source(*args)
+ if not args.empty?
+ args = Array(args).flatten
+ validate_source(args)
+ @source = args
+ elsif self.instance_variable_defined?(:@source) == true
+ @source
+ end
+ end
+
+ def checksum(args=nil)
+ set_or_return(
+ :checksum,
+ args,
+ :kind_of => String
+ )
+ end
+
+ def after_created
+ validate_source(@source)
+ end
+
+ private
+
+ def validate_source(source)
+ raise ArgumentError, "#{resource_name} has an empty source" if source.empty?
+ source.each do |src|
+ unless absolute_uri?(src)
+ raise Exceptions::InvalidRemoteFileURI,
+ "#{src.inspect} is not a valid `source` parameter for #{resource_name}. `source` must be an absolute URI or an array of URIs."
+ end
+ end
+ end
+
+ def absolute_uri?(source)
+ source.kind_of?(String) and URI.parse(source).absolute?
+ rescue URI::InvalidURIError
+ false
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/route.rb b/lib/chef/resource/route.rb
new file mode 100644
index 0000000000..cdd61f3a4b
--- /dev/null
+++ b/lib/chef/resource/route.rb
@@ -0,0 +1,140 @@
+#
+# Author:: Bryan McLellan (btm@loftninjas.org)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2009 Bryan McLellan
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/resource'
+
+class Chef
+ class Resource
+ class Route < Chef::Resource
+
+ identity_attr :target
+
+ state_attrs :netmask, :gateway
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :route
+ @target = name
+ @action = :add
+ @allowed_actions.push(:add, :delete)
+ @netmask = nil
+ @gateway = nil
+ @metric = nil
+ @device = nil
+ @route_type = :host
+ @networking = nil
+ @networking_ipv6 = nil
+ @hostname = nil
+ @domainname = nil
+ @domain = nil
+ end
+
+ def networking(arg=nil)
+ set_or_return(
+ :networking,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def networking_ipv6(arg=nil)
+ set_or_return(
+ :networking_ipv6,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def hostname(arg=nil)
+ set_or_return(
+ :hostname,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def domainname(arg=nil)
+ set_or_return(
+ :domainname,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def domain(arg=nil)
+ set_or_return(
+ :domain,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def target(arg=nil)
+ set_or_return(
+ :target,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def netmask(arg=nil)
+ set_or_return(
+ :netmask,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def gateway(arg=nil)
+ set_or_return(
+ :gateway,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def metric(arg=nil)
+ set_or_return(
+ :metric,
+ arg,
+ :kind_of => Integer
+ )
+ end
+
+ def device(arg=nil)
+ set_or_return(
+ :device,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def route_type(arg=nil)
+ real_arg = arg.kind_of?(String) ? arg.to_sym : arg
+ set_or_return(
+ :route_type,
+ real_arg,
+ :equal_to => [ :host, :net ]
+ )
+ end
+ end
+ end
+end
+
+
diff --git a/lib/chef/resource/rpm_package.rb b/lib/chef/resource/rpm_package.rb
new file mode 100644
index 0000000000..7ab1202ef2
--- /dev/null
+++ b/lib/chef/resource/rpm_package.rb
@@ -0,0 +1,34 @@
+#
+# Author:: Thomas Bishop (<bishop.thomas@gmail.com>)
+# Copyright:: Copyright (c) 2010 Thomas Bishop
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/resource/package'
+require 'chef/provider/package/rpm'
+
+class Chef
+ class Resource
+ class RpmPackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :rpm_package
+ @provider = Chef::Provider::Package::Rpm
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/ruby.rb b/lib/chef/resource/ruby.rb
new file mode 100644
index 0000000000..7617839bab
--- /dev/null
+++ b/lib/chef/resource/ruby.rb
@@ -0,0 +1,33 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/script'
+
+class Chef
+ class Resource
+ class Ruby < Chef::Resource::Script
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :ruby
+ @interpreter = "ruby"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/ruby_block.rb b/lib/chef/resource/ruby_block.rb
new file mode 100644
index 0000000000..296345bde3
--- /dev/null
+++ b/lib/chef/resource/ruby_block.rb
@@ -0,0 +1,51 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Resource
+ class RubyBlock < Chef::Resource
+
+ identity_attr :block_name
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :ruby_block
+ @action = "run"
+ @allowed_actions << :create << :run
+ @block_name = name
+ end
+
+ def block(&block)
+ if block_given? and block
+ @block = block
+ else
+ @block
+ end
+ end
+
+ def block_name(arg=nil)
+ set_or_return(
+ :block_name,
+ arg,
+ :kind_of => String
+ )
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/scm.rb b/lib/chef/resource/scm.rb
new file mode 100644
index 0000000000..781e09a2c9
--- /dev/null
+++ b/lib/chef/resource/scm.rb
@@ -0,0 +1,151 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+require 'chef/resource'
+
+class Chef
+ class Resource
+ class Scm < Chef::Resource
+
+ identity_attr :destination
+
+ state_attrs :revision
+
+ def initialize(name, run_context=nil)
+ super
+ @destination = name
+ @resource_name = :scm
+ @enable_submodules = false
+ @revision = "HEAD"
+ @remote = "origin"
+ @ssh_wrapper = nil
+ @depth = nil
+ @allowed_actions.push(:checkout, :export, :sync, :diff, :log)
+ @action = [:sync]
+ end
+
+ def destination(arg=nil)
+ set_or_return(
+ :destination,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def repository(arg=nil)
+ set_or_return(
+ :repository,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def revision(arg=nil)
+ set_or_return(
+ :revision,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def user(arg=nil)
+ set_or_return(
+ :user,
+ arg,
+ :kind_of => [String, Integer]
+ )
+ end
+
+ def group(arg=nil)
+ set_or_return(
+ :group,
+ arg,
+ :kind_of => [String, Integer]
+ )
+ end
+
+ def svn_username(arg=nil)
+ set_or_return(
+ :svn_username,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def svn_password(arg=nil)
+ set_or_return(
+ :svn_password,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def svn_arguments(arg=nil)
+ @svn_arguments, arg = nil, nil if arg == false
+ set_or_return(
+ :svn_arguments,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def svn_info_args(arg=nil)
+ @svn_info_args, arg = nil, nil if arg == false
+ set_or_return(
+ :svn_info_args,
+ arg,
+ :kind_of => String)
+ end
+
+ # Capistrano and git-deploy use ``shallow clone''
+ def depth(arg=nil)
+ set_or_return(
+ :depth,
+ arg,
+ :kind_of => Integer
+ )
+ end
+
+ def enable_submodules(arg=nil)
+ set_or_return(
+ :enable_submodules,
+ arg,
+ :kind_of => [TrueClass, FalseClass]
+ )
+ end
+
+ def remote(arg=nil)
+ set_or_return(
+ :remote,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def ssh_wrapper(arg=nil)
+ set_or_return(
+ :ssh_wrapper,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/script.rb b/lib/chef/resource/script.rb
new file mode 100644
index 0000000000..6a7c8e0d5e
--- /dev/null
+++ b/lib/chef/resource/script.rb
@@ -0,0 +1,63 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/execute'
+
+class Chef
+ class Resource
+ class Script < Chef::Resource::Execute
+
+ identity_attr :command
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :script
+ @command = name
+ @code = nil
+ @interpreter = nil
+ @flags = nil
+ end
+
+ def code(arg=nil)
+ set_or_return(
+ :code,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def interpreter(arg=nil)
+ set_or_return(
+ :interpreter,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def flags(arg=nil)
+ set_or_return(
+ :flags,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/service.rb b/lib/chef/resource/service.rb
new file mode 100644
index 0000000000..3ace5cbec3
--- /dev/null
+++ b/lib/chef/resource/service.rb
@@ -0,0 +1,164 @@
+#
+# Author:: AJ Christensen (<aj@hjksolutions.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+
+class Chef
+ class Resource
+ class Service < Chef::Resource
+
+ identity_attr :service_name
+
+ state_attrs :enabled, :running
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :service
+ @service_name = name
+ @enabled = nil
+ @running = nil
+ @parameters = nil
+ @pattern = service_name
+ @start_command = nil
+ @stop_command = nil
+ @status_command = nil
+ @restart_command = nil
+ @reload_command = nil
+ @priority = nil
+ @action = "nothing"
+ @startup_type = :automatic
+ @supports = { :restart => false, :reload => false, :status => false }
+ @allowed_actions.push(:enable, :disable, :start, :stop, :restart, :reload)
+ end
+
+ def service_name(arg=nil)
+ set_or_return(
+ :service_name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # regex for match against ps -ef when !supports[:has_status] && status == nil
+ def pattern(arg=nil)
+ set_or_return(
+ :pattern,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # command to call to start service
+ def start_command(arg=nil)
+ set_or_return(
+ :start_command,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # command to call to stop service
+ def stop_command(arg=nil)
+ set_or_return(
+ :stop_command,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # command to call to get status of service
+ def status_command(arg=nil)
+ set_or_return(
+ :status_command,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # command to call to restart service
+ def restart_command(arg=nil)
+ set_or_return(
+ :restart_command,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def reload_command(arg=nil)
+ set_or_return(
+ :reload_command,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # if the service is enabled or not
+ def enabled(arg=nil)
+ set_or_return(
+ :enabled,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ # if the service is running or not
+ def running(arg=nil)
+ set_or_return(
+ :running,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ # Priority arguments can have two forms:
+ #
+ # - a simple number, in which the default start runlevels get
+ # that as the start value and stop runlevels get 100 - value.
+ #
+ # - a hash like { 2 => [:start, 20], 3 => [:stop, 55] }, where
+ # the service will be marked as started with priority 20 in
+ # runlevel 2, stopped in 3 with priority 55 and no symlinks or
+ # similar for other runlevels
+ #
+ def priority(arg=nil)
+ set_or_return(:priority,
+ arg,
+ :kind_of => [ Integer, String, Hash ])
+ end
+
+ def parameters(arg=nil)
+ set_or_return(
+ :parameters,
+ arg,
+ :kind_of => [ Hash ] )
+ end
+
+ def supports(args={})
+ if args.is_a? Array
+ args.each { |arg| @supports[arg] = true }
+ elsif args.any?
+ @supports = args
+ else
+ @supports
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/smartos_package.rb b/lib/chef/resource/smartos_package.rb
new file mode 100644
index 0000000000..315481bd93
--- /dev/null
+++ b/lib/chef/resource/smartos_package.rb
@@ -0,0 +1,36 @@
+#
+# Author:: Toomas Pelberg (<toomasp@gmx.net>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+require 'chef/provider/package/smartos'
+
+class Chef
+ class Resource
+ class SmartOSPackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :smartos_package
+ @provider = Chef::Provider::Package::SmartOS
+ end
+
+ end
+ end
+end
+
+
diff --git a/lib/chef/resource/solaris_package.rb b/lib/chef/resource/solaris_package.rb
new file mode 100644
index 0000000000..becf0236ad
--- /dev/null
+++ b/lib/chef/resource/solaris_package.rb
@@ -0,0 +1,36 @@
+#
+# Author:: Toomas Pelberg (<toomasp@gmx.net>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+require 'chef/provider/package/solaris'
+
+class Chef
+ class Resource
+ class SolarisPackage < Chef::Resource::Package
+
+ def initialize(name, collection=nil, node=nil)
+ super(name, collection, node)
+ @resource_name = :solaris_package
+ @provider = Chef::Provider::Package::Solaris
+ end
+
+ end
+ end
+end
+
+
diff --git a/lib/chef/resource/subversion.rb b/lib/chef/resource/subversion.rb
new file mode 100644
index 0000000000..e3226d8b3b
--- /dev/null
+++ b/lib/chef/resource/subversion.rb
@@ -0,0 +1,37 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "chef/resource/scm"
+
+class Chef
+ class Resource
+ class Subversion < Chef::Resource::Scm
+
+ def initialize(name, run_context=nil)
+ super
+ @svn_arguments = '--no-auth-cache'
+ @svn_info_args = '--no-auth-cache'
+ @resource_name = :subversion
+ @provider = Chef::Provider::Subversion
+ allowed_actions << :force_export
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/template.rb b/lib/chef/resource/template.rb
new file mode 100644
index 0000000000..af51b64700
--- /dev/null
+++ b/lib/chef/resource/template.rb
@@ -0,0 +1,76 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Author:: Tyler Cloke (<tyler@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/file'
+require 'chef/provider/template'
+require 'chef/mixin/securable'
+
+class Chef
+ class Resource
+ class Template < Chef::Resource::File
+ include Chef::Mixin::Securable
+
+ provides :template, :on_platforms => :all
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :template
+ @action = "create"
+ @source = "#{::File.basename(name)}.erb"
+ @cookbook = nil
+ @local = false
+ @variables = Hash.new
+ @provider = Chef::Provider::Template
+ end
+
+ def source(file=nil)
+ set_or_return(
+ :source,
+ file,
+ :kind_of => [ String ]
+ )
+ end
+
+ def variables(args=nil)
+ set_or_return(
+ :variables,
+ args,
+ :kind_of => [ Hash ]
+ )
+ end
+
+ def cookbook(args=nil)
+ set_or_return(
+ :cookbook,
+ args,
+ :kind_of => [ String ]
+ )
+ end
+
+ def local(args=nil)
+ set_or_return(
+ :local,
+ args,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/timestamped_deploy.rb b/lib/chef/resource/timestamped_deploy.rb
new file mode 100644
index 0000000000..d89274bb44
--- /dev/null
+++ b/lib/chef/resource/timestamped_deploy.rb
@@ -0,0 +1,31 @@
+#
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class Resource
+
+ # Convenience class for using the deploy resource with the timestamped
+ # deployment strategy (provider)
+ class TimestampedDeploy < Chef::Resource::Deploy
+ def initialize(*args, &block)
+ super(*args, &block)
+ @provider = Chef::Provider::Deploy::Timestamped
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/user.rb b/lib/chef/resource/user.rb
new file mode 100644
index 0000000000..4d8c4ac11b
--- /dev/null
+++ b/lib/chef/resource/user.rb
@@ -0,0 +1,134 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+
+class Chef
+ class Resource
+ class User < Chef::Resource
+
+ identity_attr :username
+
+ state_attrs :uid, :gid, :home
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :user
+ @username = name
+ @comment = nil
+ @uid = nil
+ @gid = nil
+ @home = nil
+ @shell = nil
+ @password = nil
+ @system = false
+ @manage_home = false
+ @non_unique = false
+ @action = :create
+ @supports = {
+ :manage_home => false,
+ :non_unique => false
+ }
+ @allowed_actions.push(:create, :remove, :modify, :manage, :lock, :unlock)
+ end
+
+ def username(arg=nil)
+ set_or_return(
+ :username,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def comment(arg=nil)
+ set_or_return(
+ :comment,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def uid(arg=nil)
+ set_or_return(
+ :uid,
+ arg,
+ :kind_of => [ String, Integer ]
+ )
+ end
+
+ def gid(arg=nil)
+ set_or_return(
+ :gid,
+ arg,
+ :kind_of => [ String, Integer ]
+ )
+ end
+
+ alias_method :group, :gid
+
+ def home(arg=nil)
+ set_or_return(
+ :home,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def shell(arg=nil)
+ set_or_return(
+ :shell,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def password(arg=nil)
+ set_or_return(
+ :password,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def system(arg=nil)
+ set_or_return(
+ :system,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def manage_home(arg=nil)
+ set_or_return(
+ :manage_home,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ def non_unique(arg=nil)
+ set_or_return(
+ :non_unique,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource/yum_package.rb b/lib/chef/resource/yum_package.rb
new file mode 100644
index 0000000000..bcb1f65667
--- /dev/null
+++ b/lib/chef/resource/yum_package.rb
@@ -0,0 +1,63 @@
+#
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/package'
+require 'chef/provider/package/yum'
+
+class Chef
+ class Resource
+ class YumPackage < Chef::Resource::Package
+
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :yum_package
+ @provider = Chef::Provider::Package::Yum
+ @flush_cache = { :before => false, :after => false }
+ @allow_downgrade = false
+ end
+
+ # Install a specific arch
+ def arch(arg=nil)
+ set_or_return(
+ :arch,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def flush_cache(args={})
+ if args.is_a? Array
+ args.each { |arg| @flush_cache[arg] = true }
+ elsif args.any?
+ @flush_cache = args
+ else
+ @flush_cache
+ end
+ end
+
+ def allow_downgrade(arg=nil)
+ set_or_return(
+ :allow_downgrade,
+ arg,
+ :kind_of => [ TrueClass, FalseClass ]
+ )
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource_collection.rb b/lib/chef/resource_collection.rb
new file mode 100644
index 0000000000..51858c53d8
--- /dev/null
+++ b/lib/chef/resource_collection.rb
@@ -0,0 +1,217 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource'
+require 'chef/resource_collection/stepable_iterator'
+
+class Chef
+ class ResourceCollection
+ include Enumerable
+
+ attr_reader :iterator
+
+ def initialize
+ @resources = Array.new
+ @resources_by_name = Hash.new
+ @insert_after_idx = nil
+ end
+
+ def all_resources
+ @resources
+ end
+
+ def [](index)
+ @resources[index]
+ end
+
+ def []=(index, arg)
+ is_chef_resource(arg)
+ @resources[index] = arg
+ @resources_by_name[arg.to_s] = index
+ end
+
+ def <<(*args)
+ args.flatten.each do |a|
+ is_chef_resource(a)
+ @resources << a
+ @resources_by_name[a.to_s] = @resources.length - 1
+ end
+ end
+
+ def insert(resource)
+ is_chef_resource(resource)
+ if @insert_after_idx
+ # in the middle of executing a run, so any resources inserted now should
+ # be placed after the most recent addition done by the currently executing
+ # resource
+ @resources.insert(@insert_after_idx + 1, resource)
+ # update name -> location mappings and register new resource
+ @resources_by_name.each_key do |key|
+ @resources_by_name[key] += 1 if @resources_by_name[key] > @insert_after_idx
+ end
+ @resources_by_name[resource.to_s] = @insert_after_idx + 1
+ @insert_after_idx += 1
+ else
+ @resources << resource
+ @resources_by_name[resource.to_s] = @resources.length - 1
+ end
+ end
+
+ def push(*args)
+ args.flatten.each do |arg|
+ is_chef_resource(arg)
+ @resources.push(arg)
+ @resources_by_name[arg.to_s] = @resources.length - 1
+ end
+ end
+
+ def each
+ @resources.each do |resource|
+ yield resource
+ end
+ end
+
+ def execute_each_resource(&resource_exec_block)
+ @iterator = StepableIterator.for_collection(@resources)
+ @iterator.each_with_index do |resource, idx|
+ @insert_after_idx = idx
+ yield resource
+ end
+ end
+
+ def each_index
+ @resources.each_index do |i|
+ yield i
+ end
+ end
+
+ def lookup(resource)
+ lookup_by = nil
+ if resource.kind_of?(Chef::Resource)
+ lookup_by = resource.to_s
+ elsif resource.kind_of?(String)
+ lookup_by = resource
+ else
+ raise ArgumentError, "Must pass a Chef::Resource or String to lookup"
+ end
+ res = @resources_by_name[lookup_by]
+ unless res
+ raise Chef::Exceptions::ResourceNotFound, "Cannot find a resource matching #{lookup_by} (did you define it first?)"
+ end
+ @resources[res]
+ end
+
+ # Find existing resources by searching the list of existing resources. Possible
+ # forms are:
+ #
+ # find(:file => "foobar")
+ # find(:file => [ "foobar", "baz" ])
+ # find("file[foobar]", "file[baz]")
+ # find("file[foobar,baz]")
+ #
+ # Returns the matching resource, or an Array of matching resources.
+ #
+ # Raises an ArgumentError if you feed it bad lookup information
+ # Raises a Runtime Error if it can't find the resources you are looking for.
+ def find(*args)
+ results = Array.new
+ args.each do |arg|
+ case arg
+ when Hash
+ results << find_resource_by_hash(arg)
+ when String
+ results << find_resource_by_string(arg)
+ else
+ msg = "arguments to #{self.class.name}#find should be of the form :resource => 'name' or resource[name]"
+ raise Chef::Exceptions::InvalidResourceSpecification, msg
+ end
+ end
+ flat_results = results.flatten
+ flat_results.length == 1 ? flat_results[0] : flat_results
+ end
+
+ # resources is a poorly named, but we have to maintain it for back
+ # compat.
+ alias_method :resources, :find
+
+ # Serialize this object as a hash
+ def to_json(*a)
+ instance_vars = Hash.new
+ self.instance_variables.each do |iv|
+ instance_vars[iv] = self.instance_variable_get(iv)
+ end
+ results = {
+ 'json_class' => self.class.name,
+ 'instance_vars' => instance_vars
+ }
+ results.to_json(*a)
+ end
+
+ def self.json_create(o)
+ collection = self.new()
+ o["instance_vars"].each do |k,v|
+ collection.instance_variable_set(k.to_sym, v)
+ end
+ collection
+ end
+
+ private
+
+ def find_resource_by_hash(arg)
+ results = Array.new
+ arg.each do |resource_name, name_list|
+ names = name_list.kind_of?(Array) ? name_list : [ name_list ]
+ names.each do |name|
+ res_name = "#{resource_name.to_s}[#{name}]"
+ results << lookup(res_name)
+ end
+ end
+ return results
+ end
+
+ def find_resource_by_string(arg)
+ results = Array.new
+ case arg
+ when /^(.+)\[(.+?),(.+)\]$/
+ resource_type = $1
+ arg =~ /^.+\[(.+)\]$/
+ resource_list = $1
+ resource_list.split(",").each do |name|
+ resource_name = "#{resource_type}[#{name}]"
+ results << lookup(resource_name)
+ end
+ when /^(.+)\[(.+)\]$/
+ resource_type = $1
+ name = $2
+ resource_name = "#{resource_type}[#{name}]"
+ results << lookup(resource_name)
+ else
+ raise ArgumentError, "You must have a string like resource_type[name]!"
+ end
+ return results
+ end
+
+ def is_chef_resource(arg)
+ unless arg.kind_of?(Chef::Resource)
+ raise ArgumentError, "Members must be Chef::Resource's"
+ end
+ true
+ end
+ end
+end
diff --git a/lib/chef/resource_collection/stepable_iterator.rb b/lib/chef/resource_collection/stepable_iterator.rb
new file mode 100644
index 0000000000..ec1e244758
--- /dev/null
+++ b/lib/chef/resource_collection/stepable_iterator.rb
@@ -0,0 +1,124 @@
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ class ResourceCollection
+ class StepableIterator
+
+ def self.for_collection(new_collection)
+ instance = new(new_collection)
+ instance
+ end
+
+ attr_accessor :collection
+ attr_reader :position
+
+ def initialize(collection=[])
+ @position = 0
+ @paused = false
+ @collection = collection
+ end
+
+ def size
+ collection.size
+ end
+
+ def each(&block)
+ reset_iteration(block)
+ @iterator_type = :element
+ iterate
+ end
+
+ def each_index(&block)
+ reset_iteration(block)
+ @iterator_type = :index
+ iterate
+ end
+
+ def each_with_index(&block)
+ reset_iteration(block)
+ @iterator_type = :element_with_index
+ iterate
+ end
+
+ def paused?
+ @paused
+ end
+
+ def pause
+ @paused = true
+ end
+
+ def resume
+ @paused = false
+ iterate
+ end
+
+ def rewind
+ @position = 0
+ end
+
+ def skip_back(skips=1)
+ @position -= skips
+ end
+
+ def skip_forward(skips=1)
+ @position += skips
+ end
+
+ def step
+ return nil if @position == size
+ call_iterator_block
+ @position += 1
+ end
+
+ def iterate_on(iteration_type, &block)
+ @iterator_type = iteration_type
+ @iterator_block = block
+ end
+
+ private
+
+ def reset_iteration(iterator_block)
+ @iterator_block = iterator_block
+ @position = 0
+ @paused = false
+ end
+
+ def iterate
+ while @position < size && !paused?
+ step
+ end
+ collection
+ end
+
+ def call_iterator_block
+ case @iterator_type
+ when :element
+ @iterator_block.call(collection[@position])
+ when :index
+ @iterator_block.call(@position)
+ when :element_with_index
+ @iterator_block.call(collection[@position], @position)
+ else
+ raise "42error: someone forgot to set @iterator_type, wtf?"
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource_definition.rb b/lib/chef/resource_definition.rb
new file mode 100644
index 0000000000..a0160c5885
--- /dev/null
+++ b/lib/chef/resource_definition.rb
@@ -0,0 +1,67 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/from_file'
+require 'chef/mixin/params_validate'
+
+class Chef
+ class ResourceDefinition
+
+ include Chef::Mixin::FromFile
+ include Chef::Mixin::ParamsValidate
+
+ attr_accessor :name, :params, :recipe, :node
+
+ def initialize(node=nil)
+ @name = nil
+ @params = Hash.new
+ @recipe = nil
+ @node = node
+ end
+
+ def define(resource_name, prototype_params=nil, &block)
+ unless resource_name.kind_of?(Symbol)
+ raise ArgumentError, "You must use a symbol when defining a new resource!"
+ end
+ @name = resource_name
+ if prototype_params
+ unless prototype_params.kind_of?(Hash)
+ raise ArgumentError, "You must pass a hash as the prototype parameters for a definition."
+ end
+ @params = prototype_params
+ end
+ if Kernel.block_given?
+ @recipe = block
+ else
+ raise ArgumentError, "You must pass a block to a definition."
+ end
+ true
+ end
+
+ # When we do the resource definition, we're really just setting new values for
+ # the paramaters we prototyped at the top. This method missing is as simple as
+ # it gets.
+ def method_missing(symbol, *args)
+ @params[symbol] = args.length == 1 ? args[0] : args
+ end
+
+ def to_s
+ "#{name.to_s}"
+ end
+ end
+end
diff --git a/lib/chef/resource_definition_list.rb b/lib/chef/resource_definition_list.rb
new file mode 100644
index 0000000000..b958624208
--- /dev/null
+++ b/lib/chef/resource_definition_list.rb
@@ -0,0 +1,38 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/from_file'
+require 'chef/resource_definition'
+
+class Chef
+ class ResourceDefinitionList
+ include Chef::Mixin::FromFile
+
+ attr_accessor :defines
+
+ def initialize
+ @defines = Hash.new
+ end
+
+ def define(resource_name, prototype_params=nil, &block)
+ @defines[resource_name] = ResourceDefinition.new
+ @defines[resource_name].define(resource_name, prototype_params, &block)
+ true
+ end
+ end
+end
diff --git a/lib/chef/resource_platform_map.rb b/lib/chef/resource_platform_map.rb
new file mode 100644
index 0000000000..ad27aa91c1
--- /dev/null
+++ b/lib/chef/resource_platform_map.rb
@@ -0,0 +1,151 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/params_validate'
+require 'chef/mixin/convert_to_class_name'
+
+class Chef
+ class Resource
+ class PlatformMap
+
+ include Chef::Mixin::ParamsValidate
+ include Chef::Mixin::ConvertToClassName
+
+ attr_reader :map
+
+ def initialize(map={:default => {}})
+ @map = map
+ end
+
+ def filter(platform, version)
+ resource_map = map[:default].clone
+ platform_sym = platform
+ if platform.kind_of?(String)
+ platform.downcase!
+ platform.gsub!(/\s/, "_")
+ platform_sym = platform.to_sym
+ end
+
+ if map.has_key?(platform_sym)
+ if map[platform_sym].has_key?(version)
+ if map[platform_sym].has_key?(:default)
+ resource_map.merge!(map[platform_sym][:default])
+ end
+ resource_map.merge!(map[platform_sym][version])
+ elsif map[platform_sym].has_key?(:default)
+ resource_map.merge!(map[platform_sym][:default])
+ end
+ end
+ resource_map
+ end
+
+ def set(args)
+ validate(
+ args,
+ {
+ :platform => {
+ :kind_of => Symbol,
+ :required => false
+ },
+ :version => {
+ :kind_of => String,
+ :required => false
+ },
+ :short_name => {
+ :kind_of => Symbol,
+ :required => true
+ },
+ :resource => {
+ :kind_of => [ String, Symbol, Class ],
+ :required => true
+ }
+ }
+ )
+ if args.has_key?(:platform)
+ if args.has_key?(:version)
+ if map.has_key?(args[:platform])
+ if map[args[:platform]].has_key?(args[:version])
+ map[args[:platform]][args[:version]][args[:short_name].to_sym] = args[:resource]
+ else
+ map[args[:platform]][args[:version]] = {
+ args[:short_name].to_sym => args[:resource]
+ }
+ end
+ else
+ map[args[:platform]] = {
+ args[:version] => {
+ args[:short_name].to_sym => args[:resource]
+ }
+ }
+ end
+ else
+ if map.has_key?(args[:platform])
+ if map[args[:platform]].has_key?(:default)
+ map[args[:platform]][:default][args[:short_name].to_sym] = args[:resource]
+ else
+ map[args[:platform]] = { :default => { args[:short_name].to_sym => args[:resource] } }
+ end
+ else
+ map[args[:platform]] = {
+ :default => {
+ args[:short_name].to_sym => args[:resource]
+ }
+ }
+ end
+ end
+ else
+ if map.has_key?(:default)
+ map[:default][args[:short_name].to_sym] = args[:resource]
+ else
+ map[:default] = {
+ args[:short_name].to_sym => args[:resource]
+ }
+ end
+ end
+ end
+
+ def get(short_name, platform=nil, version=nil)
+ resource_klass = platform_resource(short_name, platform, version) ||
+ resource_matching_short_name(short_name)
+
+ raise NameError, "Cannot find a resource for #{short_name} on #{platform} version #{version}" if resource_klass.nil?
+
+ resource_klass
+ end
+
+ private
+
+ def platform_resource(short_name, platform, version)
+ pmap = filter(platform, version)
+ rtkey = short_name.kind_of?(Chef::Resource) ? short_name.resource_name.to_sym : short_name
+
+ pmap.has_key?(rtkey) ? pmap[rtkey] : nil
+ end
+
+ def resource_matching_short_name(short_name)
+ begin
+ rname = convert_to_class_name(short_name.to_s)
+ Chef::Resource.const_get(rname)
+ rescue NameError
+ nil
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb
new file mode 100644
index 0000000000..3d10c1e961
--- /dev/null
+++ b/lib/chef/resource_reporter.rb
@@ -0,0 +1,272 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Prajakta Purohit (prajakta@opscode.com>)
+# Auther:: Tyler Cloke (<tyler@opscode.com>)
+#
+# Copyright:: Copyright (c) 2012 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 'uri'
+require 'chef/event_dispatch/base'
+
+class Chef
+ class ResourceReporter < EventDispatch::Base
+
+
+
+ class ResourceReport < Struct.new(:new_resource,
+ :current_resource,
+ :action,
+ :exception,
+ :elapsed_time)
+
+ def self.new_with_current_state(new_resource, action, current_resource)
+ report = new
+ report.new_resource = new_resource
+ report.action = action
+ report.current_resource = current_resource
+ report
+ end
+
+ def self.new_for_exception(new_resource, action)
+ report = new
+ report.new_resource = new_resource
+ report.action = action
+ report
+ end
+
+ def for_json
+ as_hash = {}
+ as_hash["type"] = new_resource.class.dsl_name
+ as_hash["name"] = new_resource.name
+ as_hash["id"] = new_resource.identity
+ as_hash["after"] = new_resource.state
+ as_hash["before"] = current_resource ? current_resource.state : {}
+ as_hash["duration"] = (elapsed_time * 1000).to_i.to_s
+ as_hash["delta"] = new_resource.diff if new_resource.respond_to?("diff")
+ as_hash["delta"] = "" if as_hash["delta"].nil?
+
+ # TODO: rename as "action"
+ as_hash["result"] = action.to_s
+ if success?
+ else
+ #as_hash["result"] = "failed"
+ end
+ as_hash["cookbook_name"] = new_resource.cookbook_name
+ as_hash["cookbook_version"] = new_resource.cookbook_version.version
+ as_hash
+
+ end
+
+ def finish
+ self.elapsed_time = new_resource.elapsed_time
+ end
+
+ def success?
+ !self.exception
+ end
+ end
+
+ attr_reader :updated_resources
+ attr_reader :status
+ attr_reader :exception
+ attr_reader :run_id
+ attr_reader :error_descriptions
+ attr_reader :summary_only
+
+ def initialize(rest_client)
+ if Chef::Config[:enable_reporting] && !Chef::Config[:why_run]
+ @reporting_enabled = true
+ else
+ @reporting_enabled = false
+ end
+ @updated_resources = []
+ @total_res_count = 0
+ @pending_update = nil
+ @status = "success"
+ @exception = nil
+ @run_id = nil
+ @rest_client = rest_client
+ @node = nil
+ @error_descriptions = {}
+ @summary_only = true
+ end
+
+ def node_load_completed(node, expanded_run_list_with_versions, config)
+ @node = node
+ if reporting_enabled?
+ begin
+ resource_history_url = "reports/nodes/#{node.name}/runs"
+ server_response = @rest_client.post_rest(resource_history_url, {:action => :begin})
+ run_uri = URI.parse(server_response["uri"])
+ @run_id = ::File.basename(run_uri.path)
+ Chef::Log.info("Chef server generated run history id: #{@run_id}")
+ @summary_only = server_response["summary_only"]
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
+ if !e.response || e.response.code.to_s != 404
+ if Chef::Config[:enable_reporting_url_fatals]
+ Chef::Log.error("Received exception attempting to generate run history id (URL Path: #{resource_history_url}), and enable_reporting_url_fatals is set, aborting run.")
+ raise
+ else
+ Chef::Log.info("Received exception attempting to generate run history id (URL Path: #{resource_history_url}), disabling reporting for this run.")
+ end
+ else
+ Chef::Log.debug("Received 404 attempting to generate run history id (URL Path: #{resource_history_url}), assuming feature is not supported.")
+ end
+ @reporting_enabled = false
+ end
+ end
+ end
+
+ def resource_current_state_loaded(new_resource, action, current_resource)
+ unless nested_resource?(new_resource)
+ @pending_update = ResourceReport.new_with_current_state(new_resource, action, current_resource)
+ end
+ end
+
+ def resource_up_to_date(new_resource, action)
+ @total_res_count += 1
+ @pending_update = nil unless nested_resource?(new_resource)
+ end
+
+ def resource_skipped(resource, action, conditional)
+ @total_res_count += 1
+ @pending_update = nil unless nested_resource?(resource)
+ end
+
+ def resource_updated(new_resource, action)
+ @total_res_count += 1
+ end
+
+ def resource_failed(new_resource, action, exception)
+ @total_res_count += 1
+ unless nested_resource?(new_resource)
+ @pending_update ||= ResourceReport.new_for_exception(new_resource, action)
+ @pending_update.exception = exception
+ end
+ description = Formatters::ErrorMapper.resource_failed(new_resource, action, exception)
+ @error_descriptions = description.for_json
+ end
+
+ def resource_completed(new_resource)
+ if @pending_update && !nested_resource?(new_resource)
+ @pending_update.finish
+ @updated_resources << @pending_update
+ @pending_update = nil
+ end
+ end
+
+ def run_completed(node)
+ @status = "success"
+ post_reporting_data
+ end
+
+ def run_failed(exception)
+ @exception = exception
+ @status = "failure"
+ post_reporting_data
+ end
+
+ def post_reporting_data
+ if reporting_enabled?
+ run_data = prepare_run_data
+ resource_history_url = "reports/nodes/#{@node.name}/runs/#{@run_id}"
+ Chef::Log.info("Sending resource update report (run-id: #{@run_id})")
+ Chef::Log.debug run_data.inspect
+ compressed_data = encode_gzip(run_data.to_json)
+ #if summary only is enabled send the uncompressed run_data excluding the run_data["resources"] and some additional metrics.
+ if @summary_only
+ run_data = report_summary(run_data, compressed_data)
+ Chef::Log.info("run_data_summary: #{run_data}")
+ @rest_client.post_rest(resource_history_url, run_data)
+ else
+ Chef::Log.debug("Sending Compressed Run Data...")
+ # Since we're posting compressed data we can not directly call
+ # post_rest which expects JSON
+ reporting_url = @rest_client.create_url(resource_history_url)
+ @rest_client.raw_http_request(:POST, reporting_url, {'Content-Encoding' => 'gzip'}, compressed_data)
+ end
+ else
+ Chef::Log.debug("Server doesn't support resource history, skipping resource report.")
+ end
+ end
+
+ def prepare_run_data
+ run_data = {}
+ run_data["action"] = "end"
+ run_data["resources"] = updated_resources.map do |resource_record|
+ resource_record.for_json
+ end
+ run_data["status"] = @status
+ run_data["run_list"] = @node.run_list.to_json
+ run_data["total_res_count"] = @total_res_count.to_s
+ run_data["data"] = {}
+
+ if exception
+ exception_data = {}
+ exception_data["class"] = exception.inspect
+ exception_data["message"] = exception.message
+ exception_data["backtrace"] = exception.backtrace.to_json
+ exception_data["description"] = @error_descriptions
+ run_data["data"]["exception"] = exception_data
+ end
+ run_data
+ end
+
+ def report_summary(run_data, compressed_data)
+ run_data["updated_res_count"] = updated_resources.count.to_s
+ run_data["post_size"] = compressed_data.bytesize.to_s
+ run_data["resources"] = []
+ run_data
+ end
+
+ def run_list_expand_failed(node, exception)
+ description = Formatters::ErrorMapper.run_list_expand_failed(node, exception)
+ @error_descriptions = description.for_json
+ end
+
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ description = Formatters::ErrorMapper.cookbook_resolution_failed(expanded_run_list, exception)
+ @error_descriptions = description.for_json
+ end
+
+ def cookbook_sync_failed(cookbooks, exception)
+ description = Formatters::ErrorMapper.cookbook_sync_failed(cookbooks, exception)
+ @error_descriptions = description.for_json
+ end
+
+ def reporting_enabled?
+ @reporting_enabled
+ end
+
+ private
+
+ # If we are getting messages about a resource while we are in the middle of
+ # another resource's update, we assume that the nested resource is just the
+ # implementation of a provider, and we want to hide it from the reporting
+ # output.
+ def nested_resource?(new_resource)
+ @pending_update && @pending_update.new_resource != new_resource
+ end
+
+ def encode_gzip(data)
+ "".tap do |out|
+ Zlib::GzipWriter.wrap(StringIO.new(out)){|gz| gz << data }
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
new file mode 100644
index 0000000000..7fadb17444
--- /dev/null
+++ b/lib/chef/resources.rb
@@ -0,0 +1,67 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/resource/apt_package'
+require 'chef/resource/bash'
+require 'chef/resource/breakpoint'
+require 'chef/resource/cookbook_file'
+require 'chef/resource/chef_gem'
+require 'chef/resource/cron'
+require 'chef/resource/csh'
+require 'chef/resource/deploy'
+require 'chef/resource/deploy_revision'
+require 'chef/resource/directory'
+require 'chef/resource/dpkg_package'
+require 'chef/resource/easy_install_package'
+require 'chef/resource/env'
+require 'chef/resource/erl_call'
+require 'chef/resource/execute'
+require 'chef/resource/file'
+require 'chef/resource/freebsd_package'
+require 'chef/resource/ips_package'
+require 'chef/resource/gem_package'
+require 'chef/resource/git'
+require 'chef/resource/group'
+require 'chef/resource/http_request'
+require 'chef/resource/ifconfig'
+require 'chef/resource/link'
+require 'chef/resource/log'
+require 'chef/resource/macports_package'
+require 'chef/resource/mdadm'
+require 'chef/resource/mount'
+require 'chef/resource/ohai'
+require 'chef/resource/package'
+require 'chef/resource/pacman_package'
+require 'chef/resource/perl'
+require 'chef/resource/portage_package'
+require 'chef/resource/python'
+require 'chef/resource/remote_directory'
+require 'chef/resource/remote_file'
+require 'chef/resource/rpm_package'
+require 'chef/resource/route'
+require 'chef/resource/ruby'
+require 'chef/resource/ruby_block'
+require 'chef/resource/scm'
+require 'chef/resource/script'
+require 'chef/resource/service'
+require 'chef/resource/subversion'
+require 'chef/resource/smartos_package'
+require 'chef/resource/template'
+require 'chef/resource/timestamped_deploy'
+require 'chef/resource/user'
+require 'chef/resource/yum_package'
diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb
new file mode 100644
index 0000000000..1e67d762b3
--- /dev/null
+++ b/lib/chef/rest.rb
@@ -0,0 +1,526 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Thom May (<thom@clearairturbulence.org>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'zlib'
+require 'net/https'
+require 'uri'
+require 'chef/json_compat'
+require 'tempfile'
+require 'chef/rest/auth_credentials'
+require 'chef/rest/rest_request'
+require 'chef/monkey_patches/string'
+require 'chef/monkey_patches/net_http'
+require 'chef/config'
+require 'chef/exceptions'
+
+
+
+class Chef
+ # == Chef::REST
+ # Chef's custom REST client with built-in JSON support and RSA signed header
+ # authentication.
+ class REST
+
+ class NoopInflater
+ def inflate(chunk)
+ chunk
+ end
+ end
+
+ attr_reader :auth_credentials
+ attr_accessor :url, :cookies, :sign_on_redirect, :redirect_limit
+
+ CONTENT_ENCODING = "content-encoding".freeze
+ GZIP = "gzip".freeze
+ DEFLATE = "deflate".freeze
+ IDENTITY = "identity".freeze
+
+ # Create a REST client object. The supplied +url+ is used as the base for
+ # all subsequent requests. For example, when initialized with a base url
+ # http://localhost:4000, a call to +get_rest+ with 'nodes' will make an
+ # HTTP GET request to http://localhost:4000/nodes
+ def initialize(url, client_name=Chef::Config[:node_name], signing_key_filename=Chef::Config[:client_key], options={})
+ @url = url
+ @cookies = CookieJar.instance
+ @default_headers = options[:headers] || {}
+ @signing_key_filename = signing_key_filename
+ @key = load_signing_key(@signing_key_filename, options[:raw_key])
+ @auth_credentials = AuthCredentials.new(client_name, @key)
+ @sign_on_redirect, @sign_request = true, true
+ @redirects_followed = 0
+ @redirect_limit = 10
+ @disable_gzip = false
+ handle_options(options)
+ end
+
+ def signing_key_filename
+ @signing_key_filename
+ end
+
+ def client_name
+ @auth_credentials.client_name
+ end
+
+ def signing_key
+ @raw_key
+ end
+
+ # Register the client
+ #--
+ # Requires you to load chef/api_client beforehand. explicit require is removed since
+ # most users of this class have no need for chef/api_client. This functionality
+ # should be moved anyway...
+ def register(name=Chef::Config[:node_name], destination=Chef::Config[:client_key])
+ if (File.exists?(destination) && !File.writable?(destination))
+ raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination} - check permissions?"
+ end
+ nc = Chef::ApiClient.new
+ nc.name(name)
+
+ catch(:done) do
+ retries = config[:client_registration_retries] || 5
+ 0.upto(retries) do |n|
+ begin
+ response = nc.save(true, true)
+ Chef::Log.debug("Registration response: #{response.inspect}")
+ raise Chef::Exceptions::CannotWritePrivateKey, "The response from the server did not include a private key!" unless response.has_key?("private_key")
+ # Write out the private key
+ ::File.open(destination, "w") {|f|
+ f.chmod(0600)
+ f.print(response["private_key"])
+ }
+ throw :done
+ rescue IOError
+ raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination}"
+ rescue Net::HTTPFatalError => e
+ Chef::Log.warn("Failed attempt #{n} of #{retries+1} on client creation")
+ raise unless e.response.code == "500"
+ end
+ end
+ end
+
+ true
+ end
+
+ # Send an HTTP GET request to the path
+ #
+ # Using this method to +fetch+ a file is considered deprecated.
+ #
+ # === Parameters
+ # path:: The path to GET
+ # raw:: Whether you want the raw body returned, or JSON inflated. Defaults
+ # to JSON inflated.
+ def get_rest(path, raw=false, headers={})
+ if raw
+ streaming_request(create_url(path), headers)
+ else
+ api_request(:GET, create_url(path), headers)
+ end
+ end
+
+ # Send an HTTP DELETE request to the path
+ def delete_rest(path, headers={})
+ api_request(:DELETE, create_url(path), headers)
+ end
+
+ # Send an HTTP POST request to the path
+ def post_rest(path, json, headers={})
+ api_request(:POST, create_url(path), headers, json)
+ end
+
+ # Send an HTTP PUT request to the path
+ def put_rest(path, json, headers={})
+ api_request(:PUT, create_url(path), headers, json)
+ end
+
+ # Streams a download to a tempfile, then yields the tempfile to a block.
+ # After the download, the tempfile will be closed and unlinked.
+ # If you rename the tempfile, it will not be deleted.
+ # Beware that if the server streams infinite content, this method will
+ # stream it until you run out of disk space.
+ def fetch(path, headers={})
+ streaming_request(create_url(path), headers) {|tmp_file| yield tmp_file }
+ end
+
+ def create_url(path)
+ if path =~ /^(http|https):\/\//
+ URI.parse(path)
+ else
+ URI.parse("#{@url}/#{path}")
+ end
+ end
+
+ def sign_requests?
+ auth_credentials.sign_requests? && @sign_request
+ end
+
+ # ==== DEPRECATED
+ # Use +api_request+ instead
+ #--
+ # Actually run an HTTP request. First argument is the HTTP method,
+ # which should be one of :GET, :PUT, :POST or :DELETE. Next is the
+ # URL, then an object to include in the body (which will be converted with
+ # .to_json). The limit argument is unused, it is present for backwards
+ # compatibility. Configure the redirect limit with #redirect_limit=
+ # instead.
+ #
+ # Typically, you won't use this method -- instead, you'll use one of
+ # the helper methods (get_rest, post_rest, etc.)
+ #
+ # Will return the body of the response on success.
+ def run_request(method, url, headers={}, data=false, limit=nil, raw=false)
+ json_body = data ? Chef::JSONCompat.to_json(data) : nil
+ # Force encoding to binary to fix SSL related EOFErrors
+ # cf. http://tickets.opscode.com/browse/CHEF-2363
+ # http://redmine.ruby-lang.org/issues/5233
+ json_body.force_encoding(Encoding::BINARY) if json_body.respond_to?(:force_encoding)
+ headers = build_headers(method, url, headers, json_body, raw)
+
+ tf, response_body = nil, nil
+
+ retriable_rest_request(method, url, json_body, headers) do |rest_request|
+
+ res = rest_request.call do |response|
+ if raw
+ tf = stream_to_tempfile(url, response)
+ else
+ response_body = decompress_body(response)
+ end
+ end
+
+ case res
+ when Net::HTTPSuccess
+ if res['content-type'] =~ /json/
+ Chef::JSONCompat.from_json(response_body)
+ else
+ if method == :HEAD
+ true
+ elsif raw
+ tf
+ else
+ response_body
+ end
+ end
+ when Net::HTTPNotModified # Must be tested before Net::HTTPRedirection because it's subclass.
+ false
+ when Net::HTTPRedirection
+ follow_redirect {run_request(method, create_url(res['location']), headers, false, nil, raw)}
+ else
+ if res['content-type'] =~ /json/
+ exception = Chef::JSONCompat.from_json(response_body)
+ msg = "HTTP Request Returned #{res.code} #{res.message}: "
+ msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s)
+ Chef::Log.warn(msg)
+ end
+ res.error!
+ end
+ end
+ end
+
+ # Runs an HTTP request to a JSON API with JSON body. File Download not supported.
+ def api_request(method, url, headers={}, data=false)
+ json_body = data ? Chef::JSONCompat.to_json(data) : nil
+ # Force encoding to binary to fix SSL related EOFErrors
+ # cf. http://tickets.opscode.com/browse/CHEF-2363
+ # http://redmine.ruby-lang.org/issues/5233
+ json_body.force_encoding(Encoding::BINARY) if json_body.respond_to?(:force_encoding)
+ raw_http_request(method, url, headers, json_body)
+ end
+
+ # Runs an HTTP request to a JSON API with raw body. File Download not supported.
+ def raw_http_request(method, url, headers, body)
+ headers = build_headers(method, url, headers, body)
+ retriable_rest_request(method, url, body, headers) do |rest_request|
+ begin
+ response = rest_request.call {|r| r.read_body}
+
+ Chef::Log.debug("---- HTTP Status and Header Data: ----")
+ Chef::Log.debug("HTTP #{response.http_version} #{response.code} #{response.msg}")
+
+ response.each do |header, value|
+ Chef::Log.debug("#{header}: #{value}")
+ end
+ Chef::Log.debug("---- End HTTP Status/Header Data ----")
+
+ response_body = decompress_body(response)
+
+ if response.kind_of?(Net::HTTPSuccess)
+ if response['content-type'] =~ /json/
+ Chef::JSONCompat.from_json(response_body.chomp)
+ else
+ Chef::Log.warn("Expected JSON response, but got content-type '#{response['content-type']}'")
+ response_body
+ end
+ elsif redirect_location = redirected_to(response)
+ follow_redirect {api_request(:GET, create_url(redirect_location))}
+ else
+ # have to decompress the body before making an exception for it. But the body could be nil.
+ response.body.replace(decompress_body(response)) if response.body.respond_to?(:replace)
+
+ if response['content-type'] =~ /json/
+ exception = Chef::JSONCompat.from_json(response_body)
+ msg = "HTTP Request Returned #{response.code} #{response.message}: "
+ msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s)
+ Chef::Log.info(msg)
+ end
+ response.error!
+ end
+ rescue Exception => e
+ if e.respond_to?(:chef_rest_request=)
+ e.chef_rest_request = rest_request
+ end
+ raise
+ end
+ end
+ end
+
+ def decompress_body(response)
+ if gzip_disabled?
+ response.body
+ else
+ case response[CONTENT_ENCODING]
+ when GZIP
+ Chef::Log.debug "decompressing gzip response"
+ Zlib::Inflate.new(Zlib::MAX_WBITS + 16).inflate(response.body)
+ when DEFLATE
+ Chef::Log.debug "decompressing deflate response"
+ Zlib::Inflate.inflate(response.body)
+ else
+ response.body
+ end
+ end
+ end
+
+ # Makes a streaming download request. <b>Doesn't speak JSON.</b>
+ # Streams the response body to a tempfile. If a block is given, it's
+ # passed to Tempfile.open(), which means that the tempfile will automatically
+ # be unlinked after the block is executed.
+ #
+ # If no block is given, the tempfile is returned, which means it's up to
+ # you to unlink the tempfile when you're done with it.
+ def streaming_request(url, headers, &block)
+ headers = build_headers(:GET, url, headers, nil, true)
+ retriable_rest_request(:GET, url, nil, headers) do |rest_request|
+ begin
+ tempfile = nil
+ response = rest_request.call do |r|
+ if block_given? && r.kind_of?(Net::HTTPSuccess)
+ begin
+ tempfile = stream_to_tempfile(url, r, &block)
+ yield tempfile
+ ensure
+ tempfile.close!
+ end
+ else
+ tempfile = stream_to_tempfile(url, r)
+ end
+ end
+ if response.kind_of?(Net::HTTPSuccess)
+ tempfile
+ elsif redirect_location = redirected_to(response)
+ # TODO: test tempfile unlinked when following redirects.
+ tempfile && tempfile.close!
+ follow_redirect {streaming_request(create_url(redirect_location), {}, &block)}
+ else
+ tempfile && tempfile.close!
+ response.error!
+ end
+ rescue Exception => e
+ if e.respond_to?(:chef_rest_request=)
+ e.chef_rest_request = rest_request
+ end
+ raise
+ end
+ end
+ end
+
+ def retriable_rest_request(method, url, req_body, headers)
+ rest_request = Chef::REST::RESTRequest.new(method, url, req_body, headers)
+
+ Chef::Log.debug("Sending HTTP Request via #{method} to #{url.host}:#{url.port}#{rest_request.path}")
+
+ http_attempts = 0
+
+ begin
+ http_attempts += 1
+
+ res = yield rest_request
+
+ rescue SocketError, Errno::ETIMEDOUT => e
+ e.message.replace "Error connecting to #{url} - #{e.message}"
+ raise e
+ rescue Errno::ECONNREFUSED
+ if http_retry_count - http_attempts + 1 > 0
+ Chef::Log.error("Connection refused connecting to #{url.host}:#{url.port} for #{rest_request.path}, retry #{http_attempts}/#{http_retry_count}")
+ sleep(http_retry_delay)
+ retry
+ end
+ raise Errno::ECONNREFUSED, "Connection refused connecting to #{url.host}:#{url.port} for #{rest_request.path}, giving up"
+ rescue Timeout::Error
+ if http_retry_count - http_attempts + 1 > 0
+ Chef::Log.error("Timeout connecting to #{url.host}:#{url.port} for #{rest_request.path}, retry #{http_attempts}/#{http_retry_count}")
+ sleep(http_retry_delay)
+ retry
+ end
+ raise Timeout::Error, "Timeout connecting to #{url.host}:#{url.port} for #{rest_request.path}, giving up"
+ rescue Net::HTTPFatalError => e
+ if http_retry_count - http_attempts + 1 > 0
+ sleep_time = 1 + (2 ** http_attempts) + rand(2 ** http_attempts)
+ Chef::Log.error("Server returned error for #{url}, retrying #{http_attempts}/#{http_retry_count} in #{sleep_time}s")
+ sleep(sleep_time)
+ retry
+ end
+ raise
+ end
+ end
+
+ def authentication_headers(method, url, json_body=nil)
+ request_params = {:http_method => method, :path => url.path, :body => json_body, :host => "#{url.host}:#{url.port}"}
+ request_params[:body] ||= ""
+ auth_credentials.signature_headers(request_params)
+ end
+
+ def http_retry_delay
+ config[:http_retry_delay]
+ end
+
+ def http_retry_count
+ config[:http_retry_count]
+ end
+
+ def config
+ Chef::Config
+ end
+
+ def follow_redirect
+ raise Chef::Exceptions::RedirectLimitExceeded if @redirects_followed >= redirect_limit
+ @redirects_followed += 1
+ Chef::Log.debug("Following redirect #{@redirects_followed}/#{redirect_limit}")
+ if @sign_on_redirect
+ yield
+ else
+ @sign_request = false
+ yield
+ end
+ ensure
+ @redirects_followed = 0
+ @sign_request = true
+ end
+
+ private
+
+ def redirected_to(response)
+ return nil unless response.kind_of?(Net::HTTPRedirection)
+ # Net::HTTPNotModified is undesired subclass of Net::HTTPRedirection so test for this
+ return nil if response.kind_of?(Net::HTTPNotModified)
+ response['location']
+ end
+
+ def build_headers(method, url, headers={}, json_body=false, raw=false)
+ headers = @default_headers.merge(headers)
+ #headers['Accept'] = "application/json" unless raw
+ headers['Accept'] = "application/json" unless raw
+ headers["Content-Type"] = 'application/json' if json_body
+ headers['Content-Length'] = json_body.bytesize.to_s if json_body
+ headers[RESTRequest::ACCEPT_ENCODING] = RESTRequest::ENCODING_GZIP_DEFLATE unless gzip_disabled?
+ headers.merge!(authentication_headers(method, url, json_body)) if sign_requests?
+ headers.merge!(Chef::Config[:custom_http_headers]) if Chef::Config[:custom_http_headers]
+ headers
+ end
+
+ def stream_to_tempfile(url, response)
+ tf = Tempfile.open("chef-rest")
+ if Chef::Platform.windows?
+ tf.binmode #required for binary files on Windows platforms
+ end
+ Chef::Log.debug("Streaming download from #{url.to_s} to tempfile #{tf.path}")
+ # Stolen from http://www.ruby-forum.com/topic/166423
+ # Kudos to _why!
+ size, total = 0, response.header['Content-Length'].to_i
+
+ inflater = if gzip_disabled?
+ NoopInflater.new
+ else
+ case response[CONTENT_ENCODING]
+ when GZIP
+ Chef::Log.debug "decompressing gzip stream"
+ Zlib::Inflate.new(Zlib::MAX_WBITS + 16)
+ when DEFLATE
+ Chef::Log.debug "decompressing inflate stream"
+ Zlib::Inflate.new
+ else
+ NoopInflater.new
+ end
+ end
+
+ response.read_body do |chunk|
+ tf.write(inflater.inflate(chunk))
+ size += chunk.size
+ end
+ tf.close
+ tf
+ rescue Exception
+ tf.close!
+ raise
+ end
+
+ # gzip is disabled using the disable_gzip => true option in the
+ # constructor. When gzip is disabled, no 'Accept-Encoding' header will be
+ # set, and the response will not be decompressed, no matter what the
+ # Content-Encoding header of the response is. The intended use case for
+ # this is to work around situations where you request +file.tar.gz+, but
+ # the server responds with a content type of tar and a content encoding of
+ # gzip, tricking the client into decompressing the response so you end up
+ # with a tar archive (no gzip) named file.tar.gz
+ def gzip_disabled?
+ @disable_gzip
+ end
+
+ def handle_options(opts)
+ opts.each do |name, value|
+ case name.to_s
+ when 'disable_gzip'
+ @disable_gzip = value
+ end
+ end
+ end
+
+ def load_signing_key(key_file, raw_key = nil)
+ if (!!key_file)
+ @raw_key = IO.read(key_file).strip
+ elsif (!!raw_key)
+ @raw_key = raw_key.strip
+ else
+ return nil
+ end
+ @key = OpenSSL::PKey::RSA.new(@raw_key)
+ rescue SystemCallError, IOError => e
+ Chef::Log.warn "Failed to read the private key #{key_file}: #{e.inspect}"
+ raise Chef::Exceptions::PrivateKeyMissing, "I cannot read #{key_file}, which you told me to use to sign requests!"
+ rescue OpenSSL::PKey::RSAError
+ msg = "The file #{key_file} or :raw_key option does not contain a correctly formatted private key.\n"
+ msg << "The key file should begin with '-----BEGIN RSA PRIVATE KEY-----' and end with '-----END RSA PRIVATE KEY-----'"
+ raise Chef::Exceptions::InvalidPrivateKey, msg
+ end
+
+ end
+end
diff --git a/lib/chef/rest/auth_credentials.rb b/lib/chef/rest/auth_credentials.rb
new file mode 100644
index 0000000000..00711c960d
--- /dev/null
+++ b/lib/chef/rest/auth_credentials.rb
@@ -0,0 +1,57 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Thom May (<thom@clearairturbulence.org>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+require 'chef/log'
+require 'mixlib/authentication/signedheaderauth'
+
+class Chef
+ class REST
+ class AuthCredentials
+ attr_reader :client_name, :key
+
+ def initialize(client_name=nil, key=nil)
+ @client_name, @key = client_name, key
+ end
+
+ def sign_requests?
+ !!key
+ end
+
+ def signature_headers(request_params={})
+ raise ArgumentError, "Cannot sign the request without a client name, check that :node_name is assigned" if client_name.nil?
+ Chef::Log.debug("Signing the request as #{client_name}")
+
+ # params_in = {:http_method => :GET, :path => "/clients", :body => "", :host => "localhost"}
+ request_params = request_params.dup
+ request_params[:timestamp] = Time.now.utc.iso8601
+ request_params[:user_id] = client_name
+ request_params[:proto_version] = Chef::Config[:authentication_protocol_version]
+ host = request_params.delete(:host) || "localhost"
+
+ sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(request_params)
+ signed = sign_obj.sign(key).merge({:host => host})
+ signed.inject({}){|memo, kv| memo["#{kv[0].to_s.upcase}"] = kv[1];memo}
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/rest/cookie_jar.rb b/lib/chef/rest/cookie_jar.rb
new file mode 100644
index 0000000000..e3137708a2
--- /dev/null
+++ b/lib/chef/rest/cookie_jar.rb
@@ -0,0 +1,31 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Thom May (<thom@clearairturbulence.org>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+require 'singleton'
+
+class Chef
+ class REST
+ class CookieJar < Hash
+ include Singleton
+ end
+ end
+end
diff --git a/lib/chef/rest/rest_request.rb b/lib/chef/rest/rest_request.rb
new file mode 100644
index 0000000000..4ff0016205
--- /dev/null
+++ b/lib/chef/rest/rest_request.rb
@@ -0,0 +1,229 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Thom May (<thom@clearairturbulence.org>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+require 'uri'
+require 'net/http'
+require 'chef/rest/cookie_jar'
+
+# To load faster, we only want ohai's version string.
+# However, in ohai before 0.6.0, the version is defined
+# in ohai, not ohai/version
+begin
+ require 'ohai/version' #used in user agent string.
+rescue LoadError
+ require 'ohai'
+end
+
+require 'chef/version'
+
+class Chef
+ class REST
+ class RESTRequest
+
+ engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"
+
+ UA_COMMON = "/#{::Chef::VERSION} (#{engine}-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}; ohai-#{Ohai::VERSION}; #{RUBY_PLATFORM}; +http://opscode.com)"
+ DEFAULT_UA = "Chef Client" << UA_COMMON
+
+ USER_AGENT = "User-Agent".freeze
+
+ ACCEPT_ENCODING = "Accept-Encoding".freeze
+ ENCODING_GZIP_DEFLATE = "gzip;q=1.0,deflate;q=0.6,identity;q=0.3".freeze
+
+ GET = "get".freeze
+ PUT = "put".freeze
+ POST = "post".freeze
+ DELETE = "delete".freeze
+ HEAD = "head".freeze
+
+ HTTPS = "https".freeze
+
+ SLASH = "/".freeze
+
+ def self.user_agent=(ua)
+ @user_agent = ua
+ end
+
+ def self.user_agent
+ @user_agent ||= DEFAULT_UA
+ end
+
+ attr_reader :method, :url, :headers, :http_client, :http_request
+
+ def initialize(method, url, req_body, base_headers={})
+ @method, @url = method, url
+ @request_body = nil
+ @cookies = CookieJar.instance
+ configure_http_client
+ build_headers(base_headers)
+ configure_http_request(req_body)
+ end
+
+ def host
+ @url.host
+ end
+
+ def port
+ @url.port
+ end
+
+ def query
+ @url.query
+ end
+
+ def path
+ @url.path.empty? ? SLASH : @url.path
+ end
+
+ def call
+ hide_net_http_bug do
+ http_client.request(http_request) do |response|
+ store_cookie(response)
+ yield response if block_given?
+ response
+ end
+ end
+ end
+
+ def config
+ Chef::Config
+ end
+
+ private
+
+ def hide_net_http_bug
+ yield
+ rescue NoMethodError => e
+ # http://redmine.ruby-lang.org/issues/show/2708
+ # http://redmine.ruby-lang.org/issues/show/2758
+ if e.to_s =~ /#{Regexp.escape(%q|undefined method `closed?' for nil:NilClass|)}/
+ Chef::Log.debug("Rescued error in http connect, re-raising as Errno::ECONNREFUSED to hide bug in net/http")
+ Chef::Log.debug("#{e.class.name}: #{e.to_s}")
+ Chef::Log.debug(e.backtrace.join("\n"))
+ raise Errno::ECONNREFUSED, "Connection refused attempting to contact #{url.scheme}://#{host}:#{port}"
+ else
+ raise
+ end
+ end
+
+ def store_cookie(response)
+ if response['set-cookie']
+ @cookies["#{host}:#{port}"] = response['set-cookie']
+ end
+ end
+
+ def build_headers(headers)
+ @headers = headers.dup
+ # TODO: need to set accept somewhere else
+ # headers.merge!('Accept' => "application/json") unless raw
+ @headers['X-Chef-Version'] = ::Chef::VERSION
+ @headers[ACCEPT_ENCODING] = ENCODING_GZIP_DEFLATE
+
+ if @cookies.has_key?("#{host}:#{port}")
+ @headers['Cookie'] = @cookies["#{host}:#{port}"]
+ end
+ end
+
+ #adapted from buildr/lib/buildr/core/transports.rb
+ def proxy_uri
+ proxy = Chef::Config["#{url.scheme}_proxy"]
+ proxy = URI.parse(proxy) if String === proxy
+ excludes = Chef::Config[:no_proxy].to_s.split(/\s*,\s*/).compact
+ excludes = excludes.map { |exclude| exclude =~ /:\d+$/ ? exclude : "#{exclude}:*" }
+ return proxy unless excludes.any? { |exclude| File.fnmatch(exclude, "#{host}:#{port}") }
+ end
+
+ def configure_http_client
+ http_proxy = proxy_uri
+ if http_proxy.nil?
+ @http_client = Net::HTTP.new(host, port)
+ else
+ Chef::Log.debug("Using #{http_proxy.host}:#{http_proxy.port} for proxy")
+ user = Chef::Config["#{url.scheme}_proxy_user"]
+ pass = Chef::Config["#{url.scheme}_proxy_pass"]
+ @http_client = Net::HTTP.Proxy(http_proxy.host, http_proxy.port, user, pass).new(host, port)
+ end
+ if url.scheme == HTTPS
+ @http_client.use_ssl = true
+ if config[:ssl_verify_mode] == :verify_none
+ @http_client.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ elsif config[:ssl_verify_mode] == :verify_peer
+ @http_client.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ end
+ if config[:ssl_ca_path]
+ unless ::File.exist?(config[:ssl_ca_path])
+ raise Chef::Exceptions::ConfigurationError, "The configured ssl_ca_path #{config[:ssl_ca_path]} does not exist"
+ end
+ @http_client.ca_path = config[:ssl_ca_path]
+ elsif config[:ssl_ca_file]
+ unless ::File.exist?(config[:ssl_ca_file])
+ raise Chef::Exceptions::ConfigurationError, "The configured ssl_ca_file #{config[:ssl_ca_file]} does not exist"
+ end
+ @http_client.ca_file = config[:ssl_ca_file]
+ end
+ if (config[:ssl_client_cert] || config[:ssl_client_key])
+ unless (config[:ssl_client_cert] && config[:ssl_client_key])
+ raise Chef::Exceptions::ConfigurationError, "You must configure ssl_client_cert and ssl_client_key together"
+ end
+ unless ::File.exists?(config[:ssl_client_cert])
+ raise Chef::Exceptions::ConfigurationError, "The configured ssl_client_cert #{config[:ssl_client_cert]} does not exist"
+ end
+ unless ::File.exists?(config[:ssl_client_key])
+ raise Chef::Exceptions::ConfigurationError, "The configured ssl_client_key #{config[:ssl_client_key]} does not exist"
+ end
+ @http_client.cert = OpenSSL::X509::Certificate.new(::File.read(config[:ssl_client_cert]))
+ @http_client.key = OpenSSL::PKey::RSA.new(::File.read(config[:ssl_client_key]))
+ end
+ end
+
+ @http_client.read_timeout = config[:rest_timeout]
+ end
+
+
+ def configure_http_request(request_body=nil)
+ req_path = "#{path}"
+ req_path << "?#{query}" if query
+
+ @http_request = case method.to_s.downcase
+ when GET
+ Net::HTTP::Get.new(req_path, headers)
+ when POST
+ Net::HTTP::Post.new(req_path, headers)
+ when PUT
+ Net::HTTP::Put.new(req_path, headers)
+ when DELETE
+ Net::HTTP::Delete.new(req_path, headers)
+ when HEAD
+ Net::HTTP::Head.new(req_path, headers)
+ else
+ raise ArgumentError, "You must provide :GET, :PUT, :POST, :DELETE or :HEAD as the method"
+ end
+
+ @http_request.body = request_body if (request_body && @http_request.request_body_permitted?)
+ # Optionally handle HTTP Basic Authentication
+ @http_request.basic_auth(url.user, url.password) if url.user
+ @http_request[USER_AGENT] = self.class.user_agent
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/role.rb b/lib/chef/role.rb
new file mode 100644
index 0000000000..78bbfadb88
--- /dev/null
+++ b/lib/chef/role.rb
@@ -0,0 +1,253 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Author:: Christopher Brown (<cb@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/from_file'
+require 'chef/run_list'
+require 'chef/mash'
+require 'chef/json_compat'
+require 'chef/search/query'
+
+class Chef
+ class Role
+
+ include Chef::Mixin::FromFile
+ include Chef::Mixin::ParamsValidate
+
+ # Create a new Chef::Role object.
+ def initialize
+ @name = ''
+ @description = ''
+ @default_attributes = Mash.new
+ @override_attributes = Mash.new
+ @env_run_lists = {"_default" => Chef::RunList.new}
+ end
+
+ def chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def self.chef_server_rest
+ Chef::REST.new(Chef::Config[:chef_server_url])
+ end
+
+ def name(arg=nil)
+ set_or_return(
+ :name,
+ arg,
+ :regex => /^[\-[:alnum:]_]+$/
+ )
+ end
+
+ def description(arg=nil)
+ set_or_return(
+ :description,
+ arg,
+ :kind_of => String
+ )
+ end
+
+ def run_list(*args)
+ if (args.length > 0)
+ @env_run_lists["_default"].reset!(args)
+ end
+ @env_run_lists["_default"]
+ end
+
+ alias_method :recipes, :run_list
+
+ # For run_list expansion
+ def run_list_for(environment)
+ if env_run_lists[environment].nil?
+ env_run_lists["_default"]
+ else
+ env_run_lists[environment]
+ end
+ end
+
+ def active_run_list_for(environment)
+ @env_run_lists.has_key?(environment) ? environment : '_default'
+ end
+
+ # Per environment run lists
+ def env_run_lists(env_run_lists=nil)
+ if (!env_run_lists.nil?)
+ unless env_run_lists.key?("_default")
+ msg = "_default key is required in env_run_lists.\n"
+ msg << "(env_run_lists: #{env_run_lists.inspect})"
+ raise Chef::Exceptions::InvalidEnvironmentRunListSpecification, msg
+ end
+ @env_run_lists.clear
+ env_run_lists.each { |k,v| @env_run_lists[k] = Chef::RunList.new(*Array(v))}
+ end
+ @env_run_lists
+ end
+
+ alias :env_run_list :env_run_lists
+
+ def default_attributes(arg=nil)
+ set_or_return(
+ :default_attributes,
+ arg,
+ :kind_of => Hash
+ )
+ end
+
+ def override_attributes(arg=nil)
+ set_or_return(
+ :override_attributes,
+ arg,
+ :kind_of => Hash
+ )
+ end
+
+ def to_hash
+ env_run_lists_without_default = @env_run_lists.dup
+ env_run_lists_without_default.delete("_default")
+ result = {
+ "name" => @name,
+ "description" => @description,
+ 'json_class' => self.class.name,
+ "default_attributes" => @default_attributes,
+ "override_attributes" => @override_attributes,
+ "chef_type" => "role",
+
+ #Render to_json correctly for run_list items (both run_list and evn_run_lists)
+ #so malformed json does not result
+ "run_list" => run_list.run_list.map { |item| item.to_s },
+ "env_run_lists" => env_run_lists_without_default.inject({}) do |accumulator, (k, v)|
+ accumulator[k] = v.map { |x| x.to_s }
+ accumulator
+ end
+ }
+ result
+ end
+
+ # Serialize this object as a hash
+ def to_json(*a)
+ to_hash.to_json(*a)
+ end
+
+ def update_from!(o)
+ description(o.description)
+ recipes(o.recipes) if defined?(o.recipes)
+ default_attributes(o.default_attributes)
+ override_attributes(o.override_attributes)
+ env_run_lists(o.env_run_lists) unless o.env_run_lists.nil?
+ self
+ end
+
+ # Create a Chef::Role from JSON
+ def self.json_create(o)
+ role = new
+ role.name(o["name"])
+ role.description(o["description"])
+ role.default_attributes(o["default_attributes"])
+ role.override_attributes(o["override_attributes"])
+
+ # _default run_list is in 'run_list' for newer clients, and
+ # 'recipes' for older clients.
+ env_run_list_hash = {"_default" => (o.has_key?("run_list") ? o["run_list"] : o["recipes"])}
+
+ # Clients before 0.10 do not include env_run_lists, so only
+ # merge if it's there.
+ if o["env_run_lists"]
+ env_run_list_hash.merge!(o["env_run_lists"])
+ end
+ role.env_run_lists(env_run_list_hash)
+
+ role
+ end
+
+ # Get the list of all roles from the API.
+ def self.list(inflate=false)
+ if inflate
+ response = Hash.new
+ Chef::Search::Query.new.search(:role) do |n|
+ response[n.name] = n unless n.nil?
+ end
+ response
+ else
+ chef_server_rest.get_rest("roles")
+ end
+ end
+
+ # Load a role by name from the API
+ def self.load(name)
+ chef_server_rest.get_rest("roles/#{name}")
+ end
+
+ def environment(env_name)
+ chef_server_rest.get_rest("roles/#{@name}/environments/#{env_name}")
+ end
+
+ def environments
+ chef_server_rest.get_rest("roles/#{@name}/environments")
+ end
+
+ # Remove this role via the REST API
+ def destroy
+ chef_server_rest.delete_rest("roles/#{@name}")
+ end
+
+ # Save this role via the REST API
+ def save
+ begin
+ chef_server_rest.put_rest("roles/#{@name}", self)
+ rescue Net::HTTPServerException => e
+ raise e unless e.response.code == "404"
+ chef_server_rest.post_rest("roles", self)
+ end
+ self
+ end
+
+ # Create the role via the REST API
+ def create
+ chef_server_rest.post_rest("roles", self)
+ self
+ end
+
+ # As a string
+ def to_s
+ "role[#{@name}]"
+ end
+
+ # Load a role from disk - prefers to load the JSON, but will happily load
+ # the raw rb files as well.
+ def self.from_disk(name, force=nil)
+ js_file = File.join(Chef::Config[:role_path], "#{name}.json")
+ rb_file = File.join(Chef::Config[:role_path], "#{name}.rb")
+
+ if File.exists?(js_file) || force == "json"
+ # from_json returns object.class => json_class in the JSON.
+ Chef::JSONCompat.from_json(IO.read(js_file))
+ elsif File.exists?(rb_file) || force == "ruby"
+ role = Chef::Role.new
+ role.name(name)
+ role.from_file(rb_file)
+ role
+ else
+ raise Chef::Exceptions::RoleNotFound, "Role '#{name}' could not be loaded from disk"
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb
new file mode 100644
index 0000000000..625a993941
--- /dev/null
+++ b/lib/chef/run_context.rb
@@ -0,0 +1,290 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2008-2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/resource_collection'
+require 'chef/node'
+require 'chef/role'
+require 'chef/log'
+
+class Chef
+ # == Chef::RunContext
+ # Value object that loads and tracks the context of a Chef run
+ class RunContext
+
+ attr_reader :node, :cookbook_collection, :definitions
+
+ # Needs to be settable so deploy can run a resource_collection independent
+ # of any cookbooks.
+ attr_accessor :resource_collection, :immediate_notification_collection, :delayed_notification_collection
+
+ attr_reader :events
+
+ attr_reader :loaded_recipes
+ attr_reader :loaded_attributes
+
+ # Creates a new Chef::RunContext object and populates its fields. This object gets
+ # used by the Chef Server to generate a fully compiled recipe list for a node.
+ #
+ # === Returns
+ # object<Chef::RunContext>:: Duh. :)
+ def initialize(node, cookbook_collection, events)
+ @node = node
+ @cookbook_collection = cookbook_collection
+ @resource_collection = Chef::ResourceCollection.new
+ @immediate_notification_collection = Hash.new {|h,k| h[k] = []}
+ @delayed_notification_collection = Hash.new {|h,k| h[k] = []}
+ @definitions = Hash.new
+ @loaded_recipes = {}
+ @loaded_attributes = {}
+ @events = events
+
+ @node.run_context = self
+ end
+
+ def load(run_list_expansion)
+ load_libraries
+
+ load_lwrps
+ load_attributes
+ load_resource_definitions
+
+ # Precendence rules state that roles' attributes come after
+ # cookbooks. Now we've loaded attributes from cookbooks with
+ # load_attributes, apply the expansion attributes (loaded from
+ # roles) to the node.
+ @node.apply_expansion_attributes(run_list_expansion)
+
+ @events.recipe_load_start(run_list_expansion.recipes.size)
+ run_list_expansion.recipes.each do |recipe|
+ begin
+ include_recipe(recipe)
+ rescue Chef::Exceptions::RecipeNotFound => e
+ @events.recipe_not_found(e)
+ raise
+ rescue Exception => e
+ path = resolve_recipe(recipe)
+ @events.recipe_file_load_failed(path, e)
+ raise
+ end
+ end
+ @events.recipe_load_complete
+ end
+
+ def resolve_recipe(recipe_name)
+ cookbook_name, recipe_short_name = Chef::Recipe.parse_recipe_name(recipe_name)
+ cookbook = cookbook_collection[cookbook_name]
+ cookbook.recipe_filenames_by_name[recipe_short_name]
+ end
+
+ def resolve_attribute(cookbook_name, attr_file_name)
+ cookbook = cookbook_collection[cookbook_name]
+ raise Chef::Exceptions::CookbookNotFound, "could not find cookbook #{cookbook_name} while loading attribute #{name}" unless cookbook
+
+ attribute_filename = cookbook.attribute_filenames_by_short_filename[attr_file_name]
+ raise Chef::Exceptions::AttributeNotFound, "could not find filename for attribute #{attr_file_name} in cookbook #{cookbook_name}" unless attribute_filename
+
+ attribute_filename
+ end
+
+ def notifies_immediately(notification)
+ nr = notification.notifying_resource
+ if nr.instance_of?(Chef::Resource)
+ @immediate_notification_collection[nr.name] << notification
+ else
+ @immediate_notification_collection[nr.to_s] << notification
+ end
+ end
+
+ def notifies_delayed(notification)
+ nr = notification.notifying_resource
+ if nr.instance_of?(Chef::Resource)
+ @delayed_notification_collection[nr.name] << notification
+ else
+ @delayed_notification_collection[nr.to_s] << notification
+ end
+ end
+
+ def immediate_notifications(resource)
+ if resource.instance_of?(Chef::Resource)
+ return @immediate_notification_collection[resource.name]
+ else
+ return @immediate_notification_collection[resource.to_s]
+ end
+ end
+
+ def delayed_notifications(resource)
+ if resource.instance_of?(Chef::Resource)
+ return @delayed_notification_collection[resource.name]
+ else
+ return @delayed_notification_collection[resource.to_s]
+ end
+ end
+
+ def include_recipe(*recipe_names)
+ result_recipes = Array.new
+ recipe_names.flatten.each do |recipe_name|
+ if result = load_recipe(recipe_name)
+ result_recipes << result
+ end
+ end
+ result_recipes
+ end
+
+ def load_recipe(recipe_name)
+ Chef::Log.debug("Loading Recipe #{recipe_name} via include_recipe")
+
+ cookbook_name, recipe_short_name = Chef::Recipe.parse_recipe_name(recipe_name)
+ if loaded_fully_qualified_recipe?(cookbook_name, recipe_short_name)
+ Chef::Log.debug("I am not loading #{recipe_name}, because I have already seen it.")
+ false
+ else
+ loaded_recipe(cookbook_name, recipe_short_name)
+
+ cookbook = cookbook_collection[cookbook_name]
+ cookbook.load_recipe(recipe_short_name, self)
+ end
+ end
+
+ def loaded_fully_qualified_recipe?(cookbook, recipe)
+ @loaded_recipes.has_key?("#{cookbook}::#{recipe}")
+ end
+
+ def loaded_recipe?(recipe)
+ cookbook, recipe_name = Chef::Recipe.parse_recipe_name(recipe)
+ loaded_fully_qualified_recipe?(cookbook, recipe_name)
+ end
+
+ def loaded_fully_qualified_attribute?(cookbook, attribute_file)
+ @loaded_attributes.has_key?("#{cookbook}::#{attribute_file}")
+ end
+
+ def loaded_attribute(cookbook, attribute_file)
+ @loaded_attributes["#{cookbook}::#{attribute_file}"] = true
+ end
+
+ private
+
+ def loaded_recipe(cookbook, recipe)
+ @loaded_recipes["#{cookbook}::#{recipe}"] = true
+ end
+
+ def load_libraries
+ @events.library_load_start(count_files_by_segment(:libraries))
+
+ foreach_cookbook_load_segment(:libraries) do |cookbook_name, filename|
+ begin
+ Chef::Log.debug("Loading cookbook #{cookbook_name}'s library file: #{filename}")
+ Kernel.load(filename)
+ @events.library_file_loaded(filename)
+ rescue Exception => e
+ # TODO wrap/munge exception to highlight syntax/name/no method errors.
+ @events.library_file_load_failed(filename, e)
+ raise
+ end
+ end
+
+ @events.library_load_complete
+ end
+
+ def load_lwrps
+ lwrp_file_count = count_files_by_segment(:providers) + count_files_by_segment(:resources)
+ @events.lwrp_load_start(lwrp_file_count)
+ load_lwrp_providers
+ load_lwrp_resources
+ @events.lwrp_load_complete
+ end
+
+ def load_lwrp_providers
+ foreach_cookbook_load_segment(:providers) do |cookbook_name, filename|
+ begin
+ Chef::Log.debug("Loading cookbook #{cookbook_name}'s providers from #{filename}")
+ Chef::Provider.build_from_file(cookbook_name, filename, self)
+ @events.lwrp_file_loaded(filename)
+ rescue Exception => e
+ # TODO: wrap exception with helpful info
+ @events.lwrp_file_load_failed(filename, e)
+ raise
+ end
+ end
+ end
+
+ def load_lwrp_resources
+ foreach_cookbook_load_segment(:resources) do |cookbook_name, filename|
+ begin
+ Chef::Log.debug("Loading cookbook #{cookbook_name}'s resources from #{filename}")
+ Chef::Resource.build_from_file(cookbook_name, filename, self)
+ @events.lwrp_file_loaded(filename)
+ rescue Exception => e
+ @events.lwrp_file_load_failed(filename, e)
+ raise
+ end
+ end
+ end
+
+ def load_attributes
+ @events.attribute_load_start(count_files_by_segment(:attributes))
+ foreach_cookbook_load_segment(:attributes) do |cookbook_name, filename|
+ begin
+ Chef::Log.debug("Node #{@node.name} loading cookbook #{cookbook_name}'s attribute file #{filename}")
+ attr_file_basename = ::File.basename(filename, ".rb")
+ @node.include_attribute("#{cookbook_name}::#{attr_file_basename}")
+ rescue Exception => e
+ @events.attribute_file_load_failed(filename, e)
+ raise
+ end
+ end
+ @events.attribute_load_complete
+ end
+
+ def load_resource_definitions
+ @events.definition_load_start(count_files_by_segment(:definitions))
+ foreach_cookbook_load_segment(:definitions) do |cookbook_name, filename|
+ begin
+ Chef::Log.debug("Loading cookbook #{cookbook_name}'s definitions from #{filename}")
+ resourcelist = Chef::ResourceDefinitionList.new
+ resourcelist.from_file(filename)
+ definitions.merge!(resourcelist.defines) do |key, oldval, newval|
+ Chef::Log.info("Overriding duplicate definition #{key}, new definition found in #{filename}")
+ newval
+ end
+ @events.definition_file_loaded(filename)
+ rescue Exception => e
+ @events.definition_file_load_failed(filename, e)
+ end
+ end
+ @events.definition_load_complete
+ end
+
+ def count_files_by_segment(segment)
+ cookbook_collection.inject(0) do |count, ( cookbook_name, cookbook )|
+ count + cookbook.segment_filenames(segment).size
+ end
+ end
+
+ def foreach_cookbook_load_segment(segment, &block)
+ cookbook_collection.each do |cookbook_name, cookbook|
+ segment_filenames = cookbook.segment_filenames(segment)
+ segment_filenames.each do |segment_filename|
+ block.call(cookbook_name, segment_filename)
+ end
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/run_list.rb b/lib/chef/run_list.rb
new file mode 100644
index 0000000000..684c5e19fc
--- /dev/null
+++ b/lib/chef/run_list.rb
@@ -0,0 +1,163 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Nuo Yan (<nuoyan@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright (c) 2008-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/run_list/run_list_item'
+require 'chef/run_list/run_list_expansion'
+require 'chef/run_list/versioned_recipe_list'
+require 'chef/mixin/params_validate'
+
+class Chef
+ class RunList
+ include Enumerable
+ include Chef::Mixin::ParamsValidate
+
+ # @run_list_items is an array of RunListItems that describe the items to
+ # execute in order. RunListItems can load from and convert to the string
+ # forms users set on roles and nodes.
+ # For example:
+ # @run_list_items = ['recipe[foo::bar]', 'role[webserver]']
+ # Thus,
+ # self.role_names would return ['webserver']
+ # self.recipe_names would return ['foo::bar']
+ attr_reader :run_list_items
+
+ # For backwards compat
+ alias :run_list :run_list_items
+
+ def initialize(*run_list_items)
+ @run_list_items = run_list_items.map { |i| coerce_to_run_list_item(i) }
+ end
+
+ def role_names
+ @run_list_items.inject([]){|memo, run_list_item| memo << run_list_item.name if run_list_item.role? ; memo}
+ end
+
+ alias :roles :role_names
+
+ def recipe_names
+ @run_list_items.inject([]){|memo, run_list_item| memo << run_list_item.name if run_list_item.recipe? ; memo}
+ end
+
+ alias :recipes :recipe_names
+
+ # Add an item of the form "recipe[foo::bar]" or "role[webserver]";
+ # takes a String or a RunListItem
+ def <<(run_list_item)
+ run_list_item = coerce_to_run_list_item(run_list_item)
+ @run_list_items << run_list_item unless @run_list_items.include?(run_list_item)
+ self
+ end
+
+ alias :push :<<
+ alias :add :<<
+
+ def ==(other)
+ if other.kind_of?(Chef::RunList)
+ other.run_list_items == @run_list_items
+ else
+ return false unless other.respond_to?(:size) && (other.size == @run_list_items.size)
+ other_run_list_items = other.dup
+
+ other_run_list_items.map! { |item| coerce_to_run_list_item(item) }
+ other_run_list_items == @run_list_items
+ end
+ end
+
+ def to_s
+ @run_list_items.join(", ")
+ end
+
+ def to_json(*args)
+ to_a.map { |item| item.to_s}.to_json(*args)
+ end
+
+ def empty?
+ @run_list_items.length == 0 ? true : false
+ end
+
+ def [](pos)
+ @run_list_items[pos]
+ end
+
+ def []=(pos, item)
+ @run_list_items[pos] = parse_entry(item)
+ end
+
+ def each(&block)
+ @run_list_items.each { |i| block.call(i) }
+ end
+
+ def each_index(&block)
+ @run_list_items.each_index { |i| block.call(i) }
+ end
+
+ def include?(item)
+ @run_list_items.include?(parse_entry(item))
+ end
+
+ def reset!(*args)
+ @run_list_items.clear
+ args.flatten.each do |item|
+ if item.kind_of?(Chef::RunList)
+ item.each { |r| self << r }
+ else
+ self << item
+ end
+ end
+ self
+ end
+
+ def remove(item)
+ @run_list_items.delete_if{|i| i == item}
+ self
+ end
+ alias :delete :remove
+
+ # Expands this run_list: recursively expand roles into their included
+ # recipes.
+ # Returns a RunListExpansion object.
+ def expand(environment, data_source='server', expansion_opts={})
+ expansion = expansion_for_data_source(environment, data_source, expansion_opts)
+ expansion.expand
+ expansion
+ end
+
+ # Converts a string run list entry to a RunListItem object.
+ def parse_entry(entry)
+ RunListItem.new(entry)
+ end
+
+ def coerce_to_run_list_item(item)
+ item.kind_of?(RunListItem) ? item : parse_entry(item)
+ end
+
+ def expansion_for_data_source(environment, data_source, opts={})
+ case data_source.to_s
+ when 'disk'
+ RunListExpansionFromDisk.new(environment, @run_list_items)
+ when 'server'
+ RunListExpansionFromAPI.new(environment, @run_list_items, opts[:rest])
+ end
+ end
+
+
+ end
+end
diff --git a/lib/chef/run_list/run_list_expansion.rb b/lib/chef/run_list/run_list_expansion.rb
new file mode 100644
index 0000000000..7b8108a2d4
--- /dev/null
+++ b/lib/chef/run_list/run_list_expansion.rb
@@ -0,0 +1,191 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2010, 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'chef/mash'
+
+require 'chef/mixin/deep_merge'
+
+require 'chef/role'
+require 'chef/rest'
+
+class Chef
+ class RunList
+ # Abstract Base class for expanding a run list. Subclasses must handle
+ # fetching roles from a data source by defining +fetch_role+
+ class RunListExpansion
+
+ attr_reader :run_list_items
+
+ # A VersionedRecipeList of recipes. Populated only after #expand
+ # is called.
+ attr_reader :recipes
+
+ attr_reader :default_attrs
+
+ attr_reader :override_attrs
+
+ attr_reader :environment
+
+ attr_reader :missing_roles_with_including_role
+
+ # The data source passed to the constructor. Not used in this class.
+ # In subclasses, this is a couchdb or Chef::REST object pre-configured
+ # to fetch roles from their correct location.
+ attr_reader :source
+
+ # Returns a Hash of the form "including_role" => "included_role_or_recipe".
+ # This can be used to show the expanded run list (ordered) graph.
+ # ==== Caveats
+ # * Duplicate roles are not shown.
+ attr_reader :run_list_trace
+
+ def initialize(environment, run_list_items, source=nil)
+ @environment = environment
+ @missing_roles_with_including_role = Array.new
+
+ @run_list_items = run_list_items.dup
+ @source = source
+
+ @default_attrs = Mash.new
+ @override_attrs = Mash.new
+
+ @recipes = Chef::RunList::VersionedRecipeList.new
+
+ @applied_roles = {}
+ @run_list_trace = Hash.new {|h, key| h[key] = [] }
+ end
+
+ # Did we find any errors (expanding roles)?
+ def errors?
+ @missing_roles_with_including_role.length > 0
+ end
+
+ alias :invalid? :errors?
+
+ # Recurses over the run list items, expanding roles. After this,
+ # +recipes+ will contain the fully expanded recipe list
+ def expand
+ # Sure do miss function arity when being recursive
+ expand_run_list_items(@run_list_items)
+ end
+
+ # Fetches and inflates a role
+ # === Returns
+ # Chef::Role in most cases
+ # false if the role has already been applied
+ # nil if the role does not exist
+ def inflate_role(role_name, included_by)
+ return false if applied_role?(role_name) # Prevent infinite loops
+ applied_role(role_name)
+ fetch_role(role_name, included_by)
+ end
+
+ def apply_role_attributes(role)
+ @default_attrs = Chef::Mixin::DeepMerge.role_merge(@default_attrs, role.default_attributes)
+ @override_attrs = Chef::Mixin::DeepMerge.role_merge(@override_attrs, role.override_attributes)
+ end
+
+ def applied_role?(role_name)
+ @applied_roles.has_key?(role_name)
+ end
+
+ # Returns an array of role names that were expanded; this
+ # includes any roles that were in the original, pre-expansion
+ # run_list as well as roles processed during
+ # expansion. Populated only after #expand is called.
+ def roles
+ @applied_roles.keys
+ end
+
+ # In subclasses, this method will fetch the role from the data source.
+ def fetch_role(name, included_by)
+ raise NotImplementedError
+ end
+
+ # When a role is not found, an error message is logged, but no
+ # exception is raised. We do add an entry in the errors collection.
+ # === Returns
+ # nil
+ def role_not_found(name, included_by)
+ Chef::Log.error("Role #{name} (included by '#{included_by}') is in the runlist but does not exist. Skipping expand.")
+ @missing_roles_with_including_role << [name, included_by]
+ nil
+ end
+
+ def errors
+ @missing_roles_with_including_role.map {|item| item.first }
+ end
+
+ private
+
+ # these methods modifies internal state based on arguments, so hide it.
+
+ def applied_role(role_name)
+ @applied_roles[role_name] = true
+ end
+
+ def expand_run_list_items(items, included_by="top level")
+ if entry = items.shift
+ @run_list_trace[included_by.to_s] << entry.to_s
+
+ case entry.type
+ when :recipe
+ recipes.add_recipe(entry.name, entry.version)
+ when :role
+ if role = inflate_role(entry.name, included_by)
+ expand_run_list_items(role.run_list_for(@environment).run_list_items, role)
+ apply_role_attributes(role)
+ end
+ end
+ expand_run_list_items(items, included_by)
+ end
+ end
+
+ end
+
+ # Expand a run list from disk. Suitable for chef-solo
+ class RunListExpansionFromDisk < RunListExpansion
+
+ def fetch_role(name, included_by)
+ Chef::Role.from_disk(name)
+ rescue Chef::Exceptions::RoleNotFound
+ role_not_found(name, included_by)
+ end
+
+ end
+
+ # Expand a run list from the chef-server API.
+ class RunListExpansionFromAPI < RunListExpansion
+
+ def rest
+ @rest ||= (source || Chef::REST.new(Chef::Config[:role_url]))
+ end
+
+ def fetch_role(name, included_by)
+ rest.get_rest("roles/#{name}")
+ rescue Net::HTTPServerException => e
+ if e.message == '404 "Not Found"'
+ role_not_found(name, included_by)
+ else
+ raise
+ end
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/run_list/run_list_item.rb b/lib/chef/run_list/run_list_item.rb
new file mode 100644
index 0000000000..43bb239754
--- /dev/null
+++ b/lib/chef/run_list/run_list_item.rb
@@ -0,0 +1,99 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+ class RunList
+ class RunListItem
+ QUALIFIED_RECIPE = %r{^recipe\[([^\]@]+)(@([0-9]+(\.[0-9]+){1,2}))?\]$}
+ QUALIFIED_ROLE = %r{^role\[([^\]]+)\]$}
+ VERSIONED_UNQUALIFIED_RECIPE = %r{^([^@]+)(@([0-9]+(\.[0-9]+){1,2}))$}
+ FALSE_FRIEND = %r{[\[\]]}
+
+ attr_reader :name, :type, :version
+
+
+ def initialize(item)
+ @version = nil
+ case item
+ when Hash
+ assert_hash_is_valid_run_list_item!(item)
+ @type = (item['type'] || item[:type]).to_sym
+ @name = item['name'] || item[:name]
+ if (item.has_key?('version') || item.has_key?(:version))
+ @version = item['version'] || item[:version]
+ end
+ when String
+ if match = QUALIFIED_RECIPE.match(item)
+ # recipe[recipe_name]
+ # recipe[recipe_name@1.0.0]
+ @type = :recipe
+ @name = match[1]
+ @version = match[3] if match[3]
+ elsif match = QUALIFIED_ROLE.match(item)
+ # role[role_name]
+ @type = :role
+ @name = match[1]
+ elsif match = VERSIONED_UNQUALIFIED_RECIPE.match(item)
+ # recipe_name@1.0.0
+ @type = :recipe
+ @name = match[1]
+ @version = match[3] if match[3]
+ elsif match = FALSE_FRIEND.match(item)
+ # Recipe[recipe_name]
+ # roles[role_name]
+ name = match[1]
+ raise ArgumentError, "Unable to create #{self.class} from #{item.class}:#{item.inspect}: must be recipe[#{name}] or role[#{name}]"
+
+ else
+ # recipe_name
+ @type = :recipe
+ @name = item
+ end
+ else
+ raise ArgumentError, "Unable to create #{self.class} from #{item.class}:#{item.inspect}: must be a Hash or String"
+ end
+ end
+
+ def to_s
+ "#{@type}[#{@name}#{@version ? "@#{@version}" :""}]"
+ end
+
+ def role?
+ @type == :role
+ end
+
+ def recipe?
+ @type == :recipe
+ end
+
+ def ==(other)
+ if other.kind_of?(String)
+ self.to_s == other.to_s
+ else
+ other.respond_to?(:type) && other.respond_to?(:name) && other.respond_to?(:version) && other.type == @type && other.name == @name && other.version == @version
+ end
+ end
+
+ def assert_hash_is_valid_run_list_item!(item)
+ unless (item.key?('type')|| item.key?(:type)) && (item.key?('name') || item.key?(:name))
+ raise ArgumentError, "Initializing a #{self.class} from a hash requires that it have a 'type' and 'name' key"
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/run_list/versioned_recipe_list.rb b/lib/chef/run_list/versioned_recipe_list.rb
new file mode 100644
index 0000000000..0eefded964
--- /dev/null
+++ b/lib/chef/run_list/versioned_recipe_list.rb
@@ -0,0 +1,68 @@
+#
+# Author:: Stephen Delano (<stephen@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+require 'chef/version_class'
+require 'chef/version_constraint'
+
+# Why does this class exist?
+# Why did we not just modify RunList/RunListItem?
+class Chef
+ class RunList
+ class VersionedRecipeList < Array
+
+ def initialize
+ super
+ @versions = Hash.new
+ end
+
+ def add_recipe(name, version=nil)
+ if version && @versions.has_key?(name)
+ unless Chef::Version.new(@versions[name]) == Chef::Version.new(version)
+ raise Chef::Exceptions::CookbookVersionConflict, "Run list requires #{name} at versions #{@versions[name]} and #{version}"
+ end
+ end
+ @versions[name] = version if version
+ self << name unless self.include?(name)
+ end
+
+ def with_versions
+ self.map {|recipe_name| {:name => recipe_name, :version => @versions[recipe_name]}}
+ end
+
+ # Return an Array of Hashes, each of the form:
+ # {:name => RECIPE_NAME, :version_constraint => Chef::VersionConstraint }
+ def with_version_constraints
+ self.map do |recipe_name|
+ constraint = Chef::VersionConstraint.new(@versions[recipe_name])
+ { :name => recipe_name, :version_constraint => constraint }
+ end
+ end
+
+ # Return an Array of Strings, each of the form:
+ # "NAME@VERSION"
+ def with_version_constraints_strings
+ self.map do |recipe_name|
+ if @versions[recipe_name]
+ "#{recipe_name}@#{@versions[recipe_name]}"
+ else
+ recipe_name
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/run_lock.rb b/lib/chef/run_lock.rb
new file mode 100644
index 0000000000..ffe4d66045
--- /dev/null
+++ b/lib/chef/run_lock.rb
@@ -0,0 +1,84 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+
+ # == Chef::RunLock
+ # Provides an interface for acquiring and releasing a system-wide exclusive
+ # lock.
+ #
+ # Used by Chef::Client to ensure only one instance of chef-client (or solo)
+ # is modifying the system at a time.
+ class RunLock
+ attr_reader :runlock
+ attr_reader :runlock_file
+
+ # Create a new instance of RunLock
+ # === Arguments
+ # * config::: This will generally be the `Chef::Config`, but any Hash-like
+ # object with Symbol keys will work. See 'Parameters' section.
+ # === Parameters/Config
+ # * :lockfile::: if set, this will be used as the full path to the lockfile.
+ # * :file_cache_path::: if `:lockfile` is not set, the lock file will be
+ # named "chef-client-running.pid" and be placed in the directory given by
+ # `:file_cache_path`
+ def initialize(config)
+ @runlock_file = config[:lockfile] || "#{config[:file_cache_path]}/chef-client-running.pid"
+ @runlock = nil
+ end
+
+ # Acquire the system-wide lock. Will block indefinitely if another process
+ # already has the lock.
+ #
+ # Each call to acquire should have a corresponding call to #release.
+ #
+ # The implementation is based on File#flock (see also: flock(2)).
+ def acquire
+ @runlock = File.open(runlock_file,'w+')
+ unless runlock.flock(File::LOCK_EX|File::LOCK_NB)
+ # Another chef client running...
+ runpid = runlock.read.strip.chomp
+ Chef::Log.info("Chef client #{runpid} is running, will wait for it to finish and then run.")
+ runlock.flock(File::LOCK_EX)
+ end
+ # We grabbed the run lock. Save the pid.
+ runlock.truncate(0)
+ runlock.rewind # truncate doesn't reset position to 0.
+ runlock.write(Process.pid.to_s)
+ end
+
+ # Release the system-wide lock.
+ def release
+ if runlock
+ runlock.flock(File::LOCK_UN)
+ runlock.close
+ # Don't unlink the pid file, if another chef-client was waiting, it
+ # won't be recreated. Better to leave a "dead" pid file than not have
+ # it available if you need to break the lock.
+ reset
+ end
+ end
+
+ private
+
+ def reset
+ @runlock = nil
+ end
+
+ end
+end
+
diff --git a/lib/chef/run_status.rb b/lib/chef/run_status.rb
new file mode 100644
index 0000000000..9354f7872a
--- /dev/null
+++ b/lib/chef/run_status.rb
@@ -0,0 +1,124 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# == Chef::RunStatus
+# Tracks various aspects of a Chef run, including the Node and RunContext,
+# start and end time, and any Exception that stops the run. RunStatus objects
+# are passed to any notification or exception handlers at the completion of a
+# Chef run.
+class Chef::RunStatus
+
+ attr_reader :events
+
+ attr_reader :run_context
+
+ attr_writer :run_context
+
+ attr_reader :start_time
+
+ attr_reader :end_time
+
+ attr_reader :exception
+
+ attr_writer :exception
+
+ def initialize(node, events)
+ @node = node
+ @events = events
+ end
+
+ def node
+ @node
+ end
+
+ # sets +start_time+ to the current time.
+ def start_clock
+ @start_time = Time.now
+ end
+
+ # sets +end_time+ to the current time
+ def stop_clock
+ @end_time = Time.now
+ end
+
+ # The elapsed time between +start_time+ and +end_time+. Returns +nil+ if
+ # either value is not set.
+ def elapsed_time
+ if @start_time && @end_time
+ @end_time - @start_time
+ else
+ nil
+ end
+ end
+
+ # The list of all resources in the current run context's +resource_collection+
+ def all_resources
+ @run_context && @run_context.resource_collection.all_resources
+ end
+
+ # The list of all resources in the current run context's +resource_collection+
+ # that are marked as updated
+ def updated_resources
+ @run_context && @run_context.resource_collection.select { |r| r.updated }
+ end
+
+ # The backtrace from +exception+, if any
+ def backtrace
+ @exception && @exception.backtrace
+ end
+
+ # Did the Chef run fail?
+ def failed?
+ !success?
+ end
+
+ # Did the chef run succeed? returns +true+ if no exception has been set.
+ def success?
+ @exception.nil?
+ end
+
+ # A Hash representation of the RunStatus, with the following (Symbol) keys:
+ # * :node
+ # * :success
+ # * :start_time
+ # * :end_time
+ # * :elapsed_time
+ # * :all_resources
+ # * :updated_resources
+ # * :exception
+ # * :backtrace
+ def to_hash
+ # use a flat hash here so we can't errors from intermediate values being nil
+ { :node => node,
+ :success => success?,
+ :start_time => start_time,
+ :end_time => end_time,
+ :elapsed_time => elapsed_time,
+ :all_resources => all_resources,
+ :updated_resources => updated_resources,
+ :exception => formatted_exception,
+ :backtrace => backtrace}
+ end
+
+ # Returns a string of the format "ExceptionClass: message" or +nil+ if no
+ # +exception+ is set.
+ def formatted_exception
+ @exception && "#{@exception.class.name}: #{@exception.message}"
+ end
+
+end
diff --git a/lib/chef/runner.rb b/lib/chef/runner.rb
new file mode 100644
index 0000000000..daa928e7a3
--- /dev/null
+++ b/lib/chef/runner.rb
@@ -0,0 +1,118 @@
+#--
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/exceptions'
+require 'chef/mixin/params_validate'
+require 'chef/node'
+require 'chef/resource_collection'
+require 'chef/platform'
+
+class Chef
+ # == Chef::Runner
+ # This class is responsible for executing the steps in a Chef run.
+ class Runner
+
+ attr_reader :run_context
+
+ attr_reader :delayed_actions
+
+ include Chef::Mixin::ParamsValidate
+
+ def initialize(run_context)
+ @run_context = run_context
+ @delayed_actions = []
+ end
+
+ def events
+ @run_context.events
+ end
+
+ # Determine the appropriate provider for the given resource, then
+ # execute it.
+ def run_action(resource, action, notification_type=nil, notifying_resource=nil)
+ resource.run_action(action, notification_type, notifying_resource)
+
+ # Execute any immediate and queue up any delayed notifications
+ # associated with the resource, but only if it was updated *this time*
+ # we ran an action on it.
+ if resource.updated_by_last_action?
+ run_context.immediate_notifications(resource).each do |notification|
+ Chef::Log.info("#{resource} sending #{notification.action} action to #{notification.resource} (immediate)")
+ run_action(notification.resource, notification.action, :immediate, resource)
+ end
+
+ run_context.delayed_notifications(resource).each do |notification|
+ if delayed_actions.any? { |existing_notification| existing_notification.duplicates?(notification) }
+ Chef::Log.info( "#{resource} not queuing delayed action #{notification.action} on #{notification.resource}"\
+ " (delayed), as it's already been queued")
+ else
+ delayed_actions << notification
+ end
+ end
+ end
+ end
+
+ # Iterates over the +resource_collection+ in the +run_context+ calling
+ # +run_action+ for each resource in turn.
+ def converge
+ # Resolve all lazy/forward references in notifications
+ run_context.resource_collection.each do |resource|
+ resource.resolve_notification_references
+ end
+
+ # Execute each resource.
+ run_context.resource_collection.execute_each_resource do |resource|
+ Array(resource.action).each {|action| run_action(resource, action)}
+ end
+
+ rescue Exception => e
+ Chef::Log.info "Running queued delayed notifications before re-raising exception"
+ run_delayed_notifications(e)
+ else
+ run_delayed_notifications(nil)
+ true
+ end
+
+ private
+
+ # Run all our :delayed actions
+ def run_delayed_notifications(error=nil)
+ collected_failures = Exceptions::MultipleFailures.new
+ collected_failures.client_run_failure(error) unless error.nil?
+ delayed_actions.each do |notification|
+ result = run_delayed_notification(notification)
+ if result.kind_of?(Exception)
+ collected_failures.notification_failure(result)
+ end
+ end
+ collected_failures.raise!
+ end
+
+ def run_delayed_notification(notification)
+ Chef::Log.info( "#{notification.notifying_resource} sending #{notification.action}"\
+ " action to #{notification.resource} (delayed)")
+ # Struct of resource/action to call
+ run_action(notification.resource, notification.action, :delayed)
+ true
+ rescue Exception => e
+ e
+ end
+ end
+end
diff --git a/lib/chef/scan_access_control.rb b/lib/chef/scan_access_control.rb
new file mode 100644
index 0000000000..5863a8c7c4
--- /dev/null
+++ b/lib/chef/scan_access_control.rb
@@ -0,0 +1,135 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ # == ScanAccessControl
+ # Reads Access Control Settings on a file and writes them out to a resource
+ # (should be the current_resource), attempting to match the style used by the
+ # new resource, that is, if users are specified with usernames in
+ # new_resource, then the uids from stat will be looked up and usernames will
+ # be added to current_resource.
+ #
+ # === Why?
+ # FileAccessControl objects may operate on a temporary file, in which case we
+ # won't know if the access control settings changed (ex: rendering a template
+ # with both a change in content and ownership). For auditing purposes, we
+ # need to record the current state of a file system entity.
+ #--
+ # Not yet sure if this is the optimal way to solve the problem. But it's
+ # progress towards the end goal.
+ #
+ # TODO: figure out if all this works with OS X's negative uids
+ # TODO: windows
+ class ScanAccessControl
+
+ attr_reader :new_resource
+ attr_reader :current_resource
+
+ def initialize(new_resource, current_resource)
+ @new_resource, @current_resource = new_resource, current_resource
+ end
+
+ # Modifies @current_resource, setting the current access control state.
+ def set_all!
+ if ::File.exist?(new_resource.path)
+ set_owner
+ set_group
+ set_mode
+ else
+ # leave the values as nil.
+ end
+ end
+
+ # Set the owner attribute of +current_resource+ to whatever the current
+ # state is. Attempts to match the format given in new_resource: if the
+ # new_resource specifies the owner as a string, the username for the uid
+ # will be looked up and owner will be set to the username, and vice versa.
+ def set_owner
+ @current_resource.owner(current_owner)
+ end
+
+ def current_owner
+ case new_resource.owner
+ when String, nil
+ lookup_uid
+ when Integer
+ stat.uid
+ else
+ Chef::Log.error("The `owner` parameter of the #@new_resource resource is set to an invalid value (#{new_resource.owner.inspect})")
+ raise ArgumentError, "cannot resolve #{new_resource.owner.inspect} to uid, owner must be a string or integer"
+ end
+ end
+
+ def lookup_uid
+ unless (pwent = Etc.getpwuid(stat.uid)).nil?
+ pwent.name
+ else
+ stat.uid
+ end
+ rescue ArgumentError
+ stat.uid
+ end
+
+ # Set the group attribute of +current_resource+ to whatever the current state is.
+ def set_group
+ @current_resource.group(current_group)
+ end
+
+ def current_group
+ case new_resource.group
+ when String, nil
+ lookup_gid
+ when Integer
+ stat.gid
+ else
+ Chef::Log.error("The `group` parameter of the #@new_resource resource is set to an invalid value (#{new_resource.owner.inspect})")
+ raise ArgumentError, "cannot resolve #{new_resource.group.inspect} to gid, group must be a string or integer"
+ end
+ end
+
+ def lookup_gid
+ unless (pwent = Etc.getgrgid(stat.gid)).nil?
+ pwent.name
+ else
+ stat.gid
+ end
+ rescue ArgumentError
+ stat.gid
+ end
+
+ def set_mode
+ @current_resource.mode(current_mode)
+ end
+
+ def current_mode
+ case new_resource.mode
+ when String, nil
+ (stat.mode & 007777).to_s(8)
+ when Integer
+ stat.mode & 007777
+ else
+ Chef::Log.error("The `mode` parameter of the #@new_resource resource is set to an invalid value (#{new_resource.mode.inspect})")
+ raise ArgumentError, "Invalid value #{new_resource.mode.inspect} for `mode` on resource #@new_resource"
+ end
+ end
+
+ def stat
+ @stat ||= @new_resource.instance_of?(Chef::Resource::Link) ? ::File.lstat(@new_resource.path) : ::File.stat(@new_resource.path)
+ end
+ end
+end
diff --git a/lib/chef/search/query.rb b/lib/chef/search/query.rb
new file mode 100644
index 0000000000..d147bc0005
--- /dev/null
+++ b/lib/chef/search/query.rb
@@ -0,0 +1,65 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'uri'
+require 'chef/rest'
+require 'chef/node'
+require 'chef/role'
+require 'chef/data_bag'
+require 'chef/data_bag_item'
+
+class Chef
+ class Search
+ class Query
+
+ attr_accessor :rest
+
+ def initialize(url=nil)
+ @rest = Chef::REST.new(url ||Chef::Config[:search_url])
+ end
+
+ # Search Solr for objects of a given type, for a given query. If you give
+ # it a block, it will handle the paging for you dynamically.
+ def search(type, query="*:*", sort='X_CHEF_id_CHEF_X asc', start=0, rows=1000, &block)
+ raise ArgumentError, "Type must be a string or a symbol!" unless (type.kind_of?(String) || type.kind_of?(Symbol))
+
+ response = @rest.get_rest("search/#{type}?q=#{escape(query)}&sort=#{escape(sort)}&start=#{escape(start)}&rows=#{escape(rows)}")
+ if block
+ response["rows"].each { |o| block.call(o) unless o.nil?}
+ unless (response["start"] + response["rows"].length) >= response["total"]
+ nstart = response["start"] + rows
+ search(type, query, sort, nstart, rows, &block)
+ end
+ true
+ else
+ [ response["rows"], response["start"], response["total"] ]
+ end
+ end
+
+ def list_indexes
+ response = @rest.get_rest("search")
+ end
+
+ private
+ def escape(s)
+ s && URI.escape(s.to_s)
+ end
+ end
+ end
+end
diff --git a/lib/chef/shef/ext.rb b/lib/chef/shef/ext.rb
new file mode 100644
index 0000000000..8f03de2d04
--- /dev/null
+++ b/lib/chef/shef/ext.rb
@@ -0,0 +1,19 @@
+#--
+# Author:: Joshua Timberman (<joshua@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/shell/ext'
diff --git a/lib/chef/shell.rb b/lib/chef/shell.rb
new file mode 100644
index 0000000000..0390cfaac5
--- /dev/null
+++ b/lib/chef/shell.rb
@@ -0,0 +1,327 @@
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'singleton'
+require 'pp'
+require 'etc'
+require 'mixlib/cli'
+
+require 'chef'
+require 'chef/version'
+require 'chef/client'
+require 'chef/config'
+
+require 'chef/shell/shell_session'
+require 'chef/shell/ext'
+require 'chef/json_compat'
+
+# = Shell
+# Shell is Chef in an IRB session. Shell can interact with a Chef server via the
+# REST API, and run and debug recipes interactively.
+module Shell
+ LEADERS = Hash.new("")
+ LEADERS[Chef::Recipe] = ":recipe"
+ LEADERS[Chef::Node] = ":attributes"
+
+ class << self
+ attr_accessor :client_type
+ attr_accessor :options
+ attr_accessor :env
+ attr_writer :editor
+ end
+
+ # Start the irb REPL with chef-shell's customizations
+ def self.start
+ setup_logger
+ # FUGLY HACK: irb gives us no other choice.
+ irb_help = [:help, :irb_help, IRB::ExtendCommandBundle::NO_OVERRIDE]
+ IRB::ExtendCommandBundle.instance_variable_get(:@ALIASES).delete(irb_help)
+
+ parse_opts
+
+ # HACK: this duplicates the functions of IRB.start, but we have to do it
+ # to get access to the main object before irb starts.
+ ::IRB.setup(nil)
+
+ irb = IRB::Irb.new
+
+ init(irb.context.main)
+
+
+ irb_conf[:IRB_RC].call(irb.context) if irb_conf[:IRB_RC]
+ irb_conf[:MAIN_CONTEXT] = irb.context
+
+ trap("SIGINT") do
+ irb.signal_handle
+ end
+
+ catch(:IRB_EXIT) do
+ irb.eval_input
+ end
+ end
+
+ def self.setup_logger
+ Chef::Config[:log_level] ||= :warn
+ Chef::Log.init(STDERR)
+ Mixlib::Authentication::Log.logger = Ohai::Log.logger = Chef::Log.logger
+ Chef::Log.level = Chef::Config[:log_level] || :warn
+ end
+
+ # Shell assumes it's running whenever it is defined
+ def self.running?
+ true
+ end
+
+ # Set the irb_conf object to something other than IRB.conf
+ # usful for testing.
+ def self.irb_conf=(conf_hash)
+ @irb_conf = conf_hash
+ end
+
+ def self.irb_conf
+ @irb_conf || IRB.conf
+ end
+
+ def self.configure_irb
+ irb_conf[:HISTORY_FILE] = "~/.chef/chef_shell_history"
+ irb_conf[:SAVE_HISTORY] = 1000
+
+ irb_conf[:IRB_RC] = lambda do |conf|
+ m = conf.main
+
+ conf.prompt_c = "chef#{leader(m)} > "
+ conf.return_format = " => %s \n"
+ conf.prompt_i = "chef#{leader(m)} > "
+ conf.prompt_n = "chef#{leader(m)} ?> "
+ conf.prompt_s = "chef#{leader(m)}%l> "
+ end
+ end
+
+ def self.leader(main_object)
+ env_string = Shell.env ? " (#{Shell.env})" : ""
+ LEADERS[main_object.class] + env_string
+ end
+
+ def self.session
+ unless client_type.instance.node_built?
+ puts "Session type: #{client_type.session_type}"
+ client_type.instance.reset!
+ end
+ client_type.instance
+ end
+
+ def self.init(main)
+ parse_json
+ configure_irb
+
+ session # trigger ohai run + session load
+
+ session.node.consume_attributes(@json_attribs)
+
+ Extensions.extend_context_object(main)
+
+ main.version
+ puts
+
+ puts "run `help' for help, `exit' or ^D to quit."
+ puts
+ puts "Ohai2u#{greeting}!"
+ end
+
+ def self.greeting
+ " #{Etc.getlogin}@#{Shell.session.node.fqdn}"
+ rescue NameError, ArgumentError
+ ""
+ end
+
+ def self.parse_json
+ # HACK: copied verbatim from chef/application/client, because it's not
+ # reusable as written there :(
+ if Chef::Config[:json_attribs]
+ begin
+ json_io = open(Chef::Config[:json_attribs])
+ rescue SocketError => error
+ fatal!("I cannot connect to #{Chef::Config[:json_attribs]}", 2)
+ rescue Errno::ENOENT => error
+ fatal!("I cannot find #{Chef::Config[:json_attribs]}", 2)
+ rescue Errno::EACCES => error
+ fatal!("Permissions are incorrect on #{Chef::Config[:json_attribs]}. Please chmod a+r #{Chef::Config[:json_attribs]}", 2)
+ rescue Exception => error
+ fatal!("Got an unexpected error reading #{Chef::Config[:json_attribs]}: #{error.message}", 2)
+ end
+
+ begin
+ @json_attribs = Chef::JSONCompat.from_json(json_io.read)
+ rescue JSON::ParserError => error
+ fatal!("Could not parse the provided JSON file (#{Chef::Config[:json_attribs]})!: " + error.message, 2)
+ end
+ end
+ end
+
+ def self.fatal!(message, exit_status)
+ Chef::Log.fatal(message)
+ exit exit_status
+ end
+
+ def self.client_type
+ type = Shell::StandAloneSession
+ type = Shell::SoloSession if Chef::Config[:shell_solo]
+ type = Shell::ClientSession if Chef::Config[:client]
+ type = Shell::DoppelGangerSession if Chef::Config[:doppelganger]
+ type
+ end
+
+ def self.parse_opts
+ @options = Options.new
+ @options.parse_opts
+ end
+
+ def self.editor
+ @editor || Chef::Config[:editor] || ENV['EDITOR']
+ end
+
+ class Options
+ include Mixlib::CLI
+
+ def self.footer(text=nil)
+ @footer = text if text
+ @footer
+ end
+
+ banner("chef-shell #{Chef::VERSION}\n\nUsage: chef-shell [NAMED_CONF] (OPTIONS)")
+
+ footer(<<-FOOTER)
+When no CONFIG is specified, chef-shell attempts to load a default configuration file:
+* If a NAMED_CONF is given, chef-shell will load ~/.chef/NAMED_CONF/chef_shell.rb
+* If no NAMED_CONF is given chef-shell will load ~/.chef/chef_shell.rb if it exists
+* chef-shell falls back to loading /etc/chef/client.rb or /etc/chef/solo.rb if -z or
+ -s options are given and no chef_shell.rb can be found.
+FOOTER
+
+ option :config_file,
+ :short => "-c CONFIG",
+ :long => "--config CONFIG",
+ :description => "The configuration file to use"
+
+ option :help,
+ :short => "-h",
+ :long => "--help",
+ :description => "Show this message",
+ :on => :tail,
+ :boolean => true,
+ :proc => proc { print_help }
+
+ option :log_level,
+ :short => "-l LOG_LEVEL",
+ :long => '--log-level LOG_LEVEL',
+ :description => "Set the logging level",
+ :proc => proc { |level| Chef::Log.level = level.to_sym }
+
+ option :standalone,
+ :short => "-a",
+ :long => "--standalone",
+ :description => "standalone session",
+ :default => true,
+ :boolean => true
+
+ option :shell_solo,
+ :short => "-s",
+ :long => "--solo",
+ :description => "chef-solo session",
+ :boolean => true,
+ :proc => proc {Chef::Config[:solo] = true}
+
+ option :client,
+ :short => "-z",
+ :long => "--client",
+ :description => "chef-client session",
+ :boolean => true
+
+ option :json_attribs,
+ :short => "-j JSON_ATTRIBS",
+ :long => "--json-attributes JSON_ATTRIBS",
+ :description => "Load attributes from a JSON file or URL",
+ :proc => nil
+
+ option :chef_server_url,
+ :short => "-S CHEFSERVERURL",
+ :long => "--server CHEFSERVERURL",
+ :description => "The chef server URL",
+ :proc => nil
+
+ option :version,
+ :short => "-v",
+ :long => "--version",
+ :description => "Show chef version",
+ :boolean => true,
+ :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"},
+ :exit => 0
+
+ def self.print_help
+ instance = new
+ instance.parse_options([])
+ puts instance.opt_parser
+ puts
+ puts footer
+ puts
+ exit 1
+ end
+
+ def self.setup!
+ self.new.parse_opts
+ end
+
+ def parse_opts
+ remainder = parse_options
+ environment = remainder.first
+ # We have to nuke ARGV to make sure irb's option parser never sees it.
+ # otherwise, IRB complains about command line switches it doesn't recognize.
+ ARGV.clear
+ config[:config_file] = config_file_for_shell_mode(environment)
+ config_msg = config[:config_file] || "none (standalone session)"
+ puts "loading configuration: #{config_msg}"
+ Chef::Config.from_file(config[:config_file]) if !config[:config_file].nil? && File.exists?(config[:config_file]) && File.readable?(config[:config_file])
+ Chef::Config.merge!(config)
+ end
+
+ private
+
+ def config_file_for_shell_mode(environment)
+ if config[:config_file]
+ config[:config_file]
+ elsif environment && ENV['HOME']
+ Shell.env = environment
+ config_file_to_try = ::File.join(ENV['HOME'], '.chef', environment, 'chef_shell.rb')
+ unless ::File.exist?(config_file_to_try)
+ puts "could not find chef-shell config for environment #{environment} at #{config_file_to_try}"
+ exit 1
+ end
+ config_file_to_try
+ elsif ENV['HOME'] && ::File.exist?(File.join(ENV['HOME'], '.chef', 'chef_shell.rb'))
+ File.join(ENV['HOME'], '.chef', 'chef_shell.rb')
+ elsif config[:solo]
+ "/etc/chef/solo.rb"
+ elsif config[:client]
+ "/etc/chef/client.rb"
+ else
+ nil
+ end
+ end
+
+ end
+
+end
diff --git a/lib/chef/shell/ext.rb b/lib/chef/shell/ext.rb
new file mode 100644
index 0000000000..30d841e4cd
--- /dev/null
+++ b/lib/chef/shell/ext.rb
@@ -0,0 +1,593 @@
+#--
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'tempfile'
+require 'chef/recipe'
+require 'fileutils'
+require 'chef/dsl/platform_introspection'
+require 'chef/version'
+require 'chef/shell/shell_session'
+require 'chef/shell/model_wrapper'
+require 'chef/shell/shell_rest'
+require 'chef/json_compat'
+
+module Shell
+ module Extensions
+
+ Help = Struct.new(:cmd, :desc, :explanation)
+
+ # Extensions to be included in every 'main' object in chef-shell.
+ # These objects are extended with this module.
+ module ObjectCoreExtensions
+
+ def ensure_session_select_defined
+ # irb breaks if you prematurely define IRB::JobMangager
+ # so these methods need to be defined at the latest possible time.
+ unless jobs.respond_to?(:select_session_by_context)
+ def jobs.select_session_by_context(&block)
+ @jobs.select { |job| block.call(job[1].context.main)}
+ end
+ end
+
+ unless jobs.respond_to?(:session_select)
+ def jobs.select_shell_session(target_context)
+ session = if target_context.kind_of?(Class)
+ select_session_by_context { |main| main.kind_of?(target_context) }
+ else
+ select_session_by_context { |main| main.equal?(target_context) }
+ end
+ Array(session.first)[1]
+ end
+ end
+ end
+
+ def find_or_create_session_for(context_obj)
+ ensure_session_select_defined
+ if subsession = jobs.select_shell_session(context_obj)
+ jobs.switch(subsession)
+ else
+ irb(context_obj)
+ end
+ end
+
+ def help_banner
+ banner = []
+ banner << ""
+ banner << "chef-shell Help"
+ banner << "".ljust(80, "=")
+ banner << "| " + "Command".ljust(25) + "| " + "Description"
+ banner << "".ljust(80, "=")
+
+ self.all_help_descriptions.each do |help_text|
+ banner << "| " + help_text.cmd.ljust(25) + "| " + help_text.desc
+ end
+ banner << "".ljust(80, "=")
+ banner << "\n"
+ banner << "Use help(:command) to get detailed help with individual commands"
+ banner << "\n"
+ banner.join("\n")
+ end
+
+ def explain_command(method_name)
+ help = self.all_help_descriptions.find { |h| h.cmd.to_s == method_name.to_s }
+ if help
+ puts ""
+ puts "Command: #{method_name}"
+ puts "".ljust(80, "=")
+ puts help.explanation || help.desc
+ puts "".ljust(80, "=")
+ puts ""
+ else
+ puts ""
+ puts "command #{method_name} not found or no help available"
+ puts ""
+ end
+ end
+
+ # helpfully returns +:on+ so we can have sugary syntax like `tracing on'
+ def on
+ :on
+ end
+
+ # returns +:off+ so you can just do `tracing off'
+ def off
+ :off
+ end
+
+ def help_descriptions
+ @help_descriptions ||= []
+ end
+
+ def all_help_descriptions
+ help_descriptions
+ end
+
+ def desc(help_text)
+ @desc = help_text
+ end
+
+ def explain(explain_text)
+ @explain = explain_text
+ end
+
+ def subcommands(subcommand_help={})
+ @subcommand_help = subcommand_help
+ end
+
+ def singleton_method_added(mname)
+ if @desc
+ help_descriptions << Help.new(mname.to_s, @desc.to_s, @explain)
+ @desc, @explain = nil, nil
+ end
+ if @subcommand_help
+ @subcommand_help.each do |subcommand, text|
+ help_descriptions << Help.new("#{mname}.#{subcommand}", text.to_s, nil)
+ end
+ end
+ @subcommand_help = {}
+ end
+
+ end
+
+ module String
+ def on_off_to_bool
+ case self
+ when "on"
+ true
+ when "off"
+ false
+ else
+ self
+ end
+ end
+ end
+
+ module Symbol
+ def on_off_to_bool
+ self.to_s.on_off_to_bool
+ end
+ end
+
+ module TrueClass
+ def to_on_off_str
+ "on"
+ end
+
+ def on_off_to_bool
+ self
+ end
+ end
+
+ module FalseClass
+ def to_on_off_str
+ "off"
+ end
+
+ def on_off_to_bool
+ self
+ end
+ end
+
+ # Methods that have associated help text need to be dynamically added
+ # to the main irb objects, so we define them in a proc and later
+ # instance_eval the proc in the object.
+ ObjectUIExtensions = Proc.new do
+ extend Shell::Extensions::ObjectCoreExtensions
+
+ desc "prints this help message"
+ explain(<<-E)
+## SUMMARY ##
+ When called with no argument, +help+ prints a table of all
+ chef-shell commands. When called with an argument COMMAND, +help+
+ prints a detailed explanation of the command if available, or the
+ description if no explanation is available.
+E
+ def help(commmand=nil)
+ if commmand
+ explain_command(commmand)
+ else
+ puts help_banner
+ end
+ :ucanhaz_halp
+ end
+ alias :halp :help
+
+ desc "prints information about chef"
+ def version
+ puts "This is the chef-shell.\n" +
+ " Chef Version: #{::Chef::VERSION}\n" +
+ " http://www.opscode.com/chef\n" +
+ " http://wiki.opscode.com/display/chef/Home"
+ :ucanhaz_automation
+ end
+ alias :shell :version
+
+ desc "switch to recipe mode"
+ def recipe_mode
+ find_or_create_session_for Shell.session.recipe
+ :recipe
+ end
+
+ desc "switch to attributes mode"
+ def attributes_mode
+ find_or_create_session_for Shell.session.node
+ :attributes
+ end
+
+ desc "run chef using the current recipe"
+ def run_chef
+ Chef::Log.level = :debug
+ session = Shell.session
+ runrun = Chef::Runner.new(session.run_context).converge
+ Chef::Log.level = :info
+ runrun
+ end
+
+ desc "returns an object to control a paused chef run"
+ subcommands :resume => "resume the chef run",
+ :step => "run only the next resource",
+ :skip_back => "move back in the run list",
+ :skip_forward => "move forward in the run list"
+ def chef_run
+ Shell.session.resource_collection.iterator
+ end
+
+ desc "resets the current recipe"
+ def reset
+ Shell.session.reset!
+ end
+
+ desc "assume the identity of another node."
+ def become_node(node_name)
+ Shell::DoppelGangerSession.instance.assume_identity(node_name)
+ :doppelganger
+ end
+ alias :doppelganger :become_node
+
+ desc "turns printout of return values on or off"
+ def echo(on_or_off)
+ conf.echo = on_or_off.on_off_to_bool
+ end
+
+ desc "says if echo is on or off"
+ def echo?
+ puts "echo is #{conf.echo.to_on_off_str}"
+ end
+
+ desc "turns on or off tracing of execution. *verbose*"
+ def tracing(on_or_off)
+ conf.use_tracer = on_or_off.on_off_to_bool
+ tracing?
+ end
+ alias :trace :tracing
+
+ desc "says if tracing is on or off"
+ def tracing?
+ puts "tracing is #{conf.use_tracer.to_on_off_str}"
+ end
+ alias :trace? :tracing?
+
+ desc "simple ls style command"
+ def ls(directory)
+ Dir.entries(directory)
+ end
+ end
+
+ MainContextExtensions = Proc.new do
+ desc "returns the current node (i.e., this host)"
+ def node
+ Shell.session.node
+ end
+
+ desc "pretty print the node's attributes"
+ def ohai(key=nil)
+ pp(key ? node.attribute[key] : node.attribute)
+ end
+ end
+
+ RESTApiExtensions = Proc.new do
+ desc "edit an object in your EDITOR"
+ explain(<<-E)
+## SUMMARY ##
+ +edit(object)+ allows you to edit any object that can be converted to JSON.
+ When finished editing, this method will return the edited object:
+
+ new_node = edit(existing_node)
+
+## EDITOR SELECTION ##
+ chef-shell looks for an editor using the following logic
+ 1. Looks for an EDITOR set by Shell.editor = "EDITOR"
+ 2. Looks for an EDITOR configured in your chef-shell config file
+ 3. Uses the value of the EDITOR environment variable
+E
+ def edit(object)
+ unless Shell.editor
+ puts "Please set your editor with Shell.editor = \"vim|emacs|mate|ed\""
+ return :failburger
+ end
+
+ filename = "chef-shell-edit-#{object.class.name}-"
+ if object.respond_to?(:name)
+ filename += object.name
+ elsif object.respond_to?(:id)
+ filename += object.id
+ end
+
+ edited_data = Tempfile.open([filename, ".js"]) do |tempfile|
+ tempfile.sync = true
+ tempfile.puts Chef::JSONCompat.to_json(object)
+ system("#{Shell.editor.to_s} #{tempfile.path}")
+ tempfile.rewind
+ tempfile.read
+ end
+
+ Chef::JSONCompat.from_json(edited_data)
+ end
+
+ desc "Find and edit API clients"
+ explain(<<-E)
+## SUMMARY ##
+ +clients+ allows you to query you chef server for information about your api
+ clients.
+
+## LIST ALL CLIENTS ##
+ To see all clients on the system, use
+
+ clients.all #=> [<Chef::ApiClient...>, ...]
+
+ If the output from all is too verbose, or you're only interested in a specific
+ value from each of the objects, you can give a code block to +all+:
+
+ clients.all { |client| client.name } #=> [CLIENT1_NAME, CLIENT2_NAME, ...]
+
+## SHOW ONE CLIENT ##
+ To see a specific client, use
+
+ clients.show(CLIENT_NAME)
+
+## SEARCH FOR CLIENTS ##
+ You can also search for clients using +find+ or +search+. You can use the
+ familiar string search syntax:
+
+ clients.search("KEY:VALUE")
+
+ Just as the +all+ subcommand, the +search+ subcommand can use a code block to
+ filter or transform the information returned from the search:
+
+ clients.search("KEY:VALUE") { |c| c.name }
+
+ You can also use a Hash based syntax, multiple search conditions will be
+ joined with AND.
+
+ clients.find :KEY => :VALUE, :KEY2 => :VALUE2, ...
+
+## BULK-EDIT CLIENTS ##
+ **BE CAREFUL, THIS IS DESTRUCTIVE**
+ You can bulk edit API Clients using the +transform+ subcommand, which requires
+ a code block. Each client will be saved after the code block is run. If the
+ code block returns +nil+ or +false+, that client will be skipped:
+
+ clients.transform("*:*") do |client|
+ if client.name =~ /borat/i
+ client.admin(false)
+ true
+ else
+ nil
+ end
+ end
+
+ This will strip the admin privileges from any client named after borat.
+E
+ subcommands :all => "list all api clients",
+ :show => "load an api client by name",
+ :search => "search for API clients",
+ :transform => "edit all api clients via a code block and save them"
+ def clients
+ @clients ||= Shell::ModelWrapper.new(Chef::ApiClient, :client)
+ end
+
+ desc "Find and edit cookbooks"
+ subcommands :all => "list all cookbooks",
+ :show => "load a cookbook by name",
+ :transform => "edit all cookbooks via a code block and save them"
+ def cookbooks
+ @cookbooks ||= Shell::ModelWrapper.new(Chef::CookbookVersion)
+ end
+
+ desc "Find and edit nodes via the API"
+ explain(<<-E)
+## SUMMARY ##
+ +nodes+ Allows you to query your chef server for information about your nodes.
+
+## LIST ALL NODES ##
+ You can list all nodes using +all+ or +list+
+
+ nodes.all #=> [<Chef::Node...>, <Chef::Node...>, ...]
+
+ To limit the information returned for each node, pass a code block to the +all+
+ subcommand:
+
+ nodes.all { |node| node.name } #=> [NODE1_NAME, NODE2_NAME, ...]
+
+## SHOW ONE NODE ##
+ You can show the data for a single node using the +show+ subcommand:
+
+ nodes.show("NODE_NAME") => <Chef::Node @name="NODE_NAME" ...>
+
+## SEARCH FOR NODES ##
+ You can search for nodes using the +search+ or +find+ subcommands:
+
+ nodes.find(:name => "app*") #=> [<Chef::Node @name="app1.example.com" ...>, ...]
+
+ Similarly to +all+, you can pass a code block to limit or transform the
+ information returned:
+
+ nodes.find(:name => "app#") { |node| node.ec2 }
+
+## BULK EDIT NODES ##
+ **BE CAREFUL, THIS OPERATION IS DESTRUCTIVE**
+
+ Bulk edit nodes by passing a code block to the +transform+ or +bulk_edit+
+ subcommand. The block will be applied to each matching node, and then the node
+ will be saved. If the block returns +nil+ or +false+, that node will be
+ skipped.
+
+ nodes.transform do |node|
+ if node.fqdn =~ /.*\\.preprod\\.example\\.com/
+ node.set[:environment] = "preprod"
+ end
+ end
+
+ This will assign the attribute to every node with a FQDN matching the regex.
+E
+ subcommands :all => "list all nodes",
+ :show => "load a node by name",
+ :search => "search for nodes",
+ :transform => "edit all nodes via a code block and save them"
+ def nodes
+ @nodes ||= Shell::ModelWrapper.new(Chef::Node)
+ end
+
+ desc "Find and edit roles via the API"
+ explain(<<-E)
+## SUMMARY ##
+ +roles+ allows you to query and edit roles on your Chef server.
+
+## SUBCOMMANDS ##
+ * all (list)
+ * show (load)
+ * search (find)
+ * transform (bulk_edit)
+
+## SEE ALSO ##
+ See the help for +nodes+ for more information about the subcommands.
+E
+ subcommands :all => "list all roles",
+ :show => "load a role by name",
+ :search => "search for roles",
+ :transform => "edit all roles via a code block and save them"
+ def roles
+ @roles ||= Shell::ModelWrapper.new(Chef::Role)
+ end
+
+ desc "Find and edit +databag_name+ via the api"
+ explain(<<-E)
+## SUMMARY ##
+ +databags(DATABAG_NAME)+ allows you to query and edit data bag items on your
+ Chef server. Unlike other commands for working with data on the server,
+ +databags+ requires the databag name as an argument, for example:
+ databags(:users).all
+
+## SUBCOMMANDS ##
+ * all (list)
+ * show (load)
+ * search (find)
+ * transform (bulk_edit)
+
+## SEE ALSO ##
+ See the help for +nodes+ for more information about the subcommands.
+
+E
+ subcommands :all => "list all items in the data bag",
+ :show => "load a data bag item by id",
+ :search => "search for items in the data bag",
+ :transform => "edit all items via a code block and save them"
+ def databags(databag_name)
+ @named_databags_wrappers ||= {}
+ @named_databags_wrappers[databag_name] ||= Shell::NamedDataBagWrapper.new(databag_name)
+ end
+
+ desc "Find and edit environments via the API"
+ explain(<<-E)
+## SUMMARY ##
+ +environments+ allows you to query and edit environments on your Chef server.
+
+## SUBCOMMANDS ##
+ * all (list)
+ * show (load)
+ * search (find)
+ * transform (bulk_edit)
+
+## SEE ALSO ##
+ See the help for +nodes+ for more information about the subcommands.
+E
+ subcommands :all => "list all environments",
+ :show => "load an environment by name",
+ :search => "search for environments",
+ :transform => "edit all environments via a code block and save them"
+ def environments
+ @environments ||= Shell::ModelWrapper.new(Chef::Environment)
+ end
+
+ desc "A REST Client configured to authenticate with the API"
+ def api
+ @rest = Shell::ShellREST.new(Chef::Config[:chef_server_url])
+ end
+
+ end
+
+ RecipeUIExtensions = Proc.new do
+ alias :original_resources :resources
+
+ desc "list all the resources on the current recipe"
+ def resources(*args)
+ if args.empty?
+ pp run_context.resource_collection.instance_variable_get(:@resources_by_name).keys
+ else
+ pp resources = original_resources(*args)
+ resources
+ end
+ end
+ end
+
+ def self.extend_context_object(obj)
+ obj.instance_eval(&ObjectUIExtensions)
+ obj.instance_eval(&MainContextExtensions)
+ obj.instance_eval(&RESTApiExtensions)
+ obj.extend(FileUtils)
+ obj.extend(Chef::DSL::PlatformIntrospection)
+ obj.extend(Chef::DSL::DataQuery)
+ end
+
+ def self.extend_context_node(node_obj)
+ node_obj.instance_eval(&ObjectUIExtensions)
+ end
+
+ def self.extend_context_recipe(recipe_obj)
+ recipe_obj.instance_eval(&ObjectUIExtensions)
+ recipe_obj.instance_eval(&RecipeUIExtensions)
+ end
+
+ end
+end
+
+class String
+ include Shell::Extensions::String
+end
+
+class Symbol
+ include Shell::Extensions::Symbol
+end
+
+class TrueClass
+ include Shell::Extensions::TrueClass
+end
+
+class FalseClass
+ include Shell::Extensions::FalseClass
+end
diff --git a/lib/chef/shell/model_wrapper.rb b/lib/chef/shell/model_wrapper.rb
new file mode 100644
index 0000000000..7ee39de7eb
--- /dev/null
+++ b/lib/chef/shell/model_wrapper.rb
@@ -0,0 +1,120 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mixin/convert_to_class_name'
+require 'chef/mixin/language'
+
+module Shell
+ class ModelWrapper
+
+ include Chef::Mixin::ConvertToClassName
+
+ attr_reader :model_symbol
+
+ def initialize(model_class, symbol=nil)
+ @model_class = model_class
+ @model_symbol = symbol || convert_to_snake_case(model_class.name, "Chef").to_sym
+ end
+
+ def search(query)
+ return all if query.to_s == "all"
+ results = []
+ Chef::Search::Query.new.search(@model_symbol, format_query(query)) do |obj|
+ if block_given?
+ results << yield(obj)
+ else
+ results << obj
+ end
+ end
+ results
+ end
+
+ alias :find :search
+
+ def all(&block)
+ all_objects = list_objects
+ block_given? ? all_objects.map(&block) : all_objects
+ end
+
+ alias :list :all
+
+ def show(obj_id)
+ @model_class.load(obj_id)
+ end
+
+ alias :load :show
+
+ def transform(what_to_transform, &block)
+ if what_to_transform == :all
+ objects_to_transform = list_objects
+ else
+ objects_to_transform = search(what_to_transform)
+ end
+ objects_to_transform.each do |obj|
+ if result = yield(obj)
+ obj.save
+ end
+ end
+ end
+
+ alias :bulk_edit :transform
+
+ private
+
+ # paper over inconsistencies in the model classes APIs, and return the objects
+ # the user wanted instead of the URI=>object stuff
+ def list_objects
+ objects = @model_class.method(:list).arity == 0? @model_class.list : @model_class.list(true)
+ objects.map { |obj| Array(obj).find {|o| o.kind_of?(@model_class)} }
+ end
+
+ def format_query(query)
+ if query.respond_to?(:keys)
+ query.map { |key, value| "#{key}:#{value}" }.join(" AND ")
+ else
+ query
+ end
+ end
+ end
+
+ class NamedDataBagWrapper < ModelWrapper
+
+ def initialize(databag_name)
+ @model_symbol = @databag_name = databag_name
+ end
+
+
+ alias :list :all
+
+ def show(item)
+ Chef::DataBagItem.load(@databag_name, item)
+ end
+
+ private
+
+ def list_objects
+ all_items = []
+ Chef::Search::Query.new.search(@databag_name) do |item|
+ all_items << item
+ end
+ all_items
+ end
+
+ end
+
+end
diff --git a/lib/chef/shell/shell_rest.rb b/lib/chef/shell/shell_rest.rb
new file mode 100644
index 0000000000..a485a0a1a8
--- /dev/null
+++ b/lib/chef/shell/shell_rest.rb
@@ -0,0 +1,28 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+module Shell
+ class ShellREST < Chef::REST
+
+ alias :get :get_rest
+ alias :put :put_rest
+ alias :post :post_rest
+ alias :delete :delete_rest
+
+ end
+end
diff --git a/lib/chef/shell/shell_session.rb b/lib/chef/shell/shell_session.rb
new file mode 100644
index 0000000000..287cf0c166
--- /dev/null
+++ b/lib/chef/shell/shell_session.rb
@@ -0,0 +1,298 @@
+#--
+# Author:: Daniel DeLeo (<dan@kallistec.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Copyright:: Copyright (c) 2009 Daniel DeLeo
+# Copyright:: Copyright (c) 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/recipe'
+require 'chef/run_context'
+require 'chef/config'
+require 'chef/client'
+require 'chef/cookbook/cookbook_collection'
+require 'chef/cookbook_loader'
+require 'chef/run_list/run_list_expansion'
+require 'chef/formatters/base'
+require 'chef/formatters/doc'
+require 'chef/formatters/minimal'
+
+module Shell
+ class ShellSession
+ include Singleton
+
+ def self.session_type(type=nil)
+ @session_type = type if type
+ @session_type
+ end
+
+ attr_accessor :node, :compile, :recipe, :run_context
+ attr_reader :node_attributes, :client
+ def initialize
+ @node_built = false
+ formatter = Chef::Formatters.new(Chef::Config.formatter, STDOUT, STDERR)
+ @events = Chef::EventDispatch::Dispatcher.new(formatter)
+ end
+
+ def node_built?
+ !!@node_built
+ end
+
+ def reset!
+ loading do
+ rebuild_node
+ @node = client.node
+ shorten_node_inspect
+ Shell::Extensions.extend_context_node(@node)
+ rebuild_context
+ node.consume_attributes(node_attributes) if node_attributes
+ @recipe = Chef::Recipe.new(nil, nil, run_context)
+ Shell::Extensions.extend_context_recipe(@recipe)
+ @node_built = true
+ end
+ end
+
+ def node_attributes=(attrs)
+ @node_attributes = attrs
+ @node.consume_attributes(@node_attributes)
+ end
+
+ def resource_collection
+ run_context.resource_collection
+ end
+
+ def run_context
+ @run_context ||= rebuild_context
+ end
+
+ def definitions
+ nil
+ end
+
+ def cookbook_loader
+ nil
+ end
+
+ def save_node
+ raise "Not Supported! #{self.class.name} doesn't support #save_node, maybe you need to run chef-shell in client mode?"
+ end
+
+ def rebuild_context
+ raise "Not Implemented! :rebuild_collection should be implemented by subclasses"
+ end
+
+ private
+
+ def loading
+ show_loading_progress
+ begin
+ yield
+ rescue => e
+ loading_complete(false)
+ raise e
+ else
+ loading_complete(true)
+ end
+ end
+
+ def show_loading_progress
+ print "Loading"
+ @loading = true
+ @dot_printer = Thread.new do
+ while @loading
+ print "."
+ sleep 0.5
+ end
+ end
+ end
+
+ def loading_complete(success)
+ @loading = false
+ @dot_printer.join
+ msg = success ? "done.\n\n" : "epic fail!\n\n"
+ print msg
+ end
+
+ def shorten_node_inspect
+ def @node.inspect
+ "<Chef::Node:0x#{self.object_id.to_s(16)} @name=\"#{self.name}\">"
+ end
+ end
+
+ def rebuild_node
+ raise "Not Implemented! :rebuild_node should be implemented by subclasses"
+ end
+
+ end
+
+ class StandAloneSession < ShellSession
+
+ session_type :standalone
+
+ def rebuild_context
+ cookbook_collection = Chef::CookbookCollection.new({})
+ @run_context = Chef::RunContext.new(@node, cookbook_collection, @events) # no recipes
+ @run_context.load(Chef::RunList::RunListExpansionFromDisk.new("_default", [])) # empty recipe list
+ end
+
+ private
+
+ def rebuild_node
+ Chef::Config[:solo] = true
+ @client = Chef::Client.new
+ @client.run_ohai
+ @client.load_node
+ @client.build_node
+ end
+
+ end
+
+ class SoloSession < ShellSession
+
+ session_type :solo
+
+ def definitions
+ @run_context.definitions
+ end
+
+ def rebuild_context
+ @run_status = Chef::RunStatus.new(@node, @events)
+ Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, Chef::Config[:cookbook_path]) }
+ cl = Chef::CookbookLoader.new(Chef::Config[:cookbook_path])
+ cl.load_cookbooks
+ cookbook_collection = Chef::CookbookCollection.new(cl)
+ @run_context = Chef::RunContext.new(node, cookbook_collection, @events)
+ @run_context.load(Chef::RunList::RunListExpansionFromDisk.new("_default", []))
+ @run_status.run_context = run_context
+ end
+
+ private
+
+ def rebuild_node
+ # Tell the client we're chef solo so it won't try to contact the server
+ Chef::Config[:solo] = true
+ @client = Chef::Client.new
+ @client.run_ohai
+ @client.load_node
+ @client.build_node
+ end
+
+ end
+
+ class ClientSession < SoloSession
+
+ session_type :client
+
+ def save_node
+ @client.save_node
+ end
+
+ def rebuild_context
+ @run_status = Chef::RunStatus.new(@node, @events)
+ Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, Chef::REST.new(Chef::Config[:server_url])) }
+ cookbook_hash = @client.sync_cookbooks
+ cookbook_collection = Chef::CookbookCollection.new(cookbook_hash)
+ @run_context = Chef::RunContext.new(node, cookbook_collection, @events)
+ @run_context.load(Chef::RunList::RunListExpansionFromAPI.new("_default", []))
+ @run_status.run_context = run_context
+ end
+
+ private
+
+ def rebuild_node
+ # Make sure the client knows this is not chef solo
+ Chef::Config[:solo] = false
+ @client = Chef::Client.new
+ @client.run_ohai
+ @client.register
+ @client.load_node
+ @client.build_node
+ end
+
+ end
+
+ class DoppelGangerClient < Chef::Client
+
+ attr_reader :node_name
+
+ def initialize(node_name)
+ @node_name = node_name
+ @ohai = Ohai::System.new
+ end
+
+ # Run the very smallest amount of ohai we can get away with and still
+ # hope to have things work. Otherwise we're not very good doppelgangers
+ def run_ohai
+ @ohai.require_plugin('os')
+ end
+
+ # DoppelGanger implementation of build_node. preserves as many of the node's
+ # attributes, and does not save updates to the server
+ def build_node
+ Chef::Log.debug("Building node object for #{@node_name}")
+ @node = Chef::Node.find_or_create(node_name)
+ ohai_data = @ohai.data.merge(@node.automatic_attrs)
+ @node.consume_external_attrs(ohai_data,nil)
+ @run_list_expansion = @node.expand!('server')
+ @expanded_run_list_with_versions = @run_list_expansion.recipes.with_version_constraints_strings
+ Chef::Log.info("Run List is [#{@node.run_list}]")
+ Chef::Log.info("Run List expands to [#{@expanded_run_list_with_versions.join(', ')}]")
+ @node
+ end
+
+ def register
+ @rest = Chef::REST.new(Chef::Config[:chef_server_url], Chef::Config[:node_name], Chef::Config[:client_key])
+ end
+
+ end
+
+ class DoppelGangerSession < ClientSession
+
+ session_type "doppelganger client"
+
+ def save_node
+ puts "A doppelganger should think twice before saving the node"
+ end
+
+ def assume_identity(node_name)
+ Chef::Config[:doppelganger] = @node_name = node_name
+ reset!
+ rescue Exception => e
+ puts "#{e.class.name}: #{e.message}"
+ puts Array(e.backtrace).join("\n")
+ puts
+ puts "* " * 40
+ puts "failed to assume the identity of node '#{node_name}', resetting"
+ puts "* " * 40
+ puts
+ Chef::Config[:doppelganger] = false
+ @node_built = false
+ Shell.session
+ end
+
+ def rebuild_node
+ # Make sure the client knows this is not chef solo
+ Chef::Config[:solo] = false
+ @client = DoppelGangerClient.new(@node_name)
+ @client.run_ohai
+ @client.register
+ @client.load_node
+ @client.build_node
+ @client.sync_cookbooks
+ end
+
+ end
+
+end
diff --git a/lib/chef/shell_out.rb b/lib/chef/shell_out.rb
new file mode 100644
index 0000000000..3febe366bd
--- /dev/null
+++ b/lib/chef/shell_out.rb
@@ -0,0 +1,13 @@
+require 'mixlib/shellout'
+
+class Chef
+ class ShellOut < Mixlib::ShellOut
+
+ def initialize(*args)
+ Chef::Log.warn("Chef::ShellOut is deprecated, please use Mixlib::ShellOut")
+ called_from = caller[0..3].inject("Called from:\n") {|msg, trace_line| msg << " #{trace_line}\n" }
+ Chef::Log.warn(called_from)
+ super
+ end
+ end
+end
diff --git a/lib/chef/streaming_cookbook_uploader.rb b/lib/chef/streaming_cookbook_uploader.rb
new file mode 100644
index 0000000000..df90e0003d
--- /dev/null
+++ b/lib/chef/streaming_cookbook_uploader.rb
@@ -0,0 +1,201 @@
+# inspired by/cargo-culted from http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html
+# On Apr 6, 2010, at 3:00 PM, Stanislav Vitvitskiy wrote:
+#
+# It's free to use / modify / distribute. No need to mention anything. Just copy/paste and use.
+#
+# Regards,
+# Stan
+
+require 'net/http'
+require 'mixlib/authentication/signedheaderauth'
+require 'openssl'
+require 'chef/version'
+
+class Chef
+ class StreamingCookbookUploader
+
+ DefaultHeaders = { 'accept' => 'application/json', 'x-chef-version' => ::Chef::VERSION }
+
+ class << self
+
+ 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
+
+ timestamp = Time.now.utc.iso8601
+ secret_key = OpenSSL::PKey::RSA.new(File.read(secret_key_filename))
+
+ unless params.nil? || params.empty?
+ params.each do |key, value|
+ if value.kind_of?(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}, path: #{url.path}, 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 = Net::HTTP.new(url.host, url.port)
+ if url.scheme == "https"
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+ res = http.request(req)
+ #res = http.start {|http_proc| http_proc.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
+
+ # BUGBUG this makes the response compatible with what respsonse_steps expects to test headers (response.headers[] -> response[])
+ def headers
+ self
+ end
+
+ def status
+ 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 startiung 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)
+ return nil if @part_no >= @parts.size
+
+ 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)
+ current_part + if next_part
+ next_part
+ else
+ ''
+ end
+ else
+ @part_offset += how_much_current_part
+ current_part
+ end
+ end
+ end
+
+ end
+
+
+end
diff --git a/lib/chef/tasks/chef_repo.rake b/lib/chef/tasks/chef_repo.rake
new file mode 100644
index 0000000000..6f839a486f
--- /dev/null
+++ b/lib/chef/tasks/chef_repo.rake
@@ -0,0 +1,334 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'rubygems'
+require 'chef/json_compat'
+require 'chef'
+require 'chef/role'
+require 'chef/cookbook/metadata'
+require 'tempfile'
+require 'rake'
+
+# Allow REMOTE options to be overridden on the command line
+REMOTE_HOST = ENV["REMOTE_HOST"] if ENV["REMOTE_HOST"] != nil
+REMOTE_SUDO = ENV["REMOTE_SUDO"] if ENV["REMOTE_SUDO"] != nil
+if defined? REMOTE_HOST
+ REMOTE_PATH_PREFIX = "#{REMOTE_HOST}:"
+ REMOTE_EXEC_PREFIX = "ssh #{REMOTE_HOST}"
+ REMOTE_EXEC_PREFIX += " sudo" if defined? REMOTE_SUDO
+ LOCAL_EXEC_PREFIX = ""
+else
+ REMOTE_PATH_PREFIX = ""
+ REMOTE_EXEC_PREFIX = ""
+ LOCAL_EXEC_PREFIX = "sudo"
+end
+
+desc "Update your repository from source control"
+task :update do
+ puts "** Updating your repository"
+
+ case $vcs
+ when :svn
+ sh %{svn up}
+ when :git
+ pull = false
+ IO.foreach(File.join(TOPDIR, ".git", "config")) do |line|
+ pull = true if line =~ /\[remote "origin"\]/
+ end
+ if pull
+ sh %{git pull}
+ else
+ puts "* Skipping git pull, no origin specified"
+ end
+ else
+ puts "* No SCM configured, skipping update"
+ end
+end
+
+desc "Install the latest copy of the repository on this Chef Server"
+task :install => [ :update, :roles, :upload_cookbooks ] do
+ if File.exists?(File.join(TOPDIR, "config", "server.rb"))
+ puts "* Installing new Chef Server Config"
+ sh "#{LOCAL_EXEC_PREFIX} rsync -rlt --delete --exclude '.svn' --exclude '.git*' config/server.rb #{REMOTE_PATH_PREFIX}#{CHEF_SERVER_CONFIG}"
+ end
+ if File.exists?(File.join(TOPDIR, "config", "client.rb"))
+ puts "* Installing new Chef Client Config"
+ sh "#{LOCAL_EXEC_PREFIX} rsync -rlt --delete --exclude '.svn' --exclude '.git*' config/client.rb #{REMOTE_PATH_PREFIX}#{CHEF_CLIENT_CONFIG}"
+ end
+end
+
+desc "By default, run rake test_cookbooks"
+task :default => [ :test_cookbooks ]
+
+desc "Create a new cookbook (with COOKBOOK=name, optional CB_PREFIX=site-)"
+task :new_cookbook do
+ puts "***WARN: rake new_cookbook is deprecated. Please use 'knife cookbook create COOKBOOK' command.***"
+ create_cookbook(File.join(TOPDIR, "#{ENV["CB_PREFIX"]}cookbooks"))
+ create_readme(File.join(TOPDIR, "#{ENV["CB_PREFIX"]}cookbooks"))
+ create_metadata(File.join(TOPDIR, "#{ENV["CB_PREFIX"]}cookbooks"))
+end
+
+def create_cookbook(dir)
+ raise "Must provide a COOKBOOK=" unless ENV["COOKBOOK"]
+ puts "** Creating cookbook #{ENV["COOKBOOK"]}"
+ sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "attributes")}"
+ sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "recipes")}"
+ sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "definitions")}"
+ sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "libraries")}"
+ sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "resources")}"
+ sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "providers")}"
+ sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "files", "default")}"
+ sh "mkdir -p #{File.join(dir, ENV["COOKBOOK"], "templates", "default")}"
+ unless File.exists?(File.join(dir, ENV["COOKBOOK"], "recipes", "default.rb"))
+ open(File.join(dir, ENV["COOKBOOK"], "recipes", "default.rb"), "w") do |file|
+ file.puts <<-EOH
+#
+# Cookbook Name:: #{ENV["COOKBOOK"]}
+# Recipe:: default
+#
+# Copyright #{Time.now.year}, #{COMPANY_NAME}
+#
+EOH
+ case NEW_COOKBOOK_LICENSE
+ when :apachev2
+ file.puts <<-EOH
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+EOH
+ when :none
+ file.puts <<-EOH
+# All rights reserved - Do Not Redistribute
+#
+EOH
+ end
+ end
+ end
+end
+
+def create_readme(dir)
+ raise "Must provide a COOKBOOK=" unless ENV["COOKBOOK"]
+ puts "** Creating README for cookbook: #{ENV["COOKBOOK"]}"
+ unless File.exists?(File.join(dir, ENV["COOKBOOK"], "README.rdoc"))
+ open(File.join(dir, ENV["COOKBOOK"], "README.md"), "w") do |file|
+ file.puts <<-EOH
+Description
+===========
+
+Requirements
+============
+
+Attributes
+==========
+
+Usage
+=====
+
+EOH
+ end
+ end
+end
+
+def create_metadata(dir)
+ raise "Must provide a COOKBOOK=" unless ENV["COOKBOOK"]
+ puts "** Creating metadata for cookbook: #{ENV["COOKBOOK"]}"
+
+ case NEW_COOKBOOK_LICENSE
+ when :apachev2
+ license = "Apache 2.0"
+ when :none
+ license = "All rights reserved"
+ end
+
+ unless File.exists?(File.join(dir, ENV["COOKBOOK"], "metadata.rb"))
+ open(File.join(dir, ENV["COOKBOOK"], "metadata.rb"), "w") do |file|
+ if File.exists?(File.join(dir, ENV["COOKBOOK"], 'README.rdoc'))
+ long_description = "long_description IO.read(File.join(File.dirname(__FILE__), 'README.rdoc'))"
+ end
+ file.puts <<-EOH
+maintainer "#{COMPANY_NAME}"
+maintainer_email "#{SSL_EMAIL_ADDRESS}"
+license "#{license}"
+description "Installs/Configures #{ENV["COOKBOOK"]}"
+#{long_description}
+version "0.1"
+EOH
+ end
+ end
+end
+
+desc "Create a new self-signed SSL certificate for FQDN=foo.example.com"
+task :ssl_cert do
+ $expect_verbose = true
+ fqdn = ENV["FQDN"]
+ fqdn =~ /^(.+?)\.(.+)$/
+ hostname = $1
+ domain = $2
+ keyfile = fqdn.gsub("*", "wildcard")
+ raise "Must provide FQDN!" unless fqdn && hostname && domain
+ puts "** Creating self signed SSL Certificate for #{fqdn}"
+ sh("(cd #{CADIR} && openssl genrsa 2048 > #{keyfile}.key)")
+ sh("(cd #{CADIR} && chmod 644 #{keyfile}.key)")
+ puts "* Generating Self Signed Certificate Request"
+ tf = Tempfile.new("#{keyfile}.ssl-conf")
+ ssl_config = <<EOH
+[ req ]
+distinguished_name = req_distinguished_name
+prompt = no
+
+[ req_distinguished_name ]
+C = #{SSL_COUNTRY_NAME}
+ST = #{SSL_STATE_NAME}
+L = #{SSL_LOCALITY_NAME}
+O = #{COMPANY_NAME}
+OU = #{SSL_ORGANIZATIONAL_UNIT_NAME}
+CN = #{fqdn}
+emailAddress = #{SSL_EMAIL_ADDRESS}
+EOH
+ tf.puts(ssl_config)
+ tf.close
+ sh("(cd #{CADIR} && openssl req -config '#{tf.path}' -new -x509 -nodes -sha1 -days 3650 -key #{keyfile}.key > #{keyfile}.crt)")
+ sh("(cd #{CADIR} && openssl x509 -noout -fingerprint -text < #{keyfile}.crt > #{keyfile}.info)")
+ sh("(cd #{CADIR} && cat #{keyfile}.crt #{keyfile}.key > #{keyfile}.pem)")
+ sh("(cd #{CADIR} && chmod 644 #{keyfile}.pem)")
+end
+
+rule(%r{\b(?:site-)?cookbooks/[^/]+/metadata\.json\Z} => [ proc { |task_name| task_name.sub(/\.[^.]+$/, '.rb') } ]) do |t|
+ system("knife cookbook metadata from file #{t.source}")
+end
+
+desc "Build cookbook metadata.json from metadata.rb"
+task :metadata => FileList[File.join(TOPDIR, '*cookbooks', ENV['COOKBOOK'] || '*', 'metadata.rb')].pathmap('%X.json')
+
+rule(%r{\broles/\S+\.json\Z} => [ proc { |task_name| task_name.sub(/\.[^.]+$/, '.rb') } ]) do |t|
+ system("knife role from file #{t.source}")
+end
+
+desc "Update roles"
+task :roles => FileList[File.join(TOPDIR, 'roles', '**', '*.rb')].pathmap('%X.json')
+
+desc "Update a specific role"
+task :role, :role_name do |t, args|
+ system("knife role from file #{File.join(TOPDIR, 'roles', args.role_name)}.rb")
+end
+
+desc "Upload all cookbooks"
+task :upload_cookbooks => [ :metadata ]
+task :upload_cookbooks do
+ system("knife cookbook upload --all")
+end
+
+desc "Upload a single cookbook"
+task :upload_cookbook => [ :metadata ]
+task :upload_cookbook, :cookbook do |t, args|
+ system("knife cookbook upload #{args.cookbook}")
+end
+
+desc "Test all cookbooks"
+task :test_cookbooks do
+ system("knife cookbook test --all")
+end
+
+desc "Test a single cookbook"
+task :test_cookbook, :cookbook do |t, args|
+ system("knife cookbook test #{args.cookbook}")
+end
+
+namespace :databag do
+ path = "data_bags"
+
+ desc "Upload a single databag"
+ task :upload, :databag do |t, args|
+ input_databag = args[:databag] || 'none_specified'
+ databag = File.join(path, input_databag)
+
+ if File.exists?(databag) && File.directory?(databag)
+ system "knife data bag create #{input_databag}"
+ Dir.foreach(databag) do |item|
+ name, type = item.split('.')
+ if type == 'json' && name.length > 0
+ system "knife data bag from file #{input_databag} " + File.join(databag, item)
+ end
+ end
+ else
+ puts "ERROR: Could not find the databag in your databag path (" + File.join(path, input_databag) + "), skipping it"
+ end
+ end
+
+ desc "Upload all databags"
+ task :upload_all do
+ if File.exists?(path) && File.directory?(path)
+ Dir.foreach(path) do |databag|
+ if databag == databag[/^[\-[:alnum:]_]+$/]
+ Rake::Task['databag:upload'].execute( { :databag => databag } )
+ end
+ end
+ else
+ puts "ERROR: Could not find any databags, skipping it"
+ end
+ end
+
+ desc "Create a databag"
+ task :create, :databag do |t, args|
+ input_databag = args[:databag] || 'none_specified'
+
+ FileUtils.mkdir(path) unless File.exists?(path)
+ databag = File.join(path, input_databag)
+ FileUtils.mkdir(databag) unless File.exists?(databag)
+ end
+
+ desc "Create a databag item stub"
+ task :create_item, :databag, :item do |t, args|
+ input_databag = args[:databag] || 'none_specified'
+ input_item = args[:item] || false
+
+ databag = File.join(path, input_databag)
+ if File.exists?(databag) && File.directory?(databag)
+ if input_item
+ json_filename = File.join(databag, "#{input_item}.json")
+ if !File.exists?(json_filename)
+ stub = <<EOH
+{
+ "id" : "#{input_item}"
+}
+EOH
+ json_file = File.new(json_filename, "w")
+ json_file.write(stub)
+ json_file.close
+ else
+ puts "ERROR: databag item already exists (#{json_filename}), skipping it"
+ end
+ else
+ puts "ERROR: No item id specified, skipping it"
+ end
+ else
+ puts "ERROR: Could not find your databag (#{databag}), skipping it"
+ end
+ end
+
+end
+
diff --git a/lib/chef/util/file_edit.rb b/lib/chef/util/file_edit.rb
new file mode 100644
index 0000000000..ce37bdcdbf
--- /dev/null
+++ b/lib/chef/util/file_edit.rb
@@ -0,0 +1,132 @@
+#
+# Author:: Nuo Yan (<nuo@opscode.com>)
+# Copyright:: Copyright (c) 2009 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'fileutils'
+require 'tempfile'
+
+class Chef
+ class Util
+ class FileEdit
+
+ private
+
+ attr_accessor :original_pathname, :contents, :file_edited
+
+ public
+
+ def initialize(filepath)
+ @original_pathname = filepath
+ @file_edited = false
+
+ raise ArgumentError, "File doesn't exist" unless File.exist? @original_pathname
+ raise ArgumentError, "File is blank" unless (@contents = File.new(@original_pathname).readlines).length > 0
+ end
+
+ #search the file line by line and match each line with the given regex
+ #if matched, replace the whole line with newline.
+ def search_file_replace_line(regex, newline)
+ search_match(regex, newline, 'r', 1)
+ end
+
+ #search the file line by line and match each line with the given regex
+ #if matched, replace the match (all occurances) with the replace parameter
+ def search_file_replace(regex, replace)
+ search_match(regex, replace, 'r', 2)
+ end
+
+ #search the file line by line and match each line with the given regex
+ #if matched, delete the line
+ def search_file_delete_line(regex)
+ search_match(regex, " ", 'd', 1)
+ end
+
+ #search the file line by line and match each line with the given regex
+ #if matched, delete the match (all occurances) from the line
+ def search_file_delete(regex)
+ search_match(regex, " ", 'd', 2)
+ end
+
+ #search the file line by line and match each line with the given regex
+ #if matched, insert newline after each matching line
+ def insert_line_after_match(regex, newline)
+ search_match(regex, newline, 'i', 1)
+ end
+
+ #search the file line by line and match each line with the given regex
+ #if not matched, insert newline at the end of the file
+ def insert_line_if_no_match(regex, newline)
+ search_match(regex, newline, 'i', 2)
+ end
+
+ #Make a copy of old_file and write new file out (only if file changed)
+ def write_file
+
+ # file_edited is false when there was no match in the whole file and thus no contents have changed.
+ if file_edited
+ backup_pathname = original_pathname + ".old"
+ FileUtils.cp(original_pathname, backup_pathname, :preserve => true)
+ File.open(original_pathname, "w") do |newfile|
+ contents.each do |line|
+ newfile.puts(line)
+ end
+ newfile.flush
+ end
+ end
+ self.file_edited = false
+ end
+
+ private
+
+ #helper method to do the match, replace, delete, and insert operations
+ #command is the switch of delete, replace, and insert ('d', 'r', 'i')
+ #method is to control operation on whole line or only the match (1 for line, 2 for match)
+ def search_match(regex, replace, command, method)
+
+ #convert regex to a Regexp object (if not already is one) and store it in exp.
+ exp = Regexp.new(regex)
+
+ #loop through contents and do the appropriate operation depending on 'command' and 'method'
+ new_contents = []
+
+ contents.each do |line|
+ if line.match(exp)
+ self.file_edited = true
+ case
+ when command == 'r'
+ new_contents << ((method == 1) ? replace : line.gsub!(exp, replace))
+ when command == 'd'
+ if method == 2
+ new_contents << line.gsub!(exp, "")
+ end
+ when command == 'i'
+ new_contents << line
+ new_contents << replace unless method == 2
+ end
+ else
+ new_contents << line
+ end
+ end
+ if command == 'i' && method == 2 && ! file_edited
+ new_contents << replace
+ self.file_edited = true
+ end
+
+ self.contents = new_contents
+ end
+ end
+ end
+end
diff --git a/lib/chef/util/windows.rb b/lib/chef/util/windows.rb
new file mode 100644
index 0000000000..cba2c2a1b7
--- /dev/null
+++ b/lib/chef/util/windows.rb
@@ -0,0 +1,56 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#requires: gem install windows-pr
+require 'windows/api'
+require 'windows/error'
+require 'windows/handle'
+require 'windows/unicode'
+require 'windows/msvcrt/buffer'
+require 'windows/msvcrt/string'
+require 'windows/network/management'
+
+class Chef
+ class Util
+ class Windows
+ protected
+
+ include ::Windows::Error
+ include ::Windows::Unicode
+ include ::Windows::MSVCRT::Buffer
+ include ::Windows::MSVCRT::String
+ include ::Windows::Network::Management
+
+ PTR_SIZE = 4 #XXX 64-bit
+
+ def lpwstr_to_s(buffer, offset)
+ str = 0.chr * (256 * 2) #XXX unhardcode this length (*2 for WCHAR)
+ wcscpy str, buffer[offset*PTR_SIZE,PTR_SIZE].unpack('L')[0]
+ wide_to_multi str
+ end
+
+ def dword_to_i(buffer, offset)
+ buffer[offset*PTR_SIZE,PTR_SIZE].unpack('i')[0] || 0
+ end
+
+ #return pointer for use with pack('L')
+ def str_to_ptr(v)
+ [v].pack('p*').unpack('L')[0]
+ end
+ end
+ end
+end
diff --git a/lib/chef/util/windows/net_group.rb b/lib/chef/util/windows/net_group.rb
new file mode 100644
index 0000000000..9da0dc6557
--- /dev/null
+++ b/lib/chef/util/windows/net_group.rb
@@ -0,0 +1,101 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/util/windows'
+
+#wrapper around a subset of the NetGroup* APIs.
+#nothing Chef specific, but not complete enough to be its own gem, so util for now.
+class Chef::Util::Windows::NetGroup < Chef::Util::Windows
+
+ private
+
+ def pack_str(s)
+ [str_to_ptr(s)].pack('L')
+ end
+
+ def modify_members(members, func)
+ buffer = 0.chr * (members.size * PTR_SIZE)
+ members.each_with_index do |member,offset|
+ buffer[offset*PTR_SIZE,PTR_SIZE] = pack_str(multi_to_wide(member))
+ end
+ rc = func.call(nil, @name, 3, buffer, members.size)
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+ end
+
+ public
+
+ def initialize(groupname)
+ @name = multi_to_wide(groupname)
+ end
+
+ def local_get_members
+ group_members = []
+ handle = 0.chr * PTR_SIZE
+ rc = ERROR_MORE_DATA
+
+ while rc == ERROR_MORE_DATA
+ ptr = 0.chr * PTR_SIZE
+ nread = 0.chr * PTR_SIZE
+ total = 0.chr * PTR_SIZE
+
+ rc = NetLocalGroupGetMembers.call(nil, @name, 1, ptr, -1,
+ nread, total, handle)
+ if (rc == NERR_Success) || (rc == ERROR_MORE_DATA)
+ ptr = ptr.unpack('L')[0]
+ nread = nread.unpack('i')[0]
+ members = 0.chr * (nread * (PTR_SIZE * 3)) #nread * sizeof(LOCALGROUP_MEMBERS_INFO_1)
+ memcpy(members, ptr, members.size)
+
+ #3 pointer fields in LOCALGROUP_MEMBERS_INFO_1, offset 2*PTR_SIZE is lgrmi1_name
+ nread.times do |i|
+ offset = (i * 3) + 2
+ member = lpwstr_to_s(members, offset)
+ group_members << member
+ end
+ NetApiBufferFree(ptr)
+ else
+ raise ArgumentError, get_last_error(rc)
+ end
+ end
+ group_members
+ end
+
+ def local_add
+ rc = NetLocalGroupAdd.call(nil, 0, pack_str(@name), nil)
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+ end
+
+ def local_set_members(members)
+ modify_members(members, NetLocalGroupSetMembers)
+ end
+
+ def local_add_members(members)
+ modify_members(members, NetLocalGroupAddMembers)
+ end
+
+ def local_delete
+ rc = NetLocalGroupDel.call(nil, @name)
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+ end
+end
diff --git a/lib/chef/util/windows/net_use.rb b/lib/chef/util/windows/net_use.rb
new file mode 100644
index 0000000000..1979e095bd
--- /dev/null
+++ b/lib/chef/util/windows/net_use.rb
@@ -0,0 +1,121 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#the Win32 Volume APIs do not support mapping network drives. not supported by WMI either.
+#see also: WNetAddConnection2 and WNetAddConnection3
+#see also cmd.exe: net use /?
+
+require 'chef/util/windows'
+
+class Chef::Util::Windows::NetUse < Chef::Util::Windows
+
+ private
+
+ USE_NOFORCE = 0
+ USE_FORCE = 1
+ USE_LOTS_OF_FORCE = 2 #every windows API should support this flag
+
+ USE_INFO_2 = [
+ [:local, nil],
+ [:remote, nil],
+ [:password, nil],
+ [:status, 0],
+ [:asg_type, 0],
+ [:refcount, 0],
+ [:usecount, 0],
+ [:username, nil],
+ [:domainname, nil]
+ ]
+
+ USE_INFO_2_TEMPLATE =
+ USE_INFO_2.collect { |field| field[1].class == Fixnum ? 'i' : 'L' }.join
+
+ SIZEOF_USE_INFO_2 = #sizeof(USE_INFO_2)
+ USE_INFO_2.inject(0){|sum,item|
+ sum + (item[1].class == Fixnum ? 4 : PTR_SIZE)
+ }
+
+ def use_info_2(args)
+ USE_INFO_2.collect { |field|
+ args.include?(field[0]) ? args[field[0]] : field[1]
+ }
+ end
+
+ def use_info_2_pack(use)
+ use.collect { |v|
+ v.class == Fixnum ? v : str_to_ptr(multi_to_wide(v))
+ }.pack(USE_INFO_2_TEMPLATE)
+ end
+
+ def use_info_2_unpack(buffer)
+ use = Hash.new
+ USE_INFO_2.each_with_index do |field,offset|
+ use[field[0]] = field[1].class == Fixnum ?
+ dword_to_i(buffer, offset) : lpwstr_to_s(buffer, offset)
+ end
+ use
+ end
+
+ public
+
+ def initialize(localname)
+ @localname = localname
+ @name = multi_to_wide(localname)
+ end
+
+ def add(args)
+ if args.class == String
+ remote = args
+ args = Hash.new
+ args[:remote] = remote
+ end
+ args[:local] ||= @localname
+ use = use_info_2(args)
+ buffer = use_info_2_pack(use)
+ rc = NetUseAdd.call(nil, 2, buffer, nil)
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+ end
+
+ def get_info
+ ptr = 0.chr * PTR_SIZE
+ rc = NetUseGetInfo.call(nil, @name, 2, ptr)
+
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+
+ ptr = ptr.unpack('L')[0]
+ buffer = 0.chr * SIZEOF_USE_INFO_2
+ memcpy(buffer, ptr, buffer.size)
+ NetApiBufferFree(ptr)
+ use_info_2_unpack(buffer)
+ end
+
+ def device
+ get_info()[:remote]
+ end
+ #XXX should we use some FORCE here?
+ def delete
+ rc = NetUseDel.call(nil, @name, USE_NOFORCE)
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+ end
+end
diff --git a/lib/chef/util/windows/net_user.rb b/lib/chef/util/windows/net_user.rb
new file mode 100644
index 0000000000..97d8f33834
--- /dev/null
+++ b/lib/chef/util/windows/net_user.rb
@@ -0,0 +1,198 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES 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/util/windows'
+
+#wrapper around a subset of the NetUser* APIs.
+#nothing Chef specific, but not complete enough to be its own gem, so util for now.
+class Chef::Util::Windows::NetUser < Chef::Util::Windows
+
+ private
+
+ LogonUser = Windows::API.new('LogonUser', 'SSSLLP', 'I', 'advapi32')
+
+ DOMAIN_GROUP_RID_USERS = 0x00000201
+
+ UF_SCRIPT = 0x000001
+ UF_ACCOUNTDISABLE = 0x000002
+ UF_PASSWD_CANT_CHANGE = 0x000040
+ UF_NORMAL_ACCOUNT = 0x000200
+ UF_DONT_EXPIRE_PASSWD = 0x010000
+
+ #[:symbol_name, default_val]
+ #default_val duals as field type
+ #array index duals as structure offset
+ USER_INFO_3 = [
+ [:name, nil],
+ [:password, nil],
+ [:password_age, 0],
+ [:priv, 0], #"The NetUserAdd and NetUserSetInfo functions ignore this member"
+ [:home_dir, nil],
+ [:comment, nil],
+ [:flags, UF_SCRIPT|UF_DONT_EXPIRE_PASSWD|UF_NORMAL_ACCOUNT],
+ [:script_path, nil],
+ [:auth_flags, 0],
+ [:full_name, nil],
+ [:user_comment, nil],
+ [:parms, nil],
+ [:workstations, nil],
+ [:last_logon, 0],
+ [:last_logoff, 0],
+ [:acct_expires, -1],
+ [:max_storage, -1],
+ [:units_per_week, 0],
+ [:logon_hours, nil],
+ [:bad_pw_count, 0],
+ [:num_logons, 0],
+ [:logon_server, nil],
+ [:country_code, 0],
+ [:code_page, 0],
+ [:user_id, 0],
+ [:primary_group_id, DOMAIN_GROUP_RID_USERS],
+ [:profile, nil],
+ [:home_dir_drive, nil],
+ [:password_expired, 0]
+ ]
+
+ USER_INFO_3_TEMPLATE =
+ USER_INFO_3.collect { |field| field[1].class == Fixnum ? 'i' : 'L' }.join
+
+ SIZEOF_USER_INFO_3 = #sizeof(USER_INFO_3)
+ USER_INFO_3.inject(0){|sum,item|
+ sum + (item[1].class == Fixnum ? 4 : PTR_SIZE)
+ }
+
+ def user_info_3(args)
+ USER_INFO_3.collect { |field|
+ args.include?(field[0]) ? args[field[0]] : field[1]
+ }
+ end
+
+ def user_info_3_pack(user)
+ user.collect { |v|
+ v.class == Fixnum ? v : str_to_ptr(multi_to_wide(v))
+ }.pack(USER_INFO_3_TEMPLATE)
+ end
+
+ def user_info_3_unpack(buffer)
+ user = Hash.new
+ USER_INFO_3.each_with_index do |field,offset|
+ user[field[0]] = field[1].class == Fixnum ?
+ dword_to_i(buffer, offset) : lpwstr_to_s(buffer, offset)
+ end
+ user
+ end
+
+ def set_info(args)
+ user = user_info_3(args)
+ buffer = user_info_3_pack(user)
+ rc = NetUserSetInfo.call(nil, @name, 3, buffer, nil)
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+ end
+
+ public
+
+ def initialize(username)
+ @username = username
+ @name = multi_to_wide(username)
+ end
+
+ LOGON32_PROVIDER_DEFAULT = 0
+ LOGON32_LOGON_NETWORK = 3
+ #XXX for an extra painful alternative, see: http://support.microsoft.com/kb/180548
+ def validate_credentials(passwd)
+ token = 0.chr * PTR_SIZE
+ res = LogonUser.call(@username, nil, passwd,
+ LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, token)
+ if res == 0
+ return false
+ end
+ ::Windows::Handle::CloseHandle.call(token.unpack('L')[0])
+ return true
+ end
+
+ def get_info
+ ptr = 0.chr * PTR_SIZE
+ rc = NetUserGetInfo.call(nil, @name, 3, ptr)
+
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+
+ ptr = ptr.unpack('L')[0]
+ buffer = 0.chr * SIZEOF_USER_INFO_3
+ memcpy(buffer, ptr, buffer.size)
+ NetApiBufferFree(ptr)
+ user_info_3_unpack(buffer)
+ end
+
+ def add(args)
+ user = user_info_3(args)
+ buffer = user_info_3_pack(user)
+
+ rc = NetUserAdd.call(nil, 3, buffer, rc)
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+
+ #usri3_primary_group_id:
+ #"When you call the NetUserAdd function, this member must be DOMAIN_GROUP_RID_USERS"
+ NetLocalGroupAddMembers(nil, multi_to_wide("Users"), 3, buffer[0,PTR_SIZE], 1)
+ end
+
+ def user_modify(&proc)
+ user = get_info
+ user[:last_logon] = user[:units_per_week] = 0 #ignored as per USER_INFO_3 doc
+ user[:logon_hours] = nil #PBYTE field; \0 == no changes
+ proc.call(user)
+ set_info(user)
+ end
+
+ def update(args)
+ user_modify do |user|
+ args.each do |key,val|
+ user[key] = val
+ end
+ end
+ end
+
+ def delete
+ rc = NetUserDel.call(nil, @name)
+ if rc != NERR_Success
+ raise ArgumentError, get_last_error(rc)
+ end
+ end
+
+ def disable_account
+ user_modify do |user|
+ user[:flags] |= UF_ACCOUNTDISABLE
+ end
+ end
+
+ def enable_account
+ user_modify do |user|
+ user[:flags] &= ~UF_ACCOUNTDISABLE
+ end
+ end
+
+ def check_enabled
+ (get_info()[:flags] & UF_ACCOUNTDISABLE) != 0
+ end
+end
diff --git a/lib/chef/util/windows/volume.rb b/lib/chef/util/windows/volume.rb
new file mode 100644
index 0000000000..11f8e080b3
--- /dev/null
+++ b/lib/chef/util/windows/volume.rb
@@ -0,0 +1,59 @@
+#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#simple wrapper around Volume APIs. might be possible with WMI, but possibly more complex.
+
+require 'chef/util/windows'
+require 'windows/volume'
+
+class Chef::Util::Windows::Volume < Chef::Util::Windows
+
+ private
+ include Windows::Volume
+ #XXX not defined in the current windows-pr release
+ DeleteVolumeMountPoint =
+ Windows::API.new('DeleteVolumeMountPoint', 'S', 'B') unless defined? DeleteVolumeMountPoint
+
+ public
+
+ def initialize(name)
+ name += "\\" unless name =~ /\\$/ #trailing slash required
+ @name = name
+ end
+
+ def device
+ buffer = 0.chr * 256
+ if GetVolumeNameForVolumeMountPoint(@name, buffer, buffer.size)
+ return buffer[0,buffer.size].unpack("Z*")[0]
+ else
+ raise ArgumentError, get_last_error
+ end
+ end
+
+ def delete
+ unless DeleteVolumeMountPoint.call(@name)
+ raise ArgumentError, get_last_error
+ end
+ end
+
+ def add(device)
+ unless SetVolumeMountPoint(@name, device)
+ raise ArgumentError, get_last_error
+ end
+ end
+end
diff --git a/lib/chef/version.rb b/lib/chef/version.rb
new file mode 100644
index 0000000000..929466a35a
--- /dev/null
+++ b/lib/chef/version.rb
@@ -0,0 +1,23 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+ CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__)))
+ VERSION = '11.0.0.alpha'
+end
+
+# NOTE: the Chef::Version class is defined in version_class.rb
diff --git a/lib/chef/version_class.rb b/lib/chef/version_class.rb
new file mode 100644
index 0000000000..e9879274ad
--- /dev/null
+++ b/lib/chef/version_class.rb
@@ -0,0 +1,70 @@
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+ class Version
+ include Comparable
+ attr_reader :major, :minor, :patch
+
+ def initialize(str="")
+ parse(str)
+ end
+
+ def inspect
+ "#{@major}.#{@minor}.#{@patch}"
+ end
+
+ def to_s
+ "#{@major}.#{@minor}.#{@patch}"
+ end
+
+ def <=>(v)
+ [:major, :minor, :patch].each do |method|
+ ans = (self.send(method) <=> v.send(method))
+ return ans if ans != 0
+ end
+ 0
+ end
+
+ def hash
+ # Didn't put any thought or research into this, probably can be
+ # done better
+ to_s.hash
+ end
+
+ # For hash
+ def eql?(other)
+ other.is_a?(Version) && self == other
+ end
+
+ private
+
+ def parse(str="")
+ @major, @minor, @patch =
+ case str.to_s
+ when /^(\d+)\.(\d+)\.(\d+)$/
+ [ $1.to_i, $2.to_i, $3.to_i ]
+ when /^(\d+)\.(\d+)$/
+ [ $1.to_i, $2.to_i, 0 ]
+ else
+ msg = "'#{str.to_s}' does not match 'x.y.z' or 'x.y'"
+ raise Chef::Exceptions::InvalidCookbookVersion.new( msg )
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/version_constraint.rb b/lib/chef/version_constraint.rb
new file mode 100644
index 0000000000..b6f1f08757
--- /dev/null
+++ b/lib/chef/version_constraint.rb
@@ -0,0 +1,116 @@
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright 2010-2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+require 'chef/version_class'
+
+class Chef
+ class VersionConstraint
+ DEFAULT_CONSTRAINT = ">= 0.0.0"
+ STANDARD_OPS = %w(< > <= >=)
+ OPS = %w(< > = <= >= ~>)
+ PATTERN = /^(#{OPS.join('|')}) (.+)$/
+
+ attr_reader :op, :version
+
+ def initialize(constraint_spec=DEFAULT_CONSTRAINT)
+ case constraint_spec
+ when nil
+ parse(DEFAULT_CONSTRAINT)
+ when Array
+ parse_from_array(constraint_spec)
+ when String
+ parse(constraint_spec)
+ else
+ msg = "VersionConstraint should be created from a String. You gave: #{constraint_spec.inspect}"
+ raise Chef::Exceptions::InvalidVersionConstraint, msg
+ end
+ end
+
+ def include?(v)
+ version = if v.respond_to? :version # a CookbookVersion-like object
+ Chef::Version.new(v.version.to_s)
+ else
+ Chef::Version.new(v.to_s)
+ end
+ do_op(version)
+ end
+
+ def inspect
+ "(#{@op} #{@version})"
+ end
+
+ def to_s
+ "#{@op} #{@version}"
+ end
+
+ def eql?(o)
+ o.class == self.class && @op == o.op && @version == o.version
+ end
+ alias_method :==, :eql?
+
+ private
+
+ def do_op(other_version)
+ if STANDARD_OPS.include? @op
+ other_version.send(@op.to_sym, @version)
+ elsif @op == '='
+ other_version == @version
+ elsif @op == '~>'
+ if @missing_patch_level
+ (other_version.major == @version.major &&
+ other_version.minor >= @version.minor)
+ else
+ (other_version.major == @version.major &&
+ other_version.minor == @version.minor &&
+ other_version.patch >= @version.patch)
+ end
+ else # should never happen
+ raise "bad op #{@op}"
+ end
+ end
+
+ def parse_from_array(constraint_spec)
+ if constraint_spec.empty?
+ parse(DEFAULT_CONSTRAINT)
+ elsif constraint_spec.size == 1
+ parse(constraint_spec.first)
+ else
+ msg = "only one version constraint operation is supported, but you gave #{constraint_spec.size} "
+ msg << "['#{constraint_spec.join(', ')}']"
+ raise Chef::Exceptions::InvalidVersionConstraint, msg
+ end
+ end
+
+ def parse(str)
+ @missing_patch_level = false
+ if str.index(" ").nil? && str =~ /^[0-9]/
+ # try for lone version, implied '='
+ @version = Chef::Version.new(str)
+ @op = "="
+ elsif PATTERN.match str
+ @op = $1
+ raw_version = $2
+ @version = Chef::Version.new(raw_version)
+ if raw_version.split('.').size == 2
+ @missing_patch_level = true
+ end
+ else
+ raise Chef::Exceptions::InvalidVersionConstraint, "'#{str}'"
+ end
+ end
+
+ end
+end
diff --git a/lib/chef/win32/api.rb b/lib/chef/win32/api.rb
new file mode 100644
index 0000000000..0330810b3b
--- /dev/null
+++ b/lib/chef/win32/api.rb
@@ -0,0 +1,364 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'ffi'
+require 'chef/reserved_names'
+require 'chef/exceptions'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+
+ # Attempts to use FFI's attach_function method to link a native Win32
+ # function into the calling module. If this fails a dummy method is
+ # defined which when called, raises a helpful exception to the end-user.
+ def safe_attach_function(win32_func, *args)
+ begin
+ attach_function(win32_func.to_sym, *args)
+ rescue FFI::NotFoundError
+ define_method(win32_func.to_sym) do |*margs|
+ raise Chef::Exceptions::Win32APIFunctionNotImplemented, "This version of Windows does not implement the Win32 function [#{win32_func}]."
+ end
+ end
+ end
+
+ # put shared stuff (like constants) for all raw Win32 API calls
+ def self.extended(host)
+ host.extend FFI::Library
+ host.extend Macros
+
+ host.ffi_convention :stdcall
+
+ # Windows-specific type defs (ms-help://MS.MSDNQTR.v90.en/winprog/winprog/windows_data_types.htm):
+ host.typedef :ushort, :ATOM # Atom ~= Symbol: Atom table stores strings and corresponding identifiers. Application
+ # places a string in an atom table and receives a 16-bit integer, called an atom, that
+ # can be used to access the string. Placed string is called an atom name.
+ # See: http://msdn.microsoft.com/en-us/library/ms648708%28VS.85%29.aspx
+ host.typedef :bool, :BOOL
+ host.typedef :bool, :BOOLEAN
+ host.typedef :uchar, :BYTE # Byte (8 bits). Declared as unsigned char
+ #CALLBACK: K, # Win32.API gem-specific ?? MSDN: #define CALLBACK __stdcall
+ host.typedef :char, :CHAR # 8-bit Windows (ANSI) character. See http://msdn.microsoft.com/en-us/library/dd183415%28VS.85%29.aspx
+ host.typedef :uint32, :COLORREF # Red, green, blue (RGB) color value (32 bits). See COLORREF for more info.
+ host.typedef :uint32, :DWORD # 32-bit unsigned integer. The range is 0 through 4,294,967,295 decimal.
+ host.typedef :uint64, :DWORDLONG # 64-bit unsigned integer. The range is 0 through 18,446,744,073,709,551,615 decimal.
+ host.typedef :ulong, :DWORD_PTR # Unsigned long type for pointer precision. Use when casting a pointer to a long type
+ # to perform pointer arithmetic. (Also commonly used for general 32-bit parameters that have
+ # been extended to 64 bits in 64-bit Windows.) BaseTsd.h: #host.typedef ULONG_PTR DWORD_PTR;
+ host.typedef :uint32, :DWORD32
+ host.typedef :uint64, :DWORD64
+ host.typedef :int, :HALF_PTR # Half the size of a pointer. Use within a structure that contains a pointer and two small fields.
+ # BaseTsd.h: #ifdef (_WIN64) host.typedef int HALF_PTR; #else host.typedef short HALF_PTR;
+ host.typedef :ulong, :HACCEL # (L) Handle to an accelerator table. WinDef.h: #host.typedef HANDLE HACCEL;
+ # See http://msdn.microsoft.com/en-us/library/ms645526%28VS.85%29.aspx
+ host.typedef :ulong, :HANDLE # (L) Handle to an object. WinNT.h: #host.typedef PVOID HANDLE;
+ # todo: Platform-dependent! Need to change to :uint64 for Win64
+ host.typedef :ulong, :HBITMAP # (L) Handle to a bitmap: http://msdn.microsoft.com/en-us/library/dd183377%28VS.85%29.aspx
+ host.typedef :ulong, :HBRUSH # (L) Handle to a brush. http://msdn.microsoft.com/en-us/library/dd183394%28VS.85%29.aspx
+ host.typedef :ulong, :HCOLORSPACE # (L) Handle to a color space. http://msdn.microsoft.com/en-us/library/ms536546%28VS.85%29.aspx
+ host.typedef :ulong, :HCURSOR # (L) Handle to a cursor. http://msdn.microsoft.com/en-us/library/ms646970%28VS.85%29.aspx
+ host.typedef :ulong, :HCONV # (L) Handle to a dynamic data exchange (DDE) conversation.
+ host.typedef :ulong, :HCONVLIST # (L) Handle to a DDE conversation list. HANDLE - L ?
+ host.typedef :ulong, :HDDEDATA # (L) Handle to DDE data (structure?)
+ host.typedef :ulong, :HDC # (L) Handle to a device context (DC). http://msdn.microsoft.com/en-us/library/dd183560%28VS.85%29.aspx
+ host.typedef :ulong, :HDESK # (L) Handle to a desktop. http://msdn.microsoft.com/en-us/library/ms682573%28VS.85%29.aspx
+ host.typedef :ulong, :HDROP # (L) Handle to an internal drop structure.
+ host.typedef :ulong, :HDWP # (L) Handle to a deferred window position structure.
+ host.typedef :ulong, :HENHMETAFILE #(L) Handle to an enhanced metafile. http://msdn.microsoft.com/en-us/library/dd145051%28VS.85%29.aspx
+ host.typedef :uint, :HFILE # (I) Special file handle to a file opened by OpenFile, not CreateFile.
+ # WinDef.h: #host.typedef int HFILE;
+ host.typedef :ulong, :HFONT # (L) Handle to a font. http://msdn.microsoft.com/en-us/library/dd162470%28VS.85%29.aspx
+ host.typedef :ulong, :HGDIOBJ # (L) Handle to a GDI object.
+ host.typedef :ulong, :HGLOBAL # (L) Handle to a global memory block.
+ host.typedef :ulong, :HHOOK # (L) Handle to a hook. http://msdn.microsoft.com/en-us/library/ms632589%28VS.85%29.aspx
+ host.typedef :ulong, :HICON # (L) Handle to an icon. http://msdn.microsoft.com/en-us/library/ms646973%28VS.85%29.aspx
+ host.typedef :ulong, :HINSTANCE # (L) Handle to an instance. This is the base address of the module in memory.
+ # HMODULE and HINSTANCE are the same today, but were different in 16-bit Windows.
+ host.typedef :ulong, :HKEY # (L) Handle to a registry key.
+ host.typedef :ulong, :HKL # (L) Input locale identifier.
+ host.typedef :ulong, :HLOCAL # (L) Handle to a local memory block.
+ host.typedef :ulong, :HMENU # (L) Handle to a menu. http://msdn.microsoft.com/en-us/library/ms646977%28VS.85%29.aspx
+ host.typedef :ulong, :HMETAFILE # (L) Handle to a metafile. http://msdn.microsoft.com/en-us/library/dd145051%28VS.85%29.aspx
+ host.typedef :ulong, :HMODULE # (L) Handle to an instance. Same as HINSTANCE today, but was different in 16-bit Windows.
+ host.typedef :ulong, :HMONITOR # (L) Рandle to a display monitor. WinDef.h: if(WINVER >= 0x0500) host.typedef HANDLE HMONITOR;
+ host.typedef :ulong, :HPALETTE # (L) Handle to a palette.
+ host.typedef :ulong, :HPEN # (L) Handle to a pen. http://msdn.microsoft.com/en-us/library/dd162786%28VS.85%29.aspx
+ host.typedef :long, :HRESULT # Return code used by COM interfaces. For more info, Structure of the COM Error Codes.
+ # To test an HRESULT value, use the FAILED and SUCCEEDED macros.
+ host.typedef :ulong, :HRGN # (L) Handle to a region. http://msdn.microsoft.com/en-us/library/dd162913%28VS.85%29.aspx
+ host.typedef :ulong, :HRSRC # (L) Handle to a resource.
+ host.typedef :ulong, :HSZ # (L) Handle to a DDE string.
+ host.typedef :ulong, :HWINSTA # (L) Handle to a window station. http://msdn.microsoft.com/en-us/library/ms687096%28VS.85%29.aspx
+ host.typedef :ulong, :HWND # (L) Handle to a window. http://msdn.microsoft.com/en-us/library/ms632595%28VS.85%29.aspx
+ host.typedef :int, :INT # 32-bit signed integer. The range is -2147483648 through 2147483647 decimal.
+ host.typedef :int, :INT_PTR # Signed integer type for pointer precision. Use when casting a pointer to an integer
+ # to perform pointer arithmetic. BaseTsd.h:
+ #if defined(_WIN64) host.typedef __int64 INT_PTR; #else host.typedef int INT_PTR;
+ host.typedef :int32, :INT32 # 32-bit signed integer. The range is -2,147,483,648 through +...647 decimal.
+ host.typedef :int64, :INT64 # 64-bit signed integer. The range is –9,223,372,036,854,775,808 through +...807
+ host.typedef :ushort, :LANGID # Language identifier. For more information, see Locales. WinNT.h: #host.typedef WORD LANGID;
+ # See http://msdn.microsoft.com/en-us/library/dd318716%28VS.85%29.aspx
+ host.typedef :uint32, :LCID # Locale identifier. For more information, see Locales.
+ host.typedef :uint32, :LCTYPE # Locale information type. For a list, see Locale Information Constants.
+ host.typedef :uint32, :LGRPID # Language group identifier. For a list, see EnumLanguageGroupLocales.
+ host.typedef :long, :LONG # 32-bit signed integer. The range is -2,147,483,648 through +...647 decimal.
+ host.typedef :int32, :LONG32 # 32-bit signed integer. The range is -2,147,483,648 through +...647 decimal.
+ host.typedef :int64, :LONG64 # 64-bit signed integer. The range is –9,223,372,036,854,775,808 through +...807
+ host.typedef :int64, :LONGLONG # 64-bit signed integer. The range is –9,223,372,036,854,775,808 through +...807
+ host.typedef :long, :LONG_PTR # Signed long type for pointer precision. Use when casting a pointer to a long to
+ # perform pointer arithmetic. BaseTsd.h:
+ #if defined(_WIN64) host.typedef __int64 LONG_PTR; #else host.typedef long LONG_PTR;
+ host.typedef :long, :LPARAM # Message parameter. WinDef.h as follows: #host.typedef LONG_PTR LPARAM;
+ host.typedef :pointer, :LPBOOL # Pointer to a BOOL. WinDef.h as follows: #host.typedef BOOL far *LPBOOL;
+ host.typedef :pointer, :LPBYTE # Pointer to a BYTE. WinDef.h as follows: #host.typedef BYTE far *LPBYTE;
+ host.typedef :pointer, :LPCOLORREF # Pointer to a COLORREF value. WinDef.h as follows: #host.typedef DWORD *LPCOLORREF;
+ host.typedef :pointer, :LPCSTR # Pointer to a constant null-terminated string of 8-bit Windows (ANSI) characters.
+ # See Character Sets Used By Fonts. http://msdn.microsoft.com/en-us/library/dd183415%28VS.85%29.aspx
+ host.typedef :pointer, :LPCTSTR # An LPCWSTR if UNICODE is defined, an LPCSTR otherwise.
+ host.typedef :pointer, :LPCVOID # Pointer to a constant of any type. WinDef.h as follows: host.typedef CONST void *LPCVOID;
+ host.typedef :pointer, :LPCWSTR # Pointer to a constant null-terminated string of 16-bit Unicode characters.
+ host.typedef :pointer, :LPDWORD # Pointer to a DWORD. WinDef.h as follows: host.typedef DWORD *LPDWORD;
+ host.typedef :pointer, :LPHANDLE # Pointer to a HANDLE. WinDef.h as follows: host.typedef HANDLE *LPHANDLE;
+ host.typedef :pointer, :LPINT # Pointer to an INT.
+ host.typedef :pointer, :LPLONG # Pointer to an LONG.
+ host.typedef :pointer, :LPSECURITY_ATTRIBUTES # Pointer to SECURITY_ATTRIBUTES struct
+ host.typedef :pointer, :LPSTR # Pointer to a null-terminated string of 8-bit Windows (ANSI) characters.
+ host.typedef :pointer, :LPTSTR # An LPWSTR if UNICODE is defined, an LPSTR otherwise.
+ host.typedef :pointer, :LPVOID # Pointer to any type.
+ host.typedef :pointer, :LPWORD # Pointer to a WORD.
+ host.typedef :pointer, :LPWSTR # Pointer to a null-terminated string of 16-bit Unicode characters.
+ host.typedef :long, :LRESULT # Signed result of message processing. WinDef.h: host.typedef LONG_PTR LRESULT;
+ host.typedef :pointer, :LPWIN32_FIND_DATA # Pointer to WIN32_FIND_DATA struct
+ host.typedef :pointer, :LPBY_HANDLE_FILE_INFORMATION # Point to a BY_HANDLE_FILE_INFORMATION struct
+ host.typedef :pointer, :PBOOL # Pointer to a BOOL.
+ host.typedef :pointer, :PBOOLEAN # Pointer to a BOOL.
+ host.typedef :pointer, :PBYTE # Pointer to a BYTE.
+ host.typedef :pointer, :PCHAR # Pointer to a CHAR.
+ host.typedef :pointer, :PCSTR # Pointer to a constant null-terminated string of 8-bit Windows (ANSI) characters.
+ host.typedef :pointer, :PCTSTR # A PCWSTR if UNICODE is defined, a PCSTR otherwise.
+ host.typedef :pointer, :PCWSTR # Pointer to a constant null-terminated string of 16-bit Unicode characters.
+ host.typedef :pointer, :PDWORD # Pointer to a DWORD.
+ host.typedef :pointer, :PDWORDLONG # Pointer to a DWORDLONG.
+ host.typedef :pointer, :PDWORD_PTR # Pointer to a DWORD_PTR.
+ host.typedef :pointer, :PDWORD32 # Pointer to a DWORD32.
+ host.typedef :pointer, :PDWORD64 # Pointer to a DWORD64.
+ host.typedef :pointer, :PFLOAT # Pointer to a FLOAT.
+ host.typedef :pointer, :PHALF_PTR # Pointer to a HALF_PTR.
+ host.typedef :pointer, :PHANDLE # Pointer to a HANDLE.
+ host.typedef :pointer, :PHKEY # Pointer to an HKEY.
+ host.typedef :pointer, :PINT # Pointer to an INT.
+ host.typedef :pointer, :PINT_PTR # Pointer to an INT_PTR.
+ host.typedef :pointer, :PINT32 # Pointer to an INT32.
+ host.typedef :pointer, :PINT64 # Pointer to an INT64.
+ host.typedef :pointer, :PLCID # Pointer to an LCID.
+ host.typedef :pointer, :PLONG # Pointer to a LONG.
+ host.typedef :pointer, :PLONGLONG # Pointer to a LONGLONG.
+ host.typedef :pointer, :PLONG_PTR # Pointer to a LONG_PTR.
+ host.typedef :pointer, :PLONG32 # Pointer to a LONG32.
+ host.typedef :pointer, :PLONG64 # Pointer to a LONG64.
+ host.typedef :pointer, :PLUID # Pointer to a LUID.
+ host.typedef :pointer, :POINTER_32 # 32-bit pointer. On a 32-bit system, this is a native pointer. On a 64-bit system, this is a truncated 64-bit pointer.
+ host.typedef :pointer, :POINTER_64 # 64-bit pointer. On a 64-bit system, this is a native pointer. On a 32-bit system, this is a sign-extended 32-bit pointer.
+ host.typedef :pointer, :POINTER_SIGNED # A signed pointer.
+ host.typedef :pointer, :POINTER_UNSIGNED # An unsigned pointer.
+ host.typedef :pointer, :PSHORT # Pointer to a SHORT.
+ host.typedef :pointer, :PSIZE_T # Pointer to a SIZE_T.
+ host.typedef :pointer, :PSSIZE_T # Pointer to a SSIZE_T.
+ host.typedef :pointer, :PSTR # Pointer to a null-terminated string of 8-bit Windows (ANSI) characters. For more information, see Character Sets Used By Fonts.
+ host.typedef :pointer, :PTBYTE # Pointer to a TBYTE.
+ host.typedef :pointer, :PTCHAR # Pointer to a TCHAR.
+ host.typedef :pointer, :PTSTR # A PWSTR if UNICODE is defined, a PSTR otherwise.
+ host.typedef :pointer, :PUCHAR # Pointer to a UCHAR.
+ host.typedef :pointer, :PUHALF_PTR # Pointer to a UHALF_PTR.
+ host.typedef :pointer, :PUINT # Pointer to a UINT.
+ host.typedef :pointer, :PUINT_PTR # Pointer to a UINT_PTR.
+ host.typedef :pointer, :PUINT32 # Pointer to a UINT32.
+ host.typedef :pointer, :PUINT64 # Pointer to a UINT64.
+ host.typedef :pointer, :PULONG # Pointer to a ULONG.
+ host.typedef :pointer, :PULONGLONG # Pointer to a ULONGLONG.
+ host.typedef :pointer, :PULONG_PTR # Pointer to a ULONG_PTR.
+ host.typedef :pointer, :PULONG32 # Pointer to a ULONG32.
+ host.typedef :pointer, :PULONG64 # Pointer to a ULONG64.
+ host.typedef :pointer, :PUSHORT # Pointer to a USHORT.
+ host.typedef :pointer, :PVOID # Pointer to any type.
+ host.typedef :pointer, :PWCHAR # Pointer to a WCHAR.
+ host.typedef :pointer, :PWORD # Pointer to a WORD.
+ host.typedef :pointer, :PWSTR # Pointer to a null- terminated string of 16-bit Unicode characters.
+ # For more information, see Character Sets Used By Fonts.
+ host.typedef :ulong, :SC_HANDLE # (L) Handle to a service control manager database.
+ # See SCM Handles http://msdn.microsoft.com/en-us/library/ms685104%28VS.85%29.aspx
+ host.typedef :pointer, :SC_LOCK # Lock to a service control manager database. For more information, see SCM Handles.
+ host.typedef :ulong, :SERVICE_STATUS_HANDLE # (L) Handle to a service status value. See SCM Handles.
+ host.typedef :short, :SHORT # A 16-bit integer. The range is –32768 through 32767 decimal.
+ host.typedef :ulong, :SIZE_T # The maximum number of bytes to which a pointer can point. Use for a count that must span the full range of a pointer.
+ host.typedef :long, :SSIZE_T # Signed SIZE_T.
+ host.typedef :char, :TBYTE # A WCHAR if UNICODE is defined, a CHAR otherwise.TCHAR:
+ # http://msdn.microsoft.com/en-us/library/c426s321%28VS.80%29.aspx
+ host.typedef :char, :TCHAR # A WCHAR if UNICODE is defined, a CHAR otherwise.TCHAR:
+ host.typedef :uchar, :UCHAR # Unsigned CHAR (8 bit)
+ host.typedef :uint, :UHALF_PTR # Unsigned HALF_PTR. Use within a structure that contains a pointer and two small fields.
+ host.typedef :uint, :UINT # Unsigned INT. The range is 0 through 4294967295 decimal.
+ host.typedef :uint, :UINT_PTR # Unsigned INT_PTR.
+ host.typedef :uint32, :UINT32 # Unsigned INT32. The range is 0 through 4294967295 decimal.
+ host.typedef :uint64, :UINT64 # Unsigned INT64. The range is 0 through 18446744073709551615 decimal.
+ host.typedef :ulong, :ULONG # Unsigned LONG. The range is 0 through 4294967295 decimal.
+ host.typedef :ulong_long, :ULONGLONG # 64-bit unsigned integer. The range is 0 through 18446744073709551615 decimal.
+ host.typedef :ulong, :ULONG_PTR # Unsigned LONG_PTR.
+ host.typedef :uint32, :ULONG32 # Unsigned INT32. The range is 0 through 4294967295 decimal.
+ host.typedef :uint64, :ULONG64 # Unsigned LONG64. The range is 0 through 18446744073709551615 decimal.
+ host.typedef :pointer, :UNICODE_STRING # Pointer to some string structure??
+ host.typedef :ushort, :USHORT # Unsigned SHORT. The range is 0 through 65535 decimal.
+ host.typedef :ulong_long, :USN # Update sequence number (USN).
+ host.typedef :ushort, :WCHAR # 16-bit Unicode character. For more information, see Character Sets Used By Fonts.
+ # In WinNT.h: host.typedef wchar_t WCHAR;
+ #WINAPI: K, # Calling convention for system functions. WinDef.h: define WINAPI __stdcall
+ host.typedef :ushort, :WORD # 16-bit unsigned integer. The range is 0 through 65535 decimal.
+ host.typedef :uint, :WPARAM # Message parameter. WinDef.h as follows: host.typedef UINT_PTR WPARAM;
+ end
+
+ module Macros
+
+ ###############################################
+ # winbase.h
+ ###############################################
+
+ def LocalDiscard(pointer)
+ LocalReAlloc(pointer, 0, LMEM_MOVEABLE)
+ end
+
+ ###############################################
+ # windef.h
+ ###############################################
+
+ # Creates a WORD value by concatenating the specified values.
+ #
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms632663(v=VS.85).aspx
+ def MAKEWORD(low, high)
+ ((low & 0xff) | (high & 0xff)) << 8
+ end
+
+ # Creates a LONG value by concatenating the specified values.
+ #
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms632660(v=vs.85).aspx
+ def MAKELONG(low, high)
+ ((low & 0xffff) | (high & 0xffff)) << 16
+ end
+
+ # Retrieves the low-order word from the specified value.
+ #
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms632659(v=VS.85).aspx
+ def LOWORD(l)
+ l & 0xffff
+ end
+
+ # Retrieves the high-order word from the specified 32-bit value.
+ #
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms632657(v=VS.85).aspx
+ def HIWORD(l)
+ l >> 16
+ end
+
+ # Retrieves the low-order byte from the specified value.
+ #
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms632658(v=VS.85).aspx
+ def LOBYTE(w)
+ w & 0xff
+ end
+
+ # Retrieves the high-order byte from the given 16-bit value.
+ #
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms632656(v=VS.85).aspx
+ def HIBYTE(w)
+ w >> 8
+ end
+
+ ###############################################
+ # winerror.h
+ ###############################################
+
+ def IS_ERROR(status)
+ status >> 31 == 1
+ end
+
+ def MAKE_HRESULT(sev, fac, code)
+ sev << 31 | fac << 16 | code
+ end
+
+ def MAKE_SCODE(sev, fac, code)
+ sev << 31 | fac << 16 | code
+ end
+
+ def HRESULT_CODE(hr)
+ hr & 0xFFFF
+ end
+
+ def HRESULT_FACILITY(hr)
+ (hr >> 16) & 0x1fff
+ end
+
+ def HRESULT_FROM_NT(x)
+ x | 0x10000000 # FACILITY_NT_BIT
+ end
+
+ def HRESULT_FROM_WIN32(x)
+ if x <= 0
+ x
+ else
+ (x & 0x0000FFFF) | (7 << 16) | 0x80000000
+ end
+ end
+
+ def HRESULT_SEVERITY(hr)
+ (hr >> 31) & 0x1
+ end
+
+ def FAILED(status)
+ status < 0
+ end
+
+ def SUCCEEDED(status)
+ status >= 0
+ end
+ end
+
+ # Represents a 64-bit unsigned integer value.
+ #
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/aa383742(v=vs.85).aspx
+ def make_uint64(low, high)
+ low + (high * (2**32))
+ end
+
+ # http://blogs.msdn.com/b/oldnewthing/archive/2009/03/06/9461176.aspx
+ # January 1, 1601
+ WIN32_EPOC_MINUS_POSIX_EPOC = 116444736000000000
+
+ # Convert 64-bit FILETIME integer into Time object.
+ #
+ # FILETIME structure contains a 64-bit value representing the number
+ # of 100-nanosecond intervals since January 1, 1601 (UTC).
+ #
+ # http://msdn.microsoft.com/en-us/library/ms724284(VS.85).aspx
+ #
+ def wtime_to_time(wtime)
+ Time.at((wtime - WIN32_EPOC_MINUS_POSIX_EPOC) / 10000000)
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/win32/api/error.rb b/lib/chef/win32/api/error.rb
new file mode 100644
index 0000000000..d1f9a309fe
--- /dev/null
+++ b/lib/chef/win32/api/error.rb
@@ -0,0 +1,921 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module Error
+ extend Chef::ReservedNames::Win32::API
+
+ ###############################################
+ # Win32 API Constants
+ ###############################################
+
+ S_OK = 0
+ NO_ERROR = 0
+ ERROR_SUCCESS = 0
+ ERROR_INVALID_FUNCTION = 1
+ ERROR_FILE_NOT_FOUND = 2
+ ERROR_PATH_NOT_FOUND = 3
+ ERROR_TOO_MANY_OPEN_FILES = 4
+ ERROR_ACCESS_DENIED = 5
+ ERROR_INVALID_HANDLE = 6
+ ERROR_ARENA_TRASHED = 7
+ ERROR_NOT_ENOUGH_MEMORY = 8
+ ERROR_INVALID_BLOCK = 9
+ ERROR_BAD_ENVIRONMENT = 10
+ ERROR_BAD_FORMAT = 11
+ ERROR_INVALID_ACCESS = 12
+ ERROR_INVALID_DATA = 13
+ ERROR_INVALID_DRIVE = 15
+ ERROR_CURRENT_DIRECTORY = 16
+ ERROR_NOT_SAME_DEVICE = 17
+ ERROR_NO_MORE_FILES = 18
+ ERROR_WRITE_PROTECT = 19
+ ERROR_BAD_UNIT = 20
+ ERROR_NOT_READY = 21
+ ERROR_BAD_COMMAND = 22
+ ERROR_CRC = 23
+ ERROR_BAD_LENGTH = 24
+ ERROR_SEEK = 25
+ ERROR_NOT_DOS_DISK = 26
+ ERROR_SECTOR_NOT_FOUND = 27
+ ERROR_OUT_OF_PAPER = 28
+ ERROR_WRITE_FAULT = 29
+ ERROR_READ_FAULT = 30
+ ERROR_GEN_FAILURE = 31
+ ERROR_SHARING_VIOLATION = 32
+ ERROR_LOCK_VIOLATION = 33
+ ERROR_WRONG_DISK = 34
+ ERROR_FCB_UNAVAILABLE = 35 # gets returned for some unsuccessful DeviceIoControl calls
+ ERROR_SHARING_BUFFER_EXCEEDED = 36
+ ERROR_HANDLE_EOF = 38
+ ERROR_HANDLE_DISK_FULL = 39
+
+ ERROR_NOT_SUPPORTED = 50
+ ERROR_REM_NOT_LIST = 51
+ ERROR_DUP_NAME = 52
+ ERROR_BAD_NETPATH = 53
+ ERROR_NETWORK_BUSY = 54
+ ERROR_DEV_NOT_EXIST = 55
+ ERROR_TOO_MANY_CMDS = 56
+ ERROR_ADAP_HDW_ERR = 57
+ ERROR_BAD_NET_RESP = 58
+ ERROR_UNEXP_NET_ERR = 59
+ ERROR_BAD_REM_ADAP = 60
+ ERROR_PRINTQ_FULL = 61
+ ERROR_NO_SPOOL_SPACE = 62
+ ERROR_PRINT_CANCELLED = 63
+ ERROR_NETNAME_DELETED = 64
+ ERROR_NETWORK_ACCESS_DENIED = 65
+ ERROR_BAD_DEV_TYPE = 66
+ ERROR_BAD_NET_NAME = 67
+ ERROR_TOO_MANY_NAMES = 68
+ ERROR_TOO_MANY_SESS = 69
+ ERROR_SHARING_PAUSED = 70
+ ERROR_REQ_NOT_ACCEP = 71
+ ERROR_REDIR_PAUSED = 72
+
+ ERROR_FILE_EXISTS = 80
+ ERROR_DUP_FCB = 81
+ ERROR_CANNOT_MAKE = 82
+ ERROR_FAIL_I24 = 83
+ ERROR_OUT_OF_STRUCTURES = 84
+ ERROR_ALREADY_ASSIGNED = 85
+ ERROR_INVALID_PASSWORD = 86
+ ERROR_INVALID_PARAMETER = 87
+ ERROR_NET_WRITE_FAULT = 88
+ ERROR_NO_PROC_SLOTS = 89 # no process slots available
+ ERROR_NOT_FROZEN = 90
+ ERR_TSTOVFL = 91 # timer service table overflow
+ ERR_TSTDUP = 92 # timer service table duplicate
+ ERROR_NO_ITEMS = 93 # There were no items to operate upon
+ ERROR_INTERRUPT = 95 # interrupted system call
+
+ ERROR_TOO_MANY_SEMAPHORES = 100
+ ERROR_EXCL_SEM_ALREADY_OWNED = 101
+ ERROR_SEM_IS_SET = 102
+ ERROR_TOO_MANY_SEM_REQUESTS = 103
+ ERROR_INVALID_AT_INTERRUPT_TIME = 104
+ ERROR_SEM_OWNER_DIED = 105 # waitsem found owner died
+ ERROR_SEM_USER_LIMIT = 106 # too many procs have this sem
+ ERROR_DISK_CHANGE = 107 # insert disk b into drive a
+ ERROR_DRIVE_LOCKED = 108 # drive locked by another process
+ ERROR_BROKEN_PIPE = 109 # write on pipe with no reader
+ ERROR_OPEN_FAILED = 110 # open/created failed
+ ERROR_DISK_FULL = 112 # not enough space
+ ERROR_NO_MORE_SEARCH_HANDLES = 113 # can't allocate
+ ERROR_INVALID_TARGET_HANDLE = 114 # handle in DOSDUPHANDLE is invalid
+ ERROR_PROTECTION_VIOLATION = 115 # bad user virtual address
+ ERROR_VIOKBD_REQUEST = 116
+ ERROR_INVALID_CATEGORY = 117 # category for DEVIOCTL not defined
+ ERROR_INVALID_VERIFY_SWITCH = 118 # invalid value
+ ERROR_BAD_DRIVER_LEVEL = 119 # DosDevIOCTL not level four
+ ERROR_CALL_NOT_IMPLEMENTED = 120
+ ERROR_SEM_TIMEOUT = 121 # timeout from semaphore function
+ ERROR_INSUFFICIENT_BUFFER = 122
+ ERROR_INVALID_NAME = 123 # illegal char or malformed file system name
+ ERROR_INVALID_LEVEL = 124 # unimplemented level for info retrieval
+ ERROR_NO_VOLUME_LABEL = 125 # no volume label found
+ ERROR_MOD_NOT_FOUND = 126 # w_getprocaddr, w_getmodhandle
+ ERROR_PROC_NOT_FOUND = 127 # w_getprocaddr
+ ERROR_WAIT_NO_CHILDREN = 128 # CWait finds to children
+ ERROR_CHILD_NOT_COMPLETE = 129 # CWait children not dead yet
+ ERROR_DIRECT_ACCESS_HANDLE = 130 # invalid for direct disk access
+ ERROR_NEGATIVE_SEEK = 131 # tried to seek negative offset
+ ERROR_SEEK_ON_DEVICE = 132 # tried to seek on device or pipe
+ ERROR_IS_JOIN_TARGET = 133
+ ERROR_IS_JOINED = 134
+ ERROR_IS_SUBSTED = 135
+ ERROR_NOT_JOINED = 136
+ ERROR_NOT_SUBSTED = 137
+ ERROR_JOIN_TO_JOIN = 138
+ ERROR_SUBST_TO_SUBST = 139
+ ERROR_JOIN_TO_SUBST = 140
+ ERROR_SUBST_TO_JOIN = 141
+ ERROR_BUSY_DRIVE = 142
+ ERROR_SAME_DRIVE = 143
+ ERROR_DIR_NOT_ROOT = 144
+ ERROR_DIR_NOT_EMPTY = 145
+ ERROR_IS_SUBST_PATH = 146
+ ERROR_IS_JOIN_PATH = 147
+ ERROR_PATH_BUSY = 148
+ ERROR_IS_SUBST_TARGET = 149
+ ERROR_SYSTEM_TRACE = 150 # system trace error
+ ERROR_INVALID_EVENT_COUNT = 151 # DosMuxSemWait errors
+ ERROR_TOO_MANY_MUXWAITERS = 152
+ ERROR_INVALID_LIST_FORMAT = 153
+ ERROR_LABEL_TOO_LONG = 154
+ ERROR_TOO_MANY_TCBS = 155
+ ERROR_SIGNAL_REFUSED = 156
+ ERROR_DISCARDED = 157
+ ERROR_NOT_LOCKED = 158
+ ERROR_BAD_THREADID_ADDR = 159
+ ERROR_BAD_ARGUMENTS = 160
+ ERROR_BAD_PATHNAME = 161
+ ERROR_SIGNAL_PENDING = 162
+ ERROR_UNCERTAIN_MEDIA = 163
+ ERROR_MAX_THRDS_REACHED = 164
+ ERROR_MONITORS_NOT_SUPPORTED = 165
+
+ ERROR_LOCK_FAILED = 167
+ ERROR_BUSY = 170
+ ERROR_CANCEL_VIOLATION = 173
+ ERROR_ATOMIC_LOCKS_NOT_SUPPORTED= 174
+
+ ERROR_INVALID_SEGMENT_NUMBER = 180
+ ERROR_INVALID_CALLGATE = 181
+ ERROR_INVALID_ORDINAL = 182
+ ERROR_ALREADY_EXISTS = 183
+ ERROR_NO_CHILD_PROCESS = 184
+ ERROR_CHILD_ALIVE_NOWAIT = 185
+ ERROR_INVALID_FLAG_NUMBER = 186
+ ERROR_SEM_NOT_FOUND = 187
+ ERROR_INVALID_STARTING_CODESEG = 188
+ ERROR_INVALID_STACKSEG = 189
+ ERROR_INVALID_MODULETYPE = 190
+ ERROR_INVALID_EXE_SIGNATURE = 191
+ ERROR_EXE_MARKED_INVALID = 192
+ ERROR_BAD_EXE_FORMAT = 193
+ ERROR_ITERATED_DATA_EXCEEDS_64k = 194
+ ERROR_INVALID_MINALLOCSIZE = 195
+ ERROR_DYNLINK_FROM_INVALID_RING = 196
+ ERROR_IOPL_NOT_ENABLED = 197
+ ERROR_INVALID_SEGDPL = 198
+ ERROR_AUTODATASEG_EXCEEDS_64k = 199
+ ERROR_RING2SEG_MUST_BE_MOVABLE = 200
+ ERROR_RELOC_CHAIN_XEEDS_SEGLIM = 201
+ ERROR_INFLOOP_IN_RELOC_CHAIN = 202
+ ERROR_ENVVAR_NOT_FOUND = 203
+ ERROR_NOT_CURRENT_CTRY = 204
+ ERROR_NO_SIGNAL_SENT = 205
+ ERROR_FILENAME_EXCED_RANGE = 206 # if filename > 8.3
+ ERROR_RING2_STACK_IN_USE = 207 # for FAPI
+ ERROR_META_EXPANSION_TOO_LONG = 208 # if "*a" > 8.3
+ ERROR_INVALID_SIGNAL_NUMBER = 209
+ ERROR_THREAD_1_INACTIVE = 210
+ ERROR_INFO_NOT_AVAIL = 211 #@@ PTM 5550
+ ERROR_LOCKED = 212
+ ERROR_BAD_DYNALINK = 213 #@@ PTM 5760
+ ERROR_TOO_MANY_MODULES = 214
+ ERROR_NESTING_NOT_ALLOWED = 215
+ ERROR_EXE_MACHINE_TYPE_MISMATCH = 216
+
+ ERROR_BAD_PIPE = 230
+ ERROR_PIPE_BUSY = 231
+ ERROR_NO_DATA = 232
+ ERROR_PIPE_NOT_CONNECTED = 233
+ ERROR_MORE_DATA = 234
+
+ ERROR_VC_DISCONNECTED = 240
+ ERROR_INVALID_EA_NAME = 254
+ ERROR_EA_LIST_INCONSISTENT = 255
+ ERROR_NO_MORE_ITEMS = 259
+ ERROR_CANNOT_COPY = 266
+ ERROR_DIRECTORY = 267
+ ERROR_EAS_DIDNT_FIT = 275
+ ERROR_EA_FILE_CORRUPT = 276
+ ERROR_EA_TABLE_FULL = 277
+ ERROR_INVALID_EA_HANDLE = 278
+ ERROR_EAS_NOT_SUPPORTED = 282
+ ERROR_NOT_OWNER = 288
+ ERROR_TOO_MANY_POSTS = 298
+ ERROR_PARTIAL_COPY = 299
+ ERROR_OPLOCK_NOT_GRANTED = 300
+ ERROR_INVALID_OPLOCK_PROTOCOL = 301
+ ERROR_DISK_TOO_FRAGMENTED = 302
+ ERROR_MR_MID_NOT_FOUND = 317
+ ERROR_SCOPE_NOT_FOUND = 318
+ ERROR_FAIL_NOACTION_REBOOT = 350
+ ERROR_FAIL_SHUTDOWN = 351
+ ERROR_FAIL_RESTART = 352
+ ERROR_MAX_SESSIONS_REACHED = 353
+ ERROR_INVALID_ADDRESS = 487
+ ERROR_USER_PROFILE_LOAD = 500
+ ERROR_ARITHMETIC_OVERFLOW = 534
+ ERROR_PIPE_CONNECTED = 535
+ ERROR_PIPE_LISTENING = 536
+
+ ERROR_EA_ACCESS_DENIED = 994
+ ERROR_OPERATION_ABORTED = 995
+ ERROR_IO_INCOMPLETE = 996
+ ERROR_IO_PENDING = 997
+ ERROR_NOACCESS = 998
+ ERROR_SWAPERROR = 999
+
+ ERROR_STACK_OVERFLOW = 1001
+ ERROR_INVALID_MESSAGE = 1002
+ ERROR_CAN_NOT_COMPLETE = 1003
+ ERROR_INVALID_FLAGS = 1004
+ ERROR_UNRECOGNIZED_VOLUME = 1005
+ ERROR_FILE_INVALID = 1006
+ ERROR_FULLSCREEN_MODE = 1007
+ ERROR_NO_TOKEN = 1008
+ ERROR_BADDB = 1009
+ ERROR_BADKEY = 1010
+ ERROR_CANTOPEN = 1011
+ ERROR_CANTREAD = 1012
+ ERROR_CANTWRITE = 1013
+ ERROR_REGISTRY_RECOVERED = 1014
+ ERROR_REGISTRY_CORRUPT = 1015
+ ERROR_REGISTRY_IO_FAILED = 1016
+ ERROR_NOT_REGISTRY_FILE = 1017
+ ERROR_KEY_DELETED = 1018
+ ERROR_NO_LOG_SPACE = 1019
+ ERROR_KEY_HAS_CHILDREN = 1020
+ ERROR_CHILD_MUST_BE_VOLATILE = 1021
+ ERROR_NOTIFY_ENUM_DIR = 1022
+ ERROR_DEPENDENT_SERVICES_RUNNING = 1051
+ ERROR_INVALID_SERVICE_CONTROL = 1052
+ ERROR_SERVICE_REQUEST_TIMEOUT = 1053
+ ERROR_SERVICE_NO_THREAD = 1054
+ ERROR_SERVICE_DATABASE_LOCKED = 1055
+ ERROR_SERVICE_ALREADY_RUNNING = 1056
+ ERROR_INVALID_SERVICE_ACCOUNT = 1057
+ ERROR_SERVICE_DISABLED = 1058
+ ERROR_CIRCULAR_DEPENDENCY = 1059
+ ERROR_SERVICE_DOES_NOT_EXIST = 1060
+ ERROR_SERVICE_CANNOT_ACCEPT_CTRL = 1061
+ ERROR_SERVICE_NOT_ACTIVE = 1062
+ ERROR_FAILED_SERVICE_CONTROLLER_CONNECT = 1063
+ ERROR_EXCEPTION_IN_SERVICE = 1064
+ ERROR_DATABASE_DOES_NOT_EXIST = 1065
+ ERROR_SERVICE_SPECIFIC_ERROR = 1066
+ ERROR_PROCESS_ABORTED = 1067
+ ERROR_SERVICE_DEPENDENCY_FAIL = 1068
+ ERROR_SERVICE_LOGON_FAILED = 1069
+ ERROR_SERVICE_START_HANG = 1070
+ ERROR_INVALID_SERVICE_LOCK = 1071
+ ERROR_SERVICE_MARKED_FOR_DELETE = 1072
+ ERROR_SERVICE_EXISTS = 1073
+ ERROR_ALREADY_RUNNING_LKG = 1074
+ ERROR_SERVICE_DEPENDENCY_DELETED = 1075
+ ERROR_BOOT_ALREADY_ACCEPTED = 1076
+ ERROR_SERVICE_NEVER_STARTED = 1077
+ ERROR_DUPLICATE_SERVICE_NAME = 1078
+ ERROR_DIFFERENT_SERVICE_ACCOUNT = 1079
+ ERROR_CANNOT_DETECT_DRIVER_FAILURE = 1080
+ ERROR_CANNOT_DETECT_PROCESS_ABORT = 1081
+ ERROR_NO_RECOVERY_PROGRAM = 1082
+ ERROR_SERVICE_NOT_IN_EXE = 1083
+ ERROR_END_OF_MEDIA = 1100
+ ERROR_FILEMARK_DETECTED = 1101
+ ERROR_BEGINNING_OF_MEDIA = 1102
+ ERROR_SETMARK_DETECTED = 1103
+ ERROR_NO_DATA_DETECTED = 1104
+ ERROR_PARTITION_FAILURE = 1105
+ ERROR_INVALID_BLOCK_LENGTH = 1106
+ ERROR_DEVICE_NOT_PARTITIONED = 1107
+ ERROR_UNABLE_TO_LOCK_MEDIA = 1108
+ ERROR_UNABLE_TO_UNLOAD_MEDIA = 1109
+ ERROR_MEDIA_CHANGED = 1110
+ ERROR_BUS_RESET = 1111
+ ERROR_NO_MEDIA_IN_DRIVE = 1112
+ ERROR_NO_UNICODE_TRANSLATION = 1113
+ ERROR_DLL_INIT_FAILED = 1114
+ ERROR_SHUTDOWN_IN_PROGRESS = 1115
+ ERROR_NO_SHUTDOWN_IN_PROGRESS = 1116
+ ERROR_IO_DEVICE = 1117
+ ERROR_SERIAL_NO_DEVICE = 1118
+ ERROR_IRQ_BUSY = 1119
+ ERROR_MORE_WRITES = 1120
+ ERROR_COUNTER_TIMEOUT = 1121
+ ERROR_FLOPPY_ID_MARK_NOT_FOUND = 1122
+ ERROR_FLOPPY_WRONG_CYLINDER = 1123
+ ERROR_FLOPPY_UNKNOWN_ERROR = 1124
+ ERROR_FLOPPY_BAD_REGISTERS = 1125
+ ERROR_DISK_RECALIBRATE_FAILED = 1126
+ ERROR_DISK_OPERATION_FAILED = 1127
+ ERROR_DISK_RESET_FAILED = 1128
+ ERROR_EOM_OVERFLOW = 1129
+ ERROR_NOT_ENOUGH_SERVER_MEMORY = 1130
+ ERROR_POSSIBLE_DEADLOCK = 1131
+ ERROR_MAPPED_ALIGNMENT = 1132
+ ERROR_SET_POWER_STATE_VETOED = 1140
+ ERROR_SET_POWER_STATE_FAILED = 1141
+ ERROR_TOO_MANY_LINKS = 1142
+ ERROR_OLD_WIN_VERSION = 1150
+ ERROR_APP_WRONG_OS = 1151
+ ERROR_SINGLE_INSTANCE_APP = 1152
+ ERROR_RMODE_APP = 1153
+ ERROR_INVALID_DLL = 1154
+ ERROR_NO_ASSOCIATION = 1155
+ ERROR_DDE_FAIL = 1156
+ ERROR_DLL_NOT_FOUND = 1157
+ ERROR_NO_MORE_USER_HANDLES = 1158
+ ERROR_MESSAGE_SYNC_ONLY = 1159
+ ERROR_SOURCE_ELEMENT_EMPTY = 1160
+ ERROR_DESTINATION_ELEMENT_FULL = 1161
+ ERROR_ILLEGAL_ELEMENT_ADDRESS = 1162
+ ERROR_MAGAZINE_NOT_PRESENT = 1163
+ ERROR_DEVICE_REINITIALIZATION_NEEDED = 1164
+ ERROR_DEVICE_REQUIRES_CLEANING = 1165
+ ERROR_DEVICE_DOOR_OPEN = 1166
+ ERROR_DEVICE_NOT_CONNECTED = 1167
+ ERROR_NOT_FOUND = 1168
+ ERROR_NO_MATCH = 1169
+ ERROR_SET_NOT_FOUND = 1170
+ ERROR_POINT_NOT_FOUND = 1171
+ ERROR_NO_TRACKING_SERVICE = 1172
+ ERROR_NO_VOLUME_ID = 1173
+ ERROR_UNABLE_TO_REMOVE_REPLACED = 1175
+ ERROR_UNABLE_TO_MOVE_REPLACEMENT = 1176
+ ERROR_UNABLE_TO_MOVE_REPLACEMENT_2 = 1177
+ ERROR_JOURNAL_DELETE_IN_PROGRESS = 1178
+ ERROR_JOURNAL_NOT_ACTIVE = 1179
+ ERROR_POTENTIAL_FILE_FOUND = 1180
+ ERROR_JOURNAL_ENTRY_DELETED = 1181
+ ERROR_BAD_DEVICE = 1200
+ ERROR_CONNECTION_UNAVAIL = 1201
+ ERROR_DEVICE_ALREADY_REMEMBERED = 1202
+ ERROR_NO_NET_OR_BAD_PATH = 1203
+ ERROR_BAD_PROVIDER = 1204
+ ERROR_CANNOT_OPEN_PROFILE = 1205
+ ERROR_BAD_PROFILE = 1206
+ ERROR_NOT_CONTAINER = 1207
+ ERROR_EXTENDED_ERROR = 1208
+ ERROR_INVALID_GROUPNAME = 1209
+ ERROR_INVALID_COMPUTERNAME = 1210
+ ERROR_INVALID_EVENTNAME = 1211
+ ERROR_INVALID_DOMAINNAME = 1212
+ ERROR_INVALID_SERVICENAME = 1213
+ ERROR_INVALID_NETNAME = 1214
+ ERROR_INVALID_SHARENAME = 1215
+ ERROR_INVALID_PASSWORDNAME = 1216
+ ERROR_INVALID_MESSAGENAME = 1217
+ ERROR_INVALID_MESSAGEDEST = 1218
+ ERROR_SESSION_CREDENTIAL_CONFLICT = 1219
+ ERROR_REMOTE_SESSION_LIMIT_EXCEEDED = 1220
+ ERROR_DUP_DOMAINNAME = 1221
+ ERROR_NO_NETWORK = 1222
+ ERROR_CANCELLED = 1223
+ ERROR_USER_MAPPED_FILE = 1224
+ ERROR_CONNECTION_REFUSED = 1225
+ ERROR_GRACEFUL_DISCONNECT = 1226
+ ERROR_ADDRESS_ALREADY_ASSOCIATED = 1227
+ ERROR_ADDRESS_NOT_ASSOCIATED = 1228
+ ERROR_CONNECTION_INVALID = 1229
+ ERROR_CONNECTION_ACTIVE = 1230
+ ERROR_NETWORK_UNREACHABLE = 1231
+ ERROR_HOST_UNREACHABLE = 1232
+ ERROR_PROTOCOL_UNREACHABLE = 1233
+ ERROR_PORT_UNREACHABLE = 1234
+ ERROR_REQUEST_ABORTED = 1235
+ ERROR_CONNECTION_ABORTED = 1236
+ ERROR_RETRY = 1237
+ ERROR_CONNECTION_COUNT_LIMIT = 1238
+ ERROR_LOGIN_TIME_RESTRICTION = 1239
+ ERROR_LOGIN_WKSTA_RESTRICTION = 1240
+ ERROR_INCORRECT_ADDRESS = 1241
+ ERROR_ALREADY_REGISTERED = 1242
+ ERROR_SERVICE_NOT_FOUND = 1243
+ ERROR_NOT_AUTHENTICATED = 1244
+ ERROR_NOT_LOGGED_ON = 1245
+ ERROR_CONTINUE = 1246
+ ERROR_ALREADY_INITIALIZED = 1247
+ ERROR_NO_MORE_DEVICES = 1248
+ ERROR_NO_SUCH_SITE = 1249
+ ERROR_DOMAIN_CONTROLLER_EXISTS = 1250
+ ERROR_ONLY_IF_CONNECTED = 1251
+ ERROR_OVERRIDE_NOCHANGES = 1252
+ ERROR_BAD_USER_PROFILE = 1253
+ ERROR_NOT_SUPPORTED_ON_SBS = 1254
+ ERROR_SERVER_SHUTDOWN_IN_PROGRESS = 1255
+ ERROR_HOST_DOWN = 1256
+ ERROR_ACCESS_DISABLED_BY_POLICY = 1260
+ ERROR_REG_NAT_CONSUMPTION = 1261
+ ERROR_PKINIT_FAILURE = 1263
+ ERROR_SMARTCARD_SUBSYSTEM_FAILURE = 1264
+ ERROR_DOWNGRADE_DETECTED = 1265
+ ERROR_MACHINE_LOCKED = 1271
+ ERROR_CALLBACK_SUPPLIED_INVALID_DATA = 1273
+ ERROR_SYNC_FOREGROUND_REFRESH_REQUIRED= 1274
+ ERROR_DRIVER_BLOCKED = 1275
+ ERROR_INVALID_IMPORT_OF_NON_DLL = 1276
+ ERROR_NOT_ALL_ASSIGNED = 1300
+ ERROR_SOME_NOT_MAPPED = 1301
+ ERROR_NO_QUOTAS_FOR_ACCOUNT = 1302
+ ERROR_LOCAL_USER_SESSION_KEY = 1303
+ ERROR_NULL_LM_PASSWORD = 1304
+ ERROR_UNKNOWN_REVISION = 1305
+ ERROR_REVISION_MISMATCH = 1306
+ ERROR_INVALID_OWNER = 1307
+ ERROR_INVALID_PRIMARY_GROUP = 1308
+ ERROR_NO_IMPERSONATION_TOKEN = 1309
+ ERROR_CANT_DISABLE_MANDATORY = 1310
+ ERROR_NO_LOGON_SERVERS = 1311
+ ERROR_NO_SUCH_LOGON_SESSION = 1312
+ ERROR_NO_SUCH_PRIVILEGE = 1313
+ ERROR_PRIVILEGE_NOT_HELD = 1314
+ ERROR_INVALID_ACCOUNT_NAME = 1315
+ ERROR_USER_EXISTS = 1316
+ ERROR_NO_SUCH_USER = 1317
+ ERROR_GROUP_EXISTS = 1318
+ ERROR_NO_SUCH_GROUP = 1319
+ ERROR_MEMBER_IN_GROUP = 1320
+ ERROR_MEMBER_NOT_IN_GROUP = 1321
+ ERROR_LAST_ADMIN = 1322
+ ERROR_WRONG_PASSWORD = 1323
+ ERROR_ILL_FORMED_PASSWORD = 1324
+ ERROR_PASSWORD_RESTRICTION = 1325
+ ERROR_LOGON_FAILURE = 1326
+ ERROR_ACCOUNT_RESTRICTION = 1327
+ ERROR_INVALID_LOGON_HOURS = 1328
+ ERROR_INVALID_WORKSTATION = 1329
+ ERROR_PASSWORD_EXPIRED = 1330
+ ERROR_ACCOUNT_DISABLED = 1331
+ ERROR_NONE_MAPPED = 1332
+ ERROR_TOO_MANY_LUIDS_REQUESTED = 1333
+ ERROR_LUIDS_EXHAUSTED = 1334
+ ERROR_INVALID_SUB_AUTHORITY = 1335
+ ERROR_INVALID_ACL = 1336
+ ERROR_INVALID_SID = 1337
+ ERROR_INVALID_SECURITY_DESCR = 1338
+ ERROR_BAD_INHERITANCE_ACL = 1340
+ ERROR_SERVER_DISABLED = 1341
+ ERROR_SERVER_NOT_DISABLED = 1342
+ ERROR_INVALID_ID_AUTHORITY = 1343
+ ERROR_ALLOTTED_SPACE_EXCEEDED = 1344
+ ERROR_INVALID_GROUP_ATTRIBUTES = 1345
+ ERROR_BAD_IMPERSONATION_LEVEL = 1346
+ ERROR_CANT_OPEN_ANONYMOUS = 1347
+ ERROR_BAD_VALIDATION_CLASS = 1348
+ ERROR_BAD_TOKEN_TYPE = 1349
+ ERROR_NO_SECURITY_ON_OBJECT = 1350
+ ERROR_CANT_ACCESS_DOMAIN_INFO = 1351
+ ERROR_INVALID_SERVER_STATE = 1352
+ ERROR_INVALID_DOMAIN_STATE = 1353
+ ERROR_INVALID_DOMAIN_ROLE = 1354
+ ERROR_NO_SUCH_DOMAIN = 1355
+ ERROR_DOMAIN_EXISTS = 1356
+ ERROR_DOMAIN_LIMIT_EXCEEDED = 1357
+ ERROR_INTERNAL_DB_CORRUPTION = 1358
+ ERROR_INTERNAL_ERROR = 1359
+ ERROR_GENERIC_NOT_MAPPED = 1360
+ ERROR_BAD_DESCRIPTOR_FORMAT = 1361
+ ERROR_NOT_LOGON_PROCESS = 1362
+ ERROR_LOGON_SESSION_EXISTS = 1363
+ ERROR_NO_SUCH_PACKAGE = 1364
+ ERROR_BAD_LOGON_SESSION_STATE = 1365
+ ERROR_LOGON_SESSION_COLLISION = 1366
+ ERROR_INVALID_LOGON_TYPE = 1367
+ ERROR_CANNOT_IMPERSONATE = 1368
+ ERROR_RXACT_INVALID_STATE = 1369
+ ERROR_RXACT_COMMIT_FAILURE = 1370
+ ERROR_SPECIAL_ACCOUNT = 1371
+ ERROR_SPECIAL_GROUP = 1372
+ ERROR_SPECIAL_USER = 1373
+ ERROR_MEMBERS_PRIMARY_GROUP = 1374
+ ERROR_TOKEN_ALREADY_IN_USE = 1375
+ ERROR_NO_SUCH_ALIAS = 1376
+ ERROR_MEMBER_NOT_IN_ALIAS = 1377
+ ERROR_MEMBER_IN_ALIAS = 1378
+ ERROR_ALIAS_EXISTS = 1379
+ ERROR_LOGON_NOT_GRANTED = 1380
+ ERROR_TOO_MANY_SECRETS = 1381
+ ERROR_SECRET_TOO_LONG = 1382
+ ERROR_INTERNAL_DB_ERROR = 1383
+ ERROR_TOO_MANY_CONTEXT_IDS = 1384
+ ERROR_LOGON_TYPE_NOT_GRANTED = 1385
+ ERROR_NT_CROSS_ENCRYPTION_REQUIRED = 1386
+ ERROR_NO_SUCH_MEMBER = 1387
+ ERROR_INVALID_MEMBER = 1388
+ ERROR_TOO_MANY_SIDS = 1389
+ ERROR_LM_CROSS_ENCRYPTION_REQUIRED = 1390
+ ERROR_NO_INHERITANCE = 1391
+ ERROR_FILE_CORRUPT = 1392
+ ERROR_DISK_CORRUPT = 1393
+ ERROR_NO_USER_SESSION_KEY = 1394
+ ERROR_LICENSE_QUOTA_EXCEEDED = 1395
+ ERROR_WRONG_TARGET_NAME = 1396
+ ERROR_MUTUAL_AUTH_FAILED = 1397
+ ERROR_TIME_SKEW = 1398
+ ERROR_CURRENT_DOMAIN_NOT_ALLOWED = 1399
+ ERROR_INVALID_WINDOW_HANDLE = 1400
+ ERROR_INVALID_MENU_HANDLE = 1401
+ ERROR_INVALID_CURSOR_HANDLE = 1402
+ ERROR_INVALID_ACCEL_HANDLE = 1403
+ ERROR_INVALID_HOOK_HANDLE = 1404
+ ERROR_INVALID_DWP_HANDLE = 1405
+ ERROR_TLW_WITH_WSCHILD = 1406
+ ERROR_CANNOT_FIND_WND_CLASS = 1407
+ ERROR_WINDOW_OF_OTHER_THREAD = 1408
+ ERROR_HOTKEY_ALREADY_REGISTERED = 1409
+ ERROR_CLASS_ALREADY_EXISTS = 1410
+ ERROR_CLASS_DOES_NOT_EXIST = 1411
+ ERROR_CLASS_HAS_WINDOWS = 1412
+ ERROR_INVALID_INDEX = 1413
+ ERROR_INVALID_ICON_HANDLE = 1414
+ ERROR_PRIVATE_DIALOG_INDEX = 1415
+ ERROR_LISTBOX_ID_NOT_FOUND = 1416
+ ERROR_NO_WILDCARD_CHARACTERS = 1417
+ ERROR_CLIPBOARD_NOT_OPEN = 1418
+ ERROR_HOTKEY_NOT_REGISTERED = 1419
+ ERROR_WINDOW_NOT_DIALOG = 1420
+ ERROR_CONTROL_ID_NOT_FOUND = 1421
+ ERROR_INVALID_COMBOBOX_MESSAGE = 1422
+ ERROR_WINDOW_NOT_COMBOBOX = 1423
+ ERROR_INVALID_EDIT_HEIGHT = 1424
+ ERROR_DC_NOT_FOUND = 1425
+ ERROR_INVALID_HOOK_FILTER = 1426
+ ERROR_INVALID_FILTER_PROC = 1427
+ ERROR_HOOK_NEEDS_HMOD = 1428
+ ERROR_GLOBAL_ONLY_HOOK = 1429
+ ERROR_JOURNAL_HOOK_SET = 1430
+ ERROR_HOOK_NOT_INSTALLED = 1431
+ ERROR_INVALID_LB_MESSAGE = 1432
+ ERROR_SETCOUNT_ON_BAD_LB = 1433
+ ERROR_LB_WITHOUT_TABSTOPS = 1434
+ ERROR_DESTROY_OBJECT_OF_OTHER_THREAD = 1435
+ ERROR_CHILD_WINDOW_MENU = 1436
+ ERROR_NO_SYSTEM_MENU = 1437
+ ERROR_INVALID_MSGBOX_STYLE = 1438
+ ERROR_INVALID_SPI_VALUE = 1439
+ ERROR_SCREEN_ALREADY_LOCKED = 1440
+ ERROR_HWNDS_HAVE_DIFF_PARENT = 1441
+ ERROR_NOT_CHILD_WINDOW = 1442
+ ERROR_INVALID_GW_COMMAND = 1443
+ ERROR_INVALID_THREAD_ID = 1444
+ ERROR_NON_MDICHILD_WINDOW = 1445
+ ERROR_POPUP_ALREADY_ACTIVE = 1446
+ ERROR_NO_SCROLLBARS = 1447
+ ERROR_INVALID_SCROLLBAR_RANGE = 1448
+ ERROR_INVALID_SHOWWIN_COMMAND = 1449
+ ERROR_NO_SYSTEM_RESOURCES = 1450
+ ERROR_NONPAGED_SYSTEM_RESOURCES = 1451
+ ERROR_PAGED_SYSTEM_RESOURCES = 1452
+ ERROR_WORKING_SET_QUOTA = 1453
+ ERROR_PAGEFILE_QUOTA = 1454
+ ERROR_COMMITMENT_LIMIT = 1455
+ ERROR_MENU_ITEM_NOT_FOUND = 1456
+ ERROR_INVALID_KEYBOARD_HANDLE = 1457
+ ERROR_HOOK_TYPE_NOT_ALLOWED = 1458
+ ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION = 1459
+ ERROR_TIMEOUT = 1460
+ ERROR_INVALID_MONITOR_HANDLE = 1461
+ ERROR_EVENTLOG_FILE_CORRUPT = 1500
+ ERROR_EVENTLOG_CANT_START = 1501
+ ERROR_LOG_FILE_FULL = 1502
+ ERROR_EVENTLOG_FILE_CHANGED = 1503
+ ERROR_INVALID_TASK_NAME = 1550
+ ERROR_INVALID_TASK_INDEX = 1551
+ ERROR_THREAD_ALREADY_IN_TASK = 1552
+ ERROR_INSTALL_SERVICE_FAILURE = 1601
+ ERROR_INSTALL_USEREXIT = 1602
+ ERROR_INSTALL_FAILURE = 1603
+ ERROR_INSTALL_SUSPEND = 1604
+ ERROR_UNKNOWN_PRODUCT = 1605
+ ERROR_UNKNOWN_FEATURE = 1606
+ ERROR_UNKNOWN_COMPONENT = 1607
+ ERROR_UNKNOWN_PROPERTY = 1608
+ ERROR_INVALID_HANDLE_STATE = 1609
+ ERROR_BAD_CONFIGURATION = 1610
+ ERROR_INDEX_ABSENT = 1611
+ ERROR_INSTALL_SOURCE_ABSENT = 1612
+ ERROR_INSTALL_PACKAGE_VERSION = 1613
+ ERROR_PRODUCT_UNINSTALLED = 1614
+ ERROR_BAD_QUERY_SYNTAX = 1615
+ ERROR_INVALID_FIELD = 1616
+ ERROR_DEVICE_REMOVED = 1617
+ ERROR_INSTALL_ALREADY_RUNNING = 1618
+ ERROR_INSTALL_PACKAGE_OPEN_FAILED = 1619
+ ERROR_INSTALL_PACKAGE_INVALID = 1620
+ ERROR_INSTALL_UI_FAILURE = 1621
+ ERROR_INSTALL_LOG_FAILURE = 1622
+ ERROR_INSTALL_LANGUAGE_UNSUPPORTED = 1623
+ ERROR_INSTALL_TRANSFORM_FAILURE = 1624
+ ERROR_INSTALL_PACKAGE_REJECTED = 1625
+ ERROR_FUNCTION_NOT_CALLED = 1626
+ ERROR_FUNCTION_FAILED = 1627
+ ERROR_INVALID_TABLE = 1628
+ ERROR_DATATYPE_MISMATCH = 1629
+ ERROR_UNSUPPORTED_TYPE = 1630
+ ERROR_CREATE_FAILED = 1631
+ ERROR_INSTALL_TEMP_UNWRITABLE = 1632
+ ERROR_INSTALL_PLATFORM_UNSUPPORTED = 1633
+ ERROR_INSTALL_NOTUSED = 1634
+ ERROR_PATCH_PACKAGE_OPEN_FAILED = 1635
+ ERROR_PATCH_PACKAGE_INVALID = 1636
+ ERROR_PATCH_PACKAGE_UNSUPPORTED = 1637
+ ERROR_PRODUCT_VERSION = 1638
+ ERROR_INVALID_COMMAND_LINE = 1639
+ ERROR_INSTALL_REMOTE_DISALLOWED = 1640
+ ERROR_SUCCESS_REBOOT_INITIATED = 1641
+ ERROR_UNKNOWN_PATCH = 1647
+ RPC_S_INVALID_STRING_BINDING = 1700
+ RPC_S_WRONG_KIND_OF_BINDING = 1701
+ RPC_S_INVALID_BINDING = 1702
+ RPC_S_PROTSEQ_NOT_SUPPORTED = 1703
+ RPC_S_INVALID_RPC_PROTSEQ = 1704
+ RPC_S_INVALID_STRING_UUID = 1705
+ RPC_S_INVALID_ENDPOINT_FORMAT = 1706
+ RPC_S_INVALID_NET_ADDR = 1707
+ RPC_S_NO_ENDPOINT_FOUND = 1708
+ RPC_S_INVALID_TIMEOUT = 1709
+ RPC_S_OBJECT_NOT_FOUND = 1710
+ RPC_S_ALREADY_REGISTERED = 1711
+ RPC_S_TYPE_ALREADY_REGISTERED = 1712
+ RPC_S_ALREADY_LISTENING = 1713
+ RPC_S_NO_PROTSEQS_REGISTERED = 1714
+ RPC_S_NOT_LISTENING = 1715
+ RPC_S_UNKNOWN_MGR_TYPE = 1716
+ RPC_S_UNKNOWN_IF = 1717
+ RPC_S_NO_BINDINGS = 1718
+ RPC_S_NO_PROTSEQS = 1719
+ RPC_S_CANT_CREATE_ENDPOINT = 1720
+ RPC_S_OUT_OF_RESOURCES = 1721
+ RPC_S_SERVER_UNAVAILABLE = 1722
+ RPC_S_SERVER_TOO_BUSY = 1723
+ RPC_S_INVALID_NETWORK_OPTIONS = 1724
+ RPC_S_NO_CALL_ACTIVE = 1725
+ RPC_S_CALL_FAILED = 1726
+ RPC_S_CALL_FAILED_DNE = 1727
+ RPC_S_PROTOCOL_ERROR = 1728
+ RPC_S_UNSUPPORTED_TRANS_SYN = 1730
+ RPC_S_UNSUPPORTED_TYPE = 1732
+ RPC_S_INVALID_TAG = 1733
+ RPC_S_INVALID_BOUND = 1734
+ RPC_S_NO_ENTRY_NAME = 1735
+ RPC_S_INVALID_NAME_SYNTAX = 1736
+ RPC_S_UNSUPPORTED_NAME_SYNTAX = 1737
+ RPC_S_UUID_NO_ADDRESS = 1739
+ RPC_S_DUPLICATE_ENDPOINT = 1740
+ RPC_S_UNKNOWN_AUTHN_TYPE = 1741
+ RPC_S_MAX_CALLS_TOO_SMALL = 1742
+ RPC_S_STRING_TOO_LONG = 1743
+ RPC_S_PROTSEQ_NOT_FOUND = 1744
+ RPC_S_PROCNUM_OUT_OF_RANGE = 1745
+ RPC_S_BINDING_HAS_NO_AUTH = 1746
+ RPC_S_UNKNOWN_AUTHN_SERVICE = 1747
+ RPC_S_UNKNOWN_AUTHN_LEVEL = 1748
+ RPC_S_INVALID_AUTH_IDENTITY = 1749
+ RPC_S_UNKNOWN_AUTHZ_SERVICE = 1750
+ EPT_S_INVALID_ENTRY = 1751
+ EPT_S_CANT_PERFORM_OP = 1752
+ EPT_S_NOT_REGISTERED = 1753
+ RPC_S_NOTHING_TO_EXPORT = 1754
+ RPC_S_INCOMPLETE_NAME = 1755
+ RPC_S_INVALID_VERS_OPTION = 1756
+ RPC_S_NO_MORE_MEMBERS = 1757
+ RPC_S_NOT_ALL_OBJS_UNEXPORTED = 1758
+ RPC_S_INTERFACE_NOT_FOUND = 1759
+ RPC_S_ENTRY_ALREADY_EXISTS = 1760
+ RPC_S_ENTRY_NOT_FOUND = 1761
+ RPC_S_NAME_SERVICE_UNAVAILABLE = 1762
+ RPC_S_INVALID_NAF_ID = 1763
+ RPC_S_CANNOT_SUPPORT = 1764
+ RPC_S_NO_CONTEXT_AVAILABLE = 1765
+ RPC_S_INTERNAL_ERROR = 1766
+ RPC_S_ZERO_DIVIDE = 1767
+ RPC_S_ADDRESS_ERROR = 1768
+ RPC_S_FP_DIV_ZERO = 1769
+ RPC_S_FP_UNDERFLOW = 1770
+ RPC_S_FP_OVERFLOW = 1771
+ RPC_X_NO_MORE_ENTRIES = 1772
+ RPC_X_SS_CHAR_TRANS_OPEN_FAIL = 1773
+ RPC_X_SS_CHAR_TRANS_SHORT_FILE = 1774
+ RPC_X_SS_IN_NULL_CONTEXT = 1775
+ RPC_X_SS_CONTEXT_DAMAGED = 1777
+ RPC_X_SS_HANDLES_MISMATCH = 1778
+ RPC_X_SS_CANNOT_GET_CALL_HANDLE = 1779
+ RPC_X_NULL_REF_POINTER = 1780
+ RPC_X_ENUM_VALUE_OUT_OF_RANGE = 1781
+ RPC_X_BYTE_COUNT_TOO_SMALL = 1782
+ RPC_X_BAD_STUB_DATA = 1783
+ ERROR_INVALID_USER_BUFFER = 1784
+ ERROR_UNRECOGNIZED_MEDIA = 1785
+ ERROR_NO_TRUST_LSA_SECRET = 1786
+ ERROR_NO_TRUST_SAM_ACCOUNT = 1787
+ ERROR_TRUSTED_DOMAIN_FAILURE = 1788
+ ERROR_TRUSTED_RELATIONSHIP_FAILURE = 1789
+ ERROR_TRUST_FAILURE = 1790
+ RPC_S_CALL_IN_PROGRESS = 1791
+ ERROR_NETLOGON_NOT_STARTED = 1792
+ ERROR_ACCOUNT_EXPIRED = 1793
+ ERROR_REDIRECTOR_HAS_OPEN_HANDLES = 1794
+ ERROR_PRINTER_DRIVER_ALREADY_INSTALLED= 1795
+ ERROR_UNKNOWN_PORT = 1796
+ ERROR_UNKNOWN_PRINTER_DRIVER = 1797
+ ERROR_UNKNOWN_PRINTPROCESSOR = 1798
+ ERROR_INVALID_SEPARATOR_FILE = 1799
+ ERROR_INVALID_PRIORITY = 1800
+ ERROR_INVALID_PRINTER_NAME = 1801
+ ERROR_PRINTER_ALREADY_EXISTS = 1802
+ ERROR_INVALID_PRINTER_COMMAND = 1803
+ ERROR_INVALID_DATATYPE = 1804
+ ERROR_INVALID_ENVIRONMENT = 1805
+ RPC_S_NO_MORE_BINDINGS = 1806
+ ERROR_NOLOGON_INTERDOMAIN_TRUST_ACCOUNT = 1807
+ ERROR_NOLOGON_WORKSTATION_TRUST_ACCOUNT = 1808
+ ERROR_NOLOGON_SERVER_TRUST_ACCOUNT = 1809
+ ERROR_DOMAIN_TRUST_INCONSISTENT = 1810
+ ERROR_SERVER_HAS_OPEN_HANDLES = 1811
+ ERROR_RESOURCE_DATA_NOT_FOUND = 1812
+ ERROR_RESOURCE_TYPE_NOT_FOUND = 1813
+ ERROR_RESOURCE_NAME_NOT_FOUND = 1814
+ ERROR_RESOURCE_LANG_NOT_FOUND = 1815
+ ERROR_NOT_ENOUGH_QUOTA = 1816
+ RPC_S_NO_INTERFACES = 1817
+ RPC_S_CALL_CANCELLED = 1818
+ RPC_S_BINDING_INCOMPLETE = 1819
+ RPC_S_COMM_FAILURE = 1820
+ RPC_S_UNSUPPORTED_AUTHN_LEVEL = 1821
+ RPC_S_NO_PRINC_NAME = 1822
+ RPC_S_NOT_RPC_ERROR = 1823
+ RPC_S_UUID_LOCAL_ONLY = 1824
+ RPC_S_SEC_PKG_ERROR = 1825
+ RPC_S_NOT_CANCELLED = 1826
+ RPC_X_INVALID_ES_ACTION = 1827
+ RPC_X_WRONG_ES_VERSION = 1828
+ RPC_X_WRONG_STUB_VERSION = 1829
+ RPC_X_INVALID_PIPE_OBJECT = 1830
+ RPC_X_WRONG_PIPE_ORDER = 1831
+ RPC_X_WRONG_PIPE_VERSION = 1832
+ RPC_S_GROUP_MEMBER_NOT_FOUND = 1898
+ EPT_S_CANT_CREATE = 1899
+ RPC_S_INVALID_OBJECT = 1900
+ ERROR_INVALID_TIME = 1901
+ ERROR_INVALID_FORM_NAME = 1902
+ ERROR_INVALID_FORM_SIZE = 1903
+ ERROR_ALREADY_WAITING = 1904
+ ERROR_PRINTER_DELETED = 1905
+ ERROR_INVALID_PRINTER_STATE = 1906
+ ERROR_PASSWORD_MUST_CHANGE = 1907
+ ERROR_DOMAIN_CONTROLLER_NOT_FOUND = 1908
+ ERROR_ACCOUNT_LOCKED_OUT = 1909
+ OR_INVALID_OXID = 1910
+ OR_INVALID_OID = 1911
+ OR_INVALID_SET = 1912
+ RPC_S_SEND_INCOMPLETE = 1913
+ RPC_S_INVALID_ASYNC_HANDLE = 1914
+ RPC_S_INVALID_ASYNC_CALL = 1915
+ RPC_X_PIPE_CLOSED = 1916
+ RPC_X_PIPE_DISCIPLINE_ERROR = 1917
+ RPC_X_PIPE_EMPTY = 1918
+ ERROR_NO_SITENAME = 1919
+ ERROR_CANT_ACCESS_FILE = 1920
+ ERROR_CANT_RESOLVE_FILENAME = 1921
+ RPC_S_ENTRY_TYPE_MISMATCH = 1922
+ RPC_S_NOT_ALL_OBJS_EXPORTED = 1923
+ RPC_S_INTERFACE_NOT_EXPORTED = 1924
+ RPC_S_PROFILE_NOT_ADDED = 1925
+ RPC_S_PRF_ELT_NOT_ADDED = 1926
+ RPC_S_PRF_ELT_NOT_REMOVED = 1927
+ RPC_S_GRP_ELT_NOT_ADDED = 1928
+ RPC_S_GRP_ELT_NOT_REMOVED = 1929
+ ERROR_KM_DRIVER_BLOCKED = 1930
+ ERROR_CONTEXT_EXPIRED = 1931
+ ERROR_PER_USER_TRUST_QUOTA_EXCEEDED = 1932
+ ERROR_ALL_USER_TRUST_QUOTA_EXCEEDED = 1933
+ ERROR_USER_DELETE_TRUST_QUOTA_EXCEEDED= 1934
+ ERROR_AUTHENTICATION_FIREWALL_FAILED = 1935
+ ERROR_REMOTE_PRINT_CONNECTIONS_BLOCKED= 1936
+ ERROR_INVALID_PIXEL_FORMAT = 2000
+ ERROR_BAD_DRIVER = 2001
+ ERROR_INVALID_WINDOW_STYLE = 2002
+ ERROR_METAFILE_NOT_SUPPORTED = 2003
+ ERROR_TRANSFORM_NOT_SUPPORTED = 2004
+ ERROR_CLIPPING_NOT_SUPPORTED = 2005
+ ERROR_INVALID_CMM = 2010
+ ERROR_INVALID_PROFILE = 2011
+ ERROR_TAG_NOT_FOUND = 2012
+ ERROR_TAG_NOT_PRESENT = 2013
+ ERROR_DUPLICATE_TAG = 2014
+ ERROR_PROFILE_NOT_ASSOCIATED_WITH_DEVICE = 2015
+ ERROR_PROFILE_NOT_FOUND = 2016
+ ERROR_INVALID_COLORSPACE = 2017
+ ERROR_ICM_NOT_ENABLED = 2018
+ ERROR_DELETING_ICM_XFORM = 2019
+ ERROR_INVALID_TRANSFORM = 2020
+ ERROR_COLORSPACE_MISMATCH = 2021
+ ERROR_INVALID_COLORINDEX = 2022
+ ERROR_CONNECTED_OTHER_PASSWORD = 2108
+ ERROR_BAD_USERNAME = 2202
+ ERROR_NOT_CONNECTED = 2250
+ ERROR_OPEN_FILES = 2401
+ ERROR_ACTIVE_CONNECTIONS = 2402
+ ERROR_DEVICE_IN_USE = 2404
+ ERROR_UNKNOWN_PRINT_MONITOR = 3000
+
+ ERROR_USER_DEFINED_BASE = 0xF000
+
+ # Flags for FormatMessage function:
+
+ FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
+ FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
+ FORMAT_MESSAGE_FROM_STRING = 0x00000400
+ FORMAT_MESSAGE_FROM_HMODULE = 0x00000800
+ FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
+ FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000
+ FORMAT_MESSAGE_MAX_WIDTH_MASK = 0x000000FF
+
+ # Set/GetErrorMode values:
+
+ SEM_FAILCRITICALERRORS = 0x0001
+ SEM_NOALIGNMENTFAULTEXCEPT = 0x0004
+ SEM_NOGPFAULTERRORBOX = 0x0002
+ SEM_NOOPENFILEERRORBOX = 0x8000
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ ffi_lib 'kernel32', 'user32'
+
+=begin
+DWORD WINAPI FormatMessage(
+ __in DWORD dwFlags,
+ __in_opt LPCVOID lpSource,
+ __in DWORD dwMessageId,
+ __in DWORD dwLanguageId,
+ __out LPTSTR lpBuffer,
+ __in DWORD nSize,
+ __in_opt va_list *Arguments
+);
+=end
+ safe_attach_function :FormatMessageA, [:DWORD, :LPCVOID, :DWORD, :DWORD, :LPTSTR, :DWORD, :varargs], :DWORD
+ safe_attach_function :FormatMessageW, [:DWORD, :LPCVOID, :DWORD, :DWORD, :LPWSTR, :DWORD, :varargs], :DWORD
+
+=begin
+DWORD WINAPI GetLastError(void);
+=end
+ safe_attach_function :GetLastError, [], :DWORD
+=begin
+void WINAPI SetLastError(
+ __in DWORD dwErrCode
+);
+=end
+ safe_attach_function :SetLastError, [:DWORD], :void
+ safe_attach_function :SetLastErrorEx, [:DWORD, :DWORD], :void
+=begin
+UINT WINAPI GetErrorMode(void);s
+=end
+ safe_attach_function :GetErrorMode, [], :uint
+=begin
+UINT WINAPI SetErrorMode(
+ __in UINT uMode
+);
+=end
+ safe_attach_function :SetErrorMode, [:UINT], :UINT
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/api/file.rb b/lib/chef/win32/api/file.rb
new file mode 100644
index 0000000000..41a128ea7e
--- /dev/null
+++ b/lib/chef/win32/api/file.rb
@@ -0,0 +1,535 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Author:: Mark Mzyk (<mmzyk@ospcode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+require 'chef/win32/api/security'
+require 'chef/win32/api/system'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module File
+ extend Chef::ReservedNames::Win32::API
+ include Chef::ReservedNames::Win32::API::Security
+ include Chef::ReservedNames::Win32::API::System
+
+ ###############################################
+ # Win32 API Constants
+ ###############################################
+
+ FILE_ATTRIBUTE_READONLY = 0x00000001
+ FILE_ATTRIBUTE_HIDDEN = 0x00000002
+ FILE_ATTRIBUTE_SYSTEM = 0x00000004
+ FILE_ATTRIBUTE_DIRECTORY = 0x00000010
+ FILE_ATTRIBUTE_ARCHIVE = 0x00000020
+ FILE_ATTRIBUTE_DEVICE = 0x00000040
+ FILE_ATTRIBUTE_NORMAL = 0x00000080
+ FILE_ATTRIBUTE_TEMPORARY = 0x00000100
+ FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200
+ FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400
+ FILE_ATTRIBUTE_COMPRESSED = 0x00000800
+ FILE_ATTRIBUTE_OFFLINE = 0x00001000
+ FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000
+ FILE_ATTRIBUTE_ENCRYPTED = 0x00004000
+ FILE_ATTRIBUTE_VIRTUAL = 0x00010000
+ INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
+
+ FILE_FLAG_WRITE_THROUGH = 0x80000000
+ FILE_FLAG_OVERLAPPED = 0x40000000
+ FILE_FLAG_NO_BUFFERING = 0x20000000
+ FILE_FLAG_RANDOM_ACCESS = 0x10000000
+ FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000
+ FILE_FLAG_DELETE_ON_CLOSE = 0x04000000
+ FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+ FILE_FLAG_POSIX_SEMANTICS = 0x01000000
+ FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
+ FILE_FLAG_OPEN_NO_RECALL = 0x00100000
+ FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000
+
+ INVALID_HANDLE_VALUE = 0xFFFFFFFF
+ MAX_PATH = 260
+
+ SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1
+
+ FILE_NAME_NORMALIZED = 0x0
+ FILE_NAME_OPENED = 0x8
+
+ # TODO add the rest of these CONSTS
+ FILE_SHARE_READ = 0x00000001
+ OPEN_EXISTING = 3
+
+ # DeviceIoControl control codes
+ # -----------------------------
+ FILE_DEVICE_BEEP = 0x00000001
+ FILE_DEVICE_CD_ROM = 0x00000002
+ FILE_DEVICE_CD_ROM_FILE_SYSTEM = 0x00000003
+ FILE_DEVICE_CONTROLLER = 0x00000004
+ FILE_DEVICE_DATALINK = 0x00000005
+ FILE_DEVICE_DFS = 0x00000006
+ FILE_DEVICE_DISK = 0x00000007
+ FILE_DEVICE_DISK_FILE_SYSTEM = 0x00000008
+ FILE_DEVICE_FILE_SYSTEM = 0x00000009
+ FILE_DEVICE_INPORT_PORT = 0x0000000a
+ FILE_DEVICE_KEYBOARD = 0x0000000b
+ FILE_DEVICE_MAILSLOT = 0x0000000c
+ FILE_DEVICE_MIDI_IN = 0x0000000d
+ FILE_DEVICE_MIDI_OUT = 0x0000000e
+ FILE_DEVICE_MOUSE = 0x0000000f
+ FILE_DEVICE_MULTI_UNC_PROVIDER = 0x00000010
+ FILE_DEVICE_NAMED_PIPE = 0x00000011
+ FILE_DEVICE_NETWORK = 0x00000012
+ FILE_DEVICE_NETWORK_BROWSER = 0x00000013
+ FILE_DEVICE_NETWORK_FILE_SYSTEM = 0x00000014
+ FILE_DEVICE_NULL = 0x00000015
+ FILE_DEVICE_PARALLEL_PORT = 0x00000016
+ FILE_DEVICE_PHYSICAL_NETCARD = 0x00000017
+ FILE_DEVICE_PRINTER = 0x00000018
+ FILE_DEVICE_SCANNER = 0x00000019
+ FILE_DEVICE_SERIAL_MOUSE_PORT = 0x0000001a
+ FILE_DEVICE_SERIAL_PORT = 0x0000001b
+ FILE_DEVICE_SCREEN = 0x0000001c
+ FILE_DEVICE_SOUND = 0x0000001d
+ FILE_DEVICE_STREAMS = 0x0000001e
+ FILE_DEVICE_TAPE = 0x0000001f
+ FILE_DEVICE_TAPE_FILE_SYSTEM = 0x00000020
+ FILE_DEVICE_TRANSPORT = 0x00000021
+ FILE_DEVICE_UNKNOWN = 0x00000022
+ FILE_DEVICE_VIDEO = 0x00000023
+ FILE_DEVICE_VIRTUAL_DISK = 0x00000024
+ FILE_DEVICE_WAVE_IN = 0x00000025
+ FILE_DEVICE_WAVE_OUT = 0x00000026
+ FILE_DEVICE_8042_PORT = 0x00000027
+ FILE_DEVICE_NETWORK_REDIRECTOR = 0x00000028
+ FILE_DEVICE_BATTERY = 0x00000029
+ FILE_DEVICE_BUS_EXTENDER = 0x0000002a
+ FILE_DEVICE_MODEM = 0x0000002b
+ FILE_DEVICE_VDM = 0x0000002c
+ FILE_DEVICE_MASS_STORAGE = 0x0000002d
+ FILE_DEVICE_SMB = 0x0000002e
+ FILE_DEVICE_KS = 0x0000002f
+ FILE_DEVICE_CHANGER = 0x00000030
+ FILE_DEVICE_SMARTCARD = 0x00000031
+ FILE_DEVICE_ACPI = 0x00000032
+ FILE_DEVICE_DVD = 0x00000033
+ FILE_DEVICE_FULLSCREEN_VIDEO = 0x00000034
+ FILE_DEVICE_DFS_FILE_SYSTEM = 0x00000035
+ FILE_DEVICE_DFS_VOLUME = 0x00000036
+ FILE_DEVICE_SERENUM = 0x00000037
+ FILE_DEVICE_TERMSRV = 0x00000038
+ FILE_DEVICE_KSEC = 0x00000039
+ FILE_DEVICE_FIPS = 0x0000003A
+ FILE_DEVICE_INFINIBAND = 0x0000003B
+ FILE_DEVICE_VMBUS = 0x0000003E
+ FILE_DEVICE_CRYPT_PROVIDER = 0x0000003F
+ FILE_DEVICE_WPD = 0x00000040
+ FILE_DEVICE_BLUETOOTH = 0x00000041
+ FILE_DEVICE_MT_COMPOSITE = 0x00000042
+ FILE_DEVICE_MT_TRANSPORT = 0x00000043
+ FILE_DEVICE_BIOMETRIC = 0x00000044
+ FILE_DEVICE_PMI = 0x00000045
+
+ # Methods
+ METHOD_BUFFERED = 0
+ METHOD_IN_DIRECT = 1
+ METHOD_OUT_DIRECT = 2
+ METHOD_NEITHER = 3
+ METHOD_DIRECT_TO_HARDWARE = METHOD_IN_DIRECT
+ METHOD_DIRECT_FROM_HARDWARE = METHOD_OUT_DIRECT
+
+ # Access
+ FILE_ANY_ACCESS = 0
+ FILE_SPECIAL_ACCESS = FILE_ANY_ACCESS
+ FILE_READ_ACCESS = 0x0001
+ FILE_WRITE_ACCESS = 0x0002
+
+ def self.CTL_CODE( device_type, function, method, access )
+ (device_type << 16) | (access << 14) | (function << 2) | method
+ end
+
+ FSCTL_GET_REPARSE_POINT = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 42, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+ # Reparse point tags
+ IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
+ IO_REPARSE_TAG_HSM = 0xC0000004
+ IO_REPARSE_TAG_HSM2 = 0x80000006
+ IO_REPARSE_TAG_SIS = 0x80000007
+ IO_REPARSE_TAG_WIM = 0x80000008
+ IO_REPARSE_TAG_CSV = 0x80000009
+ IO_REPARSE_TAG_DFS = 0x8000000A
+ IO_REPARSE_TAG_SYMLINK = 0xA000000C
+ IO_REPARSE_TAG_DFSR = 0x80000012
+
+ MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16*1024
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ ffi_lib 'kernel32'
+
+=begin
+typedef struct _FILETIME {
+ DWORD dwLowDateTime;
+ DWORD dwHighDateTime;
+} FILETIME, *PFILETIME;
+=end
+ class FILETIME < FFI::Struct
+ layout :dw_low_date_time, :DWORD,
+ :dw_high_date_time, :DWORD
+ end
+
+=begin
+typedef struct _SECURITY_ATTRIBUTES {
+ DWORD nLength;
+ LPVOID lpSecurityDescriptor;
+ BOOL bInheritHandle;
+} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
+=end
+ class SECURITY_ATTRIBUTES < FFI::Struct
+ layout :n_length, :DWORD,
+ :lp_security_descriptor, :LPVOID,
+ :b_inherit_handle, :DWORD
+ end
+
+=begin
+typedef struct _WIN32_FIND_DATA {
+ DWORD dwFileAttributes;
+ FILETIME ftCreationTime;
+ FILETIME ftLastAccessTime;
+ FILETIME ftLastWriteTime;
+ DWORD nFileSizeHigh;
+ DWORD nFileSizeLow;
+ DWORD dwReserved0;
+ DWORD dwReserved1;
+ TCHAR cFileName[MAX_PATH];
+ TCHAR cAlternateFileName[14];
+} WIN32_FIND_DATA, *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA;
+=end
+ class WIN32_FIND_DATA < FFI::Struct
+ layout :dw_file_attributes, :DWORD,
+ :ft_creation_time, FILETIME,
+ :ft_last_access_time, FILETIME,
+ :ft_last_write_time, FILETIME,
+ :n_file_size_high, :DWORD,
+ :n_file_size_low, :DWORD,
+ :dw_reserved_0, :DWORD,
+ :dw_reserved_1, :DWORD,
+ :c_file_name, [:BYTE, MAX_PATH*2],
+ :c_alternate_file_name, [:BYTE, 14]
+ end
+
+=begin
+typedef struct _BY_HANDLE_FILE_INFORMATION {
+ DWORD dwFileAttributes;
+ FILETIME ftCreationTime;
+ FILETIME ftLastAccessTime;
+ FILETIME ftLastWriteTime;
+ DWORD dwVolumeSerialNumber;
+ DWORD nFileSizeHigh;
+ DWORD nFileSizeLow;
+ DWORD nNumberOfLinks;
+ DWORD nFileIndexHigh;
+ DWORD nFileIndexLow;
+} BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION;
+=end
+ class BY_HANDLE_FILE_INFORMATION < FFI::Struct
+ layout :dw_file_attributes, :DWORD,
+ :ft_creation_time, FILETIME,
+ :ft_last_access_time, FILETIME,
+ :ft_last_write_time, FILETIME,
+ :dw_volume_serial_number, :DWORD,
+ :n_file_size_high, :DWORD,
+ :n_file_size_low, :DWORD,
+ :n_number_of_links, :DWORD,
+ :n_file_index_high, :DWORD,
+ :n_file_index_low, :DWORD
+ end
+
+=begin
+typedef struct _REPARSE_DATA_BUFFER {
+ ULONG ReparseTag;
+ USHORT ReparseDataLength;
+ USHORT Reserved;
+ union {
+ struct {
+ USHORT SubstituteNameOffset;
+ USHORT SubstituteNameLength;
+ USHORT PrintNameOffset;
+ USHORT PrintNameLength;
+ ULONG Flags;
+ WCHAR PathBuffer[1];
+ } SymbolicLinkReparseBuffer;
+ struct {
+ USHORT SubstituteNameOffset;
+ USHORT SubstituteNameLength;
+ USHORT PrintNameOffset;
+ USHORT PrintNameLength;
+ WCHAR PathBuffer[1];
+ } MountPointReparseBuffer;
+ struct {
+ UCHAR DataBuffer[1];
+ } GenericReparseBuffer;
+ };
+} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
+=end
+
+ class REPARSE_DATA_BUFFER_SYMBOLIC_LINK < FFI::Struct
+ layout :SubstituteNameOffset, :ushort,
+ :SubstituteNameLength, :ushort,
+ :PrintNameOffset, :ushort,
+ :PrintNameLength, :ushort,
+ :Flags, :uint32,
+ :PathBuffer, :ushort
+
+ def substitute_name
+ string_pointer = FFI::Pointer.new(pointer.address) + offset_of(:PathBuffer) + self[:SubstituteNameOffset]
+ string_pointer.read_wstring(self[:SubstituteNameLength]/2)
+ end
+ def print_name
+ string_pointer = FFI::Pointer.new(pointer.address) + offset_of(:PathBuffer) + self[:PrintNameOffset]
+ string_pointer.read_wstring(self[:PrintNameLength]/2)
+ end
+ end
+ class REPARSE_DATA_BUFFER_MOUNT_POINT < FFI::Struct
+ layout :SubstituteNameOffset, :ushort,
+ :SubstituteNameLength, :ushort,
+ :PrintNameOffset, :ushort,
+ :PrintNameLength, :ushort,
+ :PathBuffer, :ushort
+
+ def substitute_name
+ string_pointer = FFI::Pointer.new(pointer.address) + offset_of(:PathBuffer) + self[:SubstituteNameOffset]
+ string_pointer.read_wstring(self[:SubstituteNameLength]/2)
+ end
+ def print_name
+ string_pointer = FFI::Pointer.new(pointer.address) + offset_of(:PathBuffer) + self[:PrintNameOffset]
+ string_pointer.read_wstring(self[:PrintNameLength]/2)
+ end
+ end
+ class REPARSE_DATA_BUFFER_GENERIC < FFI::Struct
+ layout :DataBuffer, :uchar
+ end
+ class REPARSE_DATA_BUFFER_UNION < FFI::Union
+ layout :SymbolicLinkReparseBuffer, REPARSE_DATA_BUFFER_SYMBOLIC_LINK,
+ :MountPointReparseBuffer, REPARSE_DATA_BUFFER_MOUNT_POINT,
+ :GenericReparseBuffer, REPARSE_DATA_BUFFER_GENERIC
+ end
+ class REPARSE_DATA_BUFFER < FFI::Struct
+ layout :ReparseTag, :uint32,
+ :ReparseDataLength, :ushort,
+ :Reserved, :ushort,
+ :ReparseBuffer, REPARSE_DATA_BUFFER_UNION
+
+ def reparse_buffer
+ if self[:ReparseTag] == IO_REPARSE_TAG_SYMLINK
+ self[:ReparseBuffer][:SymbolicLinkReparseBuffer]
+ elsif self[:ReparseTag] == IO_REPARSE_TAG_MOUNT_POINT
+ self[:ReparseBuffer][:MountPointReparseBuffer]
+ else
+ self[:ReparseBuffer][:GenericReparseBuffer]
+ end
+ end
+ end
+
+=begin
+HANDLE WINAPI CreateFile(
+ __in LPCTSTR lpFileName,
+ __in DWORD dwDesiredAccess,
+ __in DWORD dwShareMode,
+ __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,
+ __in DWORD dwCreationDisposition,
+ __in DWORD dwFlagsAndAttributes,
+ __in_opt HANDLE hTemplateFile
+);
+=end
+ safe_attach_function :CreateFileW, [:LPCTSTR, :DWORD, :DWORD, :LPSECURITY_ATTRIBUTES, :DWORD, :DWORD, :pointer], :HANDLE
+
+=begin
+BOOL WINAPI FindClose(
+ __inout HANDLE hFindFile
+);
+=end
+ safe_attach_function :FindClose, [:HANDLE], :BOOL
+
+=begin
+DWORD WINAPI GetFileAttributes(
+ __in LPCTSTR lpFileName
+);
+=end
+ safe_attach_function :GetFileAttributesW, [:LPCWSTR], :DWORD
+
+=begin
+DWORD WINAPI GetFinalPathNameByHandle(
+ __in HANDLE hFile,
+ __out LPTSTR lpszFilePath,
+ __in DWORD cchFilePath,
+ __in DWORD dwFlags
+);
+=end
+ safe_attach_function :GetFinalPathNameByHandleW, [:HANDLE, :LPTSTR, :DWORD, :DWORD], :DWORD
+
+=begin
+BOOL WINAPI GetFileInformationByHandle(
+ __in HANDLE hFile,
+ __out LPBY_HANDLE_FILE_INFORMATION lpFileInformation
+);
+=end
+ safe_attach_function :GetFileInformationByHandle, [:HANDLE, :LPBY_HANDLE_FILE_INFORMATION], :BOOL
+
+=begin
+HANDLE WINAPI FindFirstFile(
+ __in LPCTSTR lpFileName,
+ __out LPWIN32_FIND_DATA lpFindFileData
+);
+=end
+ safe_attach_function :FindFirstFileW, [:LPCTSTR, :LPWIN32_FIND_DATA], :HANDLE
+
+=begin
+BOOL WINAPI CreateHardLink(
+ __in LPCTSTR lpFileName,
+ __in LPCTSTR lpExistingFileName,
+ __reserved LPSECURITY_ATTRIBUTES lpSecurityAttributes
+);
+=end
+ safe_attach_function :CreateHardLinkW, [:LPCTSTR, :LPCTSTR, :LPSECURITY_ATTRIBUTES], :BOOLEAN
+
+=begin
+BOOLEAN WINAPI CreateSymbolicLink(
+ __in LPTSTR lpSymlinkFileName,
+ __in LPTSTR lpTargetFileName,
+ __in DWORD dwFlags
+);
+=end
+ safe_attach_function :CreateSymbolicLinkW, [:LPTSTR, :LPTSTR, :DWORD], :BOOLEAN
+
+=begin
+DWORD WINAPI GetLongPathName(
+ __in LPCTSTR lpszShortPath,
+ __out LPTSTR lpszLongPath,
+ __in DWORD cchBuffer
+);
+=end
+ safe_attach_function :GetLongPathNameW, [:LPCTSTR, :LPTSTR, :DWORD], :DWORD
+
+=begin
+DWORD WINAPI GetShortPathName(
+ __in LPCTSTR lpszLongPath,
+ __out LPTSTR lpszShortPath,
+ __in DWORD cchBuffer
+);
+=end
+ safe_attach_function :GetShortPathNameW, [:LPCTSTR, :LPTSTR, :DWORD], :DWORD
+
+=begin
+BOOL WINAPI DeviceIoControl(
+ __in HANDLE hDevice,
+ __in DWORD dwIoControlCode,
+ __in_opt LPVOID lpInBuffer,
+ __in DWORD nInBufferSize,
+ __out_opt LPVOID lpOutBuffer,
+ __in DWORD nOutBufferSize,
+ __out_opt LPDWORD lpBytesReturned,
+ __inout_opt LPOVERLAPPED lpOverlapped
+);
+=end
+ safe_attach_function :DeviceIoControl, [:HANDLE, :DWORD, :LPVOID, :DWORD, :LPVOID, :DWORD, :LPDWORD, :pointer], :BOOL
+
+ ###############################################
+ # Helpers
+ ###############################################
+
+ # takes the given path pre-pends "\\?\" and
+ # UTF-16LE encodes it. Used to prepare paths
+ # to be passed to the *W vesion of WinAPI File
+ # functions
+ def encode_path(path)
+ path.gsub!(::File::SEPARATOR, ::File::ALT_SEPARATOR)
+ (path_prepender << path).to_wstring
+ end
+
+ def path_prepender
+ "\\\\?\\"
+ end
+
+ # retrieves a file search handle and passes it
+ # to +&block+ along with the find_data. also
+ # ensures the handle is closed on exit of the block
+ def file_search_handle(path, &block)
+ begin
+ path = encode_path(path)
+ find_data = WIN32_FIND_DATA.new
+ handle = FindFirstFileW(path, find_data)
+ if handle == INVALID_HANDLE_VALUE
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ block.call(handle, find_data)
+ ensure
+ FindClose(handle) if handle && handle != INVALID_HANDLE_VALUE
+ end
+ end
+
+ # retrieves a file handle and passes it
+ # to +&block+ along with the find_data. also
+ # ensures the handle is closed on exit of the block
+ def file_handle(path, &block)
+ begin
+ path = encode_path(path)
+ handle = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ,
+ nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, nil)
+
+ if handle == INVALID_HANDLE_VALUE
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ block.call(handle)
+ ensure
+ CloseHandle(handle) if handle && handle != INVALID_HANDLE_VALUE
+ end
+ end
+
+ def symlink_file_handle(path, &block)
+ begin
+ path = encode_path(path)
+ handle = CreateFileW(path, FILE_READ_EA, FILE_SHARE_READ,
+ nil, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nil)
+
+ if handle == INVALID_HANDLE_VALUE
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ block.call(handle)
+ ensure
+ CloseHandle(handle) if handle && handle != INVALID_HANDLE_VALUE
+ end
+ end
+
+ def retrieve_file_info(file_name)
+ file_information = nil
+ file_handle(file_name) do |handle|
+ file_information = BY_HANDLE_FILE_INFORMATION.new
+ success = GetFileInformationByHandle(handle, file_information)
+ if success == 0
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+ file_information
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/api/memory.rb b/lib/chef/win32/api/memory.rb
new file mode 100644
index 0000000000..abd1191718
--- /dev/null
+++ b/lib/chef/win32/api/memory.rb
@@ -0,0 +1,105 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module Memory
+ extend Chef::ReservedNames::Win32::API
+
+ ###############################################
+ # Win32 API Constants
+ ###############################################
+
+ LMEM_FIXED = 0x0000
+ LMEM_MOVEABLE = 0x0002
+ LMEM_NOCOMPACT = 0x0010
+ LMEM_NODISCARD = 0x0020
+ LMEM_ZEROINIT = 0x0040
+ LMEM_MODIFY = 0x0080
+ LMEM_DISCARDABLE = 0x0F00
+ LMEM_VALID_FLAGS = 0x0F72
+ LMEM_INVALID_HANDLE = 0x8000
+ LHND = LMEM_MOVEABLE | LMEM_ZEROINIT
+ LPTR = LMEM_FIXED | LMEM_ZEROINIT
+ NONZEROLHND = LMEM_MOVEABLE
+ NONZEROLPTR = LMEM_FIXED
+ LMEM_DISCARDED = 0x4000
+ LMEM_LOCKCOUNT = 0x00FF
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ ffi_lib 'kernel32'
+
+=begin
+HLOCAL WINAPI LocalAlloc(
+ __in UINT uFlags,
+ __in SIZE_T uBytes
+);
+=end
+ safe_attach_function :LocalAlloc, [ :UINT, :SIZE_T ], :pointer
+
+=begin
+UINT WINAPI LocalFlags(
+ __in HLOCAL hMem
+);
+=end
+ safe_attach_function :LocalFlags, [ :pointer ], :UINT
+
+=begin
+HLOCAL WINAPI LocalFree(
+ __in HLOCAL hMem
+);
+=end
+ safe_attach_function :LocalFree, [ :pointer ], :pointer
+
+=begin
+HLOCAL WINAPI LocalReAlloc(
+ __in HLOCAL hMem,
+ __in SIZE_T uBytes,
+ __in UINT uFlags
+);
+=end
+ safe_attach_function :LocalReAlloc, [ :pointer, :SIZE_T, :UINT ], :pointer
+
+=begin
+UINT WINAPI LocalSize(
+ __in HLOCAL hMem
+);
+=end
+ safe_attach_function :LocalSize, [ :pointer ], :SIZE_T
+
+ ###############################################
+ # FFI API Bindings
+ ###############################################
+
+ ffi_lib FFI::Library::LIBC
+ safe_attach_function :malloc, [:size_t], :pointer
+ safe_attach_function :calloc, [:size_t], :pointer
+ safe_attach_function :realloc, [:pointer, :size_t], :pointer
+ safe_attach_function :free, [:pointer], :void
+ safe_attach_function :memcpy, [:pointer, :pointer, :size_t], :pointer
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/api/process.rb b/lib/chef/win32/api/process.rb
new file mode 100644
index 0000000000..d18ad411b4
--- /dev/null
+++ b/lib/chef/win32/api/process.rb
@@ -0,0 +1,40 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module Process
+ extend Chef::ReservedNames::Win32::API
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ ffi_lib 'kernel32'
+
+ safe_attach_function :GetCurrentProcess, [], :HANDLE
+ safe_attach_function :GetProcessHandleCount, [ :HANDLE, :LPDWORD ], :BOOL
+ safe_attach_function :GetProcessId, [ :HANDLE ], :DWORD
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/api/psapi.rb b/lib/chef/win32/api/psapi.rb
new file mode 100644
index 0000000000..3a5df3f179
--- /dev/null
+++ b/lib/chef/win32/api/psapi.rb
@@ -0,0 +1,51 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module PSAPI
+ extend Chef::ReservedNames::Win32::API
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ class PROCESS_MEMORY_COUNTERS < FFI::Struct
+ layout :cb, :DWORD,
+ :PageFaultCount, :DWORD,
+ :PeakWorkingSetSize, :SIZE_T,
+ :WorkingSetSize, :SIZE_T,
+ :QuotaPeakPagedPoolUsage, :SIZE_T,
+ :QuotaPagedPoolUsage, :SIZE_T,
+ :QuotaPeakNonPagedPoolUsage, :SIZE_T,
+ :QuotaNonPagedPoolUsage, :SIZE_T,
+ :PagefileUsage, :SIZE_T,
+ :PeakPagefileUsage, :SIZE_T
+ end
+
+ ffi_lib 'psapi'
+
+ safe_attach_function :GetProcessMemoryInfo, [ :HANDLE, :pointer, :DWORD ], :BOOL
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/api/security.rb b/lib/chef/win32/api/security.rb
new file mode 100644
index 0000000000..a096b40140
--- /dev/null
+++ b/lib/chef/win32/api/security.rb
@@ -0,0 +1,341 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module Security
+ extend Chef::ReservedNames::Win32::API
+
+ ###############################################
+ # Win32 API Constants
+ ###############################################
+
+ # ACE_HEADER AceType
+ ACCESS_MIN_MS_ACE_TYPE = 0x0
+ ACCESS_ALLOWED_ACE_TYPE = 0x0
+ ACCESS_DENIED_ACE_TYPE = 0x1
+ SYSTEM_AUDIT_ACE_TYPE = 0x2
+ SYSTEM_ALARM_ACE_TYPE = 0x3
+ ACCESS_MAX_MS_V2_ACE_TYPE = 0x3
+ ACCESS_ALLOWED_COMPOUND_ACE_TYPE = 0x4
+ ACCESS_MAX_MS_V3_ACE_TYPE = 0x4
+ ACCESS_MIN_MS_OBJECT_ACE_TYPE = 0x5
+ ACCESS_ALLOWED_OBJECT_ACE_TYPE = 0x5
+ ACCESS_DENIED_OBJECT_ACE_TYPE = 0x6
+ SYSTEM_AUDIT_OBJECT_ACE_TYPE = 0x7
+ SYSTEM_ALARM_OBJECT_ACE_TYPE = 0x8
+ ACCESS_MAX_MS_OBJECT_ACE_TYPE = 0x8
+ ACCESS_MAX_MS_V4_ACE_TYPE = 0x8
+ ACCESS_MAX_MS_ACE_TYPE = 0x8
+ ACCESS_ALLOWED_CALLBACK_ACE_TYPE = 0x9
+ ACCESS_DENIED_CALLBACK_ACE_TYPE = 0xA
+ ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE = 0xB
+ ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE = 0xC
+ SYSTEM_AUDIT_CALLBACK_ACE_TYPE = 0xD
+ SYSTEM_ALARM_CALLBACK_ACE_TYPE = 0xE
+ SYSTEM_AUDIT_CALLBACK_OBJECT_ACE_TYPE = 0xF
+ SYSTEM_ALARM_CALLBACK_OBJECT_ACE_TYPE = 0x10
+ SYSTEM_MANDATORY_LABEL_ACE_TYPE = 0x11
+ ACCESS_MAX_MS_V5_ACE_TYPE = 0x11
+
+ # ACE_HEADER AceFlags
+ OBJECT_INHERIT_ACE = 0x1
+ CONTAINER_INHERIT_ACE = 0x2
+ NO_PROPAGATE_INHERIT_ACE = 0x4
+ INHERIT_ONLY_ACE = 0x8
+ INHERITED_ACE = 0x10
+ VALID_INHERIT_FLAGS = 0x1F
+ SUCCESSFUL_ACCESS_ACE_FLAG = 0x40
+ FAILED_ACCESS_ACE_FLAG = 0x80
+
+ # SECURITY_INFORMATION flags (DWORD)
+ OWNER_SECURITY_INFORMATION = 0x01
+ GROUP_SECURITY_INFORMATION = 0x02
+ DACL_SECURITY_INFORMATION = 0x04
+ SACL_SECURITY_INFORMATION = 0x08
+ LABEL_SECURITY_INFORMATION = 0x10
+ UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000
+ UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000
+ PROTECTED_SACL_SECURITY_INFORMATION = 0x40000000
+ PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000
+
+ # SECURITY_DESCRIPTOR_REVISION
+ SECURITY_DESCRIPTOR_REVISION = 1
+ SECURITY_DESCRIPTOR_REVISION1 = 1
+
+ # SECURITY_DESCRIPTOR_CONTROL
+ SE_OWNER_DEFAULTED = 0x0001
+ SE_GROUP_DEFAULTED = 0x0002
+ SE_DACL_PRESENT = 0x0004
+ SE_DACL_DEFAULTED = 0x0008
+ SE_SACL_PRESENT = 0x0010
+ SE_SACL_DEFAULTED = 0x0020
+ SE_DACL_AUTO_INHERIT_REQ = 0x0100
+ SE_SACL_AUTO_INHERIT_REQ = 0x0200
+ SE_DACL_AUTO_INHERITED = 0x0400
+ SE_SACL_AUTO_INHERITED = 0x0800
+ SE_DACL_PROTECTED = 0x1000
+ SE_SACL_PROTECTED = 0x2000
+ SE_RM_CONTROL_VALID = 0x4000
+ SE_SELF_RELATIVE = 0x8000
+
+ # ACCESS_RIGHTS_MASK
+ # Generic Access Rights
+ GENERIC_READ = 0x80000000
+ GENERIC_WRITE = 0x40000000
+ GENERIC_EXECUTE = 0x20000000
+ GENERIC_ALL = 0x10000000
+ # Standard Access Rights
+ DELETE = 0x00010000
+ READ_CONTROL = 0x00020000
+ WRITE_DAC = 0x00040000
+ WRITE_OWNER = 0x00080000
+ SYNCHRONIZE = 0x00100000
+ STANDARD_RIGHTS_REQUIRED = 0x000F0000
+ STANDARD_RIGHTS_READ = READ_CONTROL
+ STANDARD_RIGHTS_WRITE = READ_CONTROL
+ STANDARD_RIGHTS_EXECUTE = READ_CONTROL
+ STANDARD_RIGHTS_ALL = 0x001F0000
+ SPECIFIC_RIGHTS_ALL = 0x0000FFFF
+ # Access System Security Right
+ ACCESS_SYSTEM_SECURITY = 0x01000000
+ # File/Directory Specific Rights
+ FILE_READ_DATA = 0x0001
+ FILE_LIST_DIRECTORY = 0x0001
+ FILE_WRITE_DATA = 0x0002
+ FILE_ADD_FILE = 0x0002
+ FILE_APPEND_DATA = 0x0004
+ FILE_ADD_SUBDIRECTORY = 0x0004
+ FILE_CREATE_PIPE_INSTANCE = 0x0004
+ FILE_READ_EA = 0x0008
+ FILE_WRITE_EA = 0x0010
+ FILE_EXECUTE = 0x0020
+ FILE_TRAVERSE = 0x0020
+ FILE_DELETE_CHILD = 0x0040
+ FILE_READ_ATTRIBUTES = 0x0080
+ FILE_WRITE_ATTRIBUTES = 0x0100
+ FILE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED |
+ SYNCHRONIZE |
+ 0x1FF
+ FILE_GENERIC_READ = STANDARD_RIGHTS_READ |
+ FILE_READ_DATA |
+ FILE_READ_ATTRIBUTES |
+ FILE_READ_EA |
+ SYNCHRONIZE
+ FILE_GENERIC_WRITE = STANDARD_RIGHTS_WRITE |
+ FILE_WRITE_DATA |
+ FILE_WRITE_ATTRIBUTES |
+ FILE_WRITE_EA |
+ FILE_APPEND_DATA |
+ SYNCHRONIZE
+ FILE_GENERIC_EXECUTE = STANDARD_RIGHTS_EXECUTE |
+ FILE_READ_ATTRIBUTES |
+ FILE_EXECUTE |
+ SYNCHRONIZE
+ # Access Token Rights (for OpenProcessToken)
+ # Access Rights for Access-Token Objects (used in OpenProcessToken)
+ TOKEN_ASSIGN_PRIMARY = 0x0001
+ TOKEN_DUPLICATE = 0x0002
+ TOKEN_IMPERSONATE = 0x0004
+ TOKEN_QUERY = 0x0008
+ TOKEN_QUERY_SOURCE = 0x0010
+ TOKEN_ADJUST_PRIVILEGES = 0x0020
+ TOKEN_ADJUST_GROUPS = 0x0040
+ TOKEN_ADJUST_DEFAULT = 0x0080
+ TOKEN_ADJUST_SESSIONID = 0x0100
+ TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY)
+ TOKEN_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY |
+ TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE |
+ TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT |
+ TOKEN_ADJUST_SESSIONID)
+
+ # AdjustTokenPrivileges
+ SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001
+ SE_PRIVILEGE_ENABLED = 0x00000002
+ SE_PRIVILEGE_REMOVED = 0X00000004
+ SE_PRIVILEGE_USED_FOR_ACCESS = 0x80000000
+ SE_PRIVILEGE_VALID_ATTRIBUTES = SE_PRIVILEGE_ENABLED_BY_DEFAULT |
+ SE_PRIVILEGE_ENABLED |
+ SE_PRIVILEGE_REMOVED |
+ SE_PRIVILEGE_USED_FOR_ACCESS
+
+ # Minimum size of a SECURITY_DESCRIPTOR. TODO: this is probably platform dependent.
+ # Make it work on 64 bit.
+ SECURITY_DESCRIPTOR_MIN_LENGTH = 20
+
+ # ACL revisions
+ ACL_REVISION = 2
+ ACL_REVISION_DS = 4
+ ACL_REVISION1 = 1
+ ACL_REVISION2 = 2
+ ACL_REVISION3 = 3
+ ACL_REVISION4 = 4
+ MIN_ACL_REVISION = ACL_REVISION2
+ MAX_ACL_REVISION = ACL_REVISION4
+
+ MAXDWORD = 0xffffffff
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ SE_OBJECT_TYPE = enum :SE_OBJECT_TYPE, [
+ :SE_UNKNOWN_OBJECT_TYPE,
+ :SE_FILE_OBJECT,
+ :SE_SERVICE,
+ :SE_PRINTER,
+ :SE_REGISTRY_KEY,
+ :SE_LMSHARE,
+ :SE_KERNEL_OBJECT,
+ :SE_WINDOW_OBJECT,
+ :SE_DS_OBJECT,
+ :SE_DS_OBJECT_ALL,
+ :SE_PROVIDER_DEFINED_OBJECT,
+ :SE_WMIGUID_OBJECT,
+ :SE_REGISTRY_WOW64_32KEY
+ ]
+
+ SID_NAME_USE = enum :SID_NAME_USE, [
+ :SidTypeUser, 1,
+ :SidTypeGroup,
+ :SidTypeDomain,
+ :SidTypeAlias,
+ :SidTypeWellKnownGroup,
+ :SidTypeDeletedAccount,
+ :SidTypeInvalid,
+ :SidTypeUnknown,
+ :SidTypeComputer,
+ :SidTypeLabel
+ ]
+
+ # SECURITY_DESCRIPTOR is an opaque structure whose contents can vary. Pass the
+ # pointer around and free it with LocalFree.
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379561(v=vs.85).aspx
+
+ # SID is an opaque structure. Pass the pointer around.
+
+ # ACL type is a header with some information, followed by an array of ACEs
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/aa374931(v=VS.85).aspx
+ class ACLStruct < FFI::Struct
+ layout :AclRevision, :uchar,
+ :Sbzl, :uchar,
+ :AclSize, :ushort,
+ :AceCount, :ushort,
+ :Sbz2, :ushort
+ end
+
+ class ACE_HEADER < FFI::Struct
+ layout :AceType, :uchar,
+ :AceFlags, :uchar,
+ :AceSize, :ushort
+ end
+
+ class ACE_WITH_MASK_AND_SID < FFI::Struct
+ layout :AceType, :uchar,
+ :AceFlags, :uchar,
+ :AceSize, :ushort,
+ :Mask, :uint32,
+ :SidStart, :uint32
+
+ # The AceTypes this structure supports
+ def self.supports?(ace_type)
+ [
+ ACCESS_ALLOWED_ACE_TYPE,
+ ACCESS_DENIED_ACE_TYPE,
+ SYSTEM_AUDIT_ACE_TYPE,
+ SYSTEM_ALARM_ACE_TYPE
+ ].include?(ace_type)
+ end
+ end
+
+ class LUID < FFI::Struct
+ layout :LowPart, :DWORD,
+ :HighPart, :LONG
+ end
+
+ class LUID_AND_ATTRIBUTES < FFI::Struct
+ layout :Luid, LUID,
+ :Attributes, :DWORD
+ end
+
+ class TOKEN_PRIVILEGES < FFI::Struct
+ layout :PrivilegeCount, :DWORD,
+ :Privileges, LUID_AND_ATTRIBUTES
+
+ def self.size_with_privileges(num_privileges)
+ offset_of(:Privileges) + LUID_AND_ATTRIBUTES.size*num_privileges
+ end
+
+ def size_with_privileges
+ TOKEN_PRIVILEGES.size_with_privileges(self[:PrivilegeCount])
+ end
+
+ def privilege(index)
+ LUID_AND_ATTRIBUTES.new(pointer + offset_of(:Privileges) + (index * LUID_AND_ATTRIBUTES.size))
+ end
+ end
+
+ ffi_lib "advapi32"
+
+ safe_attach_function :AddAce, [ :pointer, :DWORD, :DWORD, :LPVOID, :DWORD ], :BOOL
+ safe_attach_function :AddAccessAllowedAce, [ :pointer, :DWORD, :DWORD, :pointer ], :BOOL
+ safe_attach_function :AddAccessAllowedAceEx, [ :pointer, :DWORD, :DWORD, :DWORD, :pointer ], :BOOL
+ safe_attach_function :AddAccessDeniedAce, [ :pointer, :DWORD, :DWORD, :pointer ], :BOOL
+ safe_attach_function :AddAccessDeniedAceEx, [ :pointer, :DWORD, :DWORD, :DWORD, :pointer ], :BOOL
+ safe_attach_function :AdjustTokenPrivileges, [ :HANDLE, :BOOL, :pointer, :DWORD, :pointer, :PDWORD ], :BOOL
+ safe_attach_function :ConvertSidToStringSidA, [ :pointer, :pointer ], :BOOL
+ safe_attach_function :ConvertStringSidToSidW, [ :pointer, :pointer ], :BOOL
+ safe_attach_function :DeleteAce, [ :pointer, :DWORD ], :BOOL
+ safe_attach_function :EqualSid, [ :pointer, :pointer ], :BOOL
+ safe_attach_function :FreeSid, [ :pointer ], :pointer
+ safe_attach_function :GetAce, [ :pointer, :DWORD, :pointer ], :BOOL
+ safe_attach_function :GetLengthSid, [ :pointer ], :DWORD
+ safe_attach_function :GetNamedSecurityInfoW, [ :LPWSTR, :SE_OBJECT_TYPE, :DWORD, :pointer, :pointer, :pointer, :pointer, :pointer ], :DWORD
+ safe_attach_function :GetSecurityDescriptorControl, [ :pointer, :PWORD, :LPDWORD], :BOOL
+ safe_attach_function :GetSecurityDescriptorDacl, [ :pointer, :LPBOOL, :pointer, :LPBOOL ], :BOOL
+ safe_attach_function :GetSecurityDescriptorGroup, [ :pointer, :pointer, :LPBOOL], :BOOL
+ safe_attach_function :GetSecurityDescriptorOwner, [ :pointer, :pointer, :LPBOOL], :BOOL
+ safe_attach_function :GetSecurityDescriptorSacl, [ :pointer, :LPBOOL, :pointer, :LPBOOL ], :BOOL
+ safe_attach_function :InitializeAcl, [ :pointer, :DWORD, :DWORD ], :BOOL
+ safe_attach_function :InitializeSecurityDescriptor, [ :pointer, :DWORD ], :BOOL
+ safe_attach_function :IsValidAcl, [ :pointer ], :BOOL
+ safe_attach_function :IsValidSecurityDescriptor, [ :pointer ], :BOOL
+ safe_attach_function :IsValidSid, [ :pointer ], :BOOL
+ safe_attach_function :LookupAccountNameW, [ :LPCWSTR, :LPCWSTR, :pointer, :LPDWORD, :LPWSTR, :LPDWORD, :pointer ], :BOOL
+ safe_attach_function :LookupAccountSidW, [ :LPCWSTR, :pointer, :LPWSTR, :LPDWORD, :LPWSTR, :LPDWORD, :pointer ], :BOOL
+ safe_attach_function :LookupPrivilegeNameW, [ :LPCWSTR, :PLUID, :LPWSTR, :LPDWORD ], :BOOL
+ safe_attach_function :LookupPrivilegeDisplayNameW, [ :LPCWSTR, :LPCWSTR, :LPWSTR, :LPDWORD, :LPDWORD ], :BOOL
+ safe_attach_function :LookupPrivilegeValueW, [ :LPCWSTR, :LPCWSTR, :PLUID ], :BOOL
+ safe_attach_function :MakeAbsoluteSD, [ :pointer, :pointer, :LPDWORD, :pointer, :LPDWORD, :pointer, :LPDWORD, :pointer, :LPDWORD, :pointer, :LPDWORD], :BOOL
+ safe_attach_function :OpenProcessToken, [ :HANDLE, :DWORD, :PHANDLE ], :BOOL
+ safe_attach_function :QuerySecurityAccessMask, [ :DWORD, :LPDWORD ], :void
+ safe_attach_function :SetFileSecurityW, [ :LPWSTR, :DWORD, :pointer ], :BOOL
+ safe_attach_function :SetNamedSecurityInfoW, [ :LPWSTR, :SE_OBJECT_TYPE, :DWORD, :pointer, :pointer, :pointer, :pointer ], :DWORD
+ safe_attach_function :SetSecurityAccessMask, [ :DWORD, :LPDWORD ], :void
+ safe_attach_function :SetSecurityDescriptorDacl, [ :pointer, :BOOL, :pointer, :BOOL ], :BOOL
+ safe_attach_function :SetSecurityDescriptorGroup, [ :pointer, :pointer, :BOOL ], :BOOL
+ safe_attach_function :SetSecurityDescriptorOwner, [ :pointer, :pointer, :BOOL ], :BOOL
+ safe_attach_function :SetSecurityDescriptorSacl, [ :pointer, :BOOL, :pointer, :BOOL ], :BOOL
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/api/system.rb b/lib/chef/win32/api/system.rb
new file mode 100644
index 0000000000..60f381aa5a
--- /dev/null
+++ b/lib/chef/win32/api/system.rb
@@ -0,0 +1,192 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module System
+ extend Chef::ReservedNames::Win32::API
+
+ ###############################################
+ # Win32 API Constants
+ ###############################################
+
+ # http://msdn.microsoft.com/en-us/library/ms724833(v=vs.85).aspx
+
+ # Suite Masks
+ # Microsoft BackOffice components are installed.
+ VER_SUITE_BACKOFFICE = 0x00000004
+ # Windows Server 2003, Web Edition is installed.
+ VER_SUITE_BLADE = 0x00000400
+ # Windows Server 2003, Compute Cluster Edition is installed.
+ VER_SUITE_COMPUTE_SERVER = 0x00004000
+ # Windows Server 2008 Datacenter, Windows Server 2003, Datacenter Edition, or Windows 2000 Datacenter Server is installed.
+ VER_SUITE_DATACENTER = 0x00000080
+ # Windows Server 2008 Enterprise, Windows Server 2003, Enterprise Edition, or Windows 2000 Advanced Server is installed. Refer to the Remarks section for more information about this bit flag.
+ VER_SUITE_ENTERPRISE = 0x00000002
+ # Windows XP Embedded is installed.
+ VER_SUITE_EMBEDDEDNT = 0x00000040
+ # Windows Vista Home Premium, Windows Vista Home Basic, or Windows XP Home Edition is installed.
+ VER_SUITE_PERSONAL = 0x00000200
+ # Remote Desktop is supported, but only one interactive session is supported. This value is set unless the system is running in application server mode.
+ VER_SUITE_SINGLEUSERTS = 0x00000100
+ # Microsoft Small Business Server was once installed on the system, but may have been upgraded to another version of Windows. Refer to the Remarks section for more information about this bit flag.
+ VER_SUITE_SMALLBUSINESS = 0x00000001
+ # Microsoft Small Business Server is installed with the restrictive client license in force. Refer to the Remarks section for more information about this bit flag.
+ VER_SUITE_SMALLBUSINESS_RESTRICTED = 0x00000020
+ # Windows Storage Server 2003 R2 or Windows Storage Server 2003is installed.
+ VER_SUITE_STORAGE_SERVER = 0x00002000
+ # Terminal Services is installed. This value is always set.
+ # If VER_SUITE_TERMINAL is set but VER_SUITE_SINGLEUSERTS is not set, the system is running in application server mode.
+ VER_SUITE_TERMINAL = 0x00000010
+ # Windows Home Server is installed.
+ VER_SUITE_WH_SERVER = 0x00008000
+
+ # Product Type
+ # The system is a domain controller and the operating system is Windows Server 2008 R2, Windows Server 2008, Windows Server 2003, or Windows 2000 Server.
+ VER_NT_DOMAIN_CONTROLLER = 0x0000002
+ # The operating system is Windows Server 2008 R2, Windows Server 2008, Windows Server 2003, or Windows 2000 Server.
+ # Note that a server that is also a domain controller is reported as VER_NT_DOMAIN_CONTROLLER, not VER_NT_SERVER.
+ VER_NT_SERVER = 0x0000003
+ # The operating system is Windows 7, Windows Vista, Windows XP Professional, Windows XP Home Edition, or Windows 2000 Professional.
+ VER_NT_WORKSTATION = 0x0000001
+
+ # Product Info
+ # http://msdn.microsoft.com/en-us/library/ms724358(v=vs.85).aspx
+ PRODUCT_BUSINESS = 0x00000006 # Business
+ PRODUCT_BUSINESS_N = 0x00000010 # Business N
+ PRODUCT_CLUSTER_SERVER = 0x00000012 # HPC Edition
+ PRODUCT_DATACENTER_SERVER = 0x00000008 # Server Datacenter (full installation)
+ PRODUCT_DATACENTER_SERVER_CORE = 0x0000000C # Server Datacenter (core installation)
+ PRODUCT_DATACENTER_SERVER_CORE_V = 0x00000027 # Server Datacenter without Hyper-V (core installation)
+ PRODUCT_DATACENTER_SERVER_V = 0x00000025 # Server Datacenter without Hyper-V (full installation)
+ PRODUCT_ENTERPRISE = 0x00000004 # Enterprise
+ PRODUCT_ENTERPRISE_E = 0x00000046 # Not supported
+ PRODUCT_ENTERPRISE_N = 0x0000001B # Enterprise N
+ PRODUCT_ENTERPRISE_SERVER = 0x0000000A # Server Enterprise (full installation)
+ PRODUCT_ENTERPRISE_SERVER_CORE = 0x0000000E # Server Enterprise (core installation)
+ PRODUCT_ENTERPRISE_SERVER_CORE_V = 0x00000029 # Server Enterprise without Hyper-V (core installation)
+ PRODUCT_ENTERPRISE_SERVER_IA64 = 0x0000000F # Server Enterprise for Itanium-based Systems
+ PRODUCT_ENTERPRISE_SERVER_V = 0x00000026 # Server Enterprise without Hyper-V (full installation)
+ PRODUCT_HOME_BASIC = 0x00000002 # Home Basic
+ PRODUCT_HOME_BASIC_E = 0x00000043 # Not supported
+ PRODUCT_HOME_BASIC_N = 0x00000005 # Home Basic N
+ PRODUCT_HOME_PREMIUM = 0x00000003 # Home Premium
+ PRODUCT_HOME_PREMIUM_E = 0x00000044 # Not supported
+ PRODUCT_HOME_PREMIUM_N = 0x0000001A # Home Premium N
+ PRODUCT_HYPERV = 0x0000002A # Microsoft Hyper-V Server
+ PRODUCT_MEDIUMBUSINESS_SERVER_MANAGEMENT = 0x0000001E # Windows Essential Business Server Management Server
+ PRODUCT_MEDIUMBUSINESS_SERVER_MESSAGING = 0x00000020 # Windows Essential Business Server Messaging Server
+ PRODUCT_MEDIUMBUSINESS_SERVER_SECURITY = 0x0000001F # Windows Essential Business Server Security Server
+ PRODUCT_PROFESSIONAL = 0x00000030 # Professional
+ PRODUCT_PROFESSIONAL_E = 0x00000045 # Not supported
+ PRODUCT_PROFESSIONAL_N = 0x00000031 # Professional N
+ PRODUCT_SERVER_FOR_SMALLBUSINESS = 0x00000018 # Windows Server 2008 for Windows Essential Server Solutions
+ PRODUCT_SERVER_FOR_SMALLBUSINESS_V = 0x00000023 # Windows Server 2008 without Hyper-V for Windows Essential Server Solutions
+ PRODUCT_SERVER_FOUNDATION = 0x00000021 # Server Foundation
+ PRODUCT_HOME_PREMIUM_SERVER = 0x00000022 # Windows Home Server 2011
+ PRODUCT_SB_SOLUTION_SERVER = 0x00000032 # Windows Small Business Server 2011 Essentials
+ PRODUCT_HOME_SERVER = 0x00000013 # Windows Storage Server 2008 R2 Essentials
+ PRODUCT_SMALLBUSINESS_SERVER = 0x00000009 # Windows Small Business Server
+ PRODUCT_SOLUTION_EMBEDDEDSERVER = 0x00000038 # Windows MultiPoint Server
+ PRODUCT_STANDARD_SERVER = 0x00000007 # Server Standard (full installation)
+ PRODUCT_STANDARD_SERVER_CORE = 0x0000000D # Server Standard (core installation)
+ PRODUCT_STANDARD_SERVER_CORE_V = 0x00000028 # Server Standard without Hyper-V (core installation)
+ PRODUCT_STANDARD_SERVER_V = 0x00000024 # Server Standard without Hyper-V (full installation)
+ PRODUCT_STARTER = 0x0000000B # Starter
+ PRODUCT_STARTER_E = 0x00000042 # Not supported
+ PRODUCT_STARTER_N = 0x0000002F # Starter N
+ PRODUCT_STORAGE_ENTERPRISE_SERVER = 0x00000017 # Storage Server Enterprise
+ PRODUCT_STORAGE_EXPRESS_SERVER = 0x00000014 # Storage Server Express
+ PRODUCT_STORAGE_STANDARD_SERVER = 0x00000015 # Storage Server Standard
+ PRODUCT_STORAGE_WORKGROUP_SERVER = 0x00000016 # Storage Server Workgroup
+ PRODUCT_UNDEFINED = 0x00000000 # An unknown product
+ PRODUCT_ULTIMATE = 0x00000001 # Ultimate
+ PRODUCT_ULTIMATE_E = 0x00000047 # Not supported
+ PRODUCT_ULTIMATE_N = 0x0000001C # Ultimate N
+ PRODUCT_WEB_SERVER = 0x00000011 # Web Server (full installation)
+ PRODUCT_WEB_SERVER_CORE = 0x0000001D # Web Server (core installation)
+
+ # GetSystemMetrics
+ # The build number if the system is Windows Server 2003 R2; otherwise, 0.
+ SM_SERVERR2 = 89
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ ffi_lib 'kernel32', 'user32'
+
+ class OSVERSIONINFOEX < FFI::Struct
+ layout :dw_os_version_info_size, :DWORD,
+ :dw_major_version, :DWORD,
+ :dw_minor_version, :DWORD,
+ :dw_build_number, :DWORD,
+ :dw_platform_id, :DWORD,
+ :sz_csd_version, [:BYTE, 256],
+ :w_service_pack_major, :WORD,
+ :w_service_pack_minor, :WORD,
+ :w_suite_mask, :WORD,
+ :w_product_type, :BYTE,
+ :w_reserved, :BYTE
+ end
+
+=begin
+BOOL WINAPI CloseHandle(
+ __in HANDLE hObject
+);
+=end
+ safe_attach_function :CloseHandle, [ :HANDLE ], :BOOL
+
+=begin
+DWORD WINAPI GetVersion(void);
+=end
+ safe_attach_function :GetVersion, [], :DWORD
+
+=begin
+BOOL WINAPI GetVersionEx(
+ __inout LPOSVERSIONINFO lpVersionInfo
+);
+=end
+ safe_attach_function :GetVersionExW, [:pointer], :BOOL
+ safe_attach_function :GetVersionExA, [:pointer], :BOOL
+
+=begin
+BOOL WINAPI GetProductInfo(
+ __in DWORD dwOSMajorVersion,
+ __in DWORD dwOSMinorVersion,
+ __in DWORD dwSpMajorVersion,
+ __in DWORD dwSpMinorVersion,
+ __out PDWORD pdwReturnedProductType
+);
+=end
+ safe_attach_function :GetProductInfo, [:DWORD, :DWORD, :DWORD, :DWORD, :PDWORD], :BOOL
+
+=begin
+int WINAPI GetSystemMetrics(
+ __in int nIndex
+);
+=end
+ safe_attach_function :GetSystemMetrics, [:int], :int
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/api/unicode.rb b/lib/chef/win32/api/unicode.rb
new file mode 100644
index 0000000000..0b2cb09a6b
--- /dev/null
+++ b/lib/chef/win32/api/unicode.rb
@@ -0,0 +1,178 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module Unicode
+ extend Chef::ReservedNames::Win32::API
+
+ ###############################################
+ # Win32 API Constants
+ ###############################################
+
+ CP_ACP = 0
+ CP_OEMCP = 1
+ CP_MACCP = 2
+ CP_THREAD_ACP = 3
+ CP_SYMBOL = 42
+ CP_UTF7 = 65000
+ CP_UTF8 = 65001
+
+ MB_PRECOMPOSED = 0x00000001
+ MB_COMPOSITE = 0x00000002
+ MB_USEGLYPHCHARS = 0x00000004
+ MB_ERR_INVALID_CHARS = 0x00000008
+
+ WC_COMPOSITECHECK = 0x00000200
+ WC_DISCARDNS = 0x00000010
+ WC_SEPCHARS = 0x00000020
+ WC_DEFAULTCHAR = 0x00000040
+ WC_NO_BEST_FIT_CHARS = 0x00000400
+
+ ANSI_CHARSET = 0
+ DEFAULT_CHARSET = 1
+ SYMBOL_CHARSET = 2
+ SHIFTJIS_CHARSET = 128
+ HANGEUL_CHARSET = 129
+ HANGUL_CHARSET = 129
+ GB2312_CHARSET = 134
+ CHINESEBIG5_CHARSET = 136
+ OEM_CHARSET = 255
+ JOHAB_CHARSET = 130
+ HEBREW_CHARSET = 177
+ ARABIC_CHARSET = 178
+ GREEK_CHARSET = 161
+ TURKISH_CHARSET = 162
+ VIETNAMESE_CHARSET = 163
+ THAI_CHARSET = 222
+ EASTEUROPE_CHARSET = 238
+ RUSSIAN_CHARSET = 204
+
+ IS_TEXT_UNICODE_ASCII16 = 0x0001
+ IS_TEXT_UNICODE_REVERSE_ASCII16 = 0x0010
+ IS_TEXT_UNICODE_STATISTICS = 0x0002
+ IS_TEXT_UNICODE_REVERSE_STATISTICS = 0x0020
+ IS_TEXT_UNICODE_CONTROLS = 0x0004
+ IS_TEXT_UNICODE_REVERSE_CONTROLS = 0x0040
+ IS_TEXT_UNICODE_SIGNATURE = 0x0008
+ IS_TEXT_UNICODE_REVERSE_SIGNATURE = 0x0080
+ IS_TEXT_UNICODE_ILLEGAL_CHARS = 0x0100
+ IS_TEXT_UNICODE_ODD_LENGTH = 0x0200
+ IS_TEXT_UNICODE_DBCS_LEADBYTE = 0x0400
+ IS_TEXT_UNICODE_NULL_BYTES = 0x1000
+ IS_TEXT_UNICODE_UNICODE_MASK = 0x000F
+ IS_TEXT_UNICODE_REVERSE_MASK = 0x00F0
+ IS_TEXT_UNICODE_NOT_UNICODE_MASK = 0x0F00
+ IS_TEXT_UNICODE_NOT_ASCII_MASK = 0xF000
+
+ TCI_SRCCHARSET = 1
+ TCI_SRCCODEPAGE = 2
+ TCI_SRCFONTSIG = 3
+ TCI_SRCLOCALE = 0x100
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ ffi_lib 'kernel32', 'advapi32'
+
+=begin
+BOOL IsTextUnicode(
+ __in const VOID *lpv,
+ __in int iSize,
+ __inout LPINT lpiResult
+);
+=end
+ safe_attach_function :IsTextUnicode, [:pointer, :int, :LPINT], :BOOL
+
+=begin
+int MultiByteToWideChar(
+ __in UINT CodePage,
+ __in DWORD dwFlags,
+ __in LPCSTR lpMultiByteStr,
+ __in int cbMultiByte,
+ __out LPWSTR lpWideCharStr,
+ __in int cchWideChar
+);
+=end
+ safe_attach_function :MultiByteToWideChar, [:UINT, :DWORD, :LPCSTR, :int, :LPWSTR, :int], :int
+
+=begin
+int WideCharToMultiByte(
+ __in UINT CodePage,
+ __in DWORD dwFlags,
+ __in LPCWSTR lpWideCharStr,
+ __in int cchWideChar,
+ __out LPSTR lpMultiByteStr,
+ __in int cbMultiByte,
+ __in LPCSTR lpDefaultChar,
+ __out LPBOOL lpUsedDefaultChar
+);
+=end
+ safe_attach_function :WideCharToMultiByte, [:UINT, :DWORD, :LPCWSTR, :int, :LPSTR, :int, :LPCSTR, :LPBOOL], :int
+
+ ###############################################
+ # Helpers
+ ###############################################
+
+ def utf8_to_wide(ustring)
+ # ensure it is actually UTF-8
+ # Ruby likes to mark binary data as ASCII-8BIT
+ ustring = (ustring + "").force_encoding('UTF-8') if ustring.respond_to?(:force_encoding) && ustring.encoding.name != "UTF-8"
+
+ # ensure we have the double-null termination Windows Wide likes
+ ustring = ustring + "\000\000" if ustring[-1].chr != "\000"
+
+ # encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode
+ ustring = begin
+ if ustring.respond_to?(:encode)
+ ustring.encode('UTF-16LE')
+ else
+ require 'iconv'
+ Iconv.conv("UTF-16LE", "UTF-8", ustring)
+ end
+ end
+ ustring
+ end
+
+ def wide_to_utf8(wstring)
+ # ensure it is actually UTF-16LE
+ # Ruby likes to mark binary data as ASCII-8BIT
+ wstring = wstring.force_encoding('UTF-16LE') if wstring.respond_to?(:force_encoding)
+
+ # encode it all as UTF-8
+ wstring = begin
+ if wstring.respond_to?(:encode)
+ wstring.encode('UTF-8')
+ else
+ require 'iconv'
+ Iconv.conv("UTF-8", "UTF-16LE", wstring)
+ end
+ end
+ # remove trailing CRLF and NULL characters
+ wstring.strip!
+ wstring
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/error.rb b/lib/chef/win32/error.rb
new file mode 100644
index 0000000000..716ca99d01
--- /dev/null
+++ b/lib/chef/win32/error.rb
@@ -0,0 +1,73 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api/error'
+require 'chef/win32/memory'
+require 'chef/win32/unicode'
+require 'chef/exceptions'
+
+class Chef
+ module ReservedNames::Win32
+ class Error
+ include Chef::ReservedNames::Win32::API::Error
+ extend Chef::ReservedNames::Win32::API::Error
+
+ def self.format_message(message_id = 0, args = {})
+ flags = args[:flags] || FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY
+ source = args[:source]
+ language_id = args[:language_id] || 0
+ varargs = args[:varargs] || [:int, 0]
+ buffer = FFI::MemoryPointer.new :pointer
+ num_chars = FormatMessageW(flags | FORMAT_MESSAGE_ALLOCATE_BUFFER, source, message_id, language_id, buffer, 0, *varargs)
+ if num_chars == 0
+ raise!
+ end
+
+ # Extract the string
+ begin
+ return buffer.read_pointer.read_wstring(num_chars)
+ ensure
+ Chef::ReservedNames::Win32::Memory.local_free(buffer.read_pointer)
+ end
+ end
+
+ def self.get_last_error
+ GetLastError()
+ end
+
+ # Raises the last error. This should only be called by
+ # Win32 API wrapper functions, and then only when wrapped
+ # in an if() statement (since it unconditionally exits)
+ # === Returns
+ # nil::: always returns nil when it does not raise
+ # === Raises
+ # Chef::Exceptions::Win32APIError:::
+ def self.raise!(message = nil)
+ code = get_last_error
+ msg = format_message(code).strip
+ formatted_message = ""
+ formatted_message << message if message
+ formatted_message << "---- Begin Win32 API output ----\n"
+ formatted_message << "System Error Code: #{code}\n"
+ formatted_message << "System Error Message: #{msg}\n"
+ formatted_message << "---- End Win32 API output ----\n"
+ raise Chef::Exceptions::Win32APIError, msg + "\n" + formatted_message
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/file.rb b/lib/chef/win32/file.rb
new file mode 100644
index 0000000000..d489c9ce8a
--- /dev/null
+++ b/lib/chef/win32/file.rb
@@ -0,0 +1,167 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Author:: Mark Mzyk (<mmzyk@ospcode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api/file'
+require 'chef/win32/api/security'
+require 'chef/win32/error'
+
+class Chef
+ module ReservedNames::Win32
+ class File
+ include Chef::ReservedNames::Win32::API::File
+ extend Chef::ReservedNames::Win32::API::File
+
+ # Creates a symbolic link called +new_name+ for the file or directory
+ # +old_name+.
+ #
+ # This method requires Windows Vista or later to work. Otherwise, it
+ # returns nil as per MRI.
+ #
+ def self.link(old_name, new_name)
+ raise Errno::ENOENT, "(#{old_name}, #{new_name})" unless ::File.exist?(old_name)
+ # TODO do a check for CreateHardLinkW and
+ # raise NotImplemented exception on older Windows
+ old_name = encode_path(old_name)
+ new_name = encode_path(new_name)
+ unless CreateHardLinkW(new_name, old_name, nil)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ # Creates a symbolic link called +new_name+ for the file or directory
+ # +old_name+.
+ #
+ # This method requires Windows Vista or later to work. Otherwise, it
+ # returns nil as per MRI.
+ #
+ def self.symlink(old_name, new_name)
+ # raise Errno::ENOENT, "(#{old_name}, #{new_name})" unless ::File.exist?(old_name)
+ # TODO do a check for CreateSymbolicLinkW and
+ # raise NotImplemented exception on older Windows
+ flags = ::File.directory?(old_name) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0
+ old_name = encode_path(old_name)
+ new_name = encode_path(new_name)
+ unless CreateSymbolicLinkW(new_name, old_name, flags)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ # Return true if the named file is a symbolic link, false otherwise.
+ #
+ # This method requires Windows Vista or later to work. Otherwise, it
+ # always returns false as per MRI.
+ #
+ def self.symlink?(file_name)
+ is_symlink = false
+ path = encode_path(file_name)
+ if ::File.exists?(file_name)
+ if ((GetFileAttributesW(path) & FILE_ATTRIBUTE_REPARSE_POINT) > 0)
+ file_search_handle(file_name) do |handle, find_data|
+ if find_data[:dw_reserved_0] == IO_REPARSE_TAG_SYMLINK
+ is_symlink = true
+ end
+ end
+ end
+ end
+ is_symlink
+ end
+
+ # Returns the path of the of the symbolic link referred to by +file+.
+ #
+ # Requires Windows Vista or later. On older versions of Windows it
+ # will raise a NotImplementedError, as per MRI.
+ #
+ def self.readlink(link_name)
+ raise Errno::ENOENT, link_name unless ::File.exists?(link_name)
+ symlink_file_handle(link_name) do |handle|
+ # Go to DeviceIoControl to get the symlink information
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364571(v=vs.85).aspx
+ reparse_buffer = FFI::MemoryPointer.new(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
+ parsed_size = FFI::Buffer.new(:long).write_long(0)
+ if DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nil, 0, reparse_buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, parsed_size, nil) == 0
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ # Ensure it's a symbolic link
+ reparse_buffer = REPARSE_DATA_BUFFER.new(reparse_buffer)
+ if reparse_buffer[:ReparseTag] != IO_REPARSE_TAG_SYMLINK
+ raise Errno::EACCES, "#{link_name} is not a symlink"
+ end
+
+ # Return the link destination (strip off \??\ at the beginning, which is a local filesystem thing)
+ link_dest = reparse_buffer.reparse_buffer.substitute_name
+ if link_dest =~ /^\\\?\?\\/
+ link_dest = link_dest[4..-1]
+ end
+ link_dest
+ end
+ end
+
+ # Gets the short form of a path (Administrator -> ADMINI~1)
+ def self.get_short_path_name(path)
+ path = path.to_wstring
+ size = GetShortPathNameW(path, nil, 0)
+ if size == 0
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ result = FFI::MemoryPointer.new :char, (size+1)*2
+ if GetShortPathNameW(path, result, size+1) == 0
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ result.read_wstring(size)
+ end
+
+ # Gets the long form of a path (ADMINI~1 -> Administrator)
+ def self.get_long_path_name(path)
+ path = path.to_wstring
+ size = GetLongPathNameW(path, nil, 0)
+ if size == 0
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ result = FFI::MemoryPointer.new :char, (size+1)*2
+ if GetLongPathNameW(path, result, size+1) == 0
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ result.read_wstring(size)
+ end
+
+ def self.info(file_name)
+ Info.new(file_name)
+ end
+
+ def self.verify_links_supported!
+ begin
+ CreateSymbolicLinkW(nil)
+ rescue Chef::Exceptions::Win32APIFunctionNotImplemented => e
+ raise e
+ rescue Exception
+ # things are ok.
+ end
+ end
+
+ # ::File compat
+ class << self
+ alias :stat :info
+ end
+
+ end
+ end
+end
+
+require 'chef/win32/file/info'
diff --git a/lib/chef/win32/file/info.rb b/lib/chef/win32/file/info.rb
new file mode 100644
index 0000000000..0f07428106
--- /dev/null
+++ b/lib/chef/win32/file/info.rb
@@ -0,0 +1,100 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/file'
+
+class Chef
+ module ReservedNames::Win32
+ class File
+
+ # Objects of class Chef::ReservedNames::Win32::File::Stat encapsulate common status
+ # information for Chef::ReservedNames::Win32::File objects. The information
+ # is recorded at the moment the Chef::ReservedNames::Win32::File::Stat object is
+ # created; changes made to the file after that point will not be reflected.
+ class Info
+
+ include Chef::ReservedNames::Win32::API::File
+ include Chef::ReservedNames::Win32::API
+
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/aa363788(v=vs.85).aspx
+ def initialize(file_name)
+ raise Errno::ENOENT, file_name unless ::File.exist?(file_name)
+ @file_info = retrieve_file_info(file_name)
+ end
+
+ def volume_serial_number
+ @file_info[:dw_volume_serial_number]
+ end
+
+ def index
+ make_uint64(@file_info[:n_file_index_low], @file_info[:n_file_index_high])
+ end
+
+ def last_access_time
+ parse_time(@file_info[:ft_last_access_time])
+ end
+
+ def creation_time
+ parse_time(@file_info[:ft_creation_time])
+ end
+
+ def last_write_time
+ parse_time(@file_info[:ft_last_write_time])
+ end
+
+ def links
+ @file_info[:n_number_of_links]
+ end
+
+ def size
+ make_uint64(@file_info[:n_file_size_low], @file_info[:n_file_size_high])
+ end
+
+ ##############################
+ # ::File::Stat compat
+ alias :atime :last_access_time
+ alias :mtime :last_write_time
+ alias :ctime :creation_time
+
+ # we're faking it here, but this is in the spirit of ino in *nix
+ #
+ # from MSDN:
+ #
+ # "The identifier (low and high parts) and the volume serial number
+ # uniquely identify a file on a single computer. To determine whether
+ # two open handles represent the same file, combine the identifier
+ # and the volume serial number for each file and compare them.""
+ #
+ def ino
+ volume_serial_number + index
+ end
+ ##############################
+
+ # given a +Chef::ReservedNames::Win32::API::File::FILETIME+ structure convert into a
+ # Ruby +Time+ object.
+ #
+ def parse_time(file_time_struct)
+ wtime_to_time(make_uint64(file_time_struct[:dw_low_date_time],
+ file_time_struct[:dw_high_date_time]))
+ end
+
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/handle.rb b/lib/chef/win32/handle.rb
new file mode 100644
index 0000000000..60e35916ad
--- /dev/null
+++ b/lib/chef/win32/handle.rb
@@ -0,0 +1,48 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api/process'
+require 'chef/win32/api/psapi'
+require 'chef/win32/api/system'
+require 'chef/win32/error'
+
+class Chef
+ module ReservedNames::Win32
+ class Handle
+ extend Chef::ReservedNames::Win32::API::Process
+
+ def initialize(handle)
+ @handle = handle
+ ObjectSpace.define_finalizer(self, Handle.close_handle_finalizer(handle))
+ end
+
+ attr_reader :handle
+
+ def self.close_handle_finalizer(handle)
+ proc { close_handle(handle) }
+ end
+
+ def self.close_handle(handle)
+ unless CloseHandle(handle)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/win32/memory.rb b/lib/chef/win32/memory.rb
new file mode 100644
index 0000000000..8a61d27ef0
--- /dev/null
+++ b/lib/chef/win32/memory.rb
@@ -0,0 +1,101 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/error'
+require 'chef/win32/api/memory'
+
+class Chef
+ module ReservedNames::Win32
+ class Memory
+ include Chef::ReservedNames::Win32::API::Memory
+ extend Chef::ReservedNames::Win32::API::Memory
+
+ # local_alloc(length[, flags]) [BLOCK]
+ # Allocates memory using LocalAlloc
+ # If BLOCK is specified, the memory will be passed
+ # to the block and freed afterwards.
+ def self.local_alloc(length, flags = LPTR, &block)
+ result = LocalAlloc(flags, length)
+ if result.null?
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ # If a block is passed, handle freeing the memory at the end
+ if block != nil
+ begin
+ yield result
+ ensure
+ local_free(result)
+ end
+ else
+ result
+ end
+ end
+
+ # local_discard(pointer)
+ # Discard memory. Equivalent to local_realloc(pointer, 0)
+ def self.local_discard(pointer)
+ local_realloc(pointer, 0, LMEM_MOVEABLE)
+ end
+
+ # local_flags(pointer)
+ # Get lock count and Windows flags for local_alloc allocated memory.
+ # Use: flags, lock_count = local_flags(pointer)
+ def self.local_flags(pointer)
+ result = LocalFlags(pointer)
+ if result == LMEM_INVALID_HANDLE
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ [ result & ~LMEM_LOCKCOUNT, result & LMEM_LOCKCOUNT ]
+ end
+
+ # local_free(pointer)
+ # Free memory allocated using local_alloc
+ def self.local_free(pointer)
+ result = LocalFree(pointer)
+ if !result.null?
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ # local_realloc(pointer, size[, flags])
+ # Resizes memory allocated using LocalAlloc.
+ def self.local_realloc(pointer, size, flags = LMEM_MOVEABLE | LMEM_ZEROINIT)
+ result = LocalReAlloc(pointer, size, flags)
+ if result.null?
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ result
+ end
+
+ # local_size(pointer)
+ # Gets the size of memory allocated using LocalAlloc.
+ def self.local_size(pointer)
+ result = LocalSize(pointer)
+ if result == 0
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ result
+ end
+
+ def self.local_free_finalizer(pointer)
+ proc { local_free(pointer) }
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/win32/process.rb b/lib/chef/win32/process.rb
new file mode 100644
index 0000000000..2df39bb918
--- /dev/null
+++ b/lib/chef/win32/process.rb
@@ -0,0 +1,84 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api/process'
+require 'chef/win32/api/psapi'
+require 'chef/win32/error'
+require 'chef/win32/handle'
+require 'ffi'
+
+class Chef
+ module ReservedNames::Win32
+ class Process
+ include Chef::ReservedNames::Win32::API::Process
+ extend Chef::ReservedNames::Win32::API::Process
+ include Chef::ReservedNames::Win32::API::PSAPI
+ extend Chef::ReservedNames::Win32::API::PSAPI
+
+ def initialize(handle)
+ @handle = handle
+ end
+
+ attr_reader :handle
+
+ def id
+ Process.get_process_id(handle)
+ end
+
+ def handle_count
+ Process.get_process_handle_count(handle)
+ end
+
+ def memory_info
+ Process.get_process_memory_info(handle)
+ end
+
+ def self.get_current_process
+ Process.new(Handle.new(GetCurrentProcess()))
+ end
+
+ def self.get_process_handle_count(handle)
+ handle_count = FFI::MemoryPointer.new :uint32
+ unless GetProcessHandleCount(handle.handle, handle_count)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ handle_count.read_uint32
+ end
+
+ def self.get_process_id(handle)
+ # Must have PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION rights
+ result = GetProcessId(handle.handle)
+ if result == 0
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ result
+ end
+
+ # Must have PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION rights,
+ # AND the PROCESS_VM_READ right
+ def self.get_process_memory_info(handle)
+ memory_info = PROCESS_MEMORY_COUNTERS.new
+ unless GetProcessMemoryInfo(handle.handle, memory_info, memory_info.size)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ memory_info
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/win32/security.rb b/lib/chef/win32/security.rb
new file mode 100644
index 0000000000..b7b14c5652
--- /dev/null
+++ b/lib/chef/win32/security.rb
@@ -0,0 +1,489 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api/security'
+require 'chef/win32/error'
+require 'chef/win32/memory'
+require 'chef/win32/process'
+require 'chef/win32/unicode'
+require 'chef/win32/security/token'
+
+class Chef
+ module ReservedNames::Win32
+ class Security
+ include Chef::ReservedNames::Win32::API::Error
+ extend Chef::ReservedNames::Win32::API::Error
+ include Chef::ReservedNames::Win32::API::Security
+ extend Chef::ReservedNames::Win32::API::Security
+ extend Chef::ReservedNames::Win32::API::Macros
+
+ def self.add_ace(acl, ace, insert_position = MAXDWORD, revision = ACL_REVISION)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ ace = ace.pointer if ace.respond_to?(:pointer)
+ ace_size = ACE_HEADER.new(ace)[:AceSize]
+ unless AddAce(acl, revision, insert_position, ace, ace_size)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.add_access_allowed_ace(acl, sid, access_mask, revision = ACL_REVISION)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+ unless AddAccessAllowedAce(acl, revision, access_mask, sid)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.add_access_allowed_ace_ex(acl, sid, access_mask, flags = 0, revision = ACL_REVISION)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+ unless AddAccessAllowedAceEx(acl, revision, flags, access_mask, sid)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.add_access_denied_ace(acl, sid, access_mask, revision = ACL_REVISION)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+ unless AddAccessDeniedAce(acl, revision, access_mask, sid)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.add_access_denied_ace_ex(acl, sid, access_mask, flags = 0, revision = ACL_REVISION)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+ unless AddAccessDeniedAceEx(acl, revision, flags, access_mask, sid)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.adjust_token_privileges(token, privileges)
+ token = token.handle if token.respond_to?(:handle)
+ old_privileges_size = FFI::Buffer.new(:long).write_long(privileges.size_with_privileges)
+ old_privileges = TOKEN_PRIVILEGES.new(FFI::Buffer.new(old_privileges_size.read_long))
+ unless AdjustTokenPrivileges(token.handle, false, privileges, privileges.size_with_privileges, old_privileges, old_privileges_size)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ old_privileges
+ end
+
+ def self.convert_sid_to_string_sid(sid)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+ result = FFI::MemoryPointer.new :pointer
+ # TODO: use the W version
+ unless ConvertSidToStringSidA(sid, result)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ result_string = result.read_pointer.read_string
+
+ Chef::ReservedNames::Win32::Memory.local_free(result.read_pointer)
+
+ result_string
+ end
+
+ def self.convert_string_sid_to_sid(string_sid)
+ result = FFI::MemoryPointer.new :pointer
+ unless ConvertStringSidToSidW(string_sid.to_wstring, result)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ result_pointer = result.read_pointer
+ sid = SID.new(result_pointer)
+
+ # The result pointer must be freed with local_free
+ ObjectSpace.define_finalizer(sid, Memory.local_free_finalizer(result_pointer))
+
+ sid
+ end
+
+ def self.delete_ace(acl, index)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ unless DeleteAce(acl, index)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.equal_sid(sid1, sid2)
+ sid1 = sid1.pointer if sid1.respond_to?(:pointer)
+ sid2 = sid2.pointer if sid2.respond_to?(:pointer)
+ EqualSid(sid1, sid2)
+ end
+
+ def self.free_sid(sid)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+ unless FreeSid(sid).null?
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.get_ace(acl, index)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ ace = FFI::Buffer.new :pointer
+ unless GetAce(acl, index, ace)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ ACE.new(ace.read_pointer, acl)
+ end
+
+ def self.get_length_sid(sid)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+ GetLengthSid(sid)
+ end
+
+ def self.get_named_security_info(path, type = :SE_FILE_OBJECT, info = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION)
+ security_descriptor = FFI::MemoryPointer.new :pointer
+ hr = GetNamedSecurityInfoW(path.to_wstring, type, info, nil, nil, nil, nil, security_descriptor)
+ if hr != ERROR_SUCCESS
+ Chef::ReservedNames::Win32::Error.raise!("get_named_security_info(#{path}, #{type}, #{info})")
+ end
+
+ result_pointer = security_descriptor.read_pointer
+ result = SecurityDescriptor.new(result_pointer)
+
+ # This memory has to be freed with LocalFree.
+ ObjectSpace.define_finalizer(result, Memory.local_free_finalizer(result_pointer))
+
+ result
+ end
+
+ def self.get_security_descriptor_control(security_descriptor)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ result = FFI::Buffer.new :ushort
+ version = FFI::Buffer.new :uint32
+ unless GetSecurityDescriptorControl(security_descriptor, result, version)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ [ result.read_ushort, version.read_uint32 ]
+ end
+
+ def self.get_security_descriptor_dacl(security_descriptor)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ present = FFI::Buffer.new :bool
+ defaulted = FFI::Buffer.new :bool
+ acl = FFI::Buffer.new :pointer
+ unless GetSecurityDescriptorDacl(security_descriptor, present, acl, defaulted)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ acl = acl.read_pointer
+ [ present.read_char != 0, acl.null? ? nil : ACL.new(acl, security_descriptor), defaulted.read_char != 0 ]
+ end
+
+ def self.get_security_descriptor_group(security_descriptor)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ result = FFI::Buffer.new :pointer
+ defaulted = FFI::Buffer.new :long
+ unless GetSecurityDescriptorGroup(security_descriptor, result, defaulted)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ sid = SID.new(result.read_pointer, security_descriptor)
+ defaulted = defaulted.read_char != 0
+ [ sid, defaulted ]
+ end
+
+ def self.get_security_descriptor_owner(security_descriptor)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ result = FFI::Buffer.new :pointer
+ defaulted = FFI::Buffer.new :long
+ unless GetSecurityDescriptorOwner(security_descriptor, result, defaulted)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ sid = SID.new(result.read_pointer, security_descriptor)
+ defaulted = defaulted.read_char != 0
+ [ sid, defaulted ]
+ end
+
+ def self.get_security_descriptor_sacl(security_descriptor)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ present = FFI::Buffer.new :bool
+ defaulted = FFI::Buffer.new :bool
+ acl = FFI::Buffer.new :pointer
+ unless GetSecurityDescriptorSacl(security_descriptor, present, acl, defaulted)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ acl = acl.read_pointer
+ [ present.read_char != 0, acl.null? ? nil : ACL.new(acl, security_descriptor), defaulted.read_char != 0 ]
+ end
+
+ def self.initialize_acl(acl_size)
+ acl = FFI::MemoryPointer.new acl_size
+ unless InitializeAcl(acl, acl_size, ACL_REVISION)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ ACL.new(acl)
+ end
+
+ def self.initialize_security_descriptor(revision = SECURITY_DESCRIPTOR_REVISION)
+ security_descriptor = FFI::MemoryPointer.new SECURITY_DESCRIPTOR_MIN_LENGTH
+ unless InitializeSecurityDescriptor(security_descriptor, revision)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ SecurityDescriptor.new(security_descriptor)
+ end
+
+ def self.is_valid_acl(acl)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ IsValidAcl(acl) != 0
+ end
+
+ def self.is_valid_security_descriptor(security_descriptor)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ IsValidSecurityDescriptor(security_descriptor) != 0
+ end
+
+ def self.is_valid_sid(sid)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+ IsValidSid(sid) != 0
+ end
+
+ def self.lookup_account_name(name, system_name = nil)
+ # Figure out how big the buffers need to be
+ sid_size = FFI::Buffer.new(:long).write_long(0)
+ referenced_domain_name_size = FFI::Buffer.new(:long).write_long(0)
+ system_name = system_name.to_wstring if system_name
+ if LookupAccountNameW(system_name, name.to_wstring, nil, sid_size, nil, referenced_domain_name_size, nil)
+ raise "Expected ERROR_INSUFFICIENT_BUFFER from LookupAccountName, and got no error!"
+ elsif Chef::ReservedNames::Win32::Error.get_last_error != ERROR_INSUFFICIENT_BUFFER
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ sid = FFI::MemoryPointer.new :char, sid_size.read_long
+ referenced_domain_name = FFI::MemoryPointer.new :char, (referenced_domain_name_size.read_long*2)
+ use = FFI::Buffer.new(:long).write_long(0)
+ unless LookupAccountNameW(system_name, name.to_wstring, sid, sid_size, referenced_domain_name, referenced_domain_name_size, use)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ [ referenced_domain_name.read_wstring(referenced_domain_name_size.read_long), SID.new(sid), use.read_long ]
+ end
+
+ def self.lookup_account_sid(sid, system_name = nil)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+ # Figure out how big the buffer needs to be
+ name_size = FFI::Buffer.new(:long).write_long(0)
+ referenced_domain_name_size = FFI::Buffer.new(:long).write_long(0)
+ system_name = system_name.to_wstring if system_name
+ if LookupAccountSidW(system_name, sid, nil, name_size, nil, referenced_domain_name_size, nil)
+ raise "Expected ERROR_INSUFFICIENT_BUFFER from LookupAccountSid, and got no error!"
+ elsif Chef::ReservedNames::Win32::Error::get_last_error != ERROR_INSUFFICIENT_BUFFER
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ name = FFI::MemoryPointer.new :char, (name_size.read_long*2)
+ referenced_domain_name = FFI::MemoryPointer.new :char, (referenced_domain_name_size.read_long*2)
+ use = FFI::Buffer.new(:long).write_long(0)
+ unless LookupAccountSidW(system_name, sid, name, name_size, referenced_domain_name, referenced_domain_name_size, use)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ [ referenced_domain_name.read_wstring(referenced_domain_name_size.read_long), name.read_wstring(name_size.read_long), use.read_long ]
+ end
+
+ def self.lookup_privilege_name(system_name, luid)
+ system_name = system_name.to_wstring if system_name
+ name_size = FFI::Buffer.new(:long).write_long(0)
+ if LookupPrivilegeNameW(system_name, luid, nil, name_size)
+ raise "Expected ERROR_INSUFFICIENT_BUFFER from LookupPrivilegeName, and got no error!"
+ elsif Chef::ReservedNames::Win32::Error.get_last_error != ERROR_INSUFFICIENT_BUFFER
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ name = FFI::MemoryPointer.new :char, (name_size.read_long*2)
+ unless LookupPrivilegeNameW(system_name, luid, name, name_size)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ name.read_wstring(name_size.read_long)
+ end
+
+ def self.lookup_privilege_display_name(system_name, name)
+ system_name = system_name.to_wstring if system_name
+ display_name_size = FFI::Buffer.new(:long).write_long(0)
+ language_id = FFI::Buffer.new(:long)
+ if LookupPrivilegeDisplayNameW(system_name, name.to_wstring, nil, display_name_size, language_id)
+ raise "Expected ERROR_INSUFFICIENT_BUFFER from LookupPrivilegeDisplayName, and got no error!"
+ elsif Chef::ReservedNames::Win32::Error.get_last_error != ERROR_INSUFFICIENT_BUFFER
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ display_name = FFI::MemoryPointer.new :char, (display_name_size.read_long*2)
+ unless LookupPrivilegeDisplayNameW(system_name, name.to_wstring, display_name, display_name_size, language_id)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ [ display_name.read_wstring(display_name_size.read_long), language_id.read_long ]
+ end
+
+ def self.lookup_privilege_value(system_name, name)
+ luid = FFI::Buffer.new(:uint64).write_uint64(0)
+ system_name = system_name.to_wstring if system_name
+ unless LookupPrivilegeValueW(system_name, name.to_wstring, luid)
+ Win32::Error.raise!
+ end
+ luid.read_uint64
+ end
+
+ def self.make_absolute_sd(security_descriptor)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+
+ # Figure out buffer sizes
+ absolute_sd_size = FFI::Buffer.new(:long).write_long(0)
+ dacl_size = FFI::Buffer.new(:long).write_long(0)
+ sacl_size = FFI::Buffer.new(:long).write_long(0)
+ owner_size = FFI::Buffer.new(:long).write_long(0)
+ group_size = FFI::Buffer.new(:long).write_long(0)
+ if MakeAbsoluteSD(security_descriptor, nil, absolute_sd_size, nil, dacl_size, nil, sacl_size, nil, owner_size, nil, group_size)
+ raise "Expected ERROR_INSUFFICIENT_BUFFER from MakeAbsoluteSD, and got no error!"
+ elsif Chef::ReservedNames::Win32::Error.get_last_error != ERROR_INSUFFICIENT_BUFFER
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ absolute_sd = FFI::MemoryPointer.new absolute_sd_size.read_long
+ owner = FFI::MemoryPointer.new owner_size.read_long
+ group = FFI::MemoryPointer.new group_size.read_long
+ dacl = FFI::MemoryPointer.new dacl_size.read_long
+ sacl = FFI::MemoryPointer.new sacl_size.read_long
+ unless MakeAbsoluteSD(security_descriptor, absolute_sd, absolute_sd_size, dacl, dacl_size, sacl, sacl_size, owner, owner_size, group, group_size)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ [ SecurityDescriptor.new(absolute_sd), SID.new(owner), SID.new(group), ACL.new(dacl), ACL.new(sacl) ]
+ end
+
+ def self.open_process_token(process, desired_access)
+ process = process.handle if process.respond_to?(:handle)
+ process = process.handle if process.respond_to?(:handle)
+ token = FFI::Buffer.new(:ulong)
+ unless OpenProcessToken(process, desired_access, token)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ Token.new(Handle.new(token.read_ulong))
+ end
+
+ def self.query_security_access_mask(security_information)
+ result = FFI::Buffer.new(:long)
+ QuerySecurityAccessMask(security_information, result)
+ result.read_long
+ end
+
+ def self.set_file_security(path, security_information, security_descriptor)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ unless SetFileSecurityW(path.to_wstring, security_information, security_descriptor)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.set_named_security_info(path, type, args)
+ owner = args[:owner]
+ group = args[:group]
+ dacl = args[:dacl]
+ sacl = args[:sacl]
+ owner = owner.pointer if owner && owner.respond_to?(:pointer)
+ group = group.pointer if group && group.respond_to?(:pointer)
+ dacl = dacl.pointer if dacl && dacl.respond_to?(:pointer)
+ sacl = sacl.pointer if sacl && sacl.respond_to?(:pointer)
+
+ # Determine the security_information flags
+ security_information = 0
+ security_information |= OWNER_SECURITY_INFORMATION if args.has_key?(:owner)
+ security_information |= GROUP_SECURITY_INFORMATION if args.has_key?(:group)
+ security_information |= DACL_SECURITY_INFORMATION if args.has_key?(:dacl)
+ security_information |= SACL_SECURITY_INFORMATION if args.has_key?(:sacl)
+ if args.has_key?(:dacl_inherits)
+ security_information |= (args[:dacl_inherits] ? UNPROTECTED_DACL_SECURITY_INFORMATION : PROTECTED_DACL_SECURITY_INFORMATION)
+ end
+ if args.has_key?(:sacl_inherits)
+ security_information |= (args[:sacl_inherits] ? UNPROTECTED_SACL_SECURITY_INFORMATION : PROTECTED_SACL_SECURITY_INFORMATION)
+ end
+
+ hr = SetNamedSecurityInfoW(path.to_wstring, type, security_information, owner, group, dacl, sacl)
+ if hr != ERROR_SUCCESS
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.set_security_access_mask(security_information)
+ result = FFI::Buffer.new(:long)
+ SetSecurityAccessMask(security_information, result)
+ result.read_long
+ end
+
+ def set_security_descriptor_dacl(security_descriptor, acl, defaulted = false, present = nil)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ present = !security_descriptor.null? if present == nil
+
+ unless SetSecurityDescriptorDacl(security_descriptor, present, acl, defaulted)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.set_security_descriptor_group(security_descriptor, sid, defaulted = false)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+
+ unless SetSecurityDescriptorGroup(security_descriptor, sid, defaulted)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.set_security_descriptor_owner(security_descriptor, sid, defaulted = false)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ sid = sid.pointer if sid.respond_to?(:pointer)
+
+ unless SetSecurityDescriptorOwner(security_descriptor, sid, defaulted)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.set_security_descriptor_sacl(security_descriptor, acl, defaulted = false, present = nil)
+ security_descriptor = security_descriptor.pointer if security_descriptor.respond_to?(:pointer)
+ acl = acl.pointer if acl.respond_to?(:pointer)
+ present = !security_descriptor.null? if present == nil
+
+ unless SetSecurityDescriptorSacl(security_descriptor, present, acl, defaulted)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ end
+
+ def self.with_privileges(*privilege_names)
+ # Set privileges
+ token = open_process_token(Chef::ReservedNames::Win32::Process.get_current_process, TOKEN_READ | TOKEN_ADJUST_PRIVILEGES)
+ old_privileges = token.enable_privileges(*privilege_names)
+
+ # Let the caller do their privileged stuff
+ begin
+ yield
+ ensure
+ # Set privileges back to what they were before
+ token.adjust_privileges(old_privileges)
+ end
+ end
+ end
+ end
+end
+
+require 'chef/win32/security/ace'
+require 'chef/win32/security/acl'
+require 'chef/win32/security/securable_object'
+require 'chef/win32/security/security_descriptor'
+require 'chef/win32/security/sid'
diff --git a/lib/chef/win32/security/ace.rb b/lib/chef/win32/security/ace.rb
new file mode 100644
index 0000000000..efd44b1c85
--- /dev/null
+++ b/lib/chef/win32/security/ace.rb
@@ -0,0 +1,125 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/security'
+require 'chef/win32/security/sid'
+require 'chef/win32/memory'
+
+require 'ffi'
+
+class Chef
+ module ReservedNames::Win32
+ class Security
+ class ACE
+
+ def initialize(pointer, owner = nil)
+ if Chef::ReservedNames::Win32::API::Security::ACE_WITH_MASK_AND_SID.supports?(pointer.read_uchar)
+ @struct = Chef::ReservedNames::Win32::API::Security::ACE_WITH_MASK_AND_SID.new pointer
+ else
+ # TODO Support ALL the things
+ @struct = Chef::ReservedNames::Win32::API::Security::ACE_HEADER.new pointer
+ end
+ # Keep a reference to the actual owner of this memory so we don't get freed
+ @owner = owner
+ end
+
+ def self.size_with_sid(sid)
+ Chef::ReservedNames::Win32::API::Security::ACE_WITH_MASK_AND_SID.offset_of(:SidStart) + sid.size
+ end
+
+ def self.access_allowed(sid, mask, flags = 0)
+ create_ace_with_mask_and_sid(Chef::ReservedNames::Win32::API::Security::ACCESS_ALLOWED_ACE_TYPE, flags, mask, sid)
+ end
+
+ def self.access_denied(sid, mask, flags = 0)
+ create_ace_with_mask_and_sid(Chef::ReservedNames::Win32::API::Security::ACCESS_DENIED_ACE_TYPE, flags, mask, sid)
+ end
+
+ attr_reader :struct
+
+ def ==(other)
+ type == other.type && flags == other.flags && mask == other.mask && sid == other.sid
+ end
+
+ def dup
+ ACE.create_ace_with_mask_and_sid(type, flags, mask, sid)
+ end
+
+ def flags
+ struct[:AceFlags]
+ end
+
+ def flags=(val)
+ struct[:AceFlags] = val
+ end
+
+ def explicit?
+ ! inherited?
+ end
+
+ def inherited?
+ (struct[:AceFlags] & Chef::ReservedNames::Win32::API::Security::INHERITED_ACE) != 0
+ end
+
+ def mask
+ struct[:Mask]
+ end
+
+ def mask=(val)
+ struct[:Mask] = val
+ end
+
+ def pointer
+ struct.pointer
+ end
+
+ def size
+ struct[:AceSize]
+ end
+
+ def sid
+ # The SID runs off the end of the structure, starting at :SidStart.
+ # Use pointer arithmetic to get a pointer to that location.
+ Chef::ReservedNames::Win32::Security::SID.new(struct.pointer + struct.offset_of(:SidStart))
+ end
+
+ def to_s
+ "#{sid.account_name}/flags:#{flags.to_s(16)}/mask:#{mask.to_s(16)}"
+ end
+
+ def type
+ struct[:AceType]
+ end
+
+ private
+
+ def self.create_ace_with_mask_and_sid(type, flags, mask, sid)
+ size_needed = size_with_sid(sid)
+ pointer = FFI::MemoryPointer.new size_needed
+ struct = Chef::ReservedNames::Win32::API::Security::ACE_WITH_MASK_AND_SID.new pointer
+ struct[:AceType] = type
+ struct[:AceFlags] = flags
+ struct[:AceSize] = size_needed
+ struct[:Mask] = mask
+ Chef::ReservedNames::Win32::Memory.memcpy(struct.pointer + struct.offset_of(:SidStart), sid.pointer, sid.size)
+ ACE.new(struct.pointer)
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/lib/chef/win32/security/acl.rb b/lib/chef/win32/security/acl.rb
new file mode 100644
index 0000000000..fd43b75cbf
--- /dev/null
+++ b/lib/chef/win32/security/acl.rb
@@ -0,0 +1,101 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/security'
+require 'chef/win32/security/ace'
+require 'ffi'
+
+class Chef
+ module ReservedNames::Win32
+ class Security
+ class ACL
+ include Enumerable
+
+ def initialize(pointer, owner = nil)
+ @struct = Chef::ReservedNames::Win32::API::Security::ACLStruct.new pointer
+ # Keep a reference to the actual owner of this memory so that it isn't freed out from under us
+ # TODO this could be avoided if we could mark a pointer's parent manually
+ @owner = owner
+ end
+
+ def self.create(aces)
+ aces_size = aces.inject(0) { |sum,ace| sum + ace.size }
+ acl_size = align_dword(Chef::ReservedNames::Win32::API::Security::ACLStruct.size + aces_size) # What the heck is 94???
+ acl = Chef::ReservedNames::Win32::Security.initialize_acl(acl_size)
+ aces.each { |ace| Chef::ReservedNames::Win32::Security.add_ace(acl, ace) }
+ acl
+ end
+
+ attr_reader :struct
+
+ def ==(other)
+ return false if length != other.length
+ 0.upto(length-1) do |i|
+ return false if self[i] != other[i]
+ end
+ return true
+ end
+
+ def pointer
+ struct.pointer
+ end
+
+ def [](index)
+ Chef::ReservedNames::Win32::Security.get_ace(self, index)
+ end
+
+ def delete_at(index)
+ Chef::ReservedNames::Win32::Security.delete_ace(self, index)
+ end
+
+ def each
+ 0.upto(length-1) { |i| yield self[i] }
+ end
+
+ def insert(index, *aces)
+ aces.reverse_each { |ace| add_ace(self, ace, index) }
+ end
+
+ def length
+ struct[:AceCount]
+ end
+
+ def push(*aces)
+ aces.each { |ace| Chef::ReservedNames::Win32::Security.add_ace(self, ace) }
+ end
+
+ def unshift(*aces)
+ aces.each { |ace| Chef::ReservedNames::Win32::Security.add_ace(self, ace, 0) }
+ end
+
+ def valid?
+ Chef::ReservedNames::Win32::Security.is_valid_acl(self)
+ end
+
+ def to_s
+ "[#{self.collect { |ace| ace.to_s }.join(", ")}]"
+ end
+ private
+
+ def self.align_dword(size)
+ (size + 4 - 1) & 0xfffffffc
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/security/securable_object.rb b/lib/chef/win32/security/securable_object.rb
new file mode 100644
index 0000000000..00655c9bab
--- /dev/null
+++ b/lib/chef/win32/security/securable_object.rb
@@ -0,0 +1,109 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/security'
+require 'chef/win32/security/acl'
+require 'chef/win32/security/sid'
+
+class Chef
+ module ReservedNames::Win32
+ class Security
+ class SecurableObject
+
+ def initialize(path, type = :SE_FILE_OBJECT)
+ @path = path
+ @type = type
+ end
+
+ attr_reader :path
+ attr_reader :type
+
+ SecurityConst = Chef::ReservedNames::Win32::API::Security
+
+ # This method predicts what the rights mask would be on an object
+ # if you created an ACE with the given mask. Specifically, it looks for
+ # generic attributes like GENERIC_READ, and figures out what specific
+ # attributes will be set. This is important if you want to try to
+ # compare an existing ACE with one you want to create.
+ def predict_rights_mask(generic_mask)
+ mask = generic_mask
+ #mask |= Chef::ReservedNames::Win32::API::Security::STANDARD_RIGHTS_READ if (mask | Chef::ReservedNames::Win32::API::Security::GENERIC_READ) != 0
+ #mask |= Chef::ReservedNames::Win32::API::Security::STANDARD_RIGHTS_WRITE if (mask | Chef::ReservedNames::Win32::API::Security::GENERIC_WRITE) != 0
+ #mask |= Chef::ReservedNames::Win32::API::Security::STANDARD_RIGHTS_EXECUTE if (mask | Chef::ReservedNames::Win32::API::Security::GENERIC_EXECUTE) != 0
+ #mask |= Chef::ReservedNames::Win32::API::Security::STANDARD_RIGHTS_ALL if (mask | Chef::ReservedNames::Win32::API::Security::GENERIC_ALL) != 0
+ if type == :SE_FILE_OBJECT
+ mask |= Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_READ if (mask & Chef::ReservedNames::Win32::API::Security::GENERIC_READ) != 0
+ mask |= Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_WRITE if (mask & Chef::ReservedNames::Win32::API::Security::GENERIC_WRITE) != 0
+ mask |= Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_EXECUTE if (mask & Chef::ReservedNames::Win32::API::Security::GENERIC_EXECUTE) != 0
+ mask |= Chef::ReservedNames::Win32::API::Security::FILE_ALL_ACCESS if (mask & Chef::ReservedNames::Win32::API::Security::GENERIC_ALL) != 0
+ else
+ raise "Unimplemented object type for predict_security_mask: #{type}"
+ end
+ mask &= ~(Chef::ReservedNames::Win32::API::Security::GENERIC_READ | Chef::ReservedNames::Win32::API::Security::GENERIC_WRITE | Chef::ReservedNames::Win32::API::Security::GENERIC_EXECUTE | Chef::ReservedNames::Win32::API::Security::GENERIC_ALL)
+ mask
+ end
+
+ def security_descriptor(include_sacl = false)
+ security_information = Chef::ReservedNames::Win32::API::Security::OWNER_SECURITY_INFORMATION | Chef::ReservedNames::Win32::API::Security::GROUP_SECURITY_INFORMATION | Chef::ReservedNames::Win32::API::Security::DACL_SECURITY_INFORMATION
+ if include_sacl
+ security_information |= Chef::ReservedNames::Win32::API::Security::SACL_SECURITY_INFORMATION
+ Security.with_privileges("SeSecurityPrivilege") do
+ Security.get_named_security_info(path, type, security_information)
+ end
+ else
+ Security.get_named_security_info(path, type, security_information)
+ end
+ end
+
+ def dacl=(val)
+ Security.set_named_security_info(path, type, :dacl => val)
+ end
+
+ # You don't set dacl_inherits without also setting dacl,
+ # because Windows gets angry and denies you access. So
+ # if you want to do that, you may as well do both at once.
+ def set_dacl(dacl, dacl_inherits)
+ Security.set_named_security_info(path, type, :dacl => dacl, :dacl_inherits => dacl_inherits)
+ end
+
+ def group=(val)
+ Security.set_named_security_info(path, type, :group => val)
+ end
+
+ def owner=(val)
+ # TODO to fix serious permissions problems, we may need to enable SeBackupPrivilege. But we might need it (almost) everywhere else, too.
+ Security.with_privileges("SeTakeOwnershipPrivilege", "SeRestorePrivilege") do
+ Security.set_named_security_info(path, type, :owner => val)
+ end
+ end
+
+ def sacl=(val)
+ Security.with_privileges("SeSecurityPrivilege") do
+ Security.set_named_security_info(path, type, :sacl => val)
+ end
+ end
+
+ def set_sacl(sacl, sacl_inherits)
+ Security.with_privileges("SeSecurityPrivilege") do
+ Security.set_named_security_info(path, type, :sacl => sacl, :sacl_inherits => sacl_inherits)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/security/security_descriptor.rb b/lib/chef/win32/security/security_descriptor.rb
new file mode 100644
index 0000000000..658e9104b4
--- /dev/null
+++ b/lib/chef/win32/security/security_descriptor.rb
@@ -0,0 +1,93 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/security'
+require 'chef/win32/security/acl'
+require 'chef/win32/security/sid'
+
+class Chef
+ module ReservedNames::Win32
+ class Security
+ class SecurityDescriptor
+
+ def initialize(pointer)
+ @pointer = pointer
+ end
+
+ attr_reader :pointer
+
+ def absolute?
+ !self_relative?
+ end
+
+ def control
+ control, version = Chef::ReservedNames::Win32::Security.get_security_descriptor_control(self)
+ control
+ end
+
+ def dacl
+ raise "DACL not present" if !dacl_present?
+ present, acl, defaulted = Chef::ReservedNames::Win32::Security.get_security_descriptor_dacl(self)
+ acl
+ end
+
+ def dacl_inherits?
+ (control & Chef::ReservedNames::Win32::API::Security::SE_DACL_PROTECTED) == 0
+ end
+
+ def dacl_present?
+ (control & Chef::ReservedNames::Win32::API::Security::SE_DACL_PRESENT) != 0
+ end
+
+ def group
+ result, defaulted = Chef::ReservedNames::Win32::Security.get_security_descriptor_group(self)
+ result
+ end
+
+ def owner
+ result, defaulted = Chef::ReservedNames::Win32::Security.get_security_descriptor_owner(self)
+ result
+ end
+
+ def sacl
+ raise "SACL not present" if !sacl_present?
+ Security.with_privileges("SeSecurityPrivilege") do
+ present, acl, defaulted = Chef::ReservedNames::Win32::Security.get_security_descriptor_sacl(self)
+ acl
+ end
+ end
+
+ def sacl_inherits?
+ (control & Chef::ReservedNames::Win32::API::Security::SE_SACL_PROTECTED) == 0
+ end
+
+ def sacl_present?
+ (control & Chef::ReservedNames::Win32::API::Security::SE_SACL_PRESENT) != 0
+ end
+
+ def self_relative?
+ (control & Chef::ReservedNames::Win32::API::Security::SE_SELF_RELATIVE) != 0
+ end
+
+ def valid?
+ Chef::ReservedNames::Win32::Security.is_valid_security_descriptor(self)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/security/sid.rb b/lib/chef/win32/security/sid.rb
new file mode 100644
index 0000000000..7ca21eee79
--- /dev/null
+++ b/lib/chef/win32/security/sid.rb
@@ -0,0 +1,199 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/security'
+
+class Chef
+ module ReservedNames::Win32
+ class Security
+ class SID
+
+ def initialize(pointer, owner = nil)
+ @pointer = pointer
+ # Keep a reference to the actual owner of this memory so we don't get freed
+ @owner = owner
+ end
+
+ def self.from_account(name)
+ domain, sid, use = Chef::ReservedNames::Win32::Security.lookup_account_name(name)
+ sid
+ end
+
+ def self.from_string_sid(string_sid)
+ Chef::ReservedNames::Win32::Security::convert_string_sid_to_sid(string_sid)
+ end
+
+ def ==(other)
+ other != nil && Chef::ReservedNames::Win32::Security.equal_sid(self, other)
+ end
+
+ attr_reader :pointer
+
+ def account
+ Chef::ReservedNames::Win32::Security.lookup_account_sid(self)
+ end
+
+ def account_name
+ domain, name, use = account
+ (domain != nil && domain.length > 0) ? "#{domain}\\#{name}" : name
+ end
+
+ def size
+ Chef::ReservedNames::Win32::Security.get_length_sid(self)
+ end
+
+ def to_s
+ Chef::ReservedNames::Win32::Security.convert_sid_to_string_sid(self)
+ end
+
+ def valid?
+ Chef::ReservedNames::Win32::Security.is_valid_sid(self)
+ end
+
+ # Well-known SIDs
+ def self.Null
+ SID.from_string_sid('S-1-0')
+ end
+ def self.Nobody
+ SID.from_string_sid('S-1-0-0')
+ end
+ def self.World
+ SID.from_string_sid('S-1-1')
+ end
+ def self.Everyone
+ SID.from_string_sid('S-1-1-0')
+ end
+ def self.Local
+ SID.from_string_sid('S-1-2')
+ end
+ def self.Creator
+ SID.from_string_sid('S-1-3')
+ end
+ def self.CreatorOwner
+ SID.from_string_sid('S-1-3-0')
+ end
+ def self.CreatorGroup
+ SID.from_string_sid('S-1-3-1')
+ end
+ def self.CreatorOwnerServer
+ SID.from_string_sid('S-1-3-2')
+ end
+ def self.CreatorGroupServer
+ SID.from_string_sid('S-1-3-3')
+ end
+ def self.NonUnique
+ SID.from_string_sid('S-1-4')
+ end
+ def self.Nt
+ SID.from_string_sid('S-1-5')
+ end
+ def self.Dialup
+ SID.from_string_sid('S-1-5-1')
+ end
+ def self.Network
+ SID.from_string_sid('S-1-5-2')
+ end
+ def self.Batch
+ SID.from_string_sid('S-1-5-3')
+ end
+ def self.Interactive
+ SID.from_string_sid('S-1-5-4')
+ end
+ def self.Service
+ SID.from_string_sid('S-1-5-6')
+ end
+ def self.Anonymous
+ SID.from_string_sid('S-1-5-7')
+ end
+ def self.Proxy
+ SID.from_string_sid('S-1-5-8')
+ end
+ def self.EnterpriseDomainControllers
+ SID.from_string_sid('S-1-5-9')
+ end
+ def self.PrincipalSelf
+ SID.from_string_sid('S-1-5-10')
+ end
+ def self.AuthenticatedUsers
+ SID.from_string_sid('S-1-5-11')
+ end
+ def self.RestrictedCode
+ SID.from_string_sid('S-1-5-12')
+ end
+ def self.TerminalServerUsers
+ SID.from_string_sid('S-1-5-13')
+ end
+ def self.LocalSystem
+ SID.from_string_sid('S-1-5-18')
+ end
+ def self.NtLocal
+ SID.from_string_sid('S-1-5-19')
+ end
+ def self.NtNetwork
+ SID.from_string_sid('S-1-5-20')
+ end
+ def self.BuiltinAdministrators
+ SID.from_string_sid('S-1-5-32-544')
+ end
+ def self.BuiltinUsers
+ SID.from_string_sid('S-1-5-32-545')
+ end
+ def self.Guests
+ SID.from_string_sid('S-1-5-32-546')
+ end
+ def self.PowerUsers
+ SID.from_string_sid('S-1-5-32-547')
+ end
+ def self.AccountOperators
+ SID.from_string_sid('S-1-5-32-548')
+ end
+ def self.ServerOperators
+ SID.from_string_sid('S-1-5-32-549')
+ end
+ def self.PrintOperators
+ SID.from_string_sid('S-1-5-32-550')
+ end
+ def self.BackupOperators
+ SID.from_string_sid('S-1-5-32-551')
+ end
+ def self.Replicators
+ SID.from_string_sid('S-1-5-32-552')
+ end
+ def self.Administrators
+ SID.from_string_sid('S-1-5-32-544')
+ end
+
+ # Machine-specific, well-known SIDs
+ # TODO: don't use strings, dummy
+ def self.None
+ SID.from_account("#{::ENV['COMPUTERNAME']}\\None")
+ end
+ def self.Administrator
+ SID.from_account("#{::ENV['COMPUTERNAME']}\\Administrator")
+ end
+ def self.Guest
+ SID.from_account("#{::ENV['COMPUTERNAME']}\\Guest")
+ end
+
+ def self.current_user
+ SID.from_account("#{::ENV['USERDOMAIN']}\\#{::ENV['USERNAME']}")
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/lib/chef/win32/security/token.rb b/lib/chef/win32/security/token.rb
new file mode 100644
index 0000000000..ded4fc080e
--- /dev/null
+++ b/lib/chef/win32/security/token.rb
@@ -0,0 +1,64 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/security'
+require 'chef/win32/api/security'
+
+require 'ffi'
+
+class Chef
+ module ReservedNames::Win32
+ class Security
+ class Token
+
+ def initialize(handle)
+ @handle = handle
+ end
+
+ attr_reader :handle
+
+ def enable_privileges(*privilege_names)
+ # Build the list of privileges we want to set
+ new_privileges = Chef::ReservedNames::Win32::API::Security::TOKEN_PRIVILEGES.new(
+ FFI::MemoryPointer.new(Chef::ReservedNames::Win32::API::Security::TOKEN_PRIVILEGES.size_with_privileges(privilege_names.length)))
+ new_privileges[:PrivilegeCount] = 0
+ privilege_names.each do |privilege_name|
+ luid = Chef::ReservedNames::Win32::API::Security::LUID.new
+ # Ignore failure (with_privileges TRIES but does not guarantee success--
+ # APIs down the line will fail if privilege escalation fails)
+ if Chef::ReservedNames::Win32::API::Security.LookupPrivilegeValueW(nil, privilege_name.to_wstring, luid)
+ new_privilege = new_privileges.privilege(new_privileges[:PrivilegeCount])
+ new_privilege[:Luid][:LowPart] = luid[:LowPart]
+ new_privilege[:Luid][:HighPart] = luid[:HighPart]
+ new_privilege[:Attributes] = Chef::ReservedNames::Win32::API::Security::SE_PRIVILEGE_ENABLED
+ new_privileges[:PrivilegeCount] = new_privileges[:PrivilegeCount] + 1
+ end
+ end
+
+ old_privileges = Chef::ReservedNames::Win32::Security.adjust_token_privileges(self, new_privileges)
+ end
+
+ def adjust_privileges(privileges_struct)
+ if privileges_struct[:PrivilegeCount] > 0
+ Chef::ReservedNames::Win32::Security::adjust_token_privileges(self, privileges_struct)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/unicode.rb b/lib/chef/win32/unicode.rb
new file mode 100644
index 0000000000..1002a4d58f
--- /dev/null
+++ b/lib/chef/win32/unicode.rb
@@ -0,0 +1,43 @@
+#
+# Author:: John Keiser (<jkeiser@opscode.com>)
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api/unicode'
+
+class Chef
+ module ReservedNames::Win32
+ class Unicode
+ include Chef::ReservedNames::Win32::API::Unicode
+ extend Chef::ReservedNames::Win32::API::Unicode
+ end
+ end
+end
+
+module FFI
+ class Pointer
+ def read_wstring(num_wchars)
+ Chef::ReservedNames::Win32::Unicode.wide_to_utf8(self.get_bytes(0, num_wchars*2))
+ end
+ end
+end
+
+class String
+ def to_wstring
+ Chef::ReservedNames::Win32::Unicode.utf8_to_wide(self)
+ end
+end
diff --git a/lib/chef/win32/version.rb b/lib/chef/win32/version.rb
new file mode 100644
index 0000000000..004fcad5ad
--- /dev/null
+++ b/lib/chef/win32/version.rb
@@ -0,0 +1,119 @@
+#
+# Author:: Seth Chisamore (<schisamo@opscode.com>)
+# Copyright:: Copyright 2011 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+require 'chef/win32/api/system'
+
+class Chef
+ module ReservedNames::Win32
+ class Version
+ include Chef::ReservedNames::Win32::API::Macros
+ include Chef::ReservedNames::Win32::API::System
+
+ # Ruby implementation of
+ # http://msdn.microsoft.com/en-us/library/ms724833(v=vs.85).aspx
+ # http://msdn.microsoft.com/en-us/library/ms724358(v=vs.85).aspx
+
+ WIN_VERSIONS = {
+ "Windows 7" => {:major => 6, :minor => 1, :callable => lambda{ @product_type == VER_NT_WORKSTATION }},
+ "Windows Server 2008 R2" => {:major => 6, :minor => 1, :callable => lambda{ @product_type != VER_NT_WORKSTATION }},
+ "Windows Server 2008" => {:major => 6, :minor => 0, :callable => lambda{ @product_type != VER_NT_WORKSTATION }},
+ "Windows Vista" => {:major => 6, :minor => 0, :callable => lambda{ @product_type == VER_NT_WORKSTATION }},
+ "Windows Server 2003 R2" => {:major => 5, :minor => 2, :callable => lambda{ get_system_metrics(SM_SERVERR2) != 0 }},
+ "Windows Home Server" => {:major => 5, :minor => 2, :callable => lambda{ (@suite_mask & VER_SUITE_WH_SERVER) == VER_SUITE_WH_SERVER }},
+ "Windows Server 2003" => {:major => 5, :minor => 2, :callable => lambda{ get_system_metrics(SM_SERVERR2) == 0 }},
+ "Windows XP" => {:major => 5, :minor => 1},
+ "Windows 2000" => {:major => 5, :minor => 0}
+ }
+
+ def initialize
+ @major_version, @minor_version, @build_number = get_version
+ ver_info = get_version_ex
+ @product_type = ver_info[:w_product_type]
+ @suite_mask = ver_info[:w_suite_mask]
+ @sp_major_version = ver_info[:w_service_pack_major]
+ @sp_minor_version = ver_info[:w_service_pack_minor]
+ @sku = get_product_info(@major_version, @minor_version, @sp_major_version, @sp_minor_version)
+ end
+
+ marketing_names = Array.new
+
+ # General Windows checks
+ WIN_VERSIONS.each do |k,v|
+ method_name = "#{k.gsub(/\s/, '_').downcase}?"
+ define_method(method_name) do
+ (@major_version == v[:major]) &&
+ (@minor_version == v[:minor]) &&
+ (v[:callable] ? v[:callable].call : true)
+ end
+ marketing_names << [k, method_name]
+ end
+
+ define_method(:marketing_name) do
+ marketing_names.each do |mn|
+ break mn[0] if self.send(mn[1])
+ end
+ end
+
+ # Server Type checks
+ %w{ cluster core datacenter }.each do |m|
+ define_method("#{m}?") do
+ self.class.constants.any? do |c|
+ (self.class.const_get(c) == @sku) &&
+ (c.to_s =~ /#{m}/i )
+ end
+ # if @sku
+ # !(PRODUCT_TYPE[@sku][:name] =~ /#{m}/i).nil?
+ # else
+ # false
+ # end
+ end
+ end
+
+ private
+
+ def get_version
+ version = GetVersion()
+ major = LOBYTE(LOWORD(version))
+ minor = HIBYTE(LOWORD(version))
+ build = version < 0x80000000 ? HIWORD(version) : 0
+ [major, minor, build]
+ end
+
+ def get_version_ex
+ lp_version_info = OSVERSIONINFOEX.new
+ lp_version_info[:dw_os_version_info_size] = OSVERSIONINFOEX.size
+ unless GetVersionExW(lp_version_info)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ lp_version_info
+ end
+
+ def get_product_info(major, minor, sp_major, sp_minor)
+ out = FFI::MemoryPointer.new(:uint32)
+ GetProductInfo(major, minor, sp_major, sp_minor, out)
+ out.get_uint(0)
+ end
+
+ def get_system_metrics(n_index)
+ GetSystemMetrics(n_index)
+ end
+
+ end
+ end
+end