knife-flip 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -16,10 +16,12 @@ gem install knife-flip
16
16
  ## What it does
17
17
 
18
18
  ````
19
- knife node flip mynode.foo.com myenv
19
+ knife node flip mynode.foo.com myenv [--preview]
20
20
  ````
21
21
 
22
- will move the node mynode.foo.com into the environment myenv
22
+ will move the node mynode.foo.com into the environment myenv. Passing in the --preview option
23
+ it will dry-run the flip and show you what cookbooks and versions would be applied if it were actually
24
+ flipped.
23
25
 
24
26
 
25
27
  ````
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
13
13
  ## If your rubyforge_project name is different, then edit it and comment out
14
14
  ## the sub! line in the Rakefile
15
15
  s.name = 'knife-flip'
16
- s.version = '0.1.6'
17
- s.date = '2013-03-22'
16
+ s.version = '0.1.7'
17
+ s.date = '2013-06-06'
18
18
  s.rubyforge_project = 'knife-flip'
19
19
 
20
20
  ## Make sure your summary is short. The description may be as long
@@ -41,6 +41,7 @@ Gem::Specification.new do |s|
41
41
  ## List your runtime dependencies here. Runtime dependencies are those
42
42
  ## that are needed for an end user to actually USE your code.
43
43
  s.add_dependency('chef', [">= 0.10.4"])
44
+ s.add_dependency('colorize', [">= 0.5.8"])
44
45
 
45
46
  ## Leave this section as-is. It will be automatically generated from the
46
47
  ## contents of your Git repository via the gemspec task. DO NOT REMOVE
@@ -2,6 +2,7 @@
2
2
 
3
3
 
4
4
  require 'chef/knife'
5
+ require 'colorize'
5
6
 
6
7
  module KnifeFlip
7
8
  class NodeFlip < Chef::Knife
@@ -12,7 +13,13 @@ module KnifeFlip
12
13
  require 'chef/knife/core/object_loader'
13
14
  end
14
15
 
15
- banner "knife node flip NODE ENVIRONMENT"
16
+ banner "knife node flip NODE ENVIRONMENT (options)"
17
+
18
+ option :preview,
19
+ :long => '--preview',
20
+ :boolean => true,
21
+ :on => :tail,
22
+ :description => 'Preview the target environment to see affected cookbooks'
16
23
 
17
24
  def run
18
25
  unless @node_name = name_args[0]
@@ -25,42 +32,150 @@ module KnifeFlip
25
32
  exit 1
26
33
  end
27
34
 
28
- puts "Looking for an fqdn of #{@node_name} or name of #{@node_name}"
35
+ if config[:preview] then
36
+ show_environmental_differences
37
+ else
38
+ puts "Looking for an fqdn of #{@node_name} or name of #{@node_name}"
39
+
40
+ searcher = Chef::Search::Query.new
41
+ result = searcher.search(:node, "fqdn:#{@node_name} OR name:#{@node_name}")
42
+
43
+ knife_search = Chef::Knife::Search.new
44
+ node = result.first.first
45
+ if node.nil?
46
+ puts "Could not find a node with the fqdn of #{@node_name} or name of #{@node_name}"
47
+ exit 1
48
+ end
49
+
50
+ begin
51
+ e = Chef::Environment.load(@environment)
52
+ rescue Net::HTTPServerException => e
53
+ if e.response.code.to_s == "404"
54
+ ui.error "The environment #{@environment} does not exist on the server, aborting."
55
+ Chef::Log.debug(e)
56
+ exit 1
57
+ else
58
+ raise
59
+ end
60
+ end
61
+
62
+ puts "Setting environment to #{@environment}"
63
+ node.chef_environment(@environment)
64
+ node.save
65
+
66
+ knife_search = Chef::Knife::Search.new
67
+ # ensure that 'start' and 'rows' are set since we don't seem to properly inherit the +config+ hash and therefore don't get sane defaults
68
+ config[:start] = 0
69
+ config[:rows] = 1000
70
+ knife_search.config = config # without this, the +config+ hash is empty
71
+ knife_search.name_args = ['node', "fqdn:#{@node_name} OR name:#{@node_name}"]
72
+ knife_search.run
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ # Compare the two environments
79
+ def show_environmental_differences
80
+ # Re-iterate to the user that this won't flip you
81
+ remind_user_about_preview
82
+
83
+ # Load up the node information
84
+ load_node_object
85
+
86
+ # Get the source environment
87
+ get_source_environment
88
+
89
+ # Pull down the cookbooks for this node
90
+ cookbooks = get_cookbooks_for_node
91
+
92
+ # Get cookbooks for the environments we are checking
93
+ source_cookbooks = cookbooks_for_environment(@source_environment)
94
+ target_cookbooks = cookbooks_for_environment(@environment)
95
+
96
+ # Transform the uploaded cookbooks into a name => latest version hash
97
+ source_hash = get_cookbook_version_hash(source_cookbooks)
29
98
 
99
+ # Transform the production cookbooks into a name => latest version hash
100
+ target_hash = get_cookbook_version_hash(target_cookbooks)
101
+
102
+ # Intersect the production cookbook collection and ours
103
+ common_cookbooks = target_cookbooks.keys & cookbooks
104
+ changed_cookbooks = common_cookbooks.keep_if { |cookbook_key|
105
+ target_hash[cookbook_key] != source_hash[cookbook_key]
106
+ }
107
+
108
+ # Lets show what is different
109
+ show_cookbook_differences(changed_cookbooks, source_hash, target_hash)
110
+ end
111
+
112
+ # Load up the node in question into a instance variable
113
+ def load_node_object
30
114
  searcher = Chef::Search::Query.new
31
115
  result = searcher.search(:node, "fqdn:#{@node_name} OR name:#{@node_name}")
32
116
 
33
- knife_search = Chef::Knife::Search.new
34
- node = result.first.first
35
- if node.nil?
117
+ @node = result.first.first
118
+ if @node.nil?
36
119
  puts "Could not find a node with the fqdn of #{@node_name} or name of #{@node_name}"
37
120
  exit 1
38
- end
121
+ end
122
+ end
39
123
 
40
- begin
41
- e = Chef::Environment.load(@environment)
42
- rescue Net::HTTPServerException => e
43
- if e.response.code.to_s == "404"
44
- ui.error "The environment #{@environment} does not exist on the server, aborting."
45
- Chef::Log.debug(e)
46
- exit 1
47
- else
48
- raise
124
+ # Extract the source environment from the node object
125
+ def get_source_environment
126
+ @source_environment = @node.chef_environment
127
+ end
128
+
129
+ # For the node being passed in, find and return an array of cookboock names
130
+ def get_cookbooks_for_node
131
+ cookbooks = @node.recipes.map {|recipe| recipe.match('^[^:]+')[0] }.uniq
132
+
133
+ return cookbooks
134
+ end
135
+
136
+ # For an environment, get all the cookbooks associated with it (in API object array form)
137
+ def cookbooks_for_environment(environment=nil, num_versions=1)
138
+ api_endpoint = environment ? "/environments/#{environment}/cookbooks?#{num_versions}" : "/cookbooks?#{num_versions}"
139
+ cookbooks = rest.get_rest(api_endpoint)
140
+
141
+ return cookbooks
142
+ end
143
+
144
+ # Given a cookbook array returned from the API, create a Hash of its name and the latest version
145
+ def get_cookbook_version_hash(cookbooks)
146
+ Hash[cookbooks.collect { |k, v| [k, v['versions'].first['version']] }]
147
+ end
148
+
149
+ # Takes in an array of modified cookbook names, the hash of uploaded cookbooks, and the hash of cookbooks
150
+ # from the environment being checked
151
+ def show_cookbook_differences(changed_cookbook_names, source_hash, target_hash)
152
+ changed_cookbook_count = changed_cookbook_names.size
153
+ (1..110).each { print "=".colorize(:cyan) }
154
+ puts ""
155
+ if changed_cookbook_count != 0 then
156
+ puts "#{changed_cookbook_count} difference(s) between environments"
157
+ changed_cookbook_names.each do |cookbook_name|
158
+ puts "#{cookbook_name}".colorize(:yellow) + ": " + "#{@source_environment}".colorize(:magenta) +
159
+ " version: " + "#{source_hash[cookbook_name]} ".colorize(:red) +
160
+ "will be changed to " + "#{target_hash[cookbook_name]}".colorize(:green) + " in " +
161
+ "#{@environment}".colorize(:magenta)
49
162
  end
163
+ elsif (@source_environment == @environment) and (changed_cookbook_names.size == 0) then
164
+ puts "The environment the node is on and the one you are flipping to are identical, and thus "
165
+ puts "there are " + "NO".colorize(:red) +" cookbook differences"
166
+ else
167
+ puts "No differences".colorize(:red) +
168
+ " between the cookbook versions in the " + "#{@source_environment}".colorize(:magenta) + " and the " +
169
+ "#{@environment}".colorize(:magenta) + " environment for this node"
50
170
  end
51
-
52
- puts "Setting environment to #{@environment}"
53
- node.chef_environment(@environment)
54
- node.save
55
-
56
- knife_search = Chef::Knife::Search.new
57
- # ensure that 'start' and 'rows' are set since we don't seem to properly inherit the +config+ hash and therefore don't get sane defaults
58
- config[:start] = 0
59
- config[:rows] = 1000
60
- knife_search.config = config # without this, the +config+ hash is empty
61
- knife_search.name_args = ['node', "fqdn:#{@node_name} OR name:#{@node_name}"]
62
- knife_search.run
171
+ (1..110).each { print "=".colorize(:cyan) }
172
+ puts ""
173
+ end
63
174
 
175
+ # Print out warning for users so they know that preview is a dry run
176
+ def remind_user_about_preview
177
+ puts "NOTE NOTE NOTE".colorize(:red) + " Running with --preview " + "WILL NOT".colorize(:red) +
178
+ " flip your node\n"
64
179
  end
65
180
  end
66
181
  end
@@ -1,3 +1,3 @@
1
1
  module KnifeFlip
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knife-flip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-22 00:00:00.000000000 Z
12
+ date: 2013-06-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: chef
@@ -27,6 +27,22 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: 0.10.4
30
+ - !ruby/object:Gem::Dependency
31
+ name: colorize
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 0.5.8
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.5.8
30
46
  description: A knife plugin to move a node, or all nodes in a role, to a specific
31
47
  environment
32
48
  email: jonlives@gmail.com