chef-apply 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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