knife-depsolver 1.0.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b5937e944afd44c612b2fe9beb72ab82fc54b6fb
4
- data.tar.gz: 6d29b51bacad2ecc548abde5836745976cfcdfbe
3
+ metadata.gz: 6778025284948e0f288edf8c6c7f2c2524b183a4
4
+ data.tar.gz: 0ede42f4cc18050f8e27c28b96b454aa62e3c4a8
5
5
  SHA512:
6
- metadata.gz: c6458795f49739bceb38884ba4fe2d534a3f07c7bb005adf981e833230b27813d9391dd21e1259b3b1c20e9621ebfb093b28e40115bed8d7d208ddc8027296c8
7
- data.tar.gz: 3c27e32e8bba168011d2afd4407fcdf552e92035f146a0108a31c3b9f4127701e9dec02c2a6150ca22831a4e3e35775b44ad58e9e28aade5e6786e478839853c
6
+ metadata.gz: ed6a0cf93519256d488c6038f8aaf7c93647cd6620025b05cd95ba3cdb41b0b8194d012989a61c55ec1ecc208d4dd6d8adaa69619097e52748334dc2260924a0
7
+ data.tar.gz: 6d754f00136a1174ceefcd1f5c47025d4113b420e169eb9405786425de8fb308a7d0a99241677b8c261d452c52542b93cb02d5b5161f27187cb30efeec1ca58d
data/CHANGELOG.md CHANGED
@@ -1,9 +1,18 @@
1
- # Change Log
2
-
3
- ## 1.0.1
4
-
5
- Add error handling
6
-
7
- ## 1.0.0
8
-
9
- Initial release
1
+ # Change Log
2
+
3
+ ## 2.0.0 (2017-05-02)
4
+
5
+ * Set the node's environment to ensure accurate run_list expansion
6
+ * Add --capture option to capture all relevant information for local depsolver usage
7
+ * Add ability to use Chef DK's local depsolver to make calculations identical to a Chef Server
8
+ * Add --timeout option
9
+ * Add --csv-universe-to-json option for converting SQL captured cookbook universe to JSON
10
+ * Add --env-constraints-filter-universe option
11
+
12
+ ## 1.0.1 (2016-03-17)
13
+
14
+ * Add error handling
15
+
16
+ ## 1.0.0 (2016-03-17)
17
+
18
+ * Initial release
data/README.md CHANGED
@@ -1,38 +1,146 @@
1
- ## knife-depsolver
2
-
3
- Knife plugin that uses Chef Server to calculate cookbook dependencies for a given run_list.
4
-
5
- ### Install
6
-
7
- ```
8
- chef gem install knife-depsolver
9
- ```
10
-
11
- ### Usage
12
-
13
- Find the depsolver solution for an arbitrary run_list.
14
- Specifying recipes and even cookbook version constraints is allowed.
15
-
16
- ```
17
- knife depsolver 'cookbook-B,cookbook-A::foo@3.1.4,cookbook-R'
18
- ```
19
-
20
- Find the depsolver solution for an arbitrary run_list using a specific environment's cookbook version constraints.
21
- Specifying recipes and even cookbook version constraints is allowed.
22
-
23
- ```
24
- knife depsolver -E production 'cookbook-B,cookbook-A::foo@3.1.4,cookbook-R'
25
- ```
26
-
27
- Find the depsolver solution for a node's run_list.
28
-
29
- ```
30
- knife depsolver -n demo-node
31
- ```
32
-
33
- Find the depsolver solution for a node's run_list using a specific environment's cookbook version constraints.
34
-
35
- ```
36
- knife depsolver -E production -n demo-node
37
- ```
38
-
1
+ ## knife-depsolver
2
+
3
+ Knife plugin that calculates Chef cookbook dependencies for a given run_list.
4
+
5
+ Some features of knife-depsolver:
6
+
7
+ 1. Able to capture all relevant information and use Chef DK's embedded depsolver.
8
+ 2. JSON output for easy parsing
9
+ 3. API error handling provides very helpful information
10
+ 4. Accepts cookbook version constraints directly in the run_list
11
+ 5. Useful information such as environment cookbook version constraints, expanded run_list and elapsed time for depsolver request
12
+ 6. Ordering of depsolver results is maintained rather than sorted so you see what really gets passed to the chef-client
13
+
14
+ ### How Does Cookbook Dependency Solving Work?
15
+
16
+ During a chef-client run the chef-client expands the node's run_list which means the chef-client recursively expands
17
+ any existing role run_lists until the expanded run_list only consists of a list of cookbook recipes.
18
+
19
+ Then chef-client sends the expanded run_list to the Chef Server's `/organizations/ORGNAME/environments/ENVNAME/cookbook_versions`
20
+ API endpoint which is serviced by opscode-erchef.
21
+
22
+ opscode-erchef gets the ORGNAME organization's cookbook universe (cookbook dependency info for every version of every cookbook)
23
+ and the ENVNAME environment's cookbook version constraints from the database.
24
+
25
+ opscode-erchef sends the expanded run_list, environment constraints and cookbook universe to a ruby process running the
26
+ [depselector.rb script](https://github.com/chef/chef-server/blob/12.9.1/src/oc_erchef/apps/chef_objects/priv/depselector_rb/depselector.rb).
27
+
28
+ This script uses the [dep_selector gem](https://github.com/chef/dep-selector) to build a dependency graph that filters out any
29
+ cookbook versions that don't satisfy the environment cookbook version constraints.
30
+
31
+ The dep_selector gem re-formulates the dependency graph and the solution constraints as a CSP and off-loads the hard work to
32
+ [Gecode](http://www.gecode.org/), a fast, license-friendly solver written in C++. If Gecode returns a solution then that is used by
33
+ opscode-erchef to send a list of cookbook file metadata back to the chef-client so it can synchronize its cookbook cache.
34
+
35
+ ### Install
36
+
37
+ ```
38
+ chef gem install knife-depsolver
39
+ ```
40
+
41
+ ### Using knife-depsolver with Chef Server's depsolver
42
+
43
+ Find the depsolver solution for a node's run_list.
44
+
45
+ ```
46
+ knife depsolver -n demo-node
47
+ ```
48
+
49
+ Find the depsolver solution for an arbitrary run_list.
50
+ Specifying roles, recipes and even cookbook version constraints is allowed.
51
+
52
+ ```
53
+ knife depsolver 'role[base],cookbook-B,cookbook-A::foo@3.1.4,cookbook-R'
54
+ ```
55
+
56
+ The "-E <environment>" option can be added to use a specific environment's cookbook version constraints.
57
+
58
+ ```
59
+ knife depsolver -E production -n demo-node
60
+ OR
61
+ knife depsolver -E production 'role[base],cookbook-B,cookbook-A::foo@3.1.4,cookbook-R'
62
+ ```
63
+
64
+ ### Using knife-depsolver with Chef DK's embedded depsolver
65
+
66
+ It can be difficult to identify the source of the problem if we hit a depsolver issue in the Chef Server.
67
+
68
+ knife-depsolver can provide many more troubleshooting options by using the depsolver embedded in your workstation's Chef DK to make an identical calculation as the Chef Server.
69
+
70
+ First, you need to make sure that your version of Chef DK is using the same version of the dep_selector gem as your version of Chef Server. If your Chef DK is using a different version of the dep_selector gem then knife-depsolver's calculations will not be identical to the Chef Server's calculations which will confuse troubleshooting efforts.
71
+
72
+ | dep_selector gem | Chef Server | Chef DK |
73
+ | ---------------- | ----------- | ---------- |
74
+ | 1.0.3 | <= 12.8.0 | <= 0.16.28 |
75
+ | 1.0.4 | >= 12.9.0 | >= 0.17.17 |
76
+
77
+ Once you are sure you are using the correct version of Chef DK and you have installed the knife-depsolver plugin you can use the "--capture" option which queries the Chef Server and creates a separate file on the workstation for each of the following pieces of information.
78
+
79
+ 1. environment cookbook version constraints
80
+ 2. cookbook universe
81
+ 3. expanded run list
82
+
83
+ The name of each file includes a creation timestamp and a SHA-1 checksum of the contents of the file. These can help during the troubleshooting process to make sure we know when the capture was done and whether anyone has modified the contents of the file.
84
+
85
+ For example:
86
+
87
+ ```
88
+ knife depsolver -E production -n demo-node --capture
89
+ OR
90
+ knife depsolver -E production 'role[base],cookbook-B,cookbook-A::foo@3.1.4,cookbook-R' --capture
91
+ ```
92
+
93
+ Now use the "--env-constraints", "universe" and "expanded-run-list" options to provide all the information required for local depsolver calculations.
94
+
95
+ For example:
96
+
97
+ ```
98
+ knife depsolver --env-constraints rehearsal-environment-20170501185240-5f5843d819ecb0b174f308d76d4336bb7bbfacbf.txt --universe automate-universe-20170501185240-1c8e59e23530b1e1a8e0b3b3cc5236a29c84e469.txt --expanded-run-list expanded-run-list-20170501185240-387d90499514747792a805213c30be13d830d31f.txt
99
+ ```
100
+
101
+ Now it is easy to modify any combination of the expanded run list, the environment cookbook version constraints or the cookbook universe in those files and see the impact on the depsolver in an effort to isolate the problem.
102
+
103
+ Sometimes it can help to give the depsolver more than the default five seconds to perform its calculations. This can be done by using the "--timeout <seconds>" option to change the depsolver timeout.
104
+
105
+ ```
106
+ knife depsolver --env-constraints production-environment-20170501185240-5f5843d819ecb0b174f308d76d4336bb7bbfacbf.txt --universe my-org-universe-20170501185240-1c8e59e23530b1e1a8e0b3b3cc5236a29c84e469.txt --expanded-run-list expanded-run-list-20170501185240-387d90499514747792a805213c30be13d830d31f.txt --timeout 120
107
+ ```
108
+
109
+ ### Chef Server <= 12.4.0
110
+
111
+ knife-depsolver requires cookbook universe data in order to use Chef DK's embedded depsolver. The "/universe" API endpoint was added in the Chef Server 12.4.0 release.
112
+
113
+ If you are using a version of Chef Server older than 12.4.0 then you can run the following command on your Chef Server to extract the cookbook universe directly from the database in CSV format.
114
+
115
+ ```
116
+ export ORGNAME=demo
117
+
118
+ cat <<EOF | chef-server-ctl psql opscode_chef --options '-tAF,' > universe.csv
119
+ SELECT cvd.name,
120
+ cvd.major || '.' || cvd.minor || '.' || cvd.patch AS version,
121
+ updated_at,
122
+ dependencies
123
+ FROM cookbook_version_dependencies cvd
124
+ INNER JOIN cookbooks ckbks
125
+ ON cvd.org_id = ckbks.org_id
126
+ AND cvd.name = ckbks.name
127
+ INNER JOIN cookbook_versions cv
128
+ ON ckbks.id = cv.cookbook_id
129
+ AND cvd.major = cv.major
130
+ AND cvd.minor = cv.minor
131
+ AND cvd.patch = cv.patch
132
+ WHERE cvd.org_id = (SELECT id
133
+ FROM orgs
134
+ WHERE name = '$ORGNAME'
135
+ LIMIT 1)
136
+ ORDER BY updated_at DESC;
137
+ EOF
138
+ ```
139
+
140
+ Then you can run "knife depsolver --csv-universe-to-json universe.csv" to convert it to the normal JSON format.
141
+
142
+ If the Chef Server doesn't have the "chef-server-ctl psql" command available then you can try replacing that line with the following.
143
+
144
+ ```
145
+ cat <<EOF | su -l opscode-pgsql -c 'psql opscode_chef -tAF,' > universe.csv
146
+ ```
@@ -0,0 +1,138 @@
1
+ # I pulled and slightly modified this script from
2
+ # https://github.com/chef/dep-selector/blob/master/scripts/depselector.rb
3
+ # which was originally pulled and slightly modified from
4
+ # https://github.com/chef/chef-server/blob/12.9.0/src/oc_erchef/apps/chef_objects/priv/depselector_rb/depselector.rb
5
+
6
+ # Test script to take a dependency graph in JSON format and solve it
7
+ # ensure that the Gemfile is in the cwd
8
+ # Dir.chdir(File.dirname(__FILE__))
9
+
10
+ # require 'json'
11
+ # require 'pp'
12
+ require 'dep_selector'
13
+ # require 'pry'
14
+
15
+ def translate_constraint(constraint)
16
+ case constraint
17
+ when Symbol
18
+ case constraint
19
+ when :gt then ">"
20
+ when :gte then ">="
21
+ when :lt then "<"
22
+ when :lte then "<="
23
+ when :eq then "="
24
+ when :pes then "~>"
25
+ else constraint.to_s
26
+ end
27
+ when NilClass
28
+ "="
29
+ else
30
+ constraint
31
+ end
32
+ end
33
+
34
+ def constraint_to_str(constraint, constraint_version)
35
+ return nil unless constraint_version
36
+ "#{translate_constraint(constraint)} #{constraint_version}"
37
+ end
38
+
39
+ def solve(data)
40
+ begin
41
+ # create dependency graph from cookbooks
42
+ graph = DepSelector::DependencyGraph.new
43
+
44
+ env_constraints = data[:environment_constraints].inject({}) do |acc, env_constraint|
45
+ name, version, constraint = env_constraint
46
+ acc[name] = DepSelector::VersionConstraint.new(constraint_to_str(constraint, version))
47
+ acc
48
+ end
49
+
50
+ all_versions = []
51
+
52
+ data[:all_versions].each do | vsn|
53
+ name, version_constraints = vsn
54
+ version_constraints.each do |version_constraint| # todo: constraints become an array in ruby
55
+ # due to the erlectricity conversion from
56
+ # tuples
57
+ version, constraints = version_constraint
58
+
59
+ # filter versions based on environment constraints
60
+ env_constraint = env_constraints[name]
61
+ if (!env_constraint || env_constraint.include?(DepSelector::Version.new(version)))
62
+ package_version = graph.package(name).add_version(DepSelector::Version.new(version))
63
+ constraints.each do |package_constraint|
64
+ constraint_name, constraint_version, constraint = package_constraint
65
+ version_constraint = DepSelector::VersionConstraint.new(constraint_to_str(constraint, constraint_version))
66
+ dependency = DepSelector::Dependency.new(graph.package(constraint_name), version_constraint)
67
+ package_version.dependencies << dependency
68
+ end
69
+ end
70
+ end
71
+
72
+ # regardless of filter, add package reference to all_packages
73
+ all_versions << graph.package(name)
74
+ end
75
+
76
+ run_list = data[:run_list].map do |run_list_item|
77
+ item_name, item_constraint_version, item_constraint = run_list_item
78
+ version_constraint = DepSelector::VersionConstraint.new(constraint_to_str(item_constraint,
79
+ item_constraint_version))
80
+ DepSelector::SolutionConstraint.new(graph.package(item_name), version_constraint)
81
+ end
82
+
83
+ timeout_ms = data[:timeout_ms]
84
+ selector = DepSelector::Selector.new(graph, (timeout_ms / 1000.0))
85
+
86
+ answer = begin
87
+ solution = selector.find_solution(run_list, all_versions)
88
+ packages = []
89
+ solution.each do |package, v|
90
+ packages << [package, [v.major, v.minor, v.patch]]
91
+ end
92
+ [:ok, packages]
93
+ rescue DepSelector::Exceptions::InvalidSolutionConstraints => e
94
+ non_existent_cookbooks = e.non_existent_packages.inject([]) do |list, constraint|
95
+ list << constraint.package.name
96
+ end
97
+
98
+ constrained_to_no_versions = e.constrained_to_no_versions.inject([]) do |list, constraint|
99
+ list << constraint.to_s
100
+ end
101
+
102
+ error_detail = [[:non_existent_cookbooks, non_existent_cookbooks],
103
+ [:constraints_not_met, constrained_to_no_versions]]
104
+
105
+ [:error, :invalid_constraints, error_detail]
106
+ rescue DepSelector::Exceptions::NoSolutionExists => e
107
+ most_constrained_cookbooks = e.disabled_most_constrained_packages.inject([]) do |list, package|
108
+ # WTF: this is the reported error format but I can't find this anywhere in the ruby code
109
+ list << "#{package.name} = #{package.versions.first.to_s}"
110
+ end
111
+
112
+ non_existent_cookbooks = e.disabled_non_existent_packages.inject([]) do |list, package|
113
+ list << package.name
114
+ end
115
+
116
+ error_detail = [[:message, e.message],
117
+ [:unsatisfiable_run_list_item, e.unsatisfiable_solution_constraint.to_s],
118
+ [:non_existent_cookbooks, non_existent_cookbooks],
119
+ [:most_constrained_cookbooks, most_constrained_cookbooks]]
120
+
121
+ [:error, :no_solution, error_detail]
122
+ rescue DepSelector::Exceptions::TimeBoundExceeded,
123
+ DepSelector::Exceptions::TimeBoundExceededNoSolution => e
124
+ # While dep_selector differentiates between the two solutions, the opscode-chef
125
+ # API returns the same error regardless of the timeout type. We'll swallow the
126
+ # difference here and return a unified timeout to erchef
127
+ [:error, :resolution_timeout]
128
+ end
129
+ rescue => e
130
+ answer = [:error, :exception, e.message, [e.backtrace]]
131
+ end
132
+
133
+ answer
134
+ end
135
+
136
+ # data = JSON.parse!(File.read(ARGV[0]), symbolize_names: true)
137
+ # pp data
138
+ # pp solve(data)
@@ -1,77 +1,322 @@
1
- require 'chef/knife'
2
-
3
- class Chef
4
- class Knife
5
- class Depsolver < Knife
6
- deps do
7
- end
8
-
9
- banner 'knife depsolver RUN_LIST'
10
-
11
- option :node,
12
- short: '-n',
13
- long: '--node NAME',
14
- description: 'Use the run list from a given node'
15
-
16
- def run
17
- begin
18
- if config[:node]
19
- node = Chef::Node.load(config[:node])
20
- else
21
- node = Chef::Node.new
22
- node.name('depsolver-tmp-node')
23
-
24
- run_list = name_args.map {|item| item.to_s.split(/,/) }.flatten.each{|item| item.strip! }
25
- run_list.delete_if {|item| item.empty? }
26
-
27
- run_list.each do |arg|
28
- node.run_list.add(arg)
29
- end
30
- end
31
-
32
- environment = config[:environment]
33
- environment ||= node.chef_environment
34
-
35
- environment_cookbook_versions = Chef::Environment.load(environment).cookbook_versions
36
-
37
- run_list_expansion = node.expand!
38
- expanded_run_list_with_versions = run_list_expansion.recipes.with_version_constraints_strings
39
-
40
- depsolver_start_time = Time.now
41
-
42
- ckbks = rest.post_rest("environments/" + environment + "/cookbook_versions", { "run_list" => expanded_run_list_with_versions })
43
-
44
- depsolver_finish_time = Time.now
45
-
46
- depsolver_results = {}
47
- ckbks.each do |name, ckbk|
48
- version = ckbk.is_a?(Hash) ? ckbk['version'] : ckbk.version
49
- depsolver_results[name] = version
50
- end
51
-
52
- rescue => e
53
- api_error = {}
54
- api_error[:error_code] = e.response.code
55
- api_error[:error_message] = e.response.message
56
- begin
57
- api_error[:error_body] = JSON.parse(e.response.body)
58
- rescue JSON::ParserError
59
- end
60
- ensure
61
- results = {}
62
- results[:node] = node.name unless node.nil? || node.name.nil?
63
- results[:environment] = environment unless environment.nil?
64
- results[:environment_cookbook_versions] = environment_cookbook_versions unless environment_cookbook_versions.nil?
65
- results[:run_list] = node.run_list unless node.nil? || node.run_list.nil?
66
- results[:expanded_run_list] = expanded_run_list_with_versions unless expanded_run_list_with_versions.nil?
67
- results[:depsolver_results] = depsolver_results unless depsolver_results.nil? || depsolver_results.empty?
68
- results[:depsolver_cookbook_count] = ckbks.count unless ckbks.nil?
69
- results[:depsolver_elapsed_ms] = ((depsolver_finish_time - depsolver_start_time) * 1000).to_i unless depsolver_finish_time.nil?
70
- results[:api_error] = api_error unless api_error.nil?
71
-
72
- msg(JSON.pretty_generate(results))
73
- end
74
- end
75
- end
76
- end
77
- end
1
+ require 'chef/knife'
2
+ require 'digest'
3
+
4
+ class Chef
5
+ class Knife
6
+ class Depsolver < Knife
7
+ deps do
8
+ end
9
+
10
+ banner 'knife depsolver RUN_LIST'
11
+
12
+ option :node,
13
+ short: '-n',
14
+ long: '--node NAME',
15
+ description: 'Use the run list from a given node'
16
+
17
+ option :timeout,
18
+ short: '-t',
19
+ long: '--timeout SECONDS',
20
+ description: 'Set the local depsolver timeout. Only valid when using the --env-constraints and --universe options'
21
+
22
+ option :capture,
23
+ long: '--capture',
24
+ description: 'Save the expanded run list, environment cookbook constraints and cookbook universe to files for local depsolving'
25
+
26
+ option :env_constraints,
27
+ long: '--env-constraints FILENAME',
28
+ description: 'Use the environment cookbook constraints from FILENAME. REQUIRED when using the local depsolver.'
29
+
30
+ option :universe,
31
+ long: '--universe FILENAME',
32
+ description: 'Use the cookbook universe from FILENAME. REQUIRED when using the local depsolver.'
33
+
34
+ option :expanded_run_list,
35
+ long: '--expanded-run-list FILENAME',
36
+ description: 'Use the expanded run list from FILENAME. REQUIRED when using the local depsolver.'
37
+
38
+ option :csv_universe_to_json,
39
+ long: '--csv-universe-to-json FILENAME',
40
+ description: 'Convert a CSV cookbook universe, FILENAME, to JSON.'
41
+
42
+ option :env_constraints_filter_universe,
43
+ long: '--env-constraints-filter-universe',
44
+ description: 'Filter the cookbook universe using the environment cookbook version constraints.'
45
+
46
+ def run
47
+ begin
48
+ use_local_depsolver = false
49
+ if config[:env_constraints_filter_universe]
50
+ if config[:node] || config[:environment] || config[:timeout] || config[:capture] || config[:expanded_run_list] || config[:csv_universe_to_json]
51
+ msg("ERROR: The --env-constraints-filter-universe option is only compatible with the --env-constraints and --universe options")
52
+ exit!
53
+ elsif !(config[:env_constraints] && config[:universe])
54
+ msg("ERROR: The --env-constraints-filter-universe option requires the --env-constraints and --universe options to be set")
55
+ exit!
56
+ end
57
+ else
58
+ if config[:env_constraints] || config[:universe] || config[:expanded_run_list]
59
+ if config[:env_constraints] && config[:universe] && config[:expanded_run_list]
60
+ use_local_depsolver = true
61
+ unless name_args.empty?
62
+ puts "ERROR: Setting a run list on the command line is not compatible with the --env-constraints, --universe or --expanded-run-list options"
63
+ exit!
64
+ end
65
+ if config[:node]
66
+ puts "ERROR: The --node option is not compatible with the --env-constraints, --universe or --expanded-run-list options"
67
+ exit!
68
+ end
69
+ if config[:environment]
70
+ puts "ERROR: The --environment option is not compatible with the --env-constraints, --universe or --expanded-run-list options"
71
+ exit!
72
+ end
73
+ if config[:capture]
74
+ puts "ERROR: The --capture option is not compatible with the --env-constraints, --universe or --expanded-run-list options"
75
+ exit!
76
+ end
77
+ else
78
+ puts "ERROR: The --env-constraints, --universe and --expanded-run-list options must be used together to use the local depsolver"
79
+ exit!
80
+ end
81
+ end
82
+ end
83
+ if config[:timeout] && !use_local_depsolver
84
+ msg("ERROR: The --timeout option requires the --env-constraints, --universe and --expanded-run-list options to be set")
85
+ exit!
86
+ end
87
+
88
+ timeout = (config[:timeout].to_f * 1000).to_i if config[:timeout]
89
+ timeout ||= 5 * 1000
90
+
91
+ if config[:csv_universe_to_json]
92
+ unless File.file?(config[:csv_universe_to_json])
93
+ msg("ERROR: #{config[:csv_universe_to_json]} does not exist or is not a file.")
94
+ exit!
95
+ end
96
+ universe = Hash.new { |hash, key| hash[key] = Hash.new }
97
+ IO.foreach(config[:csv_universe_to_json]) do |line|
98
+ name, version, updated_at, dependencies = line.split(",", 4)
99
+ universe[name][version] = {updated_at: updated_at, dependencies: JSON.parse(dependencies)}
100
+ end
101
+
102
+ universe_json = JSON.pretty_generate(universe)
103
+ universe_filename = "universe-#{Time.now.strftime("%Y-%m-%d-%H:%M:%S")}-#{Digest::SHA1.hexdigest(universe_json)}.txt"
104
+ IO.write(universe_filename, universe_json)
105
+ puts "Cookbook universe saved to #{universe_filename}"
106
+ exit!
107
+ end
108
+
109
+ if config[:node] && !(config[:env_constraints_filter_universe] && config[:env_constraints])
110
+ node = Chef::Node.load(config[:node])
111
+ else
112
+ node = Chef::Node.new
113
+ node.name('depsolver-tmp-node')
114
+
115
+ if config[:expanded_run_list]
116
+ unless File.file?(config[:expanded_run_list])
117
+ msg("ERROR: #{config[:expanded_run_list]} does not exist or is not a file.")
118
+ exit!
119
+ end
120
+ contents = JSON.parse(IO.read(config[:expanded_run_list]))
121
+ if !(contents.key?('expanded_run_list') && contents['expanded_run_list'].is_a?(Array))
122
+ msg("ERROR: #{config[:expanded_run_list]} does not contain an expanded run list array.")
123
+ exit!
124
+ else
125
+ expanded_run_list = contents['expanded_run_list']
126
+ expanded_run_list.each do |arg|
127
+ node.run_list.add(arg)
128
+ end
129
+ end
130
+ else
131
+ run_list = name_args.map {|item| item.to_s.split(/,/) }.flatten.each{|item| item.strip! }
132
+ run_list.delete_if {|item| item.empty? }
133
+
134
+ run_list.each do |arg|
135
+ node.run_list.add(arg)
136
+ end
137
+ end
138
+ end
139
+
140
+ node.chef_environment = config[:environment] if config[:environment]
141
+
142
+ if config[:capture]
143
+ if node.chef_environment == '_default'
144
+ environment_cookbook_versions = Hash.new
145
+ else
146
+ environment_cookbook_versions = Chef::Environment.load(node.chef_environment).cookbook_versions
147
+ end
148
+ env = { name: node.chef_environment, cookbook_versions: environment_cookbook_versions }
149
+ environment_constraints_json = JSON.pretty_generate(env)
150
+ environment_constraints_filename = "#{node.chef_environment}-environment-#{Time.now.strftime("%Y-%m-%d-%H:%M:%S")}-#{Digest::SHA1.hexdigest(environment_constraints_json)}.txt"
151
+ IO.write(environment_constraints_filename, environment_constraints_json)
152
+ puts "Environment constraints saved to #{environment_constraints_filename}"
153
+
154
+ begin
155
+ universe = rest.get_rest("universe")
156
+ universe_json = JSON.pretty_generate(universe)
157
+ universe_filename = ""
158
+ rest.url.to_s.match(".*/organizations/(.*)/?") { universe_filename = "#{$1}-" }
159
+ universe_filename += "universe-#{Time.now.strftime("%Y-%m-%d-%H:%M:%S")}-#{Digest::SHA1.hexdigest(universe_json)}.txt"
160
+ IO.write(universe_filename, universe_json)
161
+ puts "Cookbook universe saved to #{universe_filename}"
162
+ rescue Net::HTTPServerException
163
+ puts "WARNING: The cookbook universe API endpoint is not available."
164
+ puts "WARNING: Try capturing the cookbook universe using the SQL query found in the knife-depsolver README."
165
+ puts "WARNING: Then convert the results using knife-depsolver's --csv-universe-to-json option"
166
+ end
167
+ end
168
+
169
+ if config[:env_constraints]
170
+ unless File.file?(config[:env_constraints])
171
+ msg("ERROR: #{config[:env_constraints]} does not exist or is not a file.")
172
+ exit!
173
+ end
174
+ env = JSON.parse(IO.read(config[:env_constraints]))
175
+ if env['name'].to_s.empty?
176
+ msg("ERROR: #{config[:env_constraints]} does not contain an environment name.")
177
+ exit!
178
+ else
179
+ node.chef_environment = env['name']
180
+ end
181
+ if !env['cookbook_versions'].is_a?(Hash)
182
+ msg("ERROR: #{config[:env_constraints]} does not contain a Hash of cookbook version constraints.")
183
+ exit!
184
+ else
185
+ environment_cookbook_versions = env['cookbook_versions']
186
+ end
187
+ end
188
+
189
+ if config[:universe]
190
+ unless File.file?(config[:universe])
191
+ msg("ERROR: #{config[:universe]} does not exist or is not a file.")
192
+ exit!
193
+ end
194
+ universe = JSON.parse(IO.read(config[:universe]))
195
+ if !universe.is_a?(Hash)
196
+ msg("ERROR: #{config[:universe]} does not contain a cookbook universe Hash.")
197
+ exit!
198
+ end
199
+ end
200
+
201
+ if config[:env_constraints_filter_universe]
202
+ env_constraints = environment_cookbook_versions.each_with_object({}) do |env_constraint, memo|
203
+ name, constraint = env_constraint
204
+ constraint, version = constraint.split
205
+ memo[name] = DepSelector::VersionConstraint.new(constraint_to_str(constraint, version))
206
+ end
207
+ universe.each do |name, versions|
208
+ versions.delete_if {|version, v| !env_constraints[name].include?(version)} if env_constraints[name]
209
+ end
210
+ filtered_universe_json = JSON.pretty_generate(universe)
211
+ filtered_universe_filename = "filtered-universe-#{Time.now.strftime("%Y-%m-%d-%H:%M:%S")}-#{Digest::SHA1.hexdigest(filtered_universe_json)}.txt"
212
+ IO.write(filtered_universe_filename, filtered_universe_json)
213
+ puts "Filtered cookbook universe saved to #{filtered_universe_filename}"
214
+ exit!
215
+ end
216
+
217
+ run_list_expansion = node.run_list.expand(node.chef_environment, 'server')
218
+ expanded_run_list_with_versions = run_list_expansion.recipes.with_version_constraints_strings
219
+
220
+ exit if config[:capture]
221
+
222
+ depsolver_results = Hash.new
223
+ if use_local_depsolver
224
+ env_ckbk_constraints = environment_cookbook_versions.map do |ckbk_name, ckbk_constraint|
225
+ [ckbk_name, ckbk_constraint.split.reverse].flatten
226
+ end
227
+
228
+ all_versions = universe.map do |ckbk_name, ckbk_metadata|
229
+ ckbk_versions = ckbk_metadata.map do |version, version_metadata|
230
+ [version, version_metadata['dependencies'].map { |dep_ckbk_name, dep_ckbk_constraint| [dep_ckbk_name, dep_ckbk_constraint.split.reverse].flatten }]
231
+ end
232
+ [ckbk_name, ckbk_versions]
233
+ end
234
+
235
+ expanded_run_list_with_split_versions = expanded_run_list_with_versions.map do |run_list_item|
236
+ name, version = run_list_item.split('@')
237
+ name.sub!(/::.*/, '')
238
+ version ? [name, version] : name
239
+ end
240
+
241
+ data = {environment_constraints: env_ckbk_constraints, all_versions: all_versions, run_list: expanded_run_list_with_split_versions, timeout_ms: timeout}
242
+
243
+ depsolver_start_time = Time.now
244
+
245
+ solution = solve(data)
246
+
247
+ depsolver_finish_time = Time.now
248
+
249
+ if solution.first == :ok
250
+ solution.last.map { |ckbk| ckbk_name, ckbk_version = ckbk; depsolver_results[ckbk_name] = ckbk_version.join('.') }
251
+ else
252
+ status, error_type, error_detail = solution
253
+ depsolver_error = { error_type => error_detail }
254
+ end
255
+ else
256
+ begin
257
+ chef_server_version = rest.get(server_url.sub(/organizations.*/, 'version')).split("\n")[0]
258
+ rescue Net::HTTPServerException
259
+ chef_server_version = "unknown"
260
+ end
261
+
262
+ depsolver_start_time = Time.now
263
+
264
+ ckbks = rest.post_rest("environments/" + node.chef_environment + "/cookbook_versions", { "run_list" => expanded_run_list_with_versions })
265
+
266
+ depsolver_finish_time = Time.now
267
+
268
+ ckbks.each do |name, ckbk|
269
+ version = ckbk.is_a?(Hash) ? ckbk['version'] : ckbk.version
270
+ depsolver_results[name] = version
271
+ end
272
+ end
273
+ rescue Net::HTTPServerException => e
274
+ api_error = {}
275
+ api_error[:error_code] = e.response.code
276
+ api_error[:error_message] = e.response.message
277
+ begin
278
+ api_error[:error_body] = JSON.parse(e.response.body)
279
+ rescue JSON::ParserError
280
+ end
281
+ rescue => e
282
+ msg("ERROR: #{e.message}")
283
+ exit!
284
+ ensure
285
+ local_software = Hash.new
286
+ %w(chef-dk chef dep_selector).each do |gem_name|
287
+ begin
288
+ local_software[gem_name] = Gem::Specification.find_by_name(gem_name).version
289
+ rescue Gem::MissingSpecError
290
+ end
291
+ end
292
+
293
+ results = {}
294
+ results[:local_software] = local_software unless local_software.empty?
295
+ if use_local_depsolver
296
+ results[:depsolver] = "used local depsolver"
297
+ else
298
+ results[:depsolver] = {"used chef server" => chef_server_version} unless chef_server_version.nil?
299
+ end
300
+ results[:node] = node.name unless node.nil? || node.name.nil?
301
+ results[:environment] = node.chef_environment unless node.chef_environment.nil?
302
+ results[:run_list] = node.run_list unless node.nil? || node.run_list.nil?
303
+ results[:expanded_run_list] = expanded_run_list_with_versions unless expanded_run_list_with_versions.nil?
304
+ results[:depsolver_results] = depsolver_results unless depsolver_results.nil? || depsolver_results.empty?
305
+ results[:depsolver_cookbook_count] = depsolver_results.count unless depsolver_results.nil? || depsolver_results.empty?
306
+ results[:depsolver_elapsed_ms] = ((depsolver_finish_time - depsolver_start_time) * 1000).to_i unless depsolver_finish_time.nil?
307
+ results[:depsolver_error] = depsolver_error unless depsolver_error.nil?
308
+ results[:api_error] = api_error unless api_error.nil?
309
+
310
+ if config[:capture]
311
+ results_json = JSON.pretty_generate(results)
312
+ expanded_run_list_filename = "expanded-run-list-#{Time.now.strftime("%Y-%m-%d-%H:%M:%S")}-#{Digest::SHA1.hexdigest(results_json)}.txt"
313
+ IO.write(expanded_run_list_filename, results_json)
314
+ puts "Expanded run list saved to #{expanded_run_list_filename}"
315
+ else
316
+ msg(JSON.pretty_generate(results))
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
@@ -1,3 +1,3 @@
1
- module KnifeDepsolver
2
- VERSION = "1.0.1"
3
- end
1
+ module KnifeDepsolver
2
+ VERSION = "2.0.0"
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knife-depsolver
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremiah Snapp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-17 00:00:00.000000000 Z
11
+ date: 2017-05-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Knife plugin that uses Chef Server to calculate cookbook dependencies
14
14
  for a given run_list.
@@ -23,6 +23,7 @@ files:
23
23
  - CHANGELOG.md
24
24
  - LICENSE
25
25
  - README.md
26
+ - lib/chef/knife/depselector.rb
26
27
  - lib/chef/knife/depsolver.rb
27
28
  - lib/knife-depsolver/version.rb
28
29
  homepage: https://github.com/jeremiahsnapp/knife-depsolver
@@ -44,10 +45,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
44
45
  version: '0'
45
46
  requirements: []
46
47
  rubyforge_project:
47
- rubygems_version: 2.5.2
48
+ rubygems_version: 2.6.11
48
49
  signing_key:
49
50
  specification_version: 4
50
51
  summary: Knife plugin that uses Chef Server to calculate cookbook dependencies for
51
52
  a given run_list.
52
53
  test_files: []
53
- has_rdoc: