Class: KnifeSharp::SharpAlign

Inherits:
Chef::Knife
  • Object
show all
Defined in:
lib/chef/knife/sharp-align.rb

Instance Method Summary collapse

Instance Method Details

#bump_cookbooksObject



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/chef/knife/sharp-align.rb', line 186

def bump_cookbooks
  unless @cookbooks.empty?
    env = Chef::Environment.load(@environment)
    cbs = Array.new
    backup_data = Hash.new
    backup_data["environment"] = @environment
    backup_data["cookbook_versions"] = Hash.new
    @cookbooks.each do |cb_name|
      cb = @loader[cb_name]
      if @cfg["rollback"] && @cfg["rollback"]["enabled"] == true
        backup_data["cookbook_versions"][cb_name] = env.cookbook_versions[cb_name]
      end
      # Force "= a.b.c" in cookbook version, as chef11 will not accept "a.b.c"
      env.cookbook_versions[cb_name] = "= #{cb.version}"
      cbs << cb
    end

    ui.msg "* Uploading cookbook(s) #{@cookbooks.join(", ")}"
    uploader = Chef::CookbookUploader.new(cbs, @cb_path)
    uploader.upload_cookbooks

    if env.save
      cbs.each do |cb|
        ui.msg "* Bumping #{cb.name} to #{cb.version} for environment #{@environment}"
        log_action("bumping #{cb.name} to #{cb.version} for environment #{@environment}")
      end
    end

    if @cfg["rollback"] && @cfg["rollback"]["enabled"] == true
      identifier = Time.now.to_i
      Dir.mkdir(@cfg["rollback"]["destination"]) unless File.exists?(@cfg["rollback"]["destination"])
      fp = open(File.join(@cfg["rollback"]["destination"], "#{identifier}.json"), "w")
      fp.write(JSON.pretty_generate(backup_data))
      fp.close()
    end
  end
end

#check_cookbooksObject

Cookbook methods ###



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/chef/knife/sharp-align.rb', line 124

def check_cookbooks
  unless File.exists?(@cb_path)
    ui.warn "Bad cookbook path, skipping cookbook sync."
    return
  end

  ui.msg(ui.color("== Cookbooks ==", :bold))

  updated_versions = Hash.new
  local_versions = Hash[Dir.glob("#{@cb_path}/*").map {|cb| [File.basename(cb), @loader[File.basename(cb)].version] }]
  remote_versions = Chef::Environment.load(@environment).cookbook_versions.each_value {|v| v.gsub!("= ", "")}

  if local_versions.empty?
    ui.warn "No local cookbooks found, is the cookbook path correct ? (#{@cb_path})"
    return
  end

  # get local-only cookbooks
  (local_versions.keys - remote_versions.keys).each do |cb|
    updated_versions[cb] = local_versions[cb]
    ui.msg "* #{cb} is local only (version #{local_versions[cb]})"
  end

  # get cookbooks not up-to-date
  (remote_versions.keys & local_versions.keys).each do |cb|
    if Chef::VersionConstraint.new("> #{remote_versions[cb]}").include?(local_versions[cb])
      updated_versions[cb] = local_versions[cb]
      ui.msg "* #{cb} is not up-to-date (local: #{local_versions[cb]}/remote: #{remote_versions[cb]})"
    end
  end

  if @cfg[@chef_server] and @cfg[@chef_server].has_key?("ignore_cookbooks")
    (updated_versions.keys & @cfg[@chef_server]["ignore_cookbooks"]).each do |cb|
      updated_versions.delete(cb)
      ui.msg "* Skipping #{cb} cookbook (ignore list)"
    end
  end

  if !updated_versions.empty?
    all = false
    updated_versions.each_pair do |cb,version|
      answer = ui.ask_question("> Update #{cb} cookbook to #{version} on server ? Y/N/(A)ll/(Q)uit ", :default => "N").upcase unless all

      if answer == "A"
        all = true
      elsif answer == "Q"
        ui.msg "* Skipping next cookbooks alignment."
        break
      end

      if all or answer == "Y"
        @cookbooks << cb
      else
        ui.msg "* Skipping #{cb} cookbook"
      end
    end
  else
    ui.msg "* Environment #{@environment} is up-to-date."
  end
end

#check_databagsObject

Databag methods ###



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/chef/knife/sharp-align.rb', line 226

def check_databags
  unless File.exists?(@db_path)
    ui.warn "Bad data bag path, skipping data bag sync."
    return
  end

  ui.msg(ui.color("== Data bags ==", :bold))

  updated_dbs = Hash.new
  local_dbs = Dir.glob(File.join(@db_path, "**/*.json")).map {|f| [File.dirname(f).split("/").last, File.basename(f, ".json")]}
  remote_dbs = Chef::DataBag.list.keys.map {|db| Chef::DataBag.load(db).keys.map{|dbi| [db, dbi]}}.flatten(1)

  if local_dbs.empty?
    ui.warn "No local data bags found, is the data bag path correct ? (#{@db_path})"
    return
  end

  # Dump missing data bags locally
  (remote_dbs - local_dbs).each do |db|
    ui.msg "* #{db.join("/")} data bag item is remote only"
    if config[:dump_remote_only]
      ui.msg "* Dumping to #{File.join(@db_path, "#{db.join("/")}.json")}"
      begin
        remote_db = Chef::DataBagItem.load(db.first, db.last).raw_data
        Dir.mkdir(File.join(@db_path, db.first)) unless File.exists?(File.join(@db_path, db.first))
        File.open(File.join(@db_path, "#{db.join("/")}.json"), "w") do |file|
          file.puts JSON.pretty_generate(remote_db)
        end
      rescue Exception => e
        ui.error "Unable to dump #{db.join("/")} data bag item (#{e.message})"
      end
    end
  end

  # Create new data bags on server
  (local_dbs - remote_dbs).each do |db|
    begin
      local_db = JSON::load(File.read(File.join(@db_path, "#{db.join("/")}.json")))
      updated_dbs[db] = local_db
      ui.msg "* #{db.join("/")} data bag item is local only"
    rescue Exception => e
      ui.error "Unable to load #{db.join("/")} data bag item (#{e.message})"
    end
  end

  # Compare roles common to local and remote
  (remote_dbs & local_dbs).each do |db|
    begin
      remote_db = Chef::DataBagItem.load(db.first, db.last).raw_data
      local_db = JSON::load(File.read(File.join(@db_path, "#{db.join("/")}.json")))
      if remote_db != local_db
        updated_dbs[db] = local_db
        ui.msg("* #{db.join("/")} data bag item is not up-to-date")
      end
    rescue Exception => e
      ui.error "Unable to load #{db.join("/")} data bag item (#{e.message})"
    end
  end

  if @cfg[@chef_server] and @cfg[@chef_server].has_key?("ignore_databags")
    (updated_dbs.keys.map{|k| k.join("/")} & @cfg[@chef_server]["ignore_databags"]).each do |db|
      updated_dbs.delete(db.split("/"))
      ui.msg "* Skipping #{db} data bag (ignore list)"
    end
  end

  if !updated_dbs.empty?
    all = false
    updated_dbs.each do |name, obj|
      answer = ui.ask_question("> Update #{name.join("/")} data bag item on server ? Y/N/(A)ll/(Q)uit ", :default => "N").upcase unless all

      if answer == "A"
        all = true
      elsif answer == "Q"
        ui.msg "* Aborting data bag alignment."
        break
      end

      if all or answer == "Y"
        @databags[name] = obj
      else
        ui.msg "* Skipping #{name.join("/")} data bag item"
      end
    end
  else
    ui.msg "* Data bags are up-to-date."
  end
end

#check_rolesObject

Role methods ###



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/chef/knife/sharp-align.rb', line 334

def check_roles
  # role sections to compare (methods)
  to_check = {
    "env_run_lists" => "run list",
    "default_attributes" => "default attributes",
    "override_attributes" => "override attributes"
  }

  unless File.exists?(@role_path)
    ui.warn "Bad role path, skipping role sync."
    return
  end

  ui.msg(ui.color("== Roles ==", :bold))

  updated_roles = Hash.new
  local_roles = Dir.glob(File.join(@role_path, "*.json")).map {|file| File.basename(file, ".json")}
  remote_roles = Chef::Role.list.keys

  if local_roles.empty?
    ui.warn "No local roles found, is the role path correct ? (#{@role_path})"
    return
  end

  # Dump missing roles locally
  (remote_roles - local_roles).each do |role|
    ui.msg "* #{role} role is remote only"
    if config[:dump_remote_only]
      ui.msg "* Dumping to #{File.join(@role_path, "#{role}.json")}"
      begin
        remote_role = Chef::Role.load(role)
        File.open(File.join(@role_path, "#{role}.json"), "w") do |file|
          file.puts JSON.pretty_generate(remote_role)
        end
      rescue Exception => e
        ui.error "Unable to dump #{role} role (#{e.message})"
      end
    end
  end

  # Create new roles on server
  (local_roles - remote_roles).each do |role|
    begin
      local_role = Chef::Role.from_disk(role)
      updated_roles[role] = local_role
      ui.msg "* #{role} role is local only"
    rescue Exception => e
      ui.error "Unable to load #{role} role (#{e.message})"
    end
  end

  # Compare roles common to local and remote
  (remote_roles & local_roles).each do |role|
    remote_role = Chef::Role.load(role)
    local_role = Chef::Role.from_disk(role)

    diffs = Array.new
    to_check.each do |method, display|
      if remote_role.send(method) != local_role.send(method)
        updated_roles[role] = local_role
        diffs << display
      end
    end
    ui.msg("* #{role} role is not up-to-date (#{diffs.join(",")})") unless diffs.empty?
  end

  if @cfg[@chef_server] and @cfg[@chef_server].has_key?("ignore_roles")
    (updated_roles.keys & @cfg[@chef_server]["ignore_roles"]).each do |r|
      updated_roles.delete(r)
      ui.msg "* Skipping #{r} role (ignore list)"
    end
  end

  if !updated_roles.empty?
    all = false
    updated_roles.each do |name, obj|
      answer = ui.ask_question("> Update #{name} role on server ? Y/N/(A)ll/(Q)uit ", :default => "N").upcase unless all

      if answer == "A"
        all = true
      elsif answer == "Q"
        ui.msg "* Aborting role alignment."
        break
      end

      if all or answer == "Y"
        @roles[name] = obj
      else
        ui.msg "* Skipping #{name} role"
      end
    end
  else
    ui.msg "* Roles are up-to-date."
  end
end

#hubot(message, config = {}) ⇒ Object



465
466
467
468
469
470
471
472
473
474
475
# File 'lib/chef/knife/sharp-align.rb', line 465

def hubot(message, config={})
  begin
    require "net/http"
    require "uri"
    uri = URI.parse("#{config["url"]}/#{config["channel"]}")
    notif = "chef: #{message} by #{config["username"]}"
    Net::HTTP.post_form(uri, { "message" => notif })
  rescue
    ui.error "Unable to notify via hubot."
  end
end

#log_action(message) ⇒ Object

Utility methods ###



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# File 'lib/chef/knife/sharp-align.rb', line 446

def log_action(message)
  # log file if enabled
  log_message = message
  log_message += " on server #{@chef_server}" if @chef_server
  @log.info(log_message) if @cfg["logging"]["enabled"]

  # any defined notification method (currently, only hubot, defined below)
  if @cfg["notification"]
    @cfg["notification"].each do |carrier, data|
      skipped = Array.new
      skipped = data["skip"] if data["skip"]

      if data["enabled"] and !skipped.include?(@chef_server)
        send(carrier, message, data)
      end
    end
  end
end

#runObject



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/chef/knife/sharp-align.rb', line 29

def run
  setup()
  ui.msg(ui.color("On server #{@chef_server}", :bold)) if @chef_server
  check_cookbooks if @do_cookbooks
  check_databags if @do_databags
  check_roles if @do_roles

  # All questions asked, can we proceed ?
  if @cookbooks.empty? and @databags.empty? and @roles.empty?
    ui.msg "Nothing else to do"
    exit 0
  end

  ui.confirm(ui.color("> Proceed ", :bold))
  bump_cookbooks if @do_cookbooks
  update_databags if @do_databags
  update_roles if @do_roles
end

#setupObject



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/chef/knife/sharp-align.rb', line 48

def setup
  # Checking args
  if name_args.count != 2
    show_usage
    exit 1
  end

  # check cli flags
  if config[:cookbooks] or config[:databags] or config[:roles]
    @do_cookbooks, @do_databags, @do_roles = config[:cookbooks], config[:databags], config[:roles]
  else
    @do_cookbooks, @do_databags, @do_roles = true, true, true
  end

  # Sharp config
  cfg_files = [ "/etc/sharp-config.yml", "~/.chef/sharp-config.yml" ]
  loaded = false
  cfg_files.each do |cfg_file|
    begin
      @cfg = YAML::load_file(File.expand_path(cfg_file))
      loaded = true
    rescue Exception => e
      ui.error "Error on loading config : #{e.inspect}" if config[:verbosity] > 0
    end
  end
  unless loaded == true
    ui.error "config could not be loaded ! Tried the following files : #{cfg_files.join(", ")}"
    exit 1
  end

  # Env setup
  @branch, @environment = name_args
  @chef_path = @cfg["global"]["git_cookbook_path"]

  # Checking current branch
  current_branch = Grit::Repo.new(@chef_path).head.name
  if @branch != current_branch then
    ui.error "Git repo is actually on branch #{current_branch} but you want to align using #{@branch}. Checkout to the desired one."
    exit 1
  end

  # Knife config
  if Chef::Knife.chef_config_dir && File.exists?(File.join(Chef::Knife.chef_config_dir, "knife.rb"))
    Chef::Config.from_file(File.join(Chef::Knife.chef_config_dir, "knife.rb"))
  else
    ui.error "Cannot find knife.rb config file"
    exit 1
  end

  # Logger
  if @cfg["logging"]["enabled"]
    begin
      require "logger"
      log_file = File.expand_path(@cfg["logging"]["destination"])
      @log = Logger.new(log_file)
    rescue Exception => e
      ui.error "Unable to set up logger (#{e.inspect})."
      exit 1
    end
  end

  chefcfg = Chef::Config
  @cb_path = chefcfg.cookbook_path.is_a?(Array) ? chefcfg.cookbook_path.first : chefcfg.cookbook_path
  @db_path = chefcfg.data_bag_path.is_a?(Array) ? chefcfg.data_bag_path.first : chefcfg.data_bag_path
  @role_path = chefcfg.role_path.is_a?(Array) ? chefcfg.role_path.first : chefcfg.role_path

  @chef_server = SharpServer.new.current_server
  @loader = Chef::CookbookLoader.new(@cb_path)

  @cookbooks = Array.new
  @databags = Hash.new
  @roles = Hash.new
end

#update_databagsObject



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/chef/knife/sharp-align.rb', line 315

def update_databags
  unless @databags.empty?
    @databags.each do |name, obj|
      begin
        db = Chef::DataBagItem.new
        db.data_bag(name.first)
        db.raw_data = obj
        db.save
        ui.msg "* Updating #{name.join("/")} data bag item"
        log_action("updating #{name.join("/")} data bag item")
      rescue Exception => e
        ui.error "Unable to update #{name.join("/")} data bag item"
      end
    end
  end
end

#update_rolesObject



430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/chef/knife/sharp-align.rb', line 430

def update_roles
  unless @roles.empty?
    @roles.each do |name, obj|
      begin
        obj.save
        ui.msg "* Updating #{name} role"
        log_action("updating #{name} role")
      rescue Exception => e
        ui.error "Unable to update #{name} role"
      end
    end
  end
end