knife-sharp 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,12 +4,20 @@ require 'grit'
4
4
  module KnifeSharp
5
5
  class SharpAlign < Chef::Knife
6
6
 
7
- banner "knife sharp align BRANCH ENVIRONMENT [--debug]"
7
+ banner "knife sharp align BRANCH ENVIRONMENT [OPTS]"
8
+
9
+ [:cookbooks, :databags, :roles].each do |opt|
10
+ option opt,
11
+ :short => "-#{opt.to_s[0].upcase}",
12
+ :long => "--#{opt}-only",
13
+ :description => "sync #{opt} only",
14
+ :default => false
15
+ end
8
16
 
9
- option :debug,
10
- :short => '-d',
11
- :long => '--debug',
12
- :description => "turn debug on",
17
+ option :dump_remote_only,
18
+ :short => "-B",
19
+ :long => "--dump-remote-only",
20
+ :description => "dump items not present locally (roles/databags)",
13
21
  :default => false
14
22
 
15
23
  deps do
@@ -20,22 +28,37 @@ module KnifeSharp
20
28
 
21
29
  def run
22
30
  setup()
23
- ui.msg "On server #{@chef_server}" if @chef_server
24
- ui.msg "Aligning cookbooks"
25
- align_cookbooks()
26
- ui.msg "Aligning data bags"
27
- align_databags()
28
- ui.msg "Aligning roles"
29
- align_roles()
31
+ ui.msg(ui.color("On server #{@chef_server}", :bold)) if @chef_server
32
+ check_cookbooks if @do_cookbooks
33
+ check_databags if @do_databags
34
+ check_roles if @do_roles
35
+
36
+ # All questions asked, can we proceed ?
37
+ if @cookbooks.empty? and @databags.empty? and @roles.empty?
38
+ ui.msg "Nothing else to do"
39
+ exit 0
40
+ end
41
+
42
+ ui.confirm(ui.color("> Proceed ", :bold))
43
+ bump_cookbooks if @do_cookbooks
44
+ update_databags if @do_databags
45
+ update_roles if @do_roles
30
46
  end
31
47
 
32
48
  def setup
33
49
  # Checking args
34
50
  if name_args.count != 2
35
- ui.error "Usage : knife align BRANCH ENVIRONMENT [--debug]"
51
+ show_usage
36
52
  exit 1
37
53
  end
38
54
 
55
+ # check cli flags
56
+ if config[:cookbooks] or config[:databags] or config[:roles]
57
+ @do_cookbooks, @do_databags, @do_roles = config[:cookbooks], config[:databags], config[:roles]
58
+ else
59
+ @do_cookbooks, @do_databags, @do_roles = true, true, true
60
+ end
61
+
39
62
  # Sharp config
40
63
  cfg_files = [ "/etc/sharp-config.yml", "~/.chef/sharp-config.yml" ]
41
64
  loaded = false
@@ -44,7 +67,7 @@ module KnifeSharp
44
67
  @cfg = YAML::load_file(File.expand_path(cfg_file))
45
68
  loaded = true
46
69
  rescue Exception => e
47
- ui.error "Error on loading config : #{e.inspect}" if config[:debug]
70
+ ui.error "Error on loading config : #{e.inspect}" if config[:verbosity] > 0
48
71
  end
49
72
  end
50
73
  unless loaded == true
@@ -52,6 +75,17 @@ module KnifeSharp
52
75
  exit 1
53
76
  end
54
77
 
78
+ # Env setup
79
+ @branch, @environment = name_args
80
+ @chef_path = @cfg["global"]["git_cookbook_path"]
81
+
82
+ # Checking current branch
83
+ current_branch = Grit::Repo.new(@chef_path).head.name
84
+ if @branch != current_branch then
85
+ ui.error "Git repo is actually on branch #{current_branch} but you want to align using #{@branch}. Checkout to the desired one."
86
+ exit 1
87
+ end
88
+
55
89
  # Knife config
56
90
  if Chef::Knife.chef_config_dir && File.exists?(File.join(Chef::Knife.chef_config_dir, "knife.rb"))
57
91
  Chef::Config.from_file(File.join(Chef::Knife.chef_config_dir, "knife.rb"))
@@ -72,10 +106,6 @@ module KnifeSharp
72
106
  end
73
107
  end
74
108
 
75
- # Env setup
76
- @branch, @environment = name_args
77
- @chef_path = @cfg["global"]["git_cookbook_path"]
78
-
79
109
  chefcfg = Chef::Config
80
110
  @cb_path = chefcfg.cookbook_path.is_a?(Array) ? chefcfg.cookbook_path.first : chefcfg.cookbook_path
81
111
  @db_path = chefcfg.data_bag_path.is_a?(Array) ? chefcfg.data_bag_path.first : chefcfg.data_bag_path
@@ -84,167 +114,203 @@ module KnifeSharp
84
114
  @chef_server = SharpServer.new.current_server
85
115
  @loader = Chef::CookbookLoader.new(@cb_path)
86
116
 
87
- # Checking current branch
88
- current_branch = Grit::Repo.new(@chef_path).head.name
89
- if @branch != current_branch then
90
- ui.error "Git repo is actually on branch #{current_branch} but you want to align using #{@branch}. Checkout to the desired one."
91
- exit 1
92
- end
117
+ @cookbooks = Array.new
118
+ @databags = Hash.new
119
+ @roles = Hash.new
93
120
  end
94
121
 
95
122
  ### Cookbook methods ###
96
123
 
97
- def align_cookbooks
98
- updated_versions = Hash.new()
124
+ def check_cookbooks
125
+ unless File.exists?(@cb_path)
126
+ ui.warn "Bad cookbook path, skipping cookbook sync."
127
+ return
128
+ end
129
+
130
+ ui.msg(ui.color("== Cookbooks ==", :bold))
131
+
132
+ updated_versions = Hash.new
99
133
  local_versions = Hash[Dir.glob("#{@cb_path}/*").map {|cb| [File.basename(cb), @loader[File.basename(cb)].version] }]
100
134
  remote_versions = Chef::Environment.load(@environment).cookbook_versions.each_value {|v| v.gsub!("= ", "")}
101
135
 
136
+ if local_versions.empty?
137
+ ui.warn "No local cookbooks found, is the cookbook path correct ? (#{@cb_path})"
138
+ return
139
+ end
140
+
141
+ # get local-only cookbooks
102
142
  (local_versions.keys - remote_versions.keys).each do |cb|
103
143
  updated_versions[cb] = local_versions[cb]
104
144
  ui.msg "* #{cb} is local only (version #{local_versions[cb]})"
105
145
  end
106
146
 
147
+ # get cookbooks not up-to-date
107
148
  (remote_versions.keys & local_versions.keys).each do |cb|
108
- if remote_versions[cb] != local_versions[cb]
149
+ if Chef::VersionConstraint.new("> #{remote_versions[cb]}").include?(local_versions[cb])
109
150
  updated_versions[cb] = local_versions[cb]
110
151
  ui.msg "* #{cb} is not up-to-date (local: #{local_versions[cb]}/remote: #{remote_versions[cb]})"
111
152
  end
112
153
  end
113
154
 
155
+ if @cfg[@chef_server] and @cfg[@chef_server].has_key?("ignore_cookbooks")
156
+ (updated_versions.keys & @cfg[@chef_server]["ignore_cookbooks"]).each do |cb|
157
+ updated_versions.delete(cb)
158
+ ui.msg "* Skipping #{cb} cookbook (ignore list)"
159
+ end
160
+ end
161
+
114
162
  if !updated_versions.empty?
115
163
  all = false
116
- bumped = Array.new
117
- env = Chef::Environment.load(@environment)
118
164
  updated_versions.each_pair do |cb,version|
119
- answer = nil
120
-
121
- if @cfg[@chef_server]
122
- if @cfg[@chef_server]["ignore_cookbooks"]
123
- if @cfg[@chef_server]["ignore_cookbooks"].include?(cb)
124
- answer = "N"
125
- end
126
- end
127
- end
128
-
129
- if answer.nil?
130
- answer = ui.ask_question("Update #{cb} cookbook item on server ? Y/N/(A)ll/(Q)uit ", :default => "N").upcase unless all
131
- end
165
+ answer = ui.ask_question("> Update #{cb} cookbook to #{version} on server ? Y/N/(A)ll/(Q)uit ", :default => "N").upcase unless all
132
166
 
133
167
  if answer == "A"
134
168
  all = true
135
169
  elsif answer == "Q"
136
- ui.msg "> Skipping next cookbooks alignment."
170
+ ui.msg "* Skipping next cookbooks alignment."
137
171
  break
138
172
  end
139
173
 
140
174
  if all or answer == "Y"
141
- # Force "= a.b.c" in cookbook version, as chef11 will not accept "a.b.c"
142
- env.cookbook_versions[cb] = "= #{version}"
143
- bumped << @loader[cb]
175
+ @cookbooks << cb
144
176
  else
145
177
  ui.msg "* Skipping #{cb} cookbook"
146
178
  end
147
179
  end
180
+ else
181
+ ui.msg "* Environment #{@environment} is up-to-date."
182
+ end
183
+ end
184
+
185
+
186
+ def bump_cookbooks
187
+ unless @cookbooks.empty?
188
+ env = Chef::Environment.load(@environment)
189
+ cbs = Array.new
190
+ @cookbooks.each do |cb_name|
191
+ cb = @loader[cb_name]
192
+ # Force "= a.b.c" in cookbook version, as chef11 will not accept "a.b.c"
193
+ env.cookbook_versions[cb_name] = "= #{cb.version}"
194
+ cbs << cb
195
+ end
148
196
 
149
- unless bumped.empty?
150
- ui.msg "* Uploading cookbook(s) #{bumped.map{|cb| cb.name.to_s}.join(",")}"
151
- uploader = Chef::CookbookUploader.new(bumped, @cb_path)
152
- uploader.upload_cookbooks
153
- if env.save
154
- bumped.each do |cb|
155
- ui.msg "* Bumping #{cb.name.to_s} to #{cb.version} for environment #{@environment}"
156
- log_action("bumping #{cb.name.to_s} to #{cb.version} for environment #{@environment}")
157
- end
197
+ ui.msg "* Uploading cookbook(s) #{@cookbooks.join(", ")}"
198
+ uploader = Chef::CookbookUploader.new(cbs, @cb_path)
199
+ uploader.upload_cookbooks
200
+
201
+ if env.save
202
+ cbs.each do |cb|
203
+ ui.msg "* Bumping #{cb.name} to #{cb.version} for environment #{@environment}"
204
+ log_action("bumping #{cb.name} to #{cb.version} for environment #{@environment}")
158
205
  end
159
206
  end
160
- else
161
- ui.msg "> Environment #{@environment} is up-to-date."
162
207
  end
163
208
  end
164
209
 
165
210
  ### Databag methods ###
166
211
 
167
- def align_databags
212
+ def check_databags
168
213
  unless File.exists?(@db_path)
169
214
  ui.warn "Bad data bag path, skipping data bag sync."
170
215
  return
171
216
  end
172
217
 
218
+ ui.msg(ui.color("== Data bags ==", :bold))
219
+
173
220
  updated_dbs = Hash.new
174
221
  local_dbs = Dir.glob(File.join(@db_path, "**/*.json")).map {|f| [File.dirname(f).split("/").last, File.basename(f, ".json")]}
175
222
  remote_dbs = Chef::DataBag.list.keys.map {|db| Chef::DataBag.load(db).keys.map{|dbi| [db, dbi]}}.flatten(1)
176
223
 
177
- ui.warn "No local data bags found, is the role path correct ? (#{@role_path})" if local_dbs.empty?
224
+ if local_dbs.empty?
225
+ ui.warn "No local data bags found, is the data bag path correct ? (#{@db_path})"
226
+ return
227
+ end
228
+
229
+ # Dump missing data bags locally
230
+ (remote_dbs - local_dbs).each do |db|
231
+ ui.msg "* #{db.join("/")} data bag item is remote only"
232
+ if config[:dump_remote_only]
233
+ ui.msg "* Dumping to #{File.join(@db_path, "#{db.join("/")}.json")}"
234
+ begin
235
+ remote_db = Chef::DataBagItem.load(db.first, db.last).raw_data
236
+ Dir.mkdir(File.join(@db_path, db.first)) unless File.exists?(File.join(@db_path, db.first))
237
+ File.open(File.join(@db_path, "#{db.join("/")}.json"), "w") do |file|
238
+ file.puts JSON.pretty_generate(remote_db)
239
+ end
240
+ rescue Exception => e
241
+ ui.error "Unable to dump #{db.join("/")} data bag item (#{e.message})"
242
+ end
243
+ end
244
+ end
178
245
 
179
246
  # Create new data bags on server
180
247
  (local_dbs - remote_dbs).each do |db|
181
- ui.msg "+ #{db.join("/")} data bag item is local only. Creating"
182
248
  begin
183
- local_db = Chef::DataBagItem.new
184
- local_db.data_bag(db.first)
185
- local_db.raw_data = JSON::load(File.read(File.join(@db_path, "#{db.join("/")}.json")))
186
- local_db.save
249
+ local_db = JSON::load(File.read(File.join(@db_path, "#{db.join("/")}.json")))
250
+ updated_dbs[db] = local_db
251
+ ui.msg "* #{db.join("/")} data bag item is local only"
187
252
  rescue Exception => e
188
- ui.error "Unable to create #{db.join("/")} data bag item (#{e.message})"
253
+ ui.error "Unable to load #{db.join("/")} data bag item (#{e.message})"
189
254
  end
190
255
  end
191
256
 
192
- # Dump missing data bags locally
193
- (remote_dbs - local_dbs).each do |db|
194
- ui.msg "- #{db.join("/")} data bag item is remote only. Dumping to #{File.join(@db_path, "#{db.join("/")}.json")}"
257
+ # Compare roles common to local and remote
258
+ (remote_dbs & local_dbs).each do |db|
195
259
  begin
196
260
  remote_db = Chef::DataBagItem.load(db.first, db.last).raw_data
197
- Dir.mkdir(File.join(@db_path, db.first)) unless File.exists?(File.join(@db_path, db.first))
198
- File.open(File.join(@db_path, "#{db.join("/")}.json"), "w") do |file|
199
- file.puts JSON.pretty_generate(remote_db)
261
+ local_db = JSON::load(File.read(File.join(@db_path, "#{db.join("/")}.json")))
262
+ if remote_db != local_db
263
+ updated_dbs[db] = local_db
264
+ ui.msg("* #{db.join("/")} data bag item is not up-to-date")
200
265
  end
201
266
  rescue Exception => e
202
- ui.error "Unable to dump #{db.join("/")} data bag item (#{e.message})"
203
- end
204
- end
205
-
206
- # Compare roles common to local and remote
207
- (remote_dbs & local_dbs).each do |db|
208
- remote_db = Chef::DataBagItem.load(db.first, db.last).raw_data
209
- local_db = JSON::load(File.read(File.join(@db_path, "#{db.join("/")}.json")))
210
-
211
- if remote_db != local_db
212
- updated_dbs[db] = local_db
213
- ui.msg("* #{db.join("/")} data bag item is not up-to-date")
267
+ ui.error "Unable to load #{db.join("/")} data bag item (#{e.message})"
214
268
  end
215
269
  end
216
270
 
217
271
  if !updated_dbs.empty?
218
272
  all = false
219
273
  updated_dbs.each do |name, obj|
220
- answer = ui.ask_question("Update #{name.join("/")} data bag item on server ? Y/N/(A)ll/(Q)uit ", :default => "N").upcase unless all
274
+ answer = ui.ask_question("> Update #{name.join("/")} data bag item on server ? Y/N/(A)ll/(Q)uit ", :default => "N").upcase unless all
221
275
 
222
276
  if answer == "A"
223
277
  all = true
224
278
  elsif answer == "Q"
225
- ui.msg "> Aborting data bag alignment."
279
+ ui.msg "* Aborting data bag alignment."
226
280
  break
227
281
  end
228
282
 
229
283
  if all or answer == "Y"
230
- ui.msg "* Updating #{name.join("/")} data bag item"
231
- log_action("updating #{name.join("/")} data bag item")
284
+ @databags[name] = obj
285
+ else
286
+ ui.msg "* Skipping #{name.join("/")} data bag item"
287
+ end
288
+ end
289
+ else
290
+ ui.msg "* Data bags are up-to-date."
291
+ end
292
+ end
293
+
294
+ def update_databags
295
+ unless @databags.empty?
296
+ @databags.each do |name, obj|
297
+ begin
232
298
  db = Chef::DataBagItem.new
233
299
  db.data_bag(name.first)
234
300
  db.raw_data = obj
235
301
  db.save
236
- else
237
- ui.msg "* Skipping #{name.join("/")} data bag item"
302
+ ui.msg "* Updating #{name.join("/")} data bag item"
303
+ log_action("updating #{name.join("/")} data bag item")
304
+ rescue Exception => e
305
+ ui.error "Unable to update #{name.join("/")} data bag item"
238
306
  end
239
307
  end
240
- else
241
- ui.msg "> Data bags are up-to-date."
242
308
  end
243
309
  end
244
310
 
245
311
  ### Role methods ###
246
312
 
247
- def align_roles
313
+ def check_roles
248
314
  # role sections to compare (methods)
249
315
  to_check = {
250
316
  "env_run_lists" => "run list",
@@ -257,33 +323,41 @@ module KnifeSharp
257
323
  return
258
324
  end
259
325
 
326
+ ui.msg(ui.color("== Roles ==", :bold))
327
+
260
328
  updated_roles = Hash.new
261
329
  local_roles = Dir.glob(File.join(@role_path, "*.json")).map {|file| File.basename(file, ".json")}
262
330
  remote_roles = Chef::Role.list.keys
263
331
 
264
- ui.warn "No local roles found, is the role path correct ? (#{@role_path})" if local_roles.empty?
265
-
266
- # Create new roles on server
267
- (local_roles - remote_roles).each do |role|
268
- ui.msg "+ #{role} role is local only. Creating"
269
- begin
270
- local_role = Chef::Role.from_disk(role)
271
- local_role.save
272
- rescue Exception => e
273
- ui.error "Unable to create #{role} role (#{e.message})"
274
- end
332
+ if local_roles.empty?
333
+ ui.warn "No local roles found, is the role path correct ? (#{@role_path})"
334
+ return
275
335
  end
276
336
 
277
337
  # Dump missing roles locally
278
338
  (remote_roles - local_roles).each do |role|
279
- ui.msg "- #{role} role is remote only. Dumping to #{File.join(@role_path, "#{role}.json")}"
280
- begin
281
- remote_role = Chef::Role.load(role)
282
- File.open(File.join(@role_path, "#{role}.json"), "w") do |file|
283
- file.puts JSON.pretty_generate(remote_role)
339
+ ui.msg "* #{role} role is remote only"
340
+ if config[:dump_remote_only]
341
+ ui.msg "* Dumping to #{File.join(@role_path, "#{role}.json")}"
342
+ begin
343
+ remote_role = Chef::Role.load(role)
344
+ File.open(File.join(@role_path, "#{role}.json"), "w") do |file|
345
+ file.puts JSON.pretty_generate(remote_role)
346
+ end
347
+ rescue Exception => e
348
+ ui.error "Unable to dump #{role} role (#{e.message})"
284
349
  end
350
+ end
351
+ end
352
+
353
+ # Create new roles on server
354
+ (local_roles - remote_roles).each do |role|
355
+ begin
356
+ local_role = Chef::Role.from_disk(role)
357
+ updated_roles[role] = local_role
358
+ ui.msg "* #{role} role is local only"
285
359
  rescue Exception => e
286
- ui.error "Unable to dump #{role} role (#{e.message})"
360
+ ui.error "Unable to load #{role} role (#{e.message})"
287
361
  end
288
362
  end
289
363
 
@@ -305,25 +379,37 @@ module KnifeSharp
305
379
  if !updated_roles.empty?
306
380
  all = false
307
381
  updated_roles.each do |name, obj|
308
- answer = ui.ask_question("Update #{name} role on server ? Y/N/(A)ll/(Q)uit ", :default => "N").upcase unless all
382
+ answer = ui.ask_question("> Update #{name} role on server ? Y/N/(A)ll/(Q)uit ", :default => "N").upcase unless all
309
383
 
310
384
  if answer == "A"
311
385
  all = true
312
386
  elsif answer == "Q"
313
- ui.msg "> Aborting role alignment."
387
+ ui.msg "* Aborting role alignment."
314
388
  break
315
389
  end
316
390
 
317
391
  if all or answer == "Y"
318
- ui.msg "* Updating #{name} role"
319
- log_action("updating #{name} role")
320
- obj.save
392
+ @roles[name] = obj
321
393
  else
322
394
  ui.msg "* Skipping #{name} role"
323
395
  end
324
396
  end
325
397
  else
326
- ui.msg "> Roles are up-to-date."
398
+ ui.msg "* Roles are up-to-date."
399
+ end
400
+ end
401
+
402
+ def update_roles
403
+ unless @roles.empty?
404
+ @roles.each do |name, obj|
405
+ begin
406
+ obj.save
407
+ ui.msg "* Updating #{name} role"
408
+ log_action("updating #{name} role")
409
+ rescue Exception => e
410
+ ui.error "Unable to update #{name} role"
411
+ end
412
+ end
327
413
  end
328
414
  end
329
415
 
@@ -359,6 +445,5 @@ module KnifeSharp
359
445
  ui.error "Unable to notify via hubot."
360
446
  end
361
447
  end
362
-
363
448
  end
364
449
  end
data/lib/knife-sharp.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module KnifeSharp
2
- VERSION = "0.1.8"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knife-sharp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-03-20 00:00:00.000000000 Z
13
+ date: 2013-04-23 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: chef