chef-apply 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +26 -0
  3. data/Gemfile.lock +423 -0
  4. data/LICENSE +201 -0
  5. data/README.md +41 -0
  6. data/Rakefile +32 -0
  7. data/bin/chef-run +23 -0
  8. data/chef-apply.gemspec +67 -0
  9. data/i18n/en.yml +513 -0
  10. data/lib/chef_apply.rb +20 -0
  11. data/lib/chef_apply/action/base.rb +158 -0
  12. data/lib/chef_apply/action/converge_target.rb +173 -0
  13. data/lib/chef_apply/action/install_chef.rb +30 -0
  14. data/lib/chef_apply/action/install_chef/base.rb +137 -0
  15. data/lib/chef_apply/action/install_chef/linux.rb +38 -0
  16. data/lib/chef_apply/action/install_chef/windows.rb +54 -0
  17. data/lib/chef_apply/action/reporter.rb +39 -0
  18. data/lib/chef_apply/cli.rb +470 -0
  19. data/lib/chef_apply/cli_options.rb +145 -0
  20. data/lib/chef_apply/config.rb +150 -0
  21. data/lib/chef_apply/error.rb +108 -0
  22. data/lib/chef_apply/errors/ccr_failure_mapper.rb +93 -0
  23. data/lib/chef_apply/file_fetcher.rb +70 -0
  24. data/lib/chef_apply/log.rb +42 -0
  25. data/lib/chef_apply/recipe_lookup.rb +117 -0
  26. data/lib/chef_apply/startup.rb +162 -0
  27. data/lib/chef_apply/status_reporter.rb +42 -0
  28. data/lib/chef_apply/target_host.rb +233 -0
  29. data/lib/chef_apply/target_resolver.rb +202 -0
  30. data/lib/chef_apply/telemeter.rb +162 -0
  31. data/lib/chef_apply/telemeter/patch.rb +32 -0
  32. data/lib/chef_apply/telemeter/sender.rb +121 -0
  33. data/lib/chef_apply/temp_cookbook.rb +159 -0
  34. data/lib/chef_apply/text.rb +77 -0
  35. data/lib/chef_apply/ui/error_printer.rb +261 -0
  36. data/lib/chef_apply/ui/plain_text_element.rb +75 -0
  37. data/lib/chef_apply/ui/terminal.rb +94 -0
  38. data/lib/chef_apply/version.rb +20 -0
  39. metadata +376 -0
@@ -0,0 +1,20 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module ChefApply
19
+
20
+ end
@@ -0,0 +1,158 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "chef_apply/telemeter"
19
+ require "chef_apply/error"
20
+
21
+ module ChefApply
22
+ module Action
23
+ # Derive new Actions from Action::Base
24
+ # "target_host" is a TargetHost that the action is being applied to. May be nil
25
+ # if the action does not require a target.
26
+ # "config" is hash containing any options that your command may need
27
+ #
28
+ # Implement perform_action to perform whatever action your class is intended to do.
29
+ # Run time will be captured via telemetry and categorized under ":action" with the
30
+ # unqualified class name of your Action.
31
+ class Base
32
+ attr_reader :target_host, :config
33
+
34
+ def initialize(config = {})
35
+ c = config.dup
36
+ @target_host = c.delete :target_host
37
+ # Remaining options are for child classes to make use of.
38
+ @config = c
39
+ end
40
+
41
+ run_report = "$env:APPDATA/chef-workstation/cache/run-report.json"
42
+ PATH_MAPPING = {
43
+ chef_client: {
44
+ windows: "cmd /c C:/opscode/chef/bin/chef-client",
45
+ other: "/opt/chef/bin/chef-client",
46
+ },
47
+ cache_path: {
48
+ windows: '#{ENV[\'APPDATA\']}/chef-workstation',
49
+ other: "/var/chef-workstation",
50
+ },
51
+ read_chef_report: {
52
+ windows: "type #{run_report}",
53
+ other: "cat /var/chef-workstation/cache/run-report.json",
54
+ },
55
+ delete_chef_report: {
56
+ windows: "If (Test-Path #{run_report}){ Remove-Item -Force -Path #{run_report} }",
57
+ other: "rm -f /var/chef-workstation/cache/run-report.json",
58
+ },
59
+ tempdir: {
60
+ windows: "%TEMP%",
61
+ other: "$TMPDIR",
62
+ },
63
+ mkdir: {
64
+ windows: "New-Item -ItemType Directory -Force -Path ",
65
+ other: "mkdir -p ",
66
+ },
67
+ # TODO this is duplicating some stuff in the install_chef folder
68
+ # TODO maybe we start to break these out into actual functions, so
69
+ # we don't have to try and make really long one-liners
70
+ mktemp: {
71
+ windows: "$parent = [System.IO.Path]::GetTempPath(); [string] $name = [System.Guid]::NewGuid(); $tmp = New-Item -ItemType Directory -Path (Join-Path $parent $name); $tmp.FullName",
72
+ other: "bash -c 'd=$(mktemp -d -p${TMPDIR:-/tmp} chef_XXXXXX); chmod 777 $d; echo $d'"
73
+ },
74
+ delete_folder: {
75
+ windows: "Remove-Item -Recurse -Force –Path",
76
+ other: "rm -rf",
77
+ }
78
+ }
79
+
80
+ PATH_MAPPING.keys.each do |m|
81
+ define_method(m) { PATH_MAPPING[m][family] }
82
+ end
83
+
84
+ # Chef will try 'downloading' the policy from the internet unless we pass it a valid, local file
85
+ # in the working directory. By pointing it at a local file it will just copy it instead of trying
86
+ # to download it.
87
+ def run_chef(working_dir, config, policy)
88
+ case family
89
+ when :windows
90
+ "Set-Location -Path #{working_dir}; " +
91
+ # We must 'wait' for chef-client to finish before changing directories and Out-Null does that
92
+ "chef-client -z --config #{config} --recipe-url #{policy} | Out-Null; " +
93
+ # We have to leave working dir so we don't hold a lock on it, which allows us to delete this tempdir later
94
+ "Set-Location C:/; " +
95
+ "exit $LASTEXITCODE"
96
+ else
97
+ # cd is shell a builtin, so much call bash. This also means all commands are executed
98
+ # with sudo (as long as we are hardcoding our sudo use)
99
+ "bash -c 'cd #{working_dir}; chef-client -z --config #{config} --recipe-url #{policy}'"
100
+ end
101
+ end
102
+
103
+ # Trying to perform File or Pathname operations on a Windows path with '\'
104
+ # characters in it fails. So lets convert them to '/' which these libraries
105
+ # handle better.
106
+ def escape_windows_path(p)
107
+ if family == :windows
108
+ p = p.tr("\\", "/")
109
+ end
110
+ p
111
+ end
112
+
113
+ def run(&block)
114
+ @notification_handler = block
115
+ Telemeter.timed_action_capture(self) do
116
+ begin
117
+ perform_action
118
+ rescue StandardError => e
119
+ # Give the caller a chance to clean up - if an exception is
120
+ # raised it'll otherwise get routed through the executing thread,
121
+ # providing no means of feedback for the caller's current task.
122
+ notify(:error, e)
123
+ @error = e
124
+ end
125
+ end
126
+ # Raise outside the block to ensure that the telemetry cpature completes
127
+ raise @error unless @error.nil?
128
+ end
129
+
130
+ def name
131
+ self.class.name.split("::").last
132
+ end
133
+
134
+ def perform_action
135
+ raise NotImplemented
136
+ end
137
+
138
+ def notify(action, *args)
139
+ return if @notification_handler.nil?
140
+ ChefApply::Log.debug("[#{self.class.name}] Action: #{action}, Action Data: #{args}")
141
+ @notification_handler.call(action, args) if @notification_handler
142
+ end
143
+
144
+ private
145
+
146
+ def family
147
+ @family ||= begin
148
+ f = target_host.platform.family
149
+ if f == "windows"
150
+ :windows
151
+ else
152
+ :other
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,173 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "chef_apply/action/base"
19
+ require "chef_apply/text"
20
+ require "pathname"
21
+ require "tempfile"
22
+ require "chef/util/path_helper"
23
+
24
+ module ChefApply::Action
25
+ class ConvergeTarget < Base
26
+
27
+ def perform_action
28
+ local_policy_path = config.delete :local_policy_path
29
+ remote_tmp = target_host.run_command!(mktemp, true)
30
+ remote_dir_path = escape_windows_path(remote_tmp.stdout.strip)
31
+ remote_policy_path = create_remote_policy(local_policy_path, remote_dir_path)
32
+ remote_config_path = create_remote_config(remote_dir_path)
33
+ create_remote_handler(remote_dir_path)
34
+ upload_trusted_certs(remote_dir_path)
35
+
36
+ notify(:running_chef)
37
+ cmd_str = run_chef(remote_dir_path,
38
+ File.basename(remote_config_path),
39
+ File.basename(remote_policy_path))
40
+ c = target_host.run_command(cmd_str)
41
+ target_host.run_command!("#{delete_folder} #{remote_dir_path}")
42
+ if c.exit_status == 0
43
+ ChefApply::Log.debug(c.stdout)
44
+ notify(:success)
45
+ elsif c.exit_status == 35
46
+ notify(:reboot)
47
+ else
48
+ notify(:converge_error)
49
+ ChefApply::Log.error("Error running command [#{cmd_str}]")
50
+ ChefApply::Log.error("stdout: #{c.stdout}")
51
+ ChefApply::Log.error("stderr: #{c.stderr}")
52
+ handle_ccr_error()
53
+ end
54
+ end
55
+
56
+ def create_remote_policy(local_policy_path, remote_dir_path)
57
+ remote_policy_path = File.join(remote_dir_path, File.basename(local_policy_path))
58
+ notify(:creating_remote_policy)
59
+ begin
60
+ target_host.upload_file(local_policy_path, remote_policy_path)
61
+ rescue RuntimeError => e
62
+ ChefApply::Log.error(e)
63
+ raise PolicyUploadFailed.new()
64
+ end
65
+ remote_policy_path
66
+ end
67
+
68
+ def create_remote_config(dir)
69
+ remote_config_path = File.join(dir, "workstation.rb")
70
+
71
+ workstation_rb = <<~EOM
72
+ local_mode true
73
+ color false
74
+ cache_path "#{cache_path}"
75
+ chef_repo_path "#{cache_path}"
76
+ require_relative "reporter"
77
+ reporter = ChefApply::Reporter.new
78
+ report_handlers << reporter
79
+ exception_handlers << reporter
80
+ EOM
81
+
82
+ # Maybe add data collector endpoint.
83
+ dc = ChefApply::Config.data_collector
84
+ if !dc.url.nil? && !dc.token.nil?
85
+ workstation_rb << <<~EOM
86
+ data_collector.server_url "#{dc.url}"
87
+ data_collector.token "#{dc.token}"
88
+ data_collector.mode :solo
89
+ data_collector.organization "Chef Workstation"
90
+ EOM
91
+ end
92
+
93
+ begin
94
+ config_file = Tempfile.new
95
+ config_file.write(workstation_rb)
96
+ config_file.close
97
+ target_host.upload_file(config_file.path, remote_config_path)
98
+ rescue RuntimeError
99
+ raise ConfigUploadFailed.new()
100
+ ensure
101
+ config_file.unlink
102
+ end
103
+ remote_config_path
104
+ end
105
+
106
+ def create_remote_handler(dir)
107
+ remote_handler_path = File.join(dir, "reporter.rb")
108
+ begin
109
+ handler_file = Tempfile.new
110
+ handler_file.write(File.read(File.join(__dir__, "reporter.rb")))
111
+ handler_file.close
112
+ target_host.upload_file(handler_file.path, remote_handler_path)
113
+ rescue RuntimeError
114
+ raise HandlerUploadFailed.new()
115
+ ensure
116
+ handler_file.unlink
117
+ end
118
+ remote_handler_path
119
+ end
120
+
121
+ def upload_trusted_certs(dir)
122
+ local_tcd = Chef::Util::PathHelper.escape_glob_dir(ChefApply::Config.chef.trusted_certs_dir)
123
+ certs = Dir.glob(File.join(local_tcd, "*.{crt,pem}"))
124
+ return if certs.empty?
125
+ notify(:uploading_trusted_certs)
126
+ remote_tcd = "#{dir}/trusted_certs"
127
+ # We create the trusted_certs dir with the connection user (instead of the root
128
+ # user it would get as default since we run in sudo mode) because the `upload_file`
129
+ # uploads as the connection user. Without this upload_file would fail because
130
+ # it tries to write to a root-owned folder.
131
+ target_host.run_command("#{mkdir} #{remote_tcd}", true)
132
+ certs.each do |cert_file|
133
+ target_host.upload_file(cert_file, "#{remote_tcd}/#{File.basename(cert_file)}")
134
+ end
135
+ end
136
+
137
+ def handle_ccr_error
138
+ require "chef_apply/errors/ccr_failure_mapper"
139
+ mapper_opts = {}
140
+ c = target_host.run_command(read_chef_report)
141
+ if c.exit_status == 0
142
+ report = JSON.parse(c.stdout)
143
+ # We need to delete the stacktrace after copying it over. Otherwise if we get a
144
+ # remote failure that does not write a chef stacktrace its possible to get an old
145
+ # stale stacktrace.
146
+ target_host.run_command!(delete_chef_report)
147
+ ChefApply::Log.error("Remote chef-client error follows:")
148
+ ChefApply::Log.error(report["exception"])
149
+ else
150
+ report = {}
151
+ ChefApply::Log.error("Could not read remote report:")
152
+ ChefApply::Log.error("stdout: #{c.stdout}")
153
+ ChefApply::Log.error("stderr: #{c.stderr}")
154
+ mapper_opts[:stdout] = c.stdout
155
+ mapper_opts[:stderr] = c.stderr
156
+ end
157
+ mapper = ChefApply::Errors::CCRFailureMapper.new(report["exception"], mapper_opts)
158
+ mapper.raise_mapped_exception!
159
+ end
160
+
161
+ class ConfigUploadFailed < ChefApply::Error
162
+ def initialize(); super("CHEFUPL003"); end
163
+ end
164
+
165
+ class HandlerUploadFailed < ChefApply::Error
166
+ def initialize(); super("CHEFUPL004"); end
167
+ end
168
+
169
+ class PolicyUploadFailed < ChefApply::Error
170
+ def initialize(); super("CHEFUPL005"); end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,30 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "chef_apply/action/install_chef/base"
19
+ require "chef_apply/action/install_chef/windows"
20
+ require "chef_apply/action/install_chef/linux"
21
+
22
+ module ChefApply::Action::InstallChef
23
+ def self.instance_for_target(target_host, opts = { check_only: false })
24
+ opts[:target_host] = target_host
25
+ case target_host.base_os
26
+ when :windows then Windows.new(opts)
27
+ when :linux then Linux.new(opts)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,137 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2017 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "chef_apply/action/base"
19
+ require "fileutils"
20
+
21
+ module ChefApply::Action::InstallChef
22
+ class Base < ChefApply::Action::Base
23
+ MIN_CHEF_VERSION = Gem::Version.new("13.0.0")
24
+
25
+ def perform_action
26
+ if target_host.installed_chef_version >= MIN_CHEF_VERSION
27
+ notify(:already_installed)
28
+ return
29
+ end
30
+ raise ClientOutdated.new(target_host.installed_chef_version, MIN_CHEF_VERSION)
31
+ # NOTE: 2018-05-10 below is an intentionally dead code path that
32
+ # will get re-visited once we determine how we want automatic
33
+ # upgrades to behave.
34
+ # @upgrading = true
35
+ # perform_local_install
36
+ rescue ChefApply::TargetHost::ChefNotInstalled
37
+ if config[:check_only]
38
+ raise ClientNotInstalled.new()
39
+ end
40
+ perform_local_install
41
+ end
42
+
43
+ def name
44
+ # We have subclasses - so this'll take the qualified name
45
+ # eg InstallChef::Windows, etc
46
+ self.class.name.split("::")[-2..-1].join("::")
47
+ end
48
+
49
+ def upgrading?
50
+ @upgrading
51
+ end
52
+
53
+ def perform_local_install
54
+ package = lookup_artifact()
55
+ notify(:downloading)
56
+ local_path = download_to_workstation(package.url)
57
+ notify(:uploading)
58
+ remote_path = upload_to_target(local_path)
59
+ notify(:installing)
60
+ install_chef_to_target(remote_path)
61
+ notify(:install_complete)
62
+ end
63
+
64
+ def perform_remote_install
65
+ raise NotImplementedError
66
+ end
67
+
68
+ def lookup_artifact
69
+ return @artifact_info if @artifact_info
70
+ require "mixlib/install"
71
+ c = train_to_mixlib(target_host.platform)
72
+ Mixlib::Install.new(c).artifact_info
73
+ end
74
+
75
+ def version_to_install
76
+ lookup_artifact.version
77
+ end
78
+
79
+ def train_to_mixlib(platform)
80
+ opts = {
81
+ platform_version: platform.release,
82
+ platform: platform.name,
83
+ architecture: platform.arch,
84
+ product_name: "chef",
85
+ product_version: :latest,
86
+ channel: :stable,
87
+ platform_version_compatibility_mode: true
88
+ }
89
+ case platform.name
90
+ when /windows/
91
+ opts[:platform] = "windows"
92
+ when "redhat", "centos"
93
+ opts[:platform] = "el"
94
+ when "suse"
95
+ opts[:platform] = "sles"
96
+ when "amazon"
97
+ opts[:platform] = "el"
98
+ if platform.release.to_i > 2010 # legacy Amazon version 1
99
+ opts[:platform_version] = "6"
100
+ else
101
+ opts[:platform_version] = "7"
102
+ end
103
+ end
104
+ opts
105
+ end
106
+
107
+ def download_to_workstation(url_path)
108
+ require "chef_apply/file_fetcher"
109
+ ChefApply::FileFetcher.fetch(url_path)
110
+ end
111
+
112
+ def upload_to_target(local_path)
113
+ installer_dir = setup_remote_temp_path()
114
+ remote_path = File.join(installer_dir, File.basename(local_path))
115
+ target_host.upload_file(local_path, remote_path)
116
+ remote_path
117
+ end
118
+
119
+ def setup_remote_temp_path
120
+ raise NotImplementedError
121
+ end
122
+
123
+ def install_chef_to_target(remote_path)
124
+ raise NotImplementedError
125
+ end
126
+ end
127
+
128
+ class ClientNotInstalled < ChefApply::ErrorNoLogs
129
+ def initialize(); super("CHEFINS002"); end
130
+ end
131
+
132
+ class ClientOutdated < ChefApply::ErrorNoLogs
133
+ def initialize(current_version, target_version)
134
+ super("CHEFINS003", current_version, target_version)
135
+ end
136
+ end
137
+ end