├── .ruby-version ├── spec ├── rcov.opts ├── fixtures │ ├── high_priority_template.rb │ └── low_priority_template.rb ├── spec_helper.rb ├── integration │ ├── output_to_stdout_spec.rb │ └── output_to_file_spec.rb └── unit │ ├── log_support_spec.rb │ ├── processor_spec.rb │ └── cli_spec.rb ├── Gemfile ├── .gitignore ├── lib ├── racker.rb └── racker │ ├── version.rb │ ├── builders │ ├── builder.rb │ ├── docker.rb │ ├── openstack.rb │ ├── vmware.rb │ ├── qemu.rb │ ├── null.rb │ ├── parallels.rb │ ├── google.rb │ ├── digitalocean.rb │ ├── virtualbox.rb │ └── amazon.rb │ ├── log_support.rb │ ├── smash │ ├── smash.rb │ ├── mash.rb │ └── deep_merge_modified.rb │ ├── processor.rb │ ├── template.rb │ └── cli.rb ├── .travis.yml ├── NOTICE ├── Guardfile ├── bin └── racker ├── Rakefile ├── LICENSE ├── example ├── template2.rb ├── packer.json └── template1.rb ├── CHANGELOG.md ├── racker.gemspec └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-1.9.3 -------------------------------------------------------------------------------- /spec/rcov.opts: -------------------------------------------------------------------------------- 1 | --exclude 2 | spec,bin -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | Gemfile.lock 3 | .bundle 4 | pkg/* 5 | coverage/* 6 | output/* -------------------------------------------------------------------------------- /lib/racker.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # Internal 4 | require 'racker/log_support' 5 | require 'racker/cli' 6 | require 'racker/processor' 7 | require 'racker/version' 8 | -------------------------------------------------------------------------------- /spec/fixtures/high_priority_template.rb: -------------------------------------------------------------------------------- 1 | Racker::Processor.register_template do |t| 2 | t.variables = { 3 | :iso_url => 'priority.img', 4 | :password => '~~', 5 | } 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/low_priority_template.rb: -------------------------------------------------------------------------------- 1 | Racker::Processor.register_template do |t| 2 | t.variables = { 3 | :iso_url => 'os.img', 4 | :password => 'password', 5 | :nil => nil, 6 | } 7 | end 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - env 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | language: ruby 9 | 10 | matrix: 11 | allow_failures: 12 | - rvm: jruby 13 | - rvm: rbx 14 | 15 | rvm: 16 | - 1.9.3 17 | - 2.0.0 18 | - 2.1.3 19 | - jruby 20 | - rbx 21 | 22 | script: 23 | - rake spec 24 | -------------------------------------------------------------------------------- /lib/racker/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Racker 4 | # This defines the version of the gem 5 | module Version 6 | MAJOR = 0 7 | MINOR = 2 8 | PATCH = 1 9 | BUILD = 'dev' 10 | 11 | STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.') 12 | 13 | BANNER = 'Racker v%s' 14 | 15 | module_function 16 | 17 | def version 18 | sprintf(BANNER, STRING) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Racker NOTICE 2 | =========== 3 | 4 | Racker incorporates code modified from Chef (https://github.com/opscode/chef), which is Copyright (c) 2008-2014 Chef Software, Inc. 5 | 6 | Racker incorporates code modified from deep_merge (http://trac.misuse.org/science/wiki/DeepMerge), which is Copyright (c) 2008 Steve Midgley 7 | 8 | Racker incorporates code modified from Vagrant (https://github.com/mitchellh/vagrant) which is Copyright (c) 2010-2014 Mitchell Hashimoto 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'racker' 2 | 3 | class RSpec::Core::ExampleGroup 4 | FIXTURE_DIR = File.expand_path('../fixtures', __FILE__) 5 | PARSED_LOW_PRIORITY_TEMPLATE = { 6 | 'variables' => { 7 | 'iso_url' => 'os.img', 8 | 'password' => 'password', 9 | }, 10 | }.freeze 11 | 12 | def fixture_path(filename) 13 | File.join(FIXTURE_DIR, filename.to_s) 14 | end 15 | 16 | def parsed_low_priority_template 17 | PARSED_LOW_PRIORITY_TEMPLATE 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/integration/output_to_stdout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe :output_to_stdout do 4 | before(:all) do 5 | @instance = Racker::CLI.new([fixture_path('low_priority_template.rb'), '-']) 6 | end 7 | 8 | context 'when successful' do 9 | it 'writes the computed template to $stdout' do 10 | pretty_output = JSON.pretty_generate(parsed_low_priority_template) 11 | expect(@instance).to receive(:puts).never 12 | expect($stdout).to receive(:write).with(pretty_output) 13 | @instance.execute! 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rspec, all_after_pass: true, all_on_start: true, keep_failed: true do 2 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 3 | watch('spec/spec_helper.rb') { "spec" } 4 | watch(%r{^spec/.+_spec\.rb$}) 5 | watch(%r{^spec/support/.+\.rb$}) { "spec" } 6 | end 7 | 8 | guard :rubocop do 9 | watch(%r{.+\.rb$}) 10 | watch(%r{bin/.*$}) 11 | watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) } 12 | end 13 | 14 | guard :bundler do 15 | watch('Gemfile') 16 | watch('racker.gemspec') 17 | end 18 | 19 | notification :gntp, :sticky => false, :host => '127.0.0.1' 20 | -------------------------------------------------------------------------------- /lib/racker/builders/builder.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Racker 4 | 5 | module Builders 6 | # This is the Builder base class 7 | class Builder 8 | include Racker::LogSupport 9 | 10 | def to_packer(name, config) 11 | logger.debug("Entering #{self.class}.#{__method__}") 12 | 13 | # Set the name of the builder 14 | logger.info("Setting config name to #{name}") 15 | config['name'] = name 16 | 17 | logger.debug("Leaving #{self.class}.#{__method__}") 18 | config 19 | end 20 | 21 | def convert_hash_to_packer_value(config) 22 | config.kind_of?(Hash) ? config.values : config 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/racker/builders/docker.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the Docker builder 7 | class Docker < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | %w(run_command).each do |key| 13 | if config.key? key 14 | logger.info("Converting #{key} to packer value...") 15 | config[key] = convert_hash_to_packer_value(config[key]) 16 | end 17 | end 18 | 19 | logger.debug("Leaving #{self.class}.#{__method__}") 20 | config 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/racker/builders/openstack.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the OpenStack builder 7 | class OpenStack < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | %w(security_groups).each do |key| 13 | if config.key? key 14 | logger.info("Converting #{key} to packer value...") 15 | config[key] = convert_hash_to_packer_value(config[key]) 16 | end 17 | end 18 | 19 | logger.debug("Leaving #{self.class}.#{__method__}") 20 | config 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/racker/builders/vmware.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the VMware builder 7 | class VMware < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | %w(boot_command floppy_files iso_urls).each do |key| 13 | if config.key? key 14 | logger.info("Converting #{key} to packer value...") 15 | config[key] = convert_hash_to_packer_value(config[key]) 16 | end 17 | end 18 | 19 | logger.debug("Leaving #{self.class}.#{__method__}") 20 | config 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/racker/builders/qemu.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the QEMU builder 7 | class QEMU < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | %w(boot_command floppy_files iso_urls qemuargs).each do |key| 13 | if config.key? key 14 | logger.info("Converting #{key} to packer value...") 15 | config[key] = convert_hash_to_packer_value(config[key]) 16 | end 17 | end 18 | 19 | logger.debug("Leaving #{self.class}.#{__method__}") 20 | config 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/integration/output_to_file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe :output_to_file do 4 | before(:all) do 5 | @output_path = "/tmp/#{SecureRandom.uuid}/this_directory_should_not_exist/template.json" 6 | @instance = Racker::CLI.new(['-q', fixture_path('low_priority_template.rb'), @output_path]) 7 | end 8 | 9 | context 'when successful' do 10 | it 'writes the computed template to the given path' do 11 | output_dir = File.dirname(@output_path) 12 | FileUtils.rm_rf(output_dir) if Dir.exists?(output_dir) 13 | 14 | @instance.execute! 15 | expect(File.exists?(@output_path)).to eq(true) 16 | 17 | result = JSON.parse(File.read(@output_path)) 18 | expect(result).to eq(parsed_low_priority_template) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/racker/builders/null.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the Null builder 7 | class Null < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | # There are no special cases at this point 13 | 14 | # %w().each do |key| 15 | # if config.key? key 16 | # logger.info("Converting #{key} to packer value...") 17 | # config[key] = convert_hash_to_packer_value(config[key]) 18 | # end 19 | # end 20 | 21 | logger.debug("Leaving #{self.class}.#{__method__}") 22 | config 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/racker/builders/parallels.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the Parallels builder 7 | class Parallels < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | %w(boot_command floppy_files host_interfaces iso_urls prlctl).each do |key| 13 | if config.key? key 14 | logger.info("Converting #{key} to packer value...") 15 | config[key] = convert_hash_to_packer_value(config[key]) 16 | end 17 | end 18 | 19 | logger.debug("Leaving #{self.class}.#{__method__}") 20 | config 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /bin/racker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | if RUBY_VERSION < '1.9.2' 5 | puts 'Racker supports only Ruby 1.9.2+' 6 | exit(-1) 7 | end 8 | 9 | begin 10 | $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))) 11 | require 'racker' 12 | 13 | logger = Racker::LogSupport.logger 14 | # Let the CLI do its thing 15 | exit_code = Racker::CLI.new(ARGV.dup).execute! 16 | 17 | logger.info("Exit code: #{exit_code}") 18 | Kernel.exit!(exit_code) 19 | rescue Exception => e 20 | logger.error("Racker has encountered an unexpected error!") 21 | logger.error() 22 | logger.error("Details:") 23 | logger.error(e.inspect) 24 | logger.error(e.message) 25 | logger.error(e.backtrace.join('\n')) 26 | exit e.respond_to?(:status_code) ? e.status_code : 999 27 | end 28 | -------------------------------------------------------------------------------- /lib/racker/builders/google.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the Google builder 7 | class Google < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | # There are no special cases at this point 13 | 14 | # %w().each do |key| 15 | # if config.key? key 16 | # logger.info("Converting #{key} to packer value...") 17 | # config[key] = convert_hash_to_packer_value(config[key]) 18 | # end 19 | # end 20 | 21 | logger.debug("Leaving #{self.class}.#{__method__}") 22 | config 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/racker/builders/digitalocean.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the DigitalOcean builder 7 | class DigitalOcean < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | # There are no special cases at this point 13 | 14 | # %w().each do |key| 15 | # if config.key? key 16 | # logger.info("Converting #{key} to packer value...") 17 | # config[key] = convert_hash_to_packer_value(config[key]) 18 | # end 19 | # end 20 | 21 | logger.debug("Leaving #{self.class}.#{__method__}") 22 | config 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/racker/builders/virtualbox.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the Virtualbox builder 7 | class Virtualbox < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | %w(boot_command export_opts floppy_files import_flags iso_urls vboxmanage vboxmanage_post).each do |key| 13 | if config.key? key 14 | logger.info("Converting #{key} to packer value...") 15 | config[key] = convert_hash_to_packer_value(config[key]) 16 | end 17 | end 18 | 19 | logger.debug("Leaving #{self.class}.#{__method__}") 20 | config 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/racker/builders/amazon.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/builders/builder' 3 | 4 | module Racker 5 | module Builders 6 | # This is the Amazon builder 7 | class Amazon < Racker::Builders::Builder 8 | def to_packer(name, config) 9 | logger.debug("Entering #{self.class}.#{__method__}") 10 | config = super(name, config) 11 | 12 | %w(ami_block_device_mappings ami_groups ami_product_codes ami_regions ami_users chroot_mounts copy_files launch_block_device_mappings security_group_ids).each do |key| 13 | if config.key? key 14 | logger.info("Converting #{key} to packer value...") 15 | config[key] = convert_hash_to_packer_value(config[key]) 16 | end 17 | end 18 | 19 | logger.debug("Leaving #{self.class}.#{__method__}") 20 | config 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | require 'bundler/gem_tasks' 4 | begin 5 | Bundler.setup(:default, :development) 6 | rescue Bundler::BundlerError => e 7 | $stderr.puts e.message 8 | $stderr.puts 'Run `bundle install` to install missing gems' 9 | exit e.status_code 10 | end 11 | 12 | require 'rake' 13 | require 'rspec/core' 14 | require 'rspec/core/rake_task' 15 | require 'rubocop/rake_task' 16 | RSpec::Core::RakeTask.new(:spec) do |t| 17 | t.pattern = FileList['spec/**/*_spec.rb'] 18 | end 19 | 20 | desc 'Run RSpec with code coverage' 21 | task :coverage do 22 | ENV['COVERAGE'] = 'true' 23 | Rake::Task['spec'].execute 24 | end 25 | 26 | require 'rubocop/rake_task' 27 | RuboCop::RakeTask.new(:rubocop) do |t| 28 | # Specify the files we will look at 29 | t.patterns = ['bin', File.join('{lib}','**', '*.rb')] 30 | 31 | # Do not fail on error 32 | t.fail_on_error = false 33 | end 34 | 35 | require 'yard' 36 | YARD::Rake::YardocTask.new 37 | 38 | task default: [:spec] 39 | -------------------------------------------------------------------------------- /lib/racker/log_support.rb: -------------------------------------------------------------------------------- 1 | require 'log4r' 2 | 3 | module Racker 4 | module LogSupport 5 | unless Log4r::Logger['racker'] 6 | # Create the initial logger 7 | logger = Log4r::Logger.new('racker') 8 | 9 | # Set the output to STDOUT 10 | logger.outputters = Log4r::Outputter.stdout 11 | 12 | # We set the initial log level to ERROR 13 | logger.level = Log4r::ERROR 14 | end 15 | 16 | def self.level=(level) 17 | log_level = log4r_level_for(level) 18 | logger.level = log_level 19 | logger.info("Log level set to: #{log_level}") 20 | end 21 | 22 | def self.log4r_level_for(level) 23 | case level 24 | when /fatal/ 25 | Log4r::FATAL 26 | when /error/ 27 | Log4r::ERROR 28 | when /warn/ 29 | Log4r::WARN 30 | when /info/ 31 | Log4r::INFO 32 | when /debug/ 33 | Log4r::DEBUG 34 | else 35 | Log4r::INFO 36 | end 37 | end 38 | 39 | def self.logger 40 | Log4r::Logger['racker'] 41 | end 42 | 43 | def logger 44 | Racker::LogSupport.logger 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2014 Anthony Spring () 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /example/template2.rb: -------------------------------------------------------------------------------- 1 | Racker::Processor.register_template do |t| 2 | # We use an alternative kickstart configuration for this template 3 | t.variables = { 4 | 'kickstart_file' => 'template2-ks.cfg', 5 | } 6 | 7 | # Knock out the amazon builder as it is not needed by this template 8 | t.builders['amazon'] = '~~' 9 | 10 | # Define the provisioners 11 | t.provisioners = { 12 | 600 => { 13 | 'chef-devops-base' => { 14 | 'type' => 'chef-solo', 15 | 'chef_environment' => 'vagrant', 16 | 'cookbook_paths' => ['{{user `chef_base_dir`}}/cookbooks'], 17 | 'data_bags_path' => '{{user `chef_base_dir`}}/data_bags', 18 | 'environments_path' => '{{user `chef_base_dir`}}/environments', 19 | 'roles_path' => '{{user `chef_base_dir`}}/roles', 20 | 'run_list' => ['role[base]'], 21 | 'skip_install' => true, 22 | 'json' => { 23 | } 24 | }, 25 | 'cleanup-after-chef-solo' => { 26 | 'type' => 'shell', 27 | 'inline' => ['rm -rf /tmp/packer-chef-solo', 'rm -rf /tmp/encrypted_data_bag_secret'] 28 | }, 29 | }, 30 | 900 => { 31 | # Knockout the ec2 instance prep 32 | 'prepare-ec2-instance' => '~~' 33 | } 34 | } 35 | end 36 | -------------------------------------------------------------------------------- /lib/racker/smash/smash.rb: -------------------------------------------------------------------------------- 1 | require 'racker/smash/mash' 2 | require 'racker/smash/deep_merge_modified' 3 | 4 | # This class wraps mash and adds extended smart functionality 5 | class Smash < Mash 6 | 7 | def [](key) 8 | if !key?(key) 9 | self[key] = self.class.new 10 | else 11 | super 12 | end 13 | end 14 | 15 | def compact(opts={}) 16 | inject(self.class.new) do |new_hash, (k,v)| 17 | unless v.nil? 18 | new_hash[k] = opts[:recurse] && v.class == self.class ? v.compact(opts) : v 19 | end 20 | new_hash 21 | end 22 | end 23 | 24 | def convert_value(value) 25 | case value 26 | when Smash 27 | value 28 | when Mash 29 | self.class.new(value) 30 | when Hash 31 | self.class.new(value) 32 | else 33 | value 34 | end 35 | end 36 | 37 | def deep_merge!(source, options = {}) 38 | default_opts = {:preserve_unmergeables => false} 39 | DeepMergeModified::deep_merge!(source, self, default_opts.merge(options)) 40 | end 41 | 42 | def dup 43 | self.class.new(self) 44 | end 45 | 46 | def method_missing(symbol, *args) 47 | if symbol == :to_ary 48 | super 49 | elsif args.empty? 50 | self[symbol] 51 | elsif symbol.to_s =~ /=$/ 52 | key_to_set = symbol.to_s[/^(.+)=$/, 1] 53 | self[key_to_set] = (args.length == 1 ? args[0] : args) 54 | else 55 | raise NoMethodError, "Undefined key or method `#{symbol}' on `Smash`." 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 (2016-03-01) 2 | 3 | * Added support for output to STDOUT. The filename of '-' is used to signify outout should go to STDOUT. (PR#7) This feature will become the default functionality in the 0.3.0 release. 4 | * Fixed issue where the `parallels` builder was not properly required. 5 | 6 | ## 0.1.6 (2014-09-16) 7 | 8 | * Fixed `uninitialized constant Racker::Builders::Null` bug. 9 | 10 | ## 0.1.5 (2014-09-08) 11 | 12 | * Added support for VirtualBox `import_flags` options which were added in Packer 0.7.0. 13 | 14 | ## 0.1.4 (2014-08-07) 15 | 16 | New Features: 17 | * Added support for OpenStack `security_groups` options. 18 | * Added `null` builder support. 19 | * Added `parallels` builder support. (UNTESTED) 20 | 21 | ## 0.1.3 (2014-05-06) 22 | 23 | * Added support for VirtualBox `export_opts` and `vboxmanage_post` options which were added in Packer 0.6.0. 24 | 25 | ## 0.1.2 (2014-03-11) 26 | 27 | * Fix quiet option cli setting 28 | 29 | ## 0.1.1 (2014-03-06) 30 | 31 | New Features: 32 | * Added command line option to surpress output. 33 | * Added the ability to set the knockout prefix from the command line. 34 | 35 | Bug Fixes: 36 | * Do not create empty Packer namespace entries if the Racker template does not contain the namespace. 37 | * Changed default knockout prefix from `--` to `~~` as it conflicted with certain Virtualbox builder keys. 38 | 39 | ## 0.1.0 (2014-02-23) 40 | 41 | * Initial public release 42 | 43 | ## 0.0.3 (Internal) 44 | 45 | * Adding support for outstanding builders (digitalocean,docker,google,openstack and qemu) 46 | * Added more robust logging support 47 | 48 | ## 0.0.2 (Internal) 49 | 50 | * Removed ability to name post-processors due to packer template change. 51 | 52 | ## 0.0.1 (Internal) 53 | 54 | * Initial private release 55 | * Supports Amazon, Virtualbox and VMWare builders -------------------------------------------------------------------------------- /racker.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require File.expand_path('../lib/racker/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = 'racker' 6 | gem.authors = [ 'Anthony Spring' ] 7 | gem.email = 'tony@porkchopsandpaintchips.com' 8 | gem.homepage = 'https://github.com/aspring/racker' 9 | gem.license = 'MIT' 10 | gem.summary = %q{ A lightweight template wrapper for Packer } 11 | gem.description = %q{ Racker allows for hierarchical template definitions for Packer. } 12 | gem.version = Racker::Version::STRING.dup 13 | gem.platform = Gem::Platform::RUBY 14 | gem.required_ruby_version = '>= 1.9.2' 15 | 16 | gem.bindir = 'bin' 17 | gem.executables = %w( racker ) 18 | gem.files = Dir['Rakefile', '{lib,spec}/**/*', 'README*', 'LICENSE*', 'NOTICE*', 'CHANGELOG*'] 19 | gem.require_paths = %w[ lib ] 20 | 21 | gem.add_dependency 'multi_json', '~> 1.8' 22 | gem.add_dependency 'log4r', '~> 1.1.10' 23 | 24 | gem.add_development_dependency 'bundler', '~> 1.3' 25 | gem.add_development_dependency 'coveralls', '~> 0.6.7' 26 | gem.add_development_dependency 'guard', '~> 2.2.3' 27 | gem.add_development_dependency 'guard-bundler', '~> 2.0.0' 28 | gem.add_development_dependency 'guard-rspec', '~> 4.0' 29 | gem.add_development_dependency 'guard-cucumber', '~> 1.4' 30 | gem.add_development_dependency 'guard-rubocop', '~> 1.0' 31 | gem.add_development_dependency 'rake' 32 | gem.add_development_dependency 'rspec' 33 | gem.add_development_dependency 'rspec-mocks' 34 | gem.add_development_dependency 'rubocop', '~> 0.26.1' 35 | gem.add_development_dependency 'ruby_gntp', '~> 0.3.4' 36 | gem.add_development_dependency 'simplecov', '~> 0.7.1' 37 | gem.add_development_dependency 'yard', '~> 0.8' 38 | end 39 | -------------------------------------------------------------------------------- /spec/unit/log_support_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Racker::LogSupport do 4 | before(:all) do 5 | # Must call described_class outside of Class.new block so method lookup 6 | # resolves against test scope rather than block scope 7 | klass = described_class 8 | DummyClass = Class.new { include klass } 9 | @instance = DummyClass.new 10 | end 11 | 12 | context '::logger' do 13 | it 'returns the global Log4r logger for racker' do 14 | expect(described_class.logger).to eq(Log4r::Logger['racker']) 15 | end 16 | end 17 | 18 | context '::level=' do 19 | it 'sets the logger level based on the provided level string' do 20 | described_class.level = 'error' 21 | expect(described_class.logger.level).to eq(Log4r::ERROR) 22 | end 23 | end 24 | 25 | context '::log4r_log_level_for' do 26 | it 'returns Log4r::DEBUG for values matching debug' do 27 | expect(described_class.log4r_level_for('debug')).to eq(Log4r::DEBUG) 28 | expect(described_class.log4r_level_for(:debug)).to eq(Log4r::DEBUG) 29 | end 30 | 31 | it 'returns Log4r::ERROR for values matching error' do 32 | expect(described_class.log4r_level_for('error')).to eq(Log4r::ERROR) 33 | expect(described_class.log4r_level_for(:error)).to eq(Log4r::ERROR) 34 | end 35 | 36 | it 'returns Log4r::FATAL for values matching fatal' do 37 | expect(described_class.log4r_level_for('fatal')).to eq(Log4r::FATAL) 38 | expect(described_class.log4r_level_for(:fatal)).to eq(Log4r::FATAL) 39 | end 40 | 41 | it 'returns Log4r::INFO for values matching info' do 42 | expect(described_class.log4r_level_for('info')).to eq(Log4r::INFO) 43 | expect(described_class.log4r_level_for(:info)).to eq(Log4r::INFO) 44 | end 45 | 46 | it 'returns Log4r::WARN for values matching warn' do 47 | expect(described_class.log4r_level_for('warn')).to eq(Log4r::WARN) 48 | expect(described_class.log4r_level_for(:warn)).to eq(Log4r::WARN) 49 | end 50 | 51 | it 'returns Log4r::INFO otherwise' do 52 | expect(described_class.log4r_level_for('emergency')).to eq(Log4r::INFO) 53 | expect(described_class.log4r_level_for(:emergency)).to eq(Log4r::INFO) 54 | end 55 | end 56 | 57 | context '#logger' do 58 | it 'returns the global Log4r logger for racker' do 59 | expect(@instance.logger).to eq(Log4r::Logger['racker']) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/racker/processor.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'fileutils' 4 | require 'json' 5 | require 'racker/template' 6 | require 'pp' 7 | 8 | module Racker 9 | # This class handles command line options. 10 | class Processor 11 | include Racker::LogSupport 12 | 13 | CONFIGURE_MUTEX = Mutex.new 14 | 15 | def initialize(options) 16 | @options = options 17 | end 18 | 19 | def execute! 20 | # Verify that the templates exist 21 | @options[:templates].each do |template| 22 | raise "File does not exist! (#{template})" unless ::File.exists?(template) 23 | end 24 | 25 | # Load the templates 26 | templates = [] 27 | 28 | # Load the template procs 29 | logger.info('Loading racker templates...') 30 | template_procs = load(@options[:templates]) 31 | 32 | # Load the actual templates 33 | logger.info('Processing racker templates...') 34 | template_procs.each do |version,proc| 35 | # Create the new template 36 | template = Racker::Template.new 37 | 38 | # Run the block with the template 39 | proc.call(template) 40 | 41 | # Store the template 42 | templates << template 43 | end 44 | logger.info('Racker template processing complete.') 45 | 46 | # Get the first template and merge each subsequent one on the latest 47 | logger.info('Merging racker templates...') 48 | current_template = templates.shift 49 | 50 | # Overlay the templates 51 | templates.each do |template| 52 | current_template = current_template.deep_merge!(template, {:knockout_prefix => @options[:knockout]}) 53 | end 54 | 55 | # Compact the residual template to remove nils 56 | logger.info('Compacting racker template...') 57 | compact_template = current_template.compact(:recurse => true) 58 | 59 | # Pretty-print the JSON template 60 | JSON.pretty_generate(compact_template.to_packer) 61 | end 62 | 63 | def load(templates) 64 | return capture_templates do 65 | templates.each do |template| 66 | puts "Loading template file: #{template}" unless @options[:quiet] 67 | Kernel.load template 68 | end 69 | end 70 | end 71 | 72 | # This is a class method so the templates can load it 73 | def self.register_template(version='1',&block) 74 | @@last_procs ||= [] 75 | @@last_procs << [version, block] 76 | end 77 | 78 | def capture_templates 79 | CONFIGURE_MUTEX.synchronize do 80 | @@last_procs = [] 81 | 82 | yield 83 | 84 | return @@last_procs 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/racker/template.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'racker/smash/smash' 3 | require 'racker/builders/amazon' 4 | require 'racker/builders/builder' 5 | require 'racker/builders/digitalocean' 6 | require 'racker/builders/docker' 7 | require 'racker/builders/google' 8 | require 'racker/builders/null' 9 | require 'racker/builders/openstack' 10 | require 'racker/builders/parallels' 11 | require 'racker/builders/qemu' 12 | require 'racker/builders/virtualbox' 13 | require 'racker/builders/vmware' 14 | 15 | module Racker 16 | # This class handles the bulk of the legwork working with Racker templates 17 | class Template < Smash 18 | include Racker::LogSupport 19 | 20 | # This formats the template into packer format hash 21 | def to_packer 22 | # Create the new smash 23 | packer = Smash.new 24 | 25 | # Variables 26 | packer['variables'] = self['variables'].dup unless self['variables'].nil? || self['variables'].empty? 27 | 28 | # Builders 29 | packer['builders'] = [] unless self['builders'].nil? || self['builders'].empty? 30 | logger.info("Processing builders...") 31 | self['builders'].each do |name,config| 32 | logger.info("Processing builder: #{name} with type: #{config['type']}") 33 | 34 | # Get the builder for this config 35 | builder = get_builder(config['type']) 36 | 37 | # Have the builder convert the config to packer format 38 | packer['builders'] << builder.to_packer(name, config.dup) 39 | end 40 | 41 | # Provisioners 42 | packer['provisioners'] = [] unless self['provisioners'].nil? || self['provisioners'].empty? 43 | logger.info("Processing provisioners...") 44 | self['provisioners'].sort.map do |index, provisioners| 45 | provisioners.each do |name,config| 46 | logger.debug("Processing provisioner: #{name}") 47 | packer['provisioners'] << config.dup 48 | end 49 | end 50 | 51 | # Post-Processors 52 | packer['post-processors'] = [] unless self['postprocessors'].nil? || self['postprocessors'].empty? 53 | logger.info("Processing post-processors...") 54 | self['postprocessors'].each do |name,config| 55 | logger.debug("Processing post-processor: #{name}") 56 | packer['post-processors'] << config.dup unless config.nil? 57 | end 58 | 59 | packer 60 | end 61 | 62 | def get_builder(type) 63 | case type 64 | when /amazon/ 65 | Racker::Builders::Amazon.new 66 | when /digitalocean/ 67 | Racker::Builders::DigitalOcean.new 68 | when /docker/ 69 | Racker::Builders::Docker.new 70 | when /googlecompute/ 71 | Racker::Builders::Google.new 72 | when /null/ 73 | Racker::Builders::Null.new 74 | when /openstack/ 75 | Racker::Builders::OpenStack.new 76 | when /parallels/ 77 | Racker::Builders::Parallels.new 78 | when /qemu/ 79 | Racker::Builders::QEMU.new 80 | when /virtualbox/ 81 | Racker::Builders::Virtualbox.new 82 | when /vmware/ 83 | Racker::Builders::VMware.new 84 | else 85 | Racker::Builders::Builder.new 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/racker/cli.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'optparse' 3 | require 'racker/processor' 4 | require 'racker/version' 5 | 6 | module Racker 7 | # The CLI is a class responsible for handling the command line interface 8 | # logic. 9 | class CLI 10 | include Racker::LogSupport 11 | 12 | STDOUT_TOKEN = '-' 13 | 14 | attr_reader :options 15 | 16 | def initialize(argv) 17 | @argv = argv 18 | end 19 | 20 | def execute! 21 | # Parse our arguments 22 | option_parser.parse!(@argv) 23 | 24 | # Set the logging level specified by the command line 25 | Racker::LogSupport.level = options[:log_level] 26 | 27 | # Display the options if a minimum of 1 template and an output file is not provided 28 | if @argv.length < 2 29 | puts option_parser 30 | Kernel.exit!(1) 31 | end 32 | 33 | # Set the output file to the last arg. A single dash can be supplied to 34 | # indicate that the compiled template should be written to STDOUT. Output 35 | # to STDOUT assumes the quiet option. 36 | options[:output] = output = @argv.pop 37 | logger.debug("Output file set to: #{output}") 38 | 39 | # Output to STDOUT assumes quiet mode 40 | @options[:quiet] = true if output == STDOUT_TOKEN 41 | 42 | # Set the input files to the remaining args 43 | options[:templates] = @argv 44 | 45 | # Run through Racker 46 | logger.debug('Executing the Racker Processor...') 47 | template = Processor.new(options).execute! 48 | 49 | write(output, template) 50 | 51 | # Thats all folks! 52 | logger.debug('Processing complete.') 53 | puts "Processing complete!" unless options[:quiet] 54 | puts "Packer file generated: #{options[:output]}" unless options[:quiet] 55 | 56 | return 0 57 | end 58 | 59 | private 60 | 61 | def options 62 | @options ||= { 63 | log_level: :warn, 64 | knockout: '~~', 65 | output: '', 66 | templates: [], 67 | quiet: false, 68 | } 69 | end 70 | 71 | def option_parser 72 | @option_parser ||= OptionParser.new do |opts| 73 | opts.banner = "Usage: #{opts.program_name} [options] [TEMPLATE1, TEMPLATE2, ...] OUTPUT" 74 | 75 | opts.on('-l', '--log-level [LEVEL]', [:fatal, :error, :warn, :info, :debug], 'Set log level') do |v| 76 | options[:log_level] = v 77 | end 78 | 79 | opts.on('-k', '--knockout PREFIX', 'Set the knockout prefix (Default: ~~)') do |v| 80 | options[:knockout] = v || '~~' 81 | end 82 | 83 | opts.on('-q', '--quiet', 'Disable unnecessary output') do |v| 84 | options[:quiet] = true 85 | end 86 | 87 | opts.on_tail('-h', '--help', 'Show this message') do 88 | puts option_parser 89 | Kernel.exit!(0) 90 | end 91 | 92 | opts.on_tail('-v', '--version', "Show #{opts.program_name} version") do 93 | puts Racker::Version.version 94 | Kernel.exit!(0) 95 | end 96 | end 97 | end 98 | 99 | private 100 | 101 | def write(output_path, template) 102 | if output_path == STDOUT_TOKEN 103 | write_to_stdout(template) 104 | else 105 | write_to_file(template, output_path) 106 | end 107 | true 108 | end 109 | 110 | def write_to_file(template, path) 111 | path = File.expand_path(path) 112 | output_dir = File.dirname(path) 113 | 114 | # Create output directory if it does not exist 115 | unless File.directory?(output_dir) 116 | logger.info(%Q[Creating output directory "#{output_dir}"]) 117 | FileUtils.mkdir_p(output_dir) 118 | end 119 | 120 | File.open(path, 'w') { |file| write_to_stream(template, file, path) } 121 | end 122 | 123 | def write_to_stdout(template) 124 | write_to_stream(template, $stdout, :STDOUT) 125 | end 126 | 127 | def write_to_stream(template, stream, stream_name) 128 | logger.info("Writing packer template to #{stream_name}") 129 | stream.write(template) 130 | stream.flush if stream.respond_to?(:flush) 131 | logger.info("Writing packer template to #{stream_name} complete.") 132 | end 133 | 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /spec/unit/processor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | require 'securerandom' 4 | 5 | RSpec.describe Racker::Processor do 6 | DUMMY_TEMPLATE_PROC = proc { |t| nil } 7 | 8 | context '::register_template' do 9 | before(:all) do 10 | @instance = described_class.new({}) 11 | end 12 | 13 | it 'takes a block and an optional version argument' do 14 | the_version = 'my_version' 15 | the_block = DUMMY_TEMPLATE_PROC 16 | 17 | captured_templates = @instance.capture_templates do 18 | described_class.register_template(the_version, &the_block) 19 | end 20 | version, captured_template = captured_templates.first 21 | expect(version).to eq(the_version) 22 | expect(captured_template).to eq(the_block) 23 | end 24 | 25 | it 'uses a default value of "1" for version if none is provided' do 26 | default_version = '1' 27 | captured_templates = @instance.capture_templates do 28 | described_class.register_template(&DUMMY_TEMPLATE_PROC) 29 | end 30 | version = captured_templates.first.first 31 | expect(version).to eq(default_version) 32 | end 33 | end 34 | 35 | context '#capture_templates' do 36 | before(:all) do 37 | @instance = described_class.new(:quiet => true) 38 | end 39 | 40 | it 'captures [version, template_proc] pairs for each call to ::register_template' do 41 | template_count = 5 42 | dummy_templates = Hash.new do |hash, key| 43 | hash[key] = DUMMY_TEMPLATE_PROC.dup 44 | end 45 | 46 | captured_templates = @instance.capture_templates do 47 | template_count.times do |version| 48 | described_class.register_template(version, &dummy_templates[version]) 49 | end 50 | end 51 | 52 | expect(captured_templates.length).to eq(template_count) 53 | expect(captured_templates.flatten).to eq(dummy_templates.to_a.flatten) 54 | end 55 | end 56 | 57 | context '#execute!' do 58 | before(:all) do 59 | @options = { 60 | :output => "/tmp/#{SecureRandom.uuid}/this_directory_should_not_exist/template.json", 61 | :knockout => '~~', 62 | :quiet => true, 63 | } 64 | @output_path = @options[:output] 65 | @instance = described_class.new(@options) 66 | end 67 | 68 | it 'raises a RuntimeError if any of the provided templates do not exist' do 69 | @options[:templates] = [ '/tmp/this_template_should_not_exists.json' ] 70 | expect { @instance.execute! }.to raise_error(RuntimeError) 71 | end 72 | 73 | it 'merges the templates with a knockout_prefix matching the provided knockout option' do 74 | @options[:templates] = [ 75 | fixture_path('low_priority_template.rb'), 76 | fixture_path('high_priority_template.rb'), 77 | ] 78 | template = @instance.execute! 79 | 80 | result = JSON.parse(template) 81 | expect(result['variables']['password']).to eq(nil) 82 | end 83 | 84 | it 'merges the templates such that each template takes presedence over its predecessors' do 85 | @options[:templates] = [ 86 | fixture_path('low_priority_template.rb'), 87 | fixture_path('high_priority_template.rb'), 88 | ] 89 | template = @instance.execute! 90 | 91 | result = JSON.parse(template) 92 | expect(result['variables']['iso_url']).to eq('priority.img') 93 | end 94 | 95 | it 'removes nil values from the generated template' do 96 | @options[:templates] = [ 97 | fixture_path('low_priority_template.rb'), 98 | fixture_path('high_priority_template.rb'), 99 | ] 100 | template = @instance.execute! 101 | 102 | result = JSON.parse(template) 103 | expect(result['variables'].key?('nil')).to eq(false) 104 | end 105 | 106 | it 'outputs the computed template in JSON format' do 107 | @options[:templates] = [ 108 | fixture_path('low_priority_template.rb'), 109 | ] 110 | template = @instance.execute! 111 | 112 | expect(JSON.parse(template)).to eq(parsed_low_priority_template) 113 | end 114 | 115 | end 116 | 117 | context '#initialize' do 118 | it 'sets the options instance variable to the given argument' do 119 | opts = {} 120 | instance = described_class.new(opts) 121 | expect(instance.instance_variable_get(:@options).object_id).to eq(opts.object_id) 122 | end 123 | end 124 | 125 | context '#load' do 126 | before(:all) do 127 | @fixture = fixture_path('low_priority_template.rb') 128 | @options = {} 129 | @instance = described_class.new(@options) 130 | end 131 | 132 | it 'puts the template file if a falsy :quiet option was provided' do 133 | expect(@instance).to receive(:puts).exactly(3).times 134 | 135 | @options.delete(:quiet) 136 | @instance.load([@fixture]) 137 | @options[:quiet] = nil 138 | @instance.load([@fixture]) 139 | @options[:quiet] = false 140 | @instance.load([@fixture]) 141 | end 142 | 143 | it 'puts no output if a truthy :quiet option was provided' do 144 | expect(@instance).to_not receive(:puts) 145 | 146 | @options[:quiet] = true 147 | @instance.load([@fixture]) 148 | @options[:quiet] = Object.new 149 | @instance.load([@fixture]) 150 | end 151 | 152 | it 'loads each given template path' do 153 | expect(Kernel).to receive(:load).with(@fixture).exactly(3).times 154 | 155 | @options[:quiet] = true 156 | @instance.load([ 157 | @fixture, 158 | @fixture, 159 | @fixture, 160 | ]) 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /example/packer.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "iso_checksum": "6232efa014d9c6798396b63152c4c9a08b279f5e", 4 | "iso_checksum_type": "sha1", 5 | "iso_url": "http://mirrors.kernel.org/centos/6.4/isos/x86_64/CentOS-6.4-x86_64-minimal.iso", 6 | "kickstart_file": "template2-ks.cfg", 7 | "vagrant_output_file": "./boxes/centos-6.4-{{.Provider}}.box" 8 | }, 9 | "builders": [ 10 | { 11 | "type": "virtualbox-iso", 12 | "guest_os_type": "RedHat_64", 13 | "headless": true, 14 | "format": "ova", 15 | "guest_additions_path": "VBoxGuestAdditions_{{.Version}}.iso", 16 | "iso_checksum": "{{user `iso_checksum`}}", 17 | "iso_checksum_type": "{{user `iso_checksum_type`}}", 18 | "iso_url": "{{user `iso_url`}}", 19 | "virtualbox_version_file": ".vbox_version", 20 | "boot_command": [ 21 | " text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/{{user `kickstart_file`}}" 22 | ], 23 | "boot_wait": "10s", 24 | "http_directory": "http_directory", 25 | "ssh_port": 22, 26 | "ssh_username": "root", 27 | "ssh_password": "asdfasdf", 28 | "ssh_wait_timeout": "10000s", 29 | "shutdown_command": "shutdown -P now", 30 | "disk_size": 8096, 31 | "vboxmanage": [ 32 | [ 33 | "modifyvm", 34 | "{{.Name}}", 35 | "--memory", 36 | "1024" 37 | ], 38 | [ 39 | "modifyvm", 40 | "{{.Name}}", 41 | "--cpus", 42 | "1" 43 | ], 44 | [ 45 | "modifyvm", 46 | "{{.Name}}", 47 | "--ioapic", 48 | "on" 49 | ] 50 | ], 51 | "name": "virtualbox-vagrant" 52 | }, 53 | { 54 | "type": "vmware-iso", 55 | "guest_os_type": "centos-64", 56 | "headless": true, 57 | "tools_upload_flavor": "linux", 58 | "iso_checksum": "{{user `iso_checksum`}}", 59 | "iso_checksum_type": "{{user `iso_checksum_type`}}", 60 | "iso_url": "{{user `iso_url`}}", 61 | "boot_command": [ 62 | " text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/{{user `kickstart_file`}}" 63 | ], 64 | "boot_wait": "10s", 65 | "http_directory": "http_directory", 66 | "ssh_port": 22, 67 | "ssh_username": "root", 68 | "ssh_password": "asdfasdf", 69 | "ssh_wait_timeout": "10000s", 70 | "shutdown_command": "shutdown -P now", 71 | "disk_size": 8096, 72 | "vmdk_name": "centos", 73 | "vmx_data": { 74 | "cpuid.coresPerSocket": "1", 75 | "memsize": "1024", 76 | "numvcpus": "1", 77 | "isolation.tools.copy.disable": "false", 78 | "isolation.tools.paste.disable": "false" 79 | }, 80 | "name": "vmware-vagrant" 81 | } 82 | ], 83 | "provisioners": [ 84 | { 85 | "type": "shell", 86 | "script": "scripts/centos-6/yum-install-packages.sh" 87 | }, 88 | { 89 | "type": "shell", 90 | "script": "scripts/common/disable-sshd-dns-lookup.sh" 91 | }, 92 | { 93 | "type": "shell", 94 | "script": "scripts/common/no-tty-sudo.sh" 95 | }, 96 | { 97 | "type": "shell", 98 | "script": "scripts/common/set-xen-device-names.sh" 99 | }, 100 | { 101 | "type": "shell", 102 | "script": "scripts/common/update-cacert-bundle.sh" 103 | }, 104 | { 105 | "type": "shell", 106 | "script": "scripts/common/install-chef-11.8.0.sh" 107 | }, 108 | { 109 | "type": "shell", 110 | "scripts": [ 111 | "scripts/centos-6/install-guest-additions-dependencies.sh", 112 | "scripts/common/install-guest-additions.sh" 113 | ], 114 | "only": [ 115 | "virtualbox-vagrant", 116 | "vmware-vagrant" 117 | ] 118 | }, 119 | { 120 | "type": "chef-solo", 121 | "chef_environment": "vagrant", 122 | "cookbook_paths": [ 123 | "{{user `chef_base_dir`}}/cookbooks" 124 | ], 125 | "data_bags_path": "{{user `chef_base_dir`}}/data_bags", 126 | "environments_path": "{{user `chef_base_dir`}}/environments", 127 | "roles_path": "{{user `chef_base_dir`}}/roles", 128 | "run_list": [ 129 | "role[base]" 130 | ], 131 | "skip_install": true, 132 | "json": { 133 | } 134 | }, 135 | { 136 | "type": "shell", 137 | "inline": [ 138 | "rm -rf /tmp/packer-chef-solo", 139 | "rm -rf /tmp/encrypted_data_bag_secret" 140 | ] 141 | }, 142 | { 143 | "type": "shell", 144 | "script": "scripts/centos-6/disable-services.sh" 145 | }, 146 | { 147 | "type": "shell", 148 | "script": "scripts/common/prepare-vagrant-instance.sh", 149 | "only": [ 150 | "virtualbox-vagrant" 151 | ] 152 | }, 153 | { 154 | "type": "file", 155 | "source": "uploads/static-ip", 156 | "destination": "/root", 157 | "only": [ 158 | "vmware-vagrant" 159 | ] 160 | }, 161 | { 162 | "type": "shell", 163 | "script": "scripts/centos-6/yum-remove-packages.sh" 164 | }, 165 | { 166 | "type": "shell", 167 | "script": "scripts/centos-6/clean-unnecessary-files.sh" 168 | }, 169 | { 170 | "type": "shell", 171 | "script": "scripts/common/zero-empty-space.sh" 172 | } 173 | ], 174 | "post-processors": [ 175 | { 176 | "type": "vagrant", 177 | "output": "{{user `vagrant_output_file`}}", 178 | "compression_level": 7, 179 | "keep_input_artifact": true, 180 | "only": [ 181 | "virtualbox-vagrant", 182 | "vmware-vagrant" 183 | ] 184 | } 185 | ] 186 | } -------------------------------------------------------------------------------- /example/template1.rb: -------------------------------------------------------------------------------- 1 | Racker::Processor.register_template do |t| 2 | 3 | # Define the variables 4 | t.variables = { 5 | 'iso_checksum' => '6232efa014d9c6798396b63152c4c9a08b279f5e', 6 | 'iso_checksum_type' => 'sha1', 7 | 'iso_url' => 'http://mirrors.kernel.org/centos/6.4/isos/x86_64/CentOS-6.4-x86_64-minimal.iso', 8 | 'kickstart_file' => 'template1-ks.cfg', 9 | 'vagrant_output_file' => "./boxes/centos-6.4-{{.Provider}}.box" 10 | } 11 | 12 | # Define the builders 13 | t.builders['virtualbox-vagrant'] = { 14 | 'type' => 'virtualbox-iso', 15 | 'guest_os_type' => 'RedHat_64', 16 | 'headless' => true, 17 | 'format' => 'ova', 18 | 'guest_additions_path' => "VBoxGuestAdditions_{{.Version}}.iso", 19 | 'iso_checksum' => "{{user `iso_checksum`}}", 20 | 'iso_checksum_type' => "{{user `iso_checksum_type`}}", 21 | 'iso_url' => "{{user `iso_url`}}", 22 | 'virtualbox_version_file' => '.vbox_version', 23 | 'boot_command' => { 24 | 0 => ' text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/{{user `kickstart_file`}}' 25 | }, 26 | 'boot_wait' => '10s', 27 | 'http_directory' => 'http_directory', 28 | 'ssh_port' => 22, 29 | 'ssh_username' => 'root', 30 | 'ssh_password' => 'asdfasdf', 31 | 'ssh_wait_timeout' => '10000s', 32 | 'shutdown_command' => 'shutdown -P now', 33 | 'disk_size' => 8096, 34 | 'vboxmanage' => { 35 | 'memory' => [ 'modifyvm', '{{.Name}}', '--memory', '1024' ], 36 | 'cpus' => [ 'modifyvm', '{{.Name}}', '--cpus', '1' ], 37 | 'ioapic' => [ 'modifyvm', '{{.Name}}', '--ioapic', 'on' ] 38 | } 39 | } 40 | 41 | t.builders['vmware-vagrant'] = { 42 | 'type' => 'vmware-iso', 43 | 'guest_os_type' => 'centos-64', 44 | 'headless' => true, 45 | 'tools_upload_flavor' => 'linux', 46 | 'iso_checksum' => '{{user `iso_checksum`}}', 47 | 'iso_checksum_type' => '{{user `iso_checksum_type`}}', 48 | 'iso_url' => '{{user `iso_url`}}', 49 | 'boot_command' => { 50 | 0 => ' text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/{{user `kickstart_file`}}' 51 | }, 52 | 'boot_wait' => '10s', 53 | 'http_directory' => 'http_directory', 54 | 'ssh_port' => 22, 55 | 'ssh_username' => 'root', 56 | 'ssh_password' => 'asdfasdf', 57 | 'ssh_wait_timeout' => '10000s', 58 | 'shutdown_command' => 'shutdown -P now', 59 | 'disk_size' => 8096, 60 | 'vmdk_name' => 'centos', 61 | 'vmx_data' => { 62 | 'cpuid.coresPerSocket' => '1', 63 | 'memsize' => '1024', 64 | 'numvcpus' => '1', 65 | 'isolation.tools.copy.disable' => 'false', 66 | 'isolation.tools.paste.disable' => 'false' 67 | } 68 | } 69 | 70 | t.builders['amazon'] = { 71 | 'type' => 'amazon-ebs', 72 | 'region' => 'us-east-1', 73 | 'source_ami' => 'ami-testami', 74 | 'instance_type' => 't1.micro', 75 | 'ssh_username' => 'root', 76 | 'ssh_timeout' => '5m', 77 | 'ami_name' => 'packer-centos-6 {{timestamp}}', 78 | 'ami_block_device_mappings' => { 79 | '/dev/sda' => { 80 | 'device_name' => '/dev/sda', 81 | 'volume_size' => 8, 82 | 'delete_on_termination' => true 83 | }, 84 | '/dev/sdb' => { 85 | 'device_name' => '/dev/sdb', 86 | 'volume_size' => 8, 87 | 'delete_on_termination' => true 88 | } 89 | }, 90 | 'ami_regions' => [ 91 | 'us-west-1', 92 | 'us-west-2' 93 | ] 94 | } 95 | 96 | # Define the provisioners 97 | t.provisioners = { 98 | 0 => { 99 | 'yum-install-packages' => { 100 | 'type' => 'shell', 101 | 'script' => 'scripts/centos-6/yum-install-packages.sh' 102 | }, 103 | 'disable-sshd-dns-lookup' => { 104 | 'type' => 'shell', 105 | 'script' => 'scripts/common/disable-sshd-dns-lookup.sh' 106 | }, 107 | 'no-tty-sudo' => { 108 | 'type' => 'shell', 109 | 'script' => 'scripts/common/no-tty-sudo.sh' 110 | }, 111 | 'set-xen-device-name' => { 112 | 'type' => 'shell', 113 | 'script' => 'scripts/common/set-xen-device-names.sh' 114 | }, 115 | 'update-cacert-bundle' => { 116 | 'type' => 'shell', 117 | 'script' => 'scripts/common/update-cacert-bundle.sh' 118 | }, 119 | }, 120 | 300 => { 121 | 'install-chef-11.8.0' => { 122 | 'type' => 'shell', 123 | 'script' => 'scripts/common/install-chef-11.8.0.sh' 124 | }, 125 | }, 126 | 500 => { 127 | 'install-guest-additions-dependencies' => { 128 | 'type' => 'shell', 129 | 'scripts' => [ 130 | 'scripts/centos-6/install-guest-additions-dependencies.sh', 131 | 'scripts/common/install-guest-additions.sh' 132 | ], 133 | 'only' => ['virtualbox-vagrant', 'vmware-vagrant'] 134 | }, 135 | }, 136 | 750 => { 137 | 'disable-services' => { 138 | 'type' => 'shell', 139 | 'script' => 'scripts/centos-6/disable-services.sh' 140 | }, 141 | }, 142 | 900 => { 143 | 'prepare-vagrant-instance' => { 144 | 'type' => 'shell', 145 | 'script' => 'scripts/common/prepare-vagrant-instance.sh', 146 | 'only' => ['virtualbox-vagrant'] 147 | }, 148 | 'prepare-ec2-instance' => { 149 | 'type' => 'shell', 150 | 'script' => 'scripts/common/prepare-ec2-instance.sh', 151 | 'only' => ['amazon'] 152 | }, 153 | 'update-staic-ip-files' => { 154 | 'type' => 'file', 155 | 'source' => 'uploads/static-ip', 156 | 'destination' => '/root', 157 | 'only' => ['vmware-vagrant'] 158 | }, 159 | }, 160 | 999 => { 161 | 'yum-remove-packages' => { 162 | 'type' => 'shell', 163 | 'script' => 'scripts/centos-6/yum-remove-packages.sh' 164 | }, 165 | 'clean-unnecessary-files' => { 166 | 'type' => 'shell', 167 | 'script' => 'scripts/centos-6/clean-unnecessary-files.sh' 168 | }, 169 | 'zero-empty-space' => { 170 | 'type' => 'shell', 171 | 'script' => 'scripts/common/zero-empty-space.sh' 172 | }, 173 | } 174 | } 175 | 176 | # Define the post-processors 177 | t.postprocessors['vagrant'] = { 178 | 'type' => 'vagrant', 179 | 'output' => '{{user `vagrant_output_file`}}', 180 | 'compression_level' => 7, 181 | 'keep_input_artifact' => true, 182 | 'only' => ['virtualbox-vagrant','vmware-vagrant'] 183 | } 184 | 185 | end 186 | -------------------------------------------------------------------------------- /spec/unit/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Racker::CLI do 4 | 5 | context '#execute!' do 6 | it 'exits with a status of 1 if fewer than 2 arguments were received' do 7 | allow(Kernel).to receive(:exit!) 8 | 9 | instance = Racker::CLI.new(['template.rb']) 10 | allow(instance).to receive(:puts) 11 | 12 | # This next call is going to break somewhere because we stubbed the exit! 13 | # call, so catch any error that occurs. 14 | instance.execute! rescue nil 15 | 16 | # set expextation on puts to silence output 17 | expect(instance).to have_received(:puts) 18 | expect(Kernel).to have_received(:exit!).with(1) 19 | end 20 | 21 | context 'with valid options' do 22 | before(:each) do 23 | @immutable_argv = ['template.rb', 'environment.rb', 'template.json'].freeze 24 | @argv = @immutable_argv.dup 25 | @instance = Racker::CLI.new(@argv) 26 | @options = @instance.send(:options) 27 | @options[:quiet] = true 28 | # Prevent fake file from being written 29 | allow(File).to receive(:open) 30 | end 31 | 32 | it 'uses the last argument for the value of the output option' do 33 | allow_any_instance_of(Racker::Processor).to receive(:execute!) 34 | @instance.execute! 35 | expect(@options[:output]).to eq(@immutable_argv.last) 36 | end 37 | 38 | it 'uses all arguments except the last for the value of the templates option' do 39 | allow_any_instance_of(Racker::Processor).to receive(:execute!) 40 | @instance.execute! 41 | expect(@options[:templates]).to eq(@immutable_argv[0..-2]) 42 | end 43 | 44 | it 'initializes and executes a new Racker::Processor with the given options' do 45 | processor_instance = Racker::Processor.new(@options) 46 | expect(Racker::Processor).to receive(:new) { processor_instance }.with(@options) 47 | expect(processor_instance).to receive(:execute!) 48 | @instance.execute! 49 | end 50 | 51 | it 'outputs no message when quieted' do 52 | @options[:quiet] = true 53 | allow_any_instance_of(Racker::Processor).to receive(:execute!) 54 | expect(@instance).not_to receive(:puts) 55 | @instance.execute! 56 | end 57 | 58 | it 'outputs a message upon success when not quieted' do 59 | @options[:quiet] = false 60 | allow_any_instance_of(Racker::Processor).to receive(:execute!) 61 | expect(@instance).to receive(:puts).at_least(1) 62 | @instance.execute! 63 | end 64 | 65 | it 'returns 0 on success' do 66 | allow_any_instance_of(Racker::Processor).to receive(:execute!) 67 | expect(@instance.execute!).to eq(0) 68 | end 69 | end 70 | end 71 | 72 | context '#initialize' do 73 | it 'sets the @argv instance variable to the provided argument' do 74 | instance = described_class.new(argv = []) 75 | expect(instance.instance_variable_get(:@argv).object_id).to eq(argv.object_id) 76 | end 77 | end 78 | 79 | context '#option_parser' do 80 | before(:each) { @instance = described_class.new([]) } 81 | 82 | it 'returns a new default OptionParser if none exists' do 83 | expect(@instance.instance_variable_get(:@option_parser)).to eq(nil) 84 | expect(@instance.send(:option_parser)).to be_an(OptionParser) 85 | end 86 | 87 | it 'returns the same OptionParser on subsequent calls' do 88 | first_option_parser = @instance.send(:option_parser) 89 | second_option_parser = @instance.send(:option_parser) 90 | expect(second_option_parser).to be(first_option_parser) 91 | end 92 | end 93 | 94 | context '#options' do 95 | before(:each) { @instance = described_class.new([]) } 96 | 97 | it 'returns a Hash of default options if none exists' do 98 | expect(@instance.instance_variable_get(:@options)).to eq(nil) 99 | options = @instance.send(:options) 100 | expect(options).to eq({ 101 | log_level: :warn, 102 | knockout: '~~', 103 | output: '', 104 | templates: [], 105 | quiet: false, 106 | }) 107 | end 108 | 109 | it 'returns the same Hash on subsequent calls' do 110 | first_options = @instance.send(:options) 111 | second_options = @instance.send(:options) 112 | expect(second_options).to be(first_options) 113 | end 114 | end 115 | 116 | context 'option parser' do 117 | before(:all) { @instance = described_class.new([]) } 118 | before(:each) do 119 | @instance.instance_variable_set(:@option_parser, nil) 120 | @instance.instance_variable_set(:@options, nil) 121 | @parser = @instance.send(:option_parser) 122 | @options = @instance.send(:options) 123 | end 124 | 125 | context 'log_level' do 126 | %w[-l --log-level].each do |format| 127 | it "is triggered by the #{format} arg" do 128 | @options.delete(:log_level) 129 | @parser.parse!(%W[#{format} fatal]) 130 | expect(@options[:log_level]).to be(:fatal) 131 | end 132 | end 133 | 134 | %w[debug error fatal info warn].each do |log_level| 135 | it "supports a log level of #{log_level}" do 136 | @options.delete(:log_level) 137 | @parser.parse!(%W[-l #{log_level}]) 138 | expect(@options[:log_level]).to be(log_level.to_sym) 139 | end 140 | end 141 | 142 | it 'defaults invalid log levels to nil' do 143 | @options.delete(:log_level) 144 | @parser.parse!(%W[-l foo]) 145 | expect(@options[:log_level]).to be(nil) 146 | end 147 | end 148 | 149 | context 'knockout' do 150 | %w[-k --knockout].each do |format| 151 | it "is triggered by the #{format} arg" do 152 | @options.delete(:knockout) 153 | @parser.parse!(%W[#{format} xxx]) 154 | expect(@options[:knockout]).to eq('xxx') 155 | end 156 | end 157 | end 158 | 159 | context 'quiet' do 160 | %w[-q --quiet].each do |format| 161 | it "is triggered by the #{format} arg" do 162 | @options.delete(:quiet) 163 | @parser.parse!([format]) 164 | expect(@options[:quiet]).to eq(true) 165 | end 166 | end 167 | end 168 | 169 | context 'help' do 170 | %w[-h --help].each do |format| 171 | it "is triggered by the #{format} arg" do 172 | expect(@instance).to receive(:puts) 173 | expect(Kernel).to receive(:exit!) 174 | @parser.parse!([format]) 175 | end 176 | end 177 | 178 | it 'outputs help then exits with a status of 0' do 179 | expect(@instance).to receive(:puts) 180 | expect(Kernel).to receive(:exit!).with(0) 181 | @parser.parse!(['--help']) 182 | end 183 | end 184 | 185 | context 'version' do 186 | %w[-v --version].each do |format| 187 | it "is triggered by the #{format} arg" do 188 | expect(@instance).to receive(:puts) 189 | expect(Kernel).to receive(:exit!) 190 | @parser.parse!([format]) 191 | end 192 | end 193 | 194 | it 'outputs the version then exits with a status of 0' do 195 | expect(@instance).to receive(:puts) 196 | expect(Kernel).to receive(:exit!).with(0) 197 | @parser.parse!(['--version']) 198 | end 199 | end 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /lib/racker/smash/mash.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009 Dan Kubb 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | # --- 23 | # --- 24 | 25 | # Some portions of blank.rb and mash.rb are verbatim copies of software 26 | # licensed under the MIT license. That license is included below: 27 | 28 | # Copyright (c) 2005-2008 David Heinemeier Hansson 29 | 30 | # Permission is hereby granted, free of charge, to any person obtaining 31 | # a copy of this software and associated documentation files (the 32 | # "Software"), to deal in the Software without restriction, including 33 | # without limitation the rights to use, copy, modify, merge, publish, 34 | # distribute, sublicense, and/or sell copies of the Software, and to 35 | # permit persons to whom the Software is furnished to do so, subject to 36 | # the following conditions: 37 | 38 | # The above copyright notice and this permission notice shall be 39 | # included in all copies or substantial portions of the Software. 40 | 41 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 42 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 43 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 44 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 45 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 46 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 47 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 48 | 49 | # This class has dubious semantics and we only have it so that people can write 50 | # params[:key] instead of params['key']. 51 | class Mash < Hash 52 | 53 | # @param constructor 54 | # The default value for the mash. Defaults to an empty hash. 55 | # 56 | # @details [Alternatives] 57 | # If constructor is a Hash, a new mash will be created based on the keys of 58 | # the hash and no default value will be set. 59 | def initialize(constructor = {}) 60 | if constructor.is_a?(Hash) 61 | super() 62 | update(constructor) 63 | else 64 | super(constructor) 65 | end 66 | end 67 | 68 | # @param orig Mash being copied 69 | # 70 | # @return [Object] A new copied Mash 71 | def initialize_copy(orig) 72 | super 73 | # Handle nested values 74 | each do |k,v| 75 | if v.kind_of?(Mash) || v.is_a?(Array) 76 | self[k] = v.dup 77 | end 78 | end 79 | self 80 | end 81 | 82 | # @param key The default value for the mash. Defaults to nil. 83 | # 84 | # @details [Alternatives] 85 | # If key is a Symbol and it is a key in the mash, then the default value will 86 | # be set to the value matching the key. 87 | def default(key = nil) 88 | if key.is_a?(Symbol) && include?(key = key.to_s) 89 | self[key] 90 | else 91 | super 92 | end 93 | end 94 | 95 | alias_method :regular_writer, :[]= unless method_defined?(:regular_writer) 96 | alias_method :regular_update, :update unless method_defined?(:regular_update) 97 | 98 | # @param key The key to set. 99 | # @param value 100 | # The value to set the key to. 101 | # 102 | # @see Mash#convert_key 103 | # @see Mash#convert_value 104 | def []=(key, value) 105 | regular_writer(convert_key(key), convert_value(value)) 106 | end 107 | 108 | # @param other_hash 109 | # A hash to update values in the mash with. The keys and the values will be 110 | # converted to Mash format. 111 | # 112 | # @return [Mash] The updated mash. 113 | def update(other_hash) 114 | other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) } 115 | self 116 | end 117 | 118 | alias_method :merge!, :update 119 | 120 | # @param key The key to check for. This will be run through convert_key. 121 | # 122 | # @return [Boolean] True if the key exists in the mash. 123 | def key?(key) 124 | super(convert_key(key)) 125 | end 126 | 127 | # def include? def has_key? def member? 128 | alias_method :include?, :key? 129 | alias_method :has_key?, :key? 130 | alias_method :member?, :key? 131 | 132 | # @param key The key to fetch. This will be run through convert_key. 133 | # @param *extras Default value. 134 | # 135 | # @return [Object] The value at key or the default value. 136 | def fetch(key, *extras) 137 | super(convert_key(key), *extras) 138 | end 139 | 140 | # @param *indices 141 | # The keys to retrieve values for. These will be run through +convert_key+. 142 | # 143 | # @return [Array] The values at each of the provided keys 144 | def values_at(*indices) 145 | indices.collect {|key| self[convert_key(key)]} 146 | end 147 | 148 | # @param hash The hash to merge with the mash. 149 | # 150 | # @return [Mash] A new mash with the hash values merged in. 151 | def merge(hash) 152 | self.dup.update(hash) 153 | end 154 | 155 | # @param key 156 | # The key to delete from the mash.\ 157 | def delete(key) 158 | super(convert_key(key)) 159 | end 160 | 161 | # @param *rejected 1, :two => 2, :three => 3 }.except(:one) 167 | # #=> { "two" => 2, "three" => 3 } 168 | def except(*keys) 169 | super(*keys.map {|k| convert_key(k)}) 170 | end 171 | 172 | # Used to provide the same interface as Hash. 173 | # 174 | # @return [Mash] This mash unchanged. 175 | def stringify_keys!; self end 176 | 177 | # @return [Hash] The mash as a Hash with symbolized keys. 178 | def symbolize_keys 179 | h = Hash.new(default) 180 | each { |key, val| h[key.to_sym] = val } 181 | h 182 | end 183 | 184 | # @return [Hash] The mash as a Hash with string keys. 185 | def to_hash 186 | Hash.new(default).merge(self) 187 | end 188 | 189 | # @return [Mash] Convert a Hash into a Mash 190 | # The input Hash's default value is maintained 191 | def self.from_hash(hash) 192 | mash = Mash.new(hash) 193 | mash.default = hash.default 194 | mash 195 | end 196 | 197 | protected 198 | # @param key The key to convert. 199 | # 200 | # @param [Object] 201 | # The converted key. If the key was a symbol, it will be converted to a 202 | # string. 203 | # 204 | # @api private 205 | def convert_key(key) 206 | key.kind_of?(Symbol) ? key.to_s : key 207 | end 208 | 209 | # @param value The value to convert. 210 | # 211 | # @return [Object] 212 | # The converted value. A Hash or an Array of hashes, will be converted to 213 | # their Mash equivalents. 214 | # 215 | # @api private 216 | def convert_value(value) 217 | if value.class == Hash 218 | Mash.from_hash(value) 219 | elsif value.is_a?(Array) 220 | value.collect { |e| convert_value(e) } 221 | else 222 | value 223 | end 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Racker 2 | [![Gem Version](https://badge.fury.io/rb/racker.svg)](http://badge.fury.io/rb/racker) 3 | [![Build Status](https://travis-ci.org/aspring/racker.svg?branch=master)](https://travis-ci.org/aspring/racker) 4 | 5 | * Website: http://github.com/aspring/racker 6 | 7 | Racker is an opinionated Ruby DSL for generating Packer(www.packer.io) templates. 8 | 9 | Racker is able to take multiple Racker templates, merge them together, and generate a single Packer template. This process allows for deep merging Packer configuration pieces to give the user a more granular approach for creating and organizing templates, not currently possible with the Packer template format. 10 | 11 | ## Features 12 | 13 | * Allows for building Packer templates from DRY modular templates 14 | * Allows for commenting sections of the template 15 | * Supports use of knockouts when merging 16 | 17 | ## Installation 18 | 19 | $ gem install racker 20 | 21 | ## Usage 22 | To generate a Packer template from a Racker template, run: 23 | 24 | $ racker rackertemplate1.rb packertemplate.json 25 | 26 | To generate a Packer template from multiple Racker templates, run: 27 | 28 | $ racker rackertemplate1.rb rackertemplate2.rb packertemplate.json 29 | 30 | To merge multiple templates you can keep adding Racker templates: 31 | 32 | $ racker rackertemplate1.rb rackertemplate2.rb rackertemplate3.rb packertemplate.json 33 | 34 | The output of the previous command will be template 1 deep merged with template2, the result of this merge is deep merged with template 3 then output as a packer template. 35 | 36 | ## Racker Template Syntax 37 | The goal of Racker is to provide a template structure that allows for allowing full control of the template merging process to achieve the desired Packer template. 38 | 39 | In order to do this Racker takes an opinionated stance on the following: 40 | 41 | * All arrays within Packer Builder namespace are converted to hashes with well defined keys -- this allows for easy knockout capability based on key name. 42 | * The provisioners hash uses a Fixnum key to allow for defining an order that provisioners will be written to the Packer template. 43 | 44 | #### Base Template Syntax 45 | 46 | The most basic Racker template would be the following: 47 | 48 | ```ruby 49 | Racker::Processor.register_template do |t| 50 | end 51 | ``` 52 | 53 | This template would not define a variable, builder, provisioner or post-processor and would be a pretty boring template. 54 | 55 | #### Variables Namespace 56 | 57 | Racker templates support the `variables` namespace which is a hash. This hash maps one to one to a Packer template's variables section. 58 | 59 | This is an example of a basic `variables` definition: 60 | 61 | ```ruby 62 | Racker::Processor.register_template do |t| 63 | # Define the variables 64 | t.variables = { 65 | 'iso_checksum' => '6232efa014d9c6798396b63152c4c9a08b279f5e', 66 | 'iso_checksum_type' => 'sha1', 67 | 'iso_url' => 'http://mirrors.kernel.org/centos/6.4/isos/x86_64/CentOS-6.4-x86_64-minimal.iso', 68 | 'kickstart_file' => 'template1-ks.cfg', 69 | 'vagrant_output_file' => "./boxes/centos-6.4-{{.Provider}}.box" 70 | } 71 | end 72 | ``` 73 | 74 | #### Builders Namespace 75 | 76 | Racker templates support the `builders` namespace which is a hash, keyed by the name of the builder. 77 | 78 | All Packer arrays inside of this namespace should be represented as hashes in Racker. Racker will use the value when creating the template, the key is there purely for allowing you to override/knockout as necessary. 79 | 80 | This is an abbreviated example of adding a builder named 'test' that is a 'virtualbox-iso' builder type: 81 | 82 | ```ruby 83 | Racker::Processor.register_template do |t| 84 | # Define the builders 85 | t.builders['test'] = { 86 | 'type' => 'virtualbox-iso', 87 | 'guest_os_type' => 'RedHat_64', 88 | 'headless' => true, 89 | 'format' => 'ova', 90 | 'guest_additions_path' => "VBoxGuestAdditions_{{.Version}}.iso", 91 | 'iso_checksum' => "{{user `iso_checksum`}}", 92 | 'iso_checksum_type' => "{{user `iso_checksum_type`}}", 93 | 'iso_url' => "{{user `iso_url`}}", 94 | 'virtualbox_version_file' => '.vbox_version', 95 | 'boot_command' => { 96 | 0 => ' text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/{{user `kickstart_file`}}' 97 | }, 98 | 'boot_wait' => '10s', 99 | 'http_directory' => 'http_directory', 100 | 'ssh_port' => 22, 101 | 'ssh_username' => 'root', 102 | 'ssh_password' => 'asdfasdf', 103 | 'ssh_wait_timeout' => '10000s', 104 | 'shutdown_command' => 'shutdown -P now', 105 | 'disk_size' => 8096, 106 | 'vboxmanage' => { 107 | 'memory' => [ 'modifyvm', '{{.Name}}', '--memory', '1024' ], 108 | 'cpus' => [ 'modifyvm', '{{.Name}}', '--cpus', '1' ], 109 | 'ioapic' => [ 'modifyvm', '{{.Name}}', '--ioapic', 'on' ] 110 | } 111 | } 112 | end 113 | ``` 114 | One of the sections of node in this builder is the `vboxmanage`. It has been converted into a Hash to make it easier to knockout. 115 | 116 | #### Provisioners Namespace 117 | 118 | Racker templates support the `provisioners` namespace which is a Fixnum keyed hash. 119 | 120 | When generating the Packer template, Racker will order the provisioners based upon the Fixnum key, this allows complete control on the ordering of provisioners throughout Racker templates. 121 | 122 | ```ruby 123 | Racker::Processor.register_template do |t| 124 | # Define the provisioners 125 | t.provisioners = { 126 | 0 => { 127 | 'update-cacert-bundle' => { 128 | 'type' => 'shell', 129 | 'script' => 'scripts/common/update-cacert-bundle.sh' 130 | }, 131 | }, 132 | 300 => { 133 | 'install-chef-11.8.0' => { 134 | 'type' => 'shell', 135 | 'script' => 'scripts/common/install-chef-11.8.0.sh' 136 | }, 137 | }, 138 | 500 => { 139 | 'install-guest-additions-dependencies' => { 140 | 'type' => 'shell', 141 | 'scripts' => [ 142 | 'scripts/common/install-guest-additions.sh' 143 | ], 144 | }, 145 | }, 146 | 750 => { 147 | 'disable-services' => { 148 | 'type' => 'shell', 149 | 'script' => 'scripts/centos-6/disable-services.sh' 150 | }, 151 | }, 152 | 900 => { 153 | 'prepare-vagrant-instance' => { 154 | 'type' => 'shell', 155 | 'script' => 'scripts/common/prepare-vagrant-instance.sh', 156 | 'only' => ['test'] 157 | }, 158 | 'prepare-ec2-instance' => { 159 | 'type' => 'shell', 160 | 'script' => 'scripts/common/prepare-ec2-instance.sh', 161 | 'only' => ['amazon'] 162 | }, 163 | } 164 | } 165 | end 166 | ``` 167 | 168 | #### Post-Processors Namespace 169 | 170 | Racker templates support the `postprocessors` namespace which is a hash, keyed by the name of the post-processor. 171 | 172 | ```ruby 173 | Racker::Processor.register_template do |t| 174 | # Define the post-processors 175 | t.postprocessors['vagrant'] = { 176 | 'type' => 'vagrant', 177 | 'output' => '{{user `vagrant_output_file`}}', 178 | 'compression_level' => 7, 179 | 'keep_input_artifact' => true, 180 | 'only' => ['virtualbox-vagrant','vmware-vagrant'] 181 | } 182 | end 183 | ``` 184 | 185 | ### Putting it all together 186 | 187 | Racker offers 2 very basic example templates `example/template1.rb` and `example/template2.rb` as well as the resulting packer template from the two templates run through Racker. 188 | 189 | To experiement with these templates, after installing Racker, and cloning this repo you can execute the following: 190 | 191 | $ racker ./example/template1rb ./example/tempalte2.rb ./example/packer.json 192 | 193 | While these two templates are not all inclusive of the capabilities of Racker, it shows off the ability to have a basic template, and a second template that removes the pieces of the template that target Amazon, as well as adds two chef solo provisioner steps. 194 | 195 | ## Testing 196 | 197 | Run the tests with `rake`: 198 | ```bash 199 | # Ensure you've installed the bundle: 200 | bundle install 201 | 202 | # Run spec tests 203 | bundle exec rake spec 204 | 205 | # Run tests and generate code coverage statistics 206 | bundle exec rake coverage 207 | 208 | # Run RuboCop assessment 209 | bundle exec rake rubocop 210 | ``` 211 | 212 | ## Outstanding Development 213 | 214 | * The following builders have not been fully tested: 215 | * docker 216 | * qemu 217 | * Implement unit testing 218 | * Additional documentation work 219 | * Add capability to target specific packer versions should the packer template format change. 220 | * Add quick init to generate a basic Racker template 221 | 222 | ## Contributions 223 | 224 | Feel free to fork and request a pull, or submit a ticket 225 | https://github.com/aspring/racker/issues 226 | 227 | ## License 228 | 229 | This project is available under the MIT license. See LICENSE for details. 230 | -------------------------------------------------------------------------------- /lib/racker/smash/deep_merge_modified.rb: -------------------------------------------------------------------------------- 1 | module DeepMergeModified 2 | 3 | class InvalidParameter < StandardError; end 4 | 5 | DEFAULT_FIELD_KNOCKOUT_PREFIX = '--' 6 | 7 | # Deep Merge core documentation. 8 | # deep_merge! method permits merging of arbitrary child elements. The two top level 9 | # elements must be hashes. These hashes can contain unlimited (to stack limit) levels 10 | # of child elements. These child elements to not have to be of the same types. 11 | # Where child elements are of the same type, deep_merge will attempt to merge them together. 12 | # Where child elements are not of the same type, deep_merge will skip or optionally overwrite 13 | # the destination element with the contents of the source element at that level. 14 | # So if you have two hashes like this: 15 | # source = {:x => [1,2,3], :y => 2} 16 | # dest = {:x => [4,5,'6'], :y => [7,8,9]} 17 | # dest.deep_merge!(source) 18 | # Results: {:x => [1,2,3,4,5,'6'], :y => 2} 19 | # By default, "deep_merge!" will overwrite any unmergeables and merge everything else. 20 | # To avoid this, use "deep_merge" (no bang/exclamation mark) 21 | # 22 | # Options: 23 | # Options are specified in the last parameter passed, which should be in hash format: 24 | # hash.deep_merge!({:x => [1,2]}, {:knockout_prefix => '--'}) 25 | # :preserve_unmergeables DEFAULT: false 26 | # Set to true to skip any unmergeable elements from source 27 | # :knockout_prefix DEFAULT: nil 28 | # Set to string value to signify prefix which deletes elements from existing element 29 | # :sort_merged_arrays DEFAULT: false 30 | # Set to true to sort all arrays that are merged together 31 | # :unpack_arrays DEFAULT: nil 32 | # Set to string value to run "Array::join" then "String::split" against all arrays 33 | # :merge_hash_arrays DEFAULT: false 34 | # Set to true to merge hashes within arrays 35 | # :merge_debug DEFAULT: false 36 | # Set to true to get console output of merge process for debugging 37 | # 38 | # Selected Options Details: 39 | # :knockout_prefix => The purpose of this is to provide a way to remove elements 40 | # from existing Hash by specifying them in a special way in incoming hash 41 | # source = {:x => ['--1', '2']} 42 | # dest = {:x => ['1', '3']} 43 | # dest.ko_deep_merge!(source) 44 | # Results: {:x => ['2','3']} 45 | # Additionally, if the knockout_prefix is passed alone as a string, it will cause 46 | # the entire element to be removed: 47 | # source = {:x => '--'} 48 | # dest = {:x => [1,2,3]} 49 | # dest.ko_deep_merge!(source) 50 | # Results: {:x => ""} 51 | # :unpack_arrays => The purpose of this is to permit compound elements to be passed 52 | # in as strings and to be converted into discrete array elements 53 | # irsource = {:x => ['1,2,3', '4']} 54 | # dest = {:x => ['5','6','7,8']} 55 | # dest.deep_merge!(source, {:unpack_arrays => ','}) 56 | # Results: {:x => ['1','2','3','4','5','6','7','8'} 57 | # Why: If receiving data from an HTML form, this makes it easy for a checkbox 58 | # to pass multiple values from within a single HTML element 59 | # 60 | # :merge_hash_arrays => merge hashes within arrays 61 | # source = {:x => [{:y => 1}]} 62 | # dest = {:x => [{:z => 2}]} 63 | # dest.deep_merge!(source, {:merge_hash_arrays => true}) 64 | # Results: {:x => [{:y => 1, :z => 2}]} 65 | # 66 | # There are many tests for this library - and you can learn more about the features 67 | # and usages of deep_merge! by just browsing the test examples 68 | def self.deep_merge!(source, dest, options = {}) 69 | # turn on this line for stdout debugging text 70 | merge_debug = options[:merge_debug] || false 71 | overwrite_unmergeable = !options[:preserve_unmergeables] 72 | knockout_prefix = options[:knockout_prefix] || nil 73 | raise InvalidParameter, "knockout_prefix cannot be an empty string in deep_merge!" if knockout_prefix == "" 74 | raise InvalidParameter, "overwrite_unmergeable must be true if knockout_prefix is specified in deep_merge!" if knockout_prefix && !overwrite_unmergeable 75 | # if present: we will split and join arrays on this char before merging 76 | array_split_char = options[:unpack_arrays] || false 77 | # request that we sort together any arrays when they are merged 78 | sort_merged_arrays = options[:sort_merged_arrays] || false 79 | # request that arrays of hashes are merged together 80 | merge_hash_arrays = options[:merge_hash_arrays] || false 81 | di = options[:debug_indent] || '' 82 | # do nothing if source is nil 83 | return dest if source.nil? 84 | # if dest doesn't exist, then simply copy source to it 85 | if !(dest) && overwrite_unmergeable 86 | dest = source; return dest 87 | end 88 | 89 | puts "#{di}Source class: #{source.class.inspect} :: Dest class: #{dest.class.inspect}" if merge_debug 90 | if source.kind_of?(Hash) 91 | puts "#{di}Hashes: #{source.inspect} :: #{dest.inspect}" if merge_debug 92 | source.each do |src_key, src_value| 93 | if dest.kind_of?(Hash) 94 | puts "#{di} looping: #{src_key.inspect} => #{src_value.inspect} :: #{dest.inspect}" if merge_debug 95 | if dest[src_key] 96 | puts "#{di} ==>merging: #{src_key.inspect} => #{src_value.inspect} :: #{dest[src_key].inspect}" if merge_debug 97 | dest[src_key] = deep_merge!(src_value, dest[src_key], options.merge(:debug_indent => di + ' ')) 98 | else # dest[src_key] doesn't exist so we want to create and overwrite it (but we do this via deep_merge!) 99 | puts "#{di} ==>merging over: #{src_key.inspect} => #{src_value.inspect}" if merge_debug 100 | # note: we rescue here b/c some classes respond to "dup" but don't implement it (Numeric, TrueClass, FalseClass, NilClass among maybe others) 101 | begin 102 | src_dup = src_value.dup # we dup src_value if possible because we're going to merge into it (since dest is empty) 103 | rescue TypeError 104 | src_dup = src_value 105 | end 106 | dest[src_key] = deep_merge!(src_value, src_dup, options.merge(:debug_indent => di + ' ')) 107 | end 108 | else # dest isn't a hash, so we overwrite it completely (if permitted) 109 | if overwrite_unmergeable 110 | puts "#{di} overwriting dest: #{src_key.inspect} => #{src_value.inspect} -over-> #{dest.inspect}" if merge_debug 111 | dest = overwrite_unmergeables(source, dest, options) 112 | end 113 | end 114 | end 115 | elsif source.kind_of?(Array) 116 | puts "#{di}Arrays: #{source.inspect} :: #{dest.inspect}" if merge_debug 117 | # if we are instructed, join/split any source arrays before processing 118 | if array_split_char 119 | puts "#{di} split/join on source: #{source.inspect}" if merge_debug 120 | source = source.join(array_split_char).split(array_split_char) 121 | if dest.kind_of?(Array) 122 | dest = dest.join(array_split_char).split(array_split_char) 123 | end 124 | end 125 | # if there's a naked knockout_prefix in source, that means we are to truncate dest 126 | if source.index(knockout_prefix) 127 | dest = clear_or_nil(dest); source.delete(knockout_prefix) 128 | end 129 | if dest.kind_of?(Array) 130 | if knockout_prefix 131 | print "#{di} knocking out: " if merge_debug 132 | # remove knockout prefix items from both source and dest 133 | source.delete_if do |ko_item| 134 | retval = false 135 | item = ko_item.respond_to?(:gsub) ? ko_item.gsub(%r{^#{knockout_prefix}}, "") : ko_item 136 | if item != ko_item 137 | print "#{ko_item} - " if merge_debug 138 | dest.delete(item) 139 | dest.delete(ko_item) 140 | retval = true 141 | end 142 | retval 143 | end 144 | puts if merge_debug 145 | end 146 | puts "#{di} merging arrays: #{source.inspect} :: #{dest.inspect}" if merge_debug 147 | source_all_hashes = source.all? { |i| i.kind_of?(Hash) } 148 | dest_all_hashes = dest.all? { |i| i.kind_of?(Hash) } 149 | if merge_hash_arrays && source_all_hashes && dest_all_hashes 150 | # merge hashes in lists 151 | list = [] 152 | dest.each_index do |i| 153 | list[i] = deep_merge!(source[i] || {}, dest[i], 154 | options.merge(:debug_indent => di + ' ')) 155 | end 156 | list += source[dest.count..-1] if source.count > dest.count 157 | dest = list 158 | else 159 | dest = dest | source 160 | end 161 | dest.sort! if sort_merged_arrays 162 | elsif overwrite_unmergeable 163 | puts "#{di} overwriting dest: #{source.inspect} -over-> #{dest.inspect}" if merge_debug 164 | dest = overwrite_unmergeables(source, dest, options) 165 | end 166 | else # src_hash is not an array or hash, so we'll have to overwrite dest 167 | puts "#{di}Others: #{source.inspect} :: #{dest.inspect}" if merge_debug 168 | dest = overwrite_unmergeables(source, dest, options) 169 | end 170 | puts "#{di}Returning #{dest.inspect}" if merge_debug 171 | dest 172 | end # deep_merge! 173 | 174 | # allows deep_merge! to uniformly handle overwriting of unmergeable entities 175 | def self.overwrite_unmergeables(source, dest, options) 176 | merge_debug = options[:merge_debug] || false 177 | overwrite_unmergeable = !options[:preserve_unmergeables] 178 | knockout_prefix = options[:knockout_prefix] || false 179 | di = options[:debug_indent] || '' 180 | if knockout_prefix && overwrite_unmergeable 181 | if source.kind_of?(String) # remove knockout string from source before overwriting dest 182 | src_tmp = source.gsub(%r{^#{knockout_prefix}},"") 183 | elsif source.kind_of?(Array) # remove all knockout elements before overwriting dest 184 | src_tmp = source.delete_if {|ko_item| ko_item.kind_of?(String) && ko_item.match(%r{^#{knockout_prefix}}) } 185 | else 186 | src_tmp = source 187 | end 188 | if src_tmp == source # if we didn't find a knockout_prefix then we just overwrite dest 189 | puts "#{di}#{src_tmp.inspect} -over-> #{dest.inspect}" if merge_debug 190 | dest = src_tmp 191 | else # if we do find a knockout_prefix, then we just delete dest 192 | puts "#{di}\"\" -over-> #{dest.inspect}" if merge_debug 193 | dest = nil 194 | end 195 | elsif overwrite_unmergeable 196 | dest = source 197 | end 198 | dest 199 | end 200 | 201 | def self.clear_or_nil(obj) 202 | if obj.respond_to?(:clear) 203 | obj.clear 204 | else 205 | obj = nil 206 | end 207 | obj 208 | end 209 | 210 | end # module DeepMergeModified 211 | --------------------------------------------------------------------------------