lib/between_meals/knife.rb (239 lines of code) (raw):
# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
# Copyright 2013-present Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'json'
require 'fileutils'
require 'digest/md5'
require 'between_meals/util'
module BetweenMeals
# Knife does not have a usable API for using it as a lib
# This could be possibly refactored to touch its internals
# instead of shelling out
class Knife
include BetweenMeals::Util
attr_accessor :cookbook_dirs
def initialize(opts = {})
@logger = opts[:logger] || nil
@user = opts[:user] || ENV['USER']
@home = opts[:home] || ENV['HOME']
# make sure people can pass in false :)
@ssl = opts[:ssl].nil? ? true : opts[:ssl]
@host = opts[:host] || 'localhost'
@port = opts[:port] || 4000
@config = opts[:config] ||
"#{@home}/.chef/knife-#{@user}-taste-tester.rb"
@knife = opts[:bin] || 'knife'
@knife_verb_option = ''
unless @logger.nil?
if @logger.level == Logger::DEBUG
@knife_verb_option = '-VV'
elsif @logger.level == Logger::INFO
@knife_verb_option = '-V'
end
end
@berks = opts[:berks_bin] || 'berks'
@berks_config = opts[:berks_config]
@pem = opts[:pem] ||
"#{@home}/.chef/#{@user}-taste-tester.pem"
@role_dir = opts[:role_dir]
@role_type = opts[:role_type] || 'rb'
@cookbook_dirs = opts[:cookbook_dirs]
@databag_dir = opts[:databag_dir]
@checksum_dir = opts[:checksum_dir]
@client_key =
File.expand_path("#{@home}/.chef/#{@user}-taste-tester.pem")
end
def role_upload_all
if File.exist?(@role_dir)
roles = File.join(@role_dir, "*.#{@role_type}")
exec!("#{@knife} role from file #{roles} #{@knife_verb_option} " +
"-c #{@config}", @logger)
end
end
def role_upload(roles)
if roles.any?
roles = roles.map do |x|
File.join(@role_dir, "#{x.name}.#{@role_type}")
end.join(' ')
exec!("#{@knife} role from file #{roles} #{@knife_verb_option} " +
"-c #{@config}", @logger)
end
end
def role_delete(roles)
if roles.any?
roles.each do |role|
exec!("#{@knife} role delete #{role.name} #{@knife_verb_option} " +
"--yes -c #{@config}", @logger)
end
end
end
def cookbook_upload_all
exec!("#{@knife} cookbook upload -a #{@knife_verb_option} " +
"-c #{@config}", @logger)
end
def berks_cookbook_upload_all
if @berks_config
berks_config = '--config=' + @berks_config
end
@cookbook_dirs.each do |path|
cookbooks = Dir["#{path}/*"].select { |o| File.directory?(o) }
cookbooks.each do |cb|
@logger.warn("Running berkshelf on cookbook: #{cb}")
exec!("cd #{cb} && #{@berks} install #{berks_config} && " +
"#{@berks} upload #{berks_config}", @logger)
end
end
end
def cookbook_upload(cookbooks)
if cookbooks.any?
cookbooks = cookbooks.map(&:name).join(' ')
exec!("#{@knife} cookbook upload #{cookbooks} #{@knife_verb_option} " +
"-c #{@config}", @logger)
end
end
def berks_cookbook_upload(cookbooks)
# cookbooks: array
# cookbook_paths: array
if @berks_config
berks_config = '--config=' + @berks_config
end
if cookbooks.any?
@cookbook_dirs.each do |path|
cookbooks.each do |cb|
next unless File.exist?("#{path}/#{cb}")
@logger.warn("Running berkshelf on cookbook: #{cb}")
exec!("cd #{path}/#{cb} && #{@berks} install #{berks_config} && " +
"#{@berks} upload #{berks_config}", @logger)
end
end
end
end
def cookbook_delete(cookbooks)
if cookbooks.any?
cookbooks.each do |cookbook|
exec!("#{@knife} cookbook delete #{cookbook.name}" +
" --purge -a --yes #{@knife_verb_option} -c #{@config}",
@logger)
end
end
end
def databag_upload_all
glob = File.join(@databag_dir, '*', '*.json')
items = Dir.glob(glob).map do |file|
BetweenMeals::Changes::Databag.new(
{ :status => :modified, :path => file }, @databag_dir
)
end
databag_upload(items)
end
def databag_upload(databags)
if databags.any?
databags.group_by(&:name).each do |dbname, dbs|
create_databag_if_missing(dbname)
dbitems = dbs.map do |x|
File.join(@databag_dir, dbname, "#{x.item}.json")
end.join(' ')
exec!(
"#{@knife} data bag from file #{dbname} #{dbitems} " +
"#{@knife_verb_option} -c #{@config}",
@logger,
)
end
end
end
def databag_delete(databags)
if databags.any?
databags.group_by(&:name).each do |dbname, dbs|
dbs.each do |db|
exec!("#{@knife} data bag delete #{dbname} #{db.item}" +
" --yes #{@knife_verb_option} -c #{@config}", @logger)
end
delete_databag_if_empty(dbname)
end
end
end
def write_user_config
scheme = @ssl ? 'https' : 'http'
cfg = <<-BLOCK
user = ENV['USER']
log_level :info
log_location STDOUT
node_name user
chef_server_url "#{scheme}://#{@host}:#{@port}"
ssl_verify_mode :verify_none
cache_type 'BasicFile'
client_key '#{@client_key}'
cache_options(:path => File.expand_path("#{@checksum_dir}"))
cookbook_path [
BLOCK
@cookbook_dirs.each do |dir|
cfg << " \"#{dir}\",\n"
end
cfg << "]\n"
begin
Dir.mkdir(File.dirname(@config), 0o755)
rescue Errno::EEXIST
# not an error if it's already there.
nil
end
if !File.exist?(@config) ||
::Digest::MD5.hexdigest(cfg) !=
::Digest::MD5.hexdigest(File.read(@config))
@logger.info("Generating #{@config}")
File.write(@config, cfg)
end
# Won't work with shorter keys
pem = <<-BLOCK
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCs4Ih8+R/2hcYS
tccwJHd0cXHcUibC2wGYmRwf1fKxXLADvfuLRVBHOI5Hgd/ZXF70dowC5mDQ03gr
ouk8e7RL72MCKzPuG2V92sh/FnyKhkNsHCOEKaRRiP9lHVbZkS9LEotCKF7eOkL0
SVkGWx8pVZzrOFmhZgHaOFJ2/2t1irUTRFqTikrRsP2KvnhHdDlnnbUumZWxSuEN
oN6aSAQEOkKbEOLSn/EIMzEb2jtks7L7wkRErajH094jGoZbQvLiwRHDeM0C9uG7
2sdQ45BG9EQOCdBzy1We5keqtJbXBcpwuBa0d1nQZIsGxnDb88+Kmh9h6k9/WmYN
zEQEeSSdAgMBAAECggEATFWQru4p6ObEwTo2y9EuVeJJzmkP6HZfzAu/WWdVFG/C
4MQgsCxY+DnGyVhViVq6KuO1iwpCsbLOmyYCKszMncMESs7czUSXmezjHwrEzz3d
w3zhSdhBUCdX7kP4N3VeFp4Hk5zT1viO2+MPRjkyF0RQV6S4HwY1xy+baiP6RRnS
dGhUYsdz6fjxSkYEQy3/xHm9VLT6ZDV4pN2aA+LOFeveHKcnOjKFCBy4WzkO6fvj
6H3jghxsHXoL7loCHfi9WX3xKjeXG/NjGbUfTH8P7IldUPha+ru/e8W/P+jjE1os
VkScWt08Vu6iTl1EkYeFxOMtSDZxeXNnDkPI0iDQgQKBgQDUMFYncQcZ4uLIfoZq
B+Lx7WJGlIwulOdobnraTd5lsrBHj1sC1+/f4cBSpJwUij2l3GdmmnOpuFAof5eu
mrBGu++5jy+0eIeT5O2d30O8GOBryJ+oAKI2/BPVCmM8d986wl5Esauycb++O7UO
RhpZFOCKbFvlNjhg+CdlvHSl7QKBgQDQkkvpnE//yWmhCPg27n7w3bTg5QPNrzTO
pF2iwvLK4XjRceTeW3P4f42HONzJNnmt5TexM9NbdE9g/exA5uNt59ZB5FeFiKAu
NmVXbmswPX6R/dlyidqzz1guGrL04e0dZehHZBNDr5Sio8IBjMWrpDIxjDJqEwUa
4qCu4e6jcQKBgQDN0FTAzRFmOnxenNsj3aJzpx27+DpAtI4A7aicNwuQ+VGjF5nf
mDRDpGU3xBLgmXZSewaQrx+hb/XQUnJ+Ge0BrylHg2tyUbav7U3N49F/kWGdKmwy
OOsfCkLyUbEP5fXQuNdXKj6wR0UE8EUeI0FLRsTFf3VjTsRAynLsa295wQKBgAo3
QDSfDWQP73aNw+qc3+bYVSW20erfLAz7DAMO3WmGha5sj7M8c3+2b64x4M6SNn+H
/KRXT4DpP4IWrd238WfOtTXhA1BtErtwuqH/rIxeVra74kyz59xqyXzond9UuZJ5
DVmB01e7X+Jfdv8wb/YqQrMelNGRQOzCMPCf7FphAoGAbUh5HzNF2aciQJGA6Qk8
zvgEHqbS0/QkJGOZ+UifPRanTDuGYQkPdHHOER4UghbM+Kz5rZbBicJ3bCyNOsah
IAMAEpsWX2s2A6phgMCx7kH6wMmoZn3hb7Thh9+PfR8Jtp2/7k+ibCeF4gEWUCs5
6wX4GR84dwyhG80yd4TP8Qo=
-----END PRIVATE KEY-----
BLOCK
begin
Dir.mkdir(File.dirname(@pem), 0o755)
rescue Errno::EEXIST
# not an error if it's already there.
nil
end
unless File.exist?(@pem)
@logger.info("Generating #{@pem}")
File.write(@pem, pem)
end
end
private
def create_databag_if_missing(databag)
s = Mixlib::ShellOut.new("#{@knife} data bag list" +
" --format json #{@knife_verb_option} " +
"-c #{@config}").run_command
s.error!
db = JSON.parse(s.stdout)
unless db.include?(databag)
exec!("#{@knife} data bag create #{databag} #{@knife_verb_option} " +
"-c #{@config}", @logger)
end
end
def delete_databag_if_empty(databag)
s = Mixlib::ShellOut.new("#{@knife} data bag show #{databag}" +
" --format json #{@knife_verb_option} " +
"-c #{@config}").run_command
s.error!
db = JSON.parse(s.stdout)
if db.empty?
exec!("#{@knife} data bag delete #{databag} --yes " +
"#{@knife_verb_option} -c #{@config}", @logger)
end
end
end
end