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.
- checksums.yaml +7 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +423 -0
- data/LICENSE +201 -0
- data/README.md +41 -0
- data/Rakefile +32 -0
- data/bin/chef-run +23 -0
- data/chef-apply.gemspec +67 -0
- data/i18n/en.yml +513 -0
- data/lib/chef_apply.rb +20 -0
- data/lib/chef_apply/action/base.rb +158 -0
- data/lib/chef_apply/action/converge_target.rb +173 -0
- data/lib/chef_apply/action/install_chef.rb +30 -0
- data/lib/chef_apply/action/install_chef/base.rb +137 -0
- data/lib/chef_apply/action/install_chef/linux.rb +38 -0
- data/lib/chef_apply/action/install_chef/windows.rb +54 -0
- data/lib/chef_apply/action/reporter.rb +39 -0
- data/lib/chef_apply/cli.rb +470 -0
- data/lib/chef_apply/cli_options.rb +145 -0
- data/lib/chef_apply/config.rb +150 -0
- data/lib/chef_apply/error.rb +108 -0
- data/lib/chef_apply/errors/ccr_failure_mapper.rb +93 -0
- data/lib/chef_apply/file_fetcher.rb +70 -0
- data/lib/chef_apply/log.rb +42 -0
- data/lib/chef_apply/recipe_lookup.rb +117 -0
- data/lib/chef_apply/startup.rb +162 -0
- data/lib/chef_apply/status_reporter.rb +42 -0
- data/lib/chef_apply/target_host.rb +233 -0
- data/lib/chef_apply/target_resolver.rb +202 -0
- data/lib/chef_apply/telemeter.rb +162 -0
- data/lib/chef_apply/telemeter/patch.rb +32 -0
- data/lib/chef_apply/telemeter/sender.rb +121 -0
- data/lib/chef_apply/temp_cookbook.rb +159 -0
- data/lib/chef_apply/text.rb +77 -0
- data/lib/chef_apply/ui/error_printer.rb +261 -0
- data/lib/chef_apply/ui/plain_text_element.rb +75 -0
- data/lib/chef_apply/ui/terminal.rb +94 -0
- data/lib/chef_apply/version.rb +20 -0
- metadata +376 -0
@@ -0,0 +1,70 @@
|
|
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 "net/http"
|
19
|
+
require "uri"
|
20
|
+
require "chef_apply/config"
|
21
|
+
require "chef_apply/log"
|
22
|
+
|
23
|
+
module ChefApply
|
24
|
+
class FileFetcher
|
25
|
+
class << self
|
26
|
+
# Simple fetcher of an http(s) url. Returns the local path
|
27
|
+
# of the downloaded file.
|
28
|
+
def fetch(path)
|
29
|
+
cache_path = ChefApply::Config.cache.path
|
30
|
+
FileUtils.mkdir_p(cache_path)
|
31
|
+
url = URI.parse(path)
|
32
|
+
name = File.basename(url.path)
|
33
|
+
local_path = File.join(cache_path, name)
|
34
|
+
|
35
|
+
# TODO header check for size or checksum?
|
36
|
+
return local_path if File.exist?(local_path)
|
37
|
+
|
38
|
+
download_file(url, local_path)
|
39
|
+
local_path
|
40
|
+
end
|
41
|
+
|
42
|
+
def download_file(url, local_path)
|
43
|
+
temp_path = "#{local_path}.downloading"
|
44
|
+
file = open(temp_path, "wb")
|
45
|
+
ChefApply::Log.debug "Downloading: #{temp_path}"
|
46
|
+
Net::HTTP.start(url.host) do |http|
|
47
|
+
begin
|
48
|
+
http.request_get(url.path) do |resp|
|
49
|
+
resp.read_body do |segment|
|
50
|
+
file.write(segment)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
rescue e
|
54
|
+
@error = true
|
55
|
+
raise
|
56
|
+
ensure
|
57
|
+
file.close()
|
58
|
+
# If any failures occurred, don't risk keeping
|
59
|
+
# an incomplete download that we'll see as 'cached'
|
60
|
+
if @error
|
61
|
+
FileUtils.rm_f(temp_path)
|
62
|
+
else
|
63
|
+
FileUtils.mv(temp_path, local_path)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,42 @@
|
|
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 "mixlib/log"
|
19
|
+
|
20
|
+
module ChefApply
|
21
|
+
class Log
|
22
|
+
extend Mixlib::Log
|
23
|
+
|
24
|
+
def self.setup(location, log_level)
|
25
|
+
@location = location
|
26
|
+
if location.is_a?(String)
|
27
|
+
if location.casecmp("stdout") == 0
|
28
|
+
location = $stdout
|
29
|
+
else
|
30
|
+
location = File.open(location, "w+")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
init(location)
|
34
|
+
Log.level = log_level
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.location
|
38
|
+
@location
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,117 @@
|
|
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-config/config"
|
19
|
+
require "chef_apply/config"
|
20
|
+
require "chef_apply/error"
|
21
|
+
require "chef_apply/log"
|
22
|
+
|
23
|
+
module ChefApply
|
24
|
+
# When users are trying to converge a local recipe on a remote target, there
|
25
|
+
# is a very specific (but expansive) set of things they can specify. This
|
26
|
+
# class encapsulates that logic for testing purposes. We either return
|
27
|
+
# a path to a recipe or we raise an error.
|
28
|
+
class RecipeLookup
|
29
|
+
|
30
|
+
attr_reader :cookbook_repo_paths
|
31
|
+
def initialize(cookbook_repo_paths)
|
32
|
+
@cookbook_repo_paths = cookbook_repo_paths
|
33
|
+
end
|
34
|
+
|
35
|
+
# The recipe specifier is provided by the customer as either a path OR
|
36
|
+
# a cookbook and optional recipe name.
|
37
|
+
def split(recipe_specifier)
|
38
|
+
recipe_specifier.split("::")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Given a cookbook path or name, try to load that cookbook. Either return
|
42
|
+
# a cookbook object or raise an error.
|
43
|
+
def load_cookbook(path_or_name)
|
44
|
+
require "chef/exceptions"
|
45
|
+
if File.directory?(path_or_name)
|
46
|
+
cookbook_path = path_or_name
|
47
|
+
# First, is there a cookbook in the specified dir that matches?
|
48
|
+
require "chef/cookbook/cookbook_version_loader"
|
49
|
+
begin
|
50
|
+
v = Chef::Cookbook::CookbookVersionLoader.new(cookbook_path)
|
51
|
+
v.load!
|
52
|
+
cookbook = v.cookbook_version
|
53
|
+
rescue Chef::Exceptions::CookbookNotFoundInRepo
|
54
|
+
raise InvalidCookbook.new(cookbook_path)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
cookbook_name = path_or_name
|
58
|
+
# Second, is there a cookbook in their local repository that matches?
|
59
|
+
require "chef/cookbook_loader"
|
60
|
+
cb_loader = Chef::CookbookLoader.new(cookbook_repo_paths)
|
61
|
+
cb_loader.load_cookbooks_without_shadow_warning
|
62
|
+
|
63
|
+
begin
|
64
|
+
cookbook = cb_loader[cookbook_name]
|
65
|
+
rescue Chef::Exceptions::CookbookNotFoundInRepo
|
66
|
+
cookbook_repo_paths.each do |repo_path|
|
67
|
+
cookbook_path = File.join(repo_path, cookbook_name)
|
68
|
+
if File.directory?(cookbook_path)
|
69
|
+
raise InvalidCookbook.new(cookbook_path)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
raise CookbookNotFound.new(cookbook_name, cookbook_repo_paths)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
cookbook
|
76
|
+
end
|
77
|
+
|
78
|
+
# Find the specified recipe or default recipe if none is specified.
|
79
|
+
# Raise an error if recipe cannot be found.
|
80
|
+
def find_recipe(cookbook, recipe_name = nil)
|
81
|
+
recipes = cookbook.recipe_filenames_by_name
|
82
|
+
if recipe_name.nil?
|
83
|
+
default_recipe = recipes["default"]
|
84
|
+
raise NoDefaultRecipe.new(cookbook.root_dir, cookbook.name) if default_recipe.nil?
|
85
|
+
default_recipe
|
86
|
+
else
|
87
|
+
recipe = recipes[recipe_name]
|
88
|
+
raise RecipeNotFound.new(cookbook.root_dir, recipe_name, recipes.keys, cookbook.name) if recipe.nil?
|
89
|
+
recipe
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class InvalidCookbook < ChefApply::Error
|
94
|
+
def initialize(cookbook_path); super("CHEFVAL005", cookbook_path); end
|
95
|
+
end
|
96
|
+
|
97
|
+
class CookbookNotFound < ChefApply::Error
|
98
|
+
def initialize(cookbook_name, repo_paths)
|
99
|
+
repo_paths = repo_paths.join("\n")
|
100
|
+
super("CHEFVAL006", cookbook_name, repo_paths)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class NoDefaultRecipe < ChefApply::Error
|
105
|
+
def initialize(cookbook_path, cookbook_name); super("CHEFVAL007", cookbook_path, cookbook_name); end
|
106
|
+
end
|
107
|
+
|
108
|
+
class RecipeNotFound < ChefApply::Error
|
109
|
+
def initialize(cookbook_path, recipe_name, available_recipes, cookbook_name)
|
110
|
+
available_recipes.map! { |r| "'#{r}'" }
|
111
|
+
available_recipes = available_recipes.join(", ")
|
112
|
+
super("CHEFVAL008", cookbook_path, recipe_name, available_recipes, cookbook_name)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,162 @@
|
|
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
|
+
require "chef_apply/config"
|
18
|
+
require "chef_apply/text"
|
19
|
+
require "chef_apply/ui/terminal"
|
20
|
+
require "chef_apply/telemeter/sender"
|
21
|
+
module ChefApply
|
22
|
+
class Startup
|
23
|
+
attr_reader :argv
|
24
|
+
T = ChefApply::Text.cli
|
25
|
+
|
26
|
+
def initialize(argv)
|
27
|
+
@term_init = false
|
28
|
+
@argv = argv.clone
|
29
|
+
# Enable CLI output via Terminal. This comes first because other startup steps may
|
30
|
+
# need to output to the terminal.
|
31
|
+
init_terminal
|
32
|
+
end
|
33
|
+
|
34
|
+
def run
|
35
|
+
# Some tasks we do only once in an installation:
|
36
|
+
first_run_tasks
|
37
|
+
|
38
|
+
# Call this every time, so that if we add or change ~/.chef-workstation
|
39
|
+
# directory structure, we can be sure that it exists. Even with a
|
40
|
+
# custom configuration, the .chef-workstation directory and subdirs
|
41
|
+
# are required.
|
42
|
+
setup_workstation_user_directories
|
43
|
+
|
44
|
+
# Startup tasks that may change behavior based on configuration value
|
45
|
+
# must be run after load_config
|
46
|
+
load_config
|
47
|
+
|
48
|
+
# Init logging using log level out of config
|
49
|
+
setup_logging
|
50
|
+
|
51
|
+
# Begin upload of previous session telemetry. (If telemetry is not enabled,
|
52
|
+
# in config the uploader will clean up previous session(s) without sending)
|
53
|
+
start_telemeter_upload
|
54
|
+
|
55
|
+
# Launch the actual Chef Apply behavior
|
56
|
+
start_chef_apply
|
57
|
+
|
58
|
+
# NOTE: Because these exceptions occur outside of the
|
59
|
+
# CLI handling, they won't be tracked in telemtry.
|
60
|
+
# We can revisit this once the pending error handling rework
|
61
|
+
# is underway.
|
62
|
+
rescue ConfigPathInvalid => e
|
63
|
+
UI::Terminal.output(T.error.bad_config_file(e.path))
|
64
|
+
rescue ConfigPathNotProvided
|
65
|
+
UI::Terminal.output(T.error.missing_config_path)
|
66
|
+
rescue Mixlib::Config::UnknownConfigOptionError => e
|
67
|
+
# Ideally we'd update the exception in mixlib to include
|
68
|
+
# a field with the faulty value, line number, and nested context -
|
69
|
+
# it's less fragile than depending on text parsing, which
|
70
|
+
# is what we'll do for now.
|
71
|
+
if e.message =~ /.*unsupported config value (.*)[.]+$/
|
72
|
+
# TODO - levenshteinian distance to figure out
|
73
|
+
# what they may have meant instead.
|
74
|
+
UI::Terminal.output(T.error.invalid_config_key($1, Config.location))
|
75
|
+
else
|
76
|
+
# Safety net in case the error text changes from under us.
|
77
|
+
UI::Terminal.output(T.error.unknown_config_error(e.message, Config.location))
|
78
|
+
end
|
79
|
+
rescue Tomlrb::ParseError => e
|
80
|
+
UI::Terminal.output(T.error.unknown_config_error(e.message, Config.location))
|
81
|
+
end
|
82
|
+
|
83
|
+
def init_terminal
|
84
|
+
UI::Terminal.init($stdout)
|
85
|
+
end
|
86
|
+
|
87
|
+
def first_run_tasks
|
88
|
+
return if Dir.exist?(Config::WS_BASE_PATH)
|
89
|
+
create_default_config
|
90
|
+
setup_telemetry
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_default_config
|
94
|
+
UI::Terminal.output T.creating_config(Config.default_location)
|
95
|
+
UI::Terminal.output ""
|
96
|
+
FileUtils.mkdir_p(Config::WS_BASE_PATH)
|
97
|
+
FileUtils.touch(Config.default_location)
|
98
|
+
end
|
99
|
+
|
100
|
+
def setup_telemetry
|
101
|
+
require "securerandom"
|
102
|
+
installation_id = SecureRandom.uuid
|
103
|
+
File.write(Config.telemetry_installation_identifier_file, installation_id)
|
104
|
+
|
105
|
+
# Tell the user we're anonymously tracking, give brief opt-out
|
106
|
+
# and a link to detailed information.
|
107
|
+
UI::Terminal.output T.telemetry_enabled(Config.location)
|
108
|
+
UI::Terminal.output ""
|
109
|
+
end
|
110
|
+
|
111
|
+
def start_telemeter_upload
|
112
|
+
ChefApply::Telemeter::Sender.start_upload_thread()
|
113
|
+
end
|
114
|
+
|
115
|
+
def setup_workstation_user_directories
|
116
|
+
# Note that none of these paths are customizable in config, so
|
117
|
+
# it's safe to do before we load config.
|
118
|
+
FileUtils.mkdir_p(Config::WS_BASE_PATH)
|
119
|
+
FileUtils.mkdir_p(Config.base_log_directory)
|
120
|
+
FileUtils.mkdir_p(Config.telemetry_path)
|
121
|
+
end
|
122
|
+
|
123
|
+
def load_config
|
124
|
+
path = custom_config_path
|
125
|
+
Config.custom_location(path) unless path.nil?
|
126
|
+
Config.load
|
127
|
+
end
|
128
|
+
|
129
|
+
# Look for a user-supplied config path by manually parsing the option.
|
130
|
+
# Note that we can't use Mixlib::CLI for this.
|
131
|
+
# To ensure that ChefApply::CLI initializes with correct
|
132
|
+
# option defaults, we need to have configuraton loaded before initializing it.
|
133
|
+
def custom_config_path
|
134
|
+
argv.each_with_index do |arg, index|
|
135
|
+
if arg == "--config-path" || arg == "-c"
|
136
|
+
next_arg = argv[index + 1]
|
137
|
+
raise ConfigPathNotProvided.new if next_arg.nil?
|
138
|
+
raise ConfigPathInvalid.new(next_arg) unless File.file?(next_arg) && File.readable?(next_arg)
|
139
|
+
return next_arg
|
140
|
+
end
|
141
|
+
end
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
|
145
|
+
def setup_logging
|
146
|
+
ChefApply::Log.setup(Config.log.location, Config.log.level.to_sym)
|
147
|
+
ChefApply::Log.info("Initialized logger")
|
148
|
+
end
|
149
|
+
|
150
|
+
def start_chef_apply
|
151
|
+
require "chef_apply/cli"
|
152
|
+
ChefApply::CLI.new(@argv).run
|
153
|
+
end
|
154
|
+
class ConfigPathNotProvided < StandardError; end
|
155
|
+
class ConfigPathInvalid < StandardError
|
156
|
+
attr_reader :path
|
157
|
+
def initialize(path)
|
158
|
+
@path = path
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,42 @@
|
|
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
|
+
module ChefApply
|
19
|
+
class StatusReporter
|
20
|
+
|
21
|
+
def initialize(ui_element, prefix: nil, key: nil)
|
22
|
+
@ui_element = ui_element
|
23
|
+
@key = key
|
24
|
+
@ui_element.update(prefix: prefix)
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(msg)
|
28
|
+
@ui_element.update({ @key => msg })
|
29
|
+
end
|
30
|
+
|
31
|
+
def success(msg)
|
32
|
+
update(msg)
|
33
|
+
@ui_element.success
|
34
|
+
end
|
35
|
+
|
36
|
+
def error(msg)
|
37
|
+
update(msg)
|
38
|
+
@ui_element.error
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,233 @@
|
|
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/log"
|
19
|
+
require "chef_apply/error"
|
20
|
+
require "train"
|
21
|
+
module ChefApply
|
22
|
+
class TargetHost
|
23
|
+
attr_reader :config, :reporter, :backend, :transport_type
|
24
|
+
# These values may exist in .ssh/config but will be ignored by train
|
25
|
+
# in favor of its defaults unless we specify them explicitly.
|
26
|
+
# See #apply_ssh_config
|
27
|
+
SSH_CONFIG_OVERRIDE_KEYS = [:user, :port, :proxy]
|
28
|
+
|
29
|
+
def self.instance_for_url(target, opts = {})
|
30
|
+
opts = { target: @url }
|
31
|
+
target_host = new(target, opts)
|
32
|
+
target_host.connect!
|
33
|
+
target_host
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(host_url, opts = {}, logger = nil)
|
37
|
+
@config = connection_config(host_url, opts, logger)
|
38
|
+
@transport_type = Train.validate_backend(@config)
|
39
|
+
apply_ssh_config(@config, opts) if @transport_type == "ssh"
|
40
|
+
@train_connection = Train.create(@transport_type, config)
|
41
|
+
end
|
42
|
+
|
43
|
+
def connection_config(host_url, opts_in, logger)
|
44
|
+
connection_opts = { target: host_url,
|
45
|
+
sudo: opts_in[:sudo] === false ? false : true,
|
46
|
+
www_form_encoded_password: true,
|
47
|
+
key_files: opts_in[:identity_file],
|
48
|
+
logger: ChefApply::Log }
|
49
|
+
if opts_in.has_key? :ssl
|
50
|
+
connection_opts[:ssl] = opts_in[:ssl]
|
51
|
+
connection_opts[:self_signed] = (opts_in[:ssl_verify] === false ? true : false)
|
52
|
+
end
|
53
|
+
|
54
|
+
[:sudo_password, :sudo, :sudo_command, :password, :user].each do |key|
|
55
|
+
connection_opts[key] = opts_in[key] if opts_in.has_key? key
|
56
|
+
end
|
57
|
+
|
58
|
+
Train.target_config(connection_opts)
|
59
|
+
end
|
60
|
+
|
61
|
+
def apply_ssh_config(config, opts_in)
|
62
|
+
# If we don't provide certain options, they will be defaulted
|
63
|
+
# within train - in the case of ssh, this will prevent the .ssh/config
|
64
|
+
# values from being picked up.
|
65
|
+
# Here we'll modify the returned @config to specify
|
66
|
+
# values that we get out of .ssh/config if present and if they haven't
|
67
|
+
# been explicitly given.
|
68
|
+
host_cfg = ssh_config_for_host(config[:host])
|
69
|
+
SSH_CONFIG_OVERRIDE_KEYS.each do |key|
|
70
|
+
if host_cfg.has_key?(key) && opts_in[key].nil?
|
71
|
+
config[key] = host_cfg[key]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def connect!
|
77
|
+
return unless @backend.nil?
|
78
|
+
@backend = train_connection.connection
|
79
|
+
@backend.wait_until_ready
|
80
|
+
rescue Train::UserError => e
|
81
|
+
raise ConnectionFailure.new(e, config)
|
82
|
+
rescue Train::Error => e
|
83
|
+
# These are typically wrapper errors for other problems,
|
84
|
+
# so we'll prefer to use e.cause over e if available.
|
85
|
+
raise ConnectionFailure.new(e.cause || e, config)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the user being used to connect. Defaults to train's default user if not specified
|
89
|
+
# defaulted in .ssh/config (for ssh connections), as set up in '#apply_ssh_config'.
|
90
|
+
def user
|
91
|
+
return config[:user] unless config[:user].nil?
|
92
|
+
require "train/transports/ssh"
|
93
|
+
Train::Transports::SSH.default_options[:user][:default]
|
94
|
+
end
|
95
|
+
|
96
|
+
def hostname
|
97
|
+
config[:host]
|
98
|
+
end
|
99
|
+
|
100
|
+
def architecture
|
101
|
+
platform.arch
|
102
|
+
end
|
103
|
+
|
104
|
+
def version
|
105
|
+
platform.release
|
106
|
+
end
|
107
|
+
|
108
|
+
def base_os
|
109
|
+
if platform.family == "windows"
|
110
|
+
:windows
|
111
|
+
elsif platform.linux?
|
112
|
+
:linux
|
113
|
+
else
|
114
|
+
# TODO - this seems like it shouldn't happen here, when
|
115
|
+
# all the caller is doing is asking about the OS
|
116
|
+
raise ChefApply::TargetHost::UnsupportedTargetOS.new(platform.name)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def platform
|
121
|
+
backend.platform
|
122
|
+
end
|
123
|
+
|
124
|
+
def run_command!(command, sudo_as_user = false)
|
125
|
+
result = run_command(command, sudo_as_user)
|
126
|
+
if result.exit_status != 0
|
127
|
+
raise RemoteExecutionFailed.new(@config[:host], command, result)
|
128
|
+
end
|
129
|
+
result
|
130
|
+
end
|
131
|
+
|
132
|
+
def run_command(command, sudo_as_user = false)
|
133
|
+
if config[:sudo] && sudo_as_user && base_os == :linux
|
134
|
+
command = "-u #{config[:user]} #{command}"
|
135
|
+
end
|
136
|
+
backend.run_command command
|
137
|
+
end
|
138
|
+
|
139
|
+
def upload_file(local_path, remote_path)
|
140
|
+
backend.upload(local_path, remote_path)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns the installed chef version as a Gem::Version,
|
144
|
+
# or raised ChefNotInstalled if chef client version manifest can't
|
145
|
+
# be found.
|
146
|
+
def installed_chef_version
|
147
|
+
return @installed_chef_version if @installed_chef_version
|
148
|
+
# Note: In the case of a very old version of chef (that has no manifest - pre 12.0?)
|
149
|
+
# this will report as not installed.
|
150
|
+
manifest = get_chef_version_manifest()
|
151
|
+
raise ChefNotInstalled.new if manifest == :not_found
|
152
|
+
# We'll split the version here because unstable builds (where we currently
|
153
|
+
# install from) are in the form "Major.Minor.Build+HASH" which is not a valid
|
154
|
+
# version string.
|
155
|
+
@installed_chef_version = Gem::Version.new(manifest["build_version"].split("+")[0])
|
156
|
+
end
|
157
|
+
|
158
|
+
MANIFEST_PATHS = {
|
159
|
+
# TODO - use a proper method to query the win installation path -
|
160
|
+
# currently we're assuming the default, but this can be customized
|
161
|
+
# at install time.
|
162
|
+
# A working approach is below - but it runs very slowly in testing
|
163
|
+
# on a virtualbox windows vm:
|
164
|
+
# (over winrm) Get-WmiObject Win32_Product | Where {$_.Name -match 'Chef Client'}
|
165
|
+
windows: "c:\\opscode\\chef\\version-manifest.json",
|
166
|
+
linux: "/opt/chef/version-manifest.json"
|
167
|
+
}
|
168
|
+
|
169
|
+
def get_chef_version_manifest
|
170
|
+
path = MANIFEST_PATHS[base_os()]
|
171
|
+
manifest = backend.file(path)
|
172
|
+
return :not_found unless manifest.file?
|
173
|
+
JSON.parse(manifest.content)
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def train_connection
|
179
|
+
@train_connection
|
180
|
+
end
|
181
|
+
|
182
|
+
def ssh_config_for_host(host)
|
183
|
+
require "net/ssh"
|
184
|
+
Net::SSH::Config.for(host)
|
185
|
+
end
|
186
|
+
|
187
|
+
class RemoteExecutionFailed < ChefApply::ErrorNoLogs
|
188
|
+
attr_reader :stdout, :stderr
|
189
|
+
def initialize(host, command, result)
|
190
|
+
super("CHEFRMT001",
|
191
|
+
command,
|
192
|
+
result.exit_status,
|
193
|
+
host,
|
194
|
+
result.stderr.empty? ? result.stdout : result.stderr)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class ConnectionFailure < ChefApply::ErrorNoLogs
|
199
|
+
# TODO: Currently this only handles sudo-related errors;
|
200
|
+
# we should also look at e.cause for underlying connection errors
|
201
|
+
# which are presently only visible in log files.
|
202
|
+
def initialize(original_exception, connection_opts)
|
203
|
+
sudo_command = connection_opts[:sudo_command]
|
204
|
+
init_params =
|
205
|
+
# Comments below show the original_exception.reason values to check for instead of strings,
|
206
|
+
# after train 1.4.12 is consumable.
|
207
|
+
case original_exception.message # original_exception.reason
|
208
|
+
when /Sudo requires a password/ # :sudo_password_required
|
209
|
+
"CHEFTRN003"
|
210
|
+
when /Wrong sudo password/ #:bad_sudo_password
|
211
|
+
"CHEFTRN004"
|
212
|
+
when /Can't find sudo command/, /No such file/, /command not found/ # :sudo_command_not_found
|
213
|
+
# NOTE: In the /No such file/ case, reason will be nil - we still have
|
214
|
+
# to check message text. (Or PR to train to handle this case)
|
215
|
+
["CHEFTRN005", sudo_command] # :sudo_command_not_found
|
216
|
+
when /Sudo requires a TTY.*/ # :sudo_no_tty
|
217
|
+
"CHEFTRN006"
|
218
|
+
when /has no keys added/
|
219
|
+
"CHEFTRN007"
|
220
|
+
else
|
221
|
+
["CHEFTRN999", original_exception.message]
|
222
|
+
end
|
223
|
+
super(*(Array(init_params).flatten))
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
class ChefNotInstalled < StandardError; end
|
228
|
+
|
229
|
+
class UnsupportedTargetOS < ChefApply::ErrorNoLogs
|
230
|
+
def initialize(os_name); super("CHEFTARG001", os_name); end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|