diff --git a/.gitignore b/.gitignore index 7af0955..0b71c30 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ tmp # editors [#]*[#] .\#* +.idea diff --git a/ERB_VARS.md b/ERB_VARS.md index 12cb5ef..7cf79d2 100644 --- a/ERB_VARS.md +++ b/ERB_VARS.md @@ -19,6 +19,14 @@ The Chef-based examples in the README illustrate the included Chef support. * `:chef_attributes` - a Hash of Chef attributes that is read as `node[chef_client][config]` by the [Opscode chef-client cookbook][1]. This is where you specify your `node_name` (if desired), `chef_server_url`, `validation_client_name`, etc. *Always use strings for chef attribute keys, not symbols!* +Ansible settings +---------------- +The Ansible-based examples in the README illustrate the included Ansible support. +* `:tower_url` - When set to any non-nil value, enables the built-in Ansible support. The value is the URL at which Ansible Tower listens for requests to run playbooks. +* `:tower_post_data_script` - The path to the script that will generate the body of the POST request made to the tower_url. +* `:tower_post_data_file` - The path to the file containing the POST request made to the tower_url. + + RAMdisk settings ---------------- * `:create_ramdisk` - Setting this to `true` generates a bootscript that creates a RAMdisk of a configurable size and at a configurable filesystem location. This happens even before the archive is unpacked, so you can extract files into the RAMdisk. diff --git a/README.md b/README.md index 11c7387..8440eff 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Why is it? ---------------- * makes specification of complex, cross-platform boot data simple and portable. * simplifies initial Chef setup +* enables an initial request to a Url for bootstrapping (as for Ansible) ---------------- Where is it? (Installation) @@ -34,7 +35,7 @@ Install the gem and its dependencies from RubyGems: ---------------- How is it [done]? (Usage) ---------------- -Call the gem's main public method: `Bootscript.generate()`. It accepts a Hash of template variables as its first argument, which is passed directly to any ERB template files as they render. All the data in the Hash is available to the templates, but some of the key-value pairs also control the gem's rendering behavior, as demonstrated in the following examples. (There's also a [list of such variables](ERB_VARS.md).) +Call the gem's main public method: `Bootscript.generate()`. It accepts a Hash of template variables as its first argument, which is passed directly to any ERB template files as they render. All the data in the Hash is available to the templates, but some of the key-value pairs also control the gem's rendering behavior, as demonstrated in the following examples. (There's also a [list of such variables](ERB_VARS.md).) ### Simplest - make a RAMdisk @@ -140,12 +141,47 @@ Finally, generate *without* an explicit node name, but filling in the other valu }, data_map) +### Ansible support + +The software's Ansible support is triggered when you pass a `:tower_url` option to the `generate()` method. +A request will be made to the specified url, sending data via POST. The data to send may require information that +is only available on the bootstrapped machine. The data to be POSTed is created by a script executed on the +bootstrapping node, immediately prior to making the POST request to `:tower_url`. The path to this script is +specified in the call to `generate()`: + + script = Bootscript.generate( + logger: Logger.new(STDOUT), # Monitor progress + tower_url: 'https://tower-host.foo.com', # Obtain from Ansible Tower administrators + tower_post_data_script: '/path/to/your/script', + tower_post_data_file: '/path/to/the/file', + ) + +The script specified by `:tower_post_data_script` is expected to create the file specified by `:tower_post_data_file`. +If the bootstrap call to `:tower_url` succeeds (i.e., the call to curl returns a zero exit status), both the data +file `:tower_post_data_file` and script `:tower_post_data_script` will be deleted from the filesystem. + +### Failure (inconceivable!) + +The `generate()` method accepts an option, `:startup_failed_command`, which is a string. If the startup command +exits with a non-zero status, the command configured with this option will be executed. One could, for example, +provide a template for a script that marks an EC2 instance as unhealthy using the AWS CLI, and then invoke that +script by supplying the path to the script as the value of `:startup_failed_command`. + +The default value for `:startup_failed_command` is an empty string, so the default is to take no action on failure. + +### Using Chef and Ansible together + +The code is written so that if `generate()` receives both `:chef_validation_pem` and `:tower_url` parameters, +installers for both Chef and Ansible will be created and executed. The installer for Chef will be executed first. Either +of these is complicated enough, so using just one seems wise. That said, one may need to switch from one to the other, +and having both available may assist in that transition. + ---------------- *Known Limitations / Bugs* ---------------- -* bash and tar are required on Unix boot targets +* bash, curl, and tar are required on Unix boot targets * Powershell is required on Windows boot targets -* bash, tar and uudecode are required to run the tests +* bash, tar, and uudecode are required to run the tests ---------------- diff --git a/lib/bootscript.rb b/lib/bootscript.rb index d168b18..3e8a623 100644 --- a/lib/bootscript.rb +++ b/lib/bootscript.rb @@ -3,6 +3,7 @@ require 'bootscript/script' require 'bootscript/uu_writer' require 'bootscript/chef' +require 'bootscript/ansible' # provides the software's only public method, generate() module Bootscript @@ -10,17 +11,22 @@ module Bootscript # These values are interpolated into all templates, and can be overridden # in calls to {Bootscript#generate} DEFAULT_VARS = { - platform: :unix, # or :windows - create_ramdisk: false, + platform: :unix, # or :windows + create_ramdisk: false, startup_command: '', # customized by platform if chef used - ramdisk_mount: '', # customized by platform, see platform_defaults - ramdisk_size: 20, # Megabytes + ramdisk_mount: '', # customized by platform, see platform_defaults + ramdisk_size: 20, # Megabytes add_script_tags: false, - script_name: 'bootscript', # base name of the boot script - strip_comments: true, - imdisk_url: 'http://www.ltr-data.se/files/imdiskinst.exe', - update_os: false, - inst_pkgs: 'bash openssl openssh-server' #list of packages you want upgraded + script_name: 'bootscript', # base name of the boot script + strip_comments: true, + imdisk_url: 'http://www.ltr-data.se/files/imdiskinst.exe', + update_os: false, + inst_pkgs: 'bash openssl openssh-server', #list of packages you want upgraded + curl_options: '', + bash_options: '', + use_chef: false, + use_ansible: false, + package_update: true } # Generates the full text of a boot script based on the supplied @@ -53,28 +59,54 @@ def self.default_logger(output = nil, level = Logger::FATAL) logger end + def self.powershell_cmd(script_path, script_log) + %Q{PowerShell -Command "& {#{script_path}}" > #{script_log} 2>&1} + end + # Returns the passed Hash of template vars, merged over a set of # computed, platform-specific default variables def self.merge_platform_defaults(vars) defaults = DEFAULT_VARS.merge(vars) + defaults[:use_chef] = Chef::included?(defaults) + defaults[:use_ansible] = Ansible::included?(defaults) + startup_cmd = [] if defaults[:platform].to_s == 'windows' defaults[:ramdisk_mount] = 'R:' defaults[:script_name] = 'bootscript.ps1' - if Chef::included?(defaults) - defaults[:startup_command] = 'PowerShell -Command "& '+ - '{C:/chef/chef-install.ps1}" > c:/chef/bootscript_setup.log 2>&1' + if defaults[:use_ansible] + startup_cmd << powershell_cmd( + 'C:/ansible/ansible-install.ps1', + 'C:/ansible/bootscript_setup.log') + defaults[:tower_post_data_file] = 'C:/ansible/ansible-post-data.txt' + end + if defaults[:use_chef] + startup_cmd << powershell_cmd( + 'C:/chef/chef-install.ps1', + 'C:/chef/bootscript_setup.log') end - else + if startup_cmd.size > 0 + defaults[:startup_command] = startup_cmd.join(' ; ') + end + else # unix! the only way it should be... defaults[:ramdisk_mount] = '/etc/secrets' defaults[:script_name] = 'bootscript.sh' - if Chef::included?(defaults) - defaults[:startup_command] = '/usr/local/sbin/chef-install.sh' + if defaults[:use_chef] + startup_cmd << 'chef-install.sh' + end + if defaults[:use_ansible] + startup_cmd << 'ansible-install.sh' + defaults[:tower_post_data_file] = '/tmp/ansible-post-data' + end + if startup_cmd.size > 0 + defaults[:startup_command] = startup_cmd.join(' && ') end end + defaults[:startup_failed_command] ||= '' + defaults[:startup_command] ||= 'echo "No bootstrap requested"' defaults.merge(vars) # return user vars merged over platform defaults end - BUILTIN_TEMPLATE_DIR = File.dirname(__FILE__)+"/templates" + BUILTIN_TEMPLATE_DIR = "#{File.dirname(__FILE__)}/templates" UNIX_TEMPLATE = "#{BUILTIN_TEMPLATE_DIR}/bootscript.sh.erb" WINDOWS_TEMPLATE = "#{BUILTIN_TEMPLATE_DIR}/bootscript.ps1.erb" diff --git a/lib/bootscript/ansible.rb b/lib/bootscript/ansible.rb new file mode 100644 index 0000000..dd0fecd --- /dev/null +++ b/lib/bootscript/ansible.rb @@ -0,0 +1,51 @@ +module Bootscript + # provides built-in Ansible templates and attributes + module Ansible + + # returns a map of the built-in Ansible templates, in the context of erb_vars + # The presence of :tower_url triggers the inclusion of Ansible + def self.files(erb_vars) + if Bootscript.windows?(erb_vars) + files_for_windows(erb_vars) + else + files_for_unix(erb_vars) + end + end + + # defines whether or not Ansible support will be included in the boot script, + # based on the presence of a certain key or keys in erb_vars + # @param [Hash] erb_vars template vars to use for determining Ansible inclusion + # @return [Boolean] true if erb_vars has the key :tower_url + def self.included?(erb_vars = {}) + erb_vars.has_key? :tower_url + end + + private + + # Callers will almost certainly replace the ansible-post-data-script.{sh,ps1}.erb + # template with their own. But they don't have to. We supply here a default + # script which simply creates an empty body for the POST request to the Ansible + # Tower url. + + def self.files_for_unix(erb_vars) + template_dir = "#{Bootscript::BUILTIN_TEMPLATE_DIR}/ansible" + { # built-in files + '/usr/local/sbin/ansible-install.sh' => + File.new("#{template_dir}/ansible-install.sh.erb"), + '/usr/local/sbin/ansible-post-data-script.sh' => + File.new("#{template_dir}/ansible-post-data-script.sh.erb"), + } + end + + def self.files_for_windows(erb_vars) + template_dir = "#{Bootscript::BUILTIN_TEMPLATE_DIR}/ansible" + { # built-in files + 'ansible/ansible-install.ps1' => + File.new("#{template_dir}/ansible-install.ps1.erb"), + 'ansible/ansible-post-data-script.ps1' => + File.new("#{template_dir}/ansible-post-data-script.ps1.erb"), + } + end + + end +end diff --git a/lib/bootscript/script.rb b/lib/bootscript/script.rb index fb99a6e..3eb3f55 100644 --- a/lib/bootscript/script.rb +++ b/lib/bootscript/script.rb @@ -123,13 +123,13 @@ def write_windows_archive(destination) # Archive::Tar::Minitar::Writer (if unix), or a Zip::OutputStream (windows) def render_data_map_into(archive) full_data_map.each do |remote_path, item| - if item.is_a? String # case 1: data item is a String + if item.is_a?(String) # case 1: data item is a String @log.debug "Rendering ERB data (#{item[0..16]}...) into archive" data = render_erb_text(item) input = StringIO.open(data, 'r') size = data.bytes.count elsif item.is_a?(File) # case 2: data item is an ERB file - if item.path.upcase.sub(/\A.*\./,'') == 'ERB' + if item.path.upcase.end_with?('.ERB') @log.debug "Rendering ERB file #{item.path} into archive" data = render_erb_text(item.read) input = StringIO.open(data, 'r') @@ -154,10 +154,11 @@ def render_data_map_into(archive) end end - # merges the @data_map with the Chef built-ins, as-needed + # merges the @data_map with the Chef and/or Ansible built-ins, as-needed def full_data_map - Bootscript::Chef.included?(@vars) ? - @data_map.merge(Bootscript::Chef.files(@vars)) : @data_map + ansible_vars = Ansible.included?(@vars) ? Ansible.files(@vars) : {} + chef_vars = Chef.included?(@vars) ? Chef.files(@vars) : {} + @data_map.merge(ansible_vars).merge(chef_vars) # Chef wins collisions. end # renders erb_text, using @vars @@ -170,8 +171,8 @@ def render_erb_text(erb_text) def strip_shell_comments(text) lines = text.lines.to_a return text if lines.count < 2 - lines.first + lines[1..lines.count]. - reject{|l| (l =~ /^\s*#/) || (l =~ /^\s+$/)}.join('') + lines.first + lines.drop(1). + reject { |l| (l =~ /^\s*#/) || (l =~ /^\s+$/) }.join('') end end diff --git a/lib/templates/ansible/ansible-install.ps1.erb b/lib/templates/ansible/ansible-install.ps1.erb new file mode 100644 index 0000000..3b0c231 --- /dev/null +++ b/lib/templates/ansible/ansible-install.ps1.erb @@ -0,0 +1,32 @@ +echo "Starting Ansible installation..." + +$AnsiblePath = "C:\Ansible" + +function main() +{ + try + { + Create-Ansible-Directory-Structure + Call-Ansible-Tower + } + catch { + write-error $error[0] + exit 1 + } +} + +function Create-Ansible-Directory-Structure() +{ + New-Item -force -type directory -Path $AnsiblePath | out-null +} + +function Call-Ansible-Tower() +{ + echo "Calling Ansible Tower..." + $wc = new-object System.Net.WebClient + $pathToFile = Resolve-Path <%= tower_post_data_file %> + $bootRequest = Get-Content $pathToFile -Raw + $wc.UploadString("<%= tower_url %>", $bootRequest) +} + +main diff --git a/lib/templates/ansible/ansible-install.sh.erb b/lib/templates/ansible/ansible-install.sh.erb new file mode 100644 index 0000000..182d0ee --- /dev/null +++ b/lib/templates/ansible/ansible-install.sh.erb @@ -0,0 +1,51 @@ +#!/usr/bin/env bash <%= bash_options %> +# Calls out to Ansible Tower to request bootstrapping actions. +set -e +test $UID == 0 || (echo "Error: must run as root"; exit 1) +echo "Calling Ansible Tower with ${0}..." + +######### STEP 0: CONFIG, rendered by bootscript gem +TOWER_URL="<%= tower_url %>" + +######### STEP 1: CONFIGURE OPERATING SYSTEM AND INSTALL DEPENDENCIES +<% if package_update %> +echo "Performing package update..." +if [ "$DistroBasedOn" == 'debian' ] ; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -y + echo "Installing build-essential and curl..." + apt-get --force-yes -y install build-essential curl +else + echo "Skipping..." +fi +<% else %> +echo "Initial package update not configured; skipping..." +<% end %> + +######### STEP 2: SIGNAL TO TOWER +echo "Call out to Ansible Tower to bootstrap us..." +if which curl > /dev/null; then + + # Run the command to create the file curl will read + <%= tower_post_data_script %> + + curl <%= curl_options %> \ + -H "Content-Type: application/json" \ + -X POST \ + $TOWER_URL \ + --data-binary "@<%= tower_post_data_file %>" + + exit_status="$?" + if [ "${exit_status}" != "0" ]; then + echo "The call to Ansible Tower (via curl) exited with status code: ${exit_status}" + exit $exit_status + else + echo "Cleaning up after successful curl request to ${TOWER_URL}" + rm -f "<%= tower_post_data_file %>" "<%= tower_post_data_script %>" + fi +else + echo "The curl command is not available; unable to signal Tower for bootstrap" + exit 1 +fi + +echo "Done." diff --git a/lib/templates/ansible/ansible-post-data-script.ps1.erb b/lib/templates/ansible/ansible-post-data-script.ps1.erb new file mode 100644 index 0000000..acea3f6 --- /dev/null +++ b/lib/templates/ansible/ansible-post-data-script.ps1.erb @@ -0,0 +1,3 @@ +echo "Creating data file for POST to Ansible Tower..." + +New-Item <%= tower_post_data_file %> -type file -force -value "{}" diff --git a/lib/templates/ansible/ansible-post-data-script.sh.erb b/lib/templates/ansible/ansible-post-data-script.sh.erb new file mode 100644 index 0000000..2b17918 --- /dev/null +++ b/lib/templates/ansible/ansible-post-data-script.sh.erb @@ -0,0 +1,4 @@ +#!/usr/bin/env bash <%= bash_options %> +# Creates the body of the POST request to Ansible Tower. + +echo "{}" > "<%= tower_post_data_file %>" diff --git a/lib/templates/bootscript.sh.erb b/lib/templates/bootscript.sh.erb index 9b580ee..a0651ab 100644 --- a/lib/templates/bootscript.sh.erb +++ b/lib/templates/bootscript.sh.erb @@ -148,39 +148,19 @@ rm -f $SCRIPT_PATH # this script removes itself! #################################### #### STEP 6 - Execute startup command echo "Executing user startup command..." -<% if defined? chef_validation_pem %> +<% if use_chef %> chmod 0744 /usr/local/sbin/chef-install.sh <% end %> -if ! <%= startup_command %> ; then - echo "Startup command failed. This system may be unhealthy." - - # The startup command failed so this instance is likely to be - # broken in some way. We should mark the instance as unhealthy - # if it's part of an AWS autoscaling group. This relies on the - # AWS CLI tools being installed. - - aws_cli_version=$(aws --version 2>&1 | awk '{print $1}' | awk -F/ '{print $2}') - aws_major=$(echo $aws_cli_version | awk -F. '{print $1}' | sed -e 's/[^0-9]//g') - aws_minor=$(echo $aws_cli_version | awk -F. '{print $2}' | sed -e 's/[^0-9]//g') - if [ "$aws_major" -le "1" -a "$aws_minor" -lt "11" ]; then - echo "The aws-cli package version is out of date; please upgrade your AMI." - else - if ec2_instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id) ; then - ec2_region=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e s'/.$//') - which aws >/dev/null && \ - aws autoscaling describe-auto-scaling-instances \ - --instance-ids $ec2_instance_id \ - --region $ec2_region \ - | grep -F $ec2_instance_id >/dev/null && - aws autoscaling set-instance-health \ - --instance-id $ec2_instance_id \ - --health-status Unhealthy \ - --no-should-respect-grace-period \ - --region $ec2_region - fi +<% if use_ansible %> +chmod 0744 /usr/local/sbin/ansible-install.sh +<% end %> - fi +<%= startup_command %> +exit_status="$?" +if [ "${exit_status}" != "0" ]; then + echo "Startup command failed (exit status: ${exit_status}). This system may be unhealthy." + <%= startup_failed_command %> exit 1 fi <% end %> diff --git a/lib/templates/chef/chef-install.sh.erb b/lib/templates/chef/chef-install.sh.erb index 838c3dc..63b2335 100644 --- a/lib/templates/chef/chef-install.sh.erb +++ b/lib/templates/chef/chef-install.sh.erb @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env bash <%= bash_options %> # Installs chef-client via the ombnibus installer (with minimal dependencies), # and tries to converge twice: first, with just this runlist - # recipe[chef-client::config],recipe[chef-client::service] @@ -33,6 +33,7 @@ if [ -e "$DATABAG_SECRET" ] ; then fi ######### STEP 2: CONFIGURE OPERATING SYSTEM AND INSTALL DEPENDENCIES +<% if package_update %> echo "Performing package update..." if [ "$DistroBasedOn" == 'debian' ] ; then export DEBIAN_FRONTEND=noninteractive @@ -42,6 +43,9 @@ if [ "$DistroBasedOn" == 'debian' ] ; then else echo "Skipping..." fi +<% else %> +echo "Initial package update not configured; skipping..." +<% end %> ######### STEP 3: INSTALL CHEF VIA OMNIBUS INSTALLER echo "Downloading Chef..." diff --git a/spec/bootscript/ansible_spec.rb b/spec/bootscript/ansible_spec.rb new file mode 100644 index 0000000..772c75b --- /dev/null +++ b/spec/bootscript/ansible_spec.rb @@ -0,0 +1,39 @@ +require 'bootscript' +include Bootscript + +describe Ansible do + + describe :files do + context "given a set of ERB template vars" do + erb_vars = { + tower_url: 'https://foo.imedidata.net' + } + it "returns a Hash mapping locations on the boot target to local data" do + expect(Ansible.files(erb_vars)).to be_a Hash + end + it "maps the Chef Validation data into place on the target's RAMdisk" do + expect(Ansible.files(erb_vars).keys.select { |k| k =~ /ansible-install/ }.size).to eq(1) + end + end + end + + describe :included? do + desired_key = :tower_url + context "given a set of ERB template vars with key :#{desired_key}" do + it "returns true" do + expect(Ansible.included?(tower_url: 'https://foo.imedidata.net')).to be true + end + end + context "given a set of ERB template vars without key :#{desired_key}" do + it "returns false" do + expect(Ansible.included?({})).to be false + end + end + context "given nothing" do + it "returns false" do + expect(Ansible.included?()).to be false + end + end + end + +end diff --git a/spec/bootscript/chef_spec.rb b/spec/bootscript/chef_spec.rb index 1fdb972..9a0a47b 100644 --- a/spec/bootscript/chef_spec.rb +++ b/spec/bootscript/chef_spec.rb @@ -11,17 +11,17 @@ chef_databag_secret: 'SECRET', } it "returns a Hash mapping locations on the boot target to local data" do - Chef.files(erb_vars).should be_a Hash + expect(Chef.files(erb_vars)).to be_a Hash end it "maps the Chef Validation data into place on the target's RAMdisk" do - Chef.files(erb_vars)[ + expect(Chef.files(erb_vars)[ "#{erb_vars[:ramdisk_mount]}/chef/validation.pem" - ].should be erb_vars[:chef_validation_pem] + ]).to be erb_vars[:chef_validation_pem] end it "maps the Chef data bag secret into place on the target's RAMdisk" do - Chef.files(erb_vars)[ + expect(Chef.files(erb_vars)[ "#{erb_vars[:ramdisk_mount]}/chef/encrypted_data_bag_secret" - ].should be erb_vars[:chef_databag_secret] + ]).to be erb_vars[:chef_databag_secret] end end end @@ -30,17 +30,17 @@ desired_key = :chef_validation_pem context "given a set of ERB template vars with key :#{desired_key}" do it "returns true" do - Chef.included?(chef_validation_pem: 'SOME DATA').should be true + expect(Chef.included?(chef_validation_pem: 'SOME DATA')).to be true end end context "given a set of ERB template vars without key :#{desired_key}" do it "returns false" do - Chef.included?({}).should be false + expect(Chef.included?({})).to be false end end context "given nothing" do it "returns false" do - Chef.included?().should be false + expect(Chef.included?()).to be false end end end diff --git a/spec/bootscript/script_spec.rb b/spec/bootscript/script_spec.rb index 8443a3c..6fb0d38 100644 --- a/spec/bootscript/script_spec.rb +++ b/spec/bootscript/script_spec.rb @@ -13,13 +13,13 @@ #### TEST PUBLIC INSTANCE MEMBER VARIABLES it "has a public @data_map Hash, for mapping local data to the boot target" do - Script.new().should respond_to(:data_map) - Script.new().data_map.should be_a Hash + expect(Script.new()).to respond_to(:data_map) + expect(Script.new().data_map).to be_a Hash end it "exposes a Ruby Logger as its public @log member, to adjust log level" do - Script.new().should respond_to(:log) - Script.new().log.should be_a Logger + expect(Script.new()).to respond_to(:log) + expect(Script.new().log).to be_a Logger end #### TEST PUBLIC METHODS @@ -28,12 +28,12 @@ context "when invoked with a logger" do it "sets the BootScript's @log to the passed Logger object" do my_logger = Logger.new(STDOUT) - Script.new(my_logger).log.should be my_logger + expect(Script.new(my_logger).log).to be my_logger end end context "when invoked with no logger" do it "assigns a default Logger the BootScript's @log" do - Script.new().log.should be_a Logger + expect(Script.new().log).to be_a Logger end end end @@ -51,23 +51,23 @@ end # test output format it "produces a Bash script" do - @script.generate.lines.first.chomp.should eq '#!/usr/bin/env bash' + expect(@script.generate.lines.first.chomp).to eq '#!/usr/bin/env bash' end # test stripping of empty lines and comments context "when invoked with :strip_comments = true (the default)" do it "strips all empty lines and comments from the output" do lines = @script.generate.lines.to_a lines[1..lines.count].each do |line| - line.should_not match /^#/ - line.should_not match /^\s+$/ + expect(line).to_not match /^#/ + expect(line).to_not match /^\s+$/ end end end context "when invoked with :strip_comments = false" do it "leaves empty lines and comments in the output" do lines = @script.generate(strip_comments: false).lines.to_a - lines.select{|l| l =~ /^\s+$/}.count.should be > 0 # check empty lines - lines.select{|l| l =~ /^#/}.count.should be > 1 # check comments + expect(lines.select{|l| l =~ /^\s+$/}).to_not be_empty # check empty lines + expect(lines.select{|l| l =~ /^#/}.size).to be > 1 # check comments end end @@ -77,7 +77,7 @@ vars.keys.each do |var| it "renders template variable :#{var} as Bash variable #{var.upcase}" do rendered_config = Unpacker.new(Script.new.generate(vars)).config - vars[var].to_s.should eq rendered_config[var.upcase.to_s] + expect(vars[var].to_s).to eq rendered_config[var.upcase.to_s] end end # test rendering of custom templates @@ -86,8 +86,8 @@ text = @script.generate(my_name: 'H. L. Mencken') Dir.mktmpdir do |tmp_dir| # do unarchiving in a temp dir Unpacker.new(text).unpack_to tmp_dir - File.exists?("#{tmp_dir}/hello.sh").should == true - File.read("#{tmp_dir}/hello.sh").should eq 'echo Hello, H. L. Mencken.' + expect(File.exists?("#{tmp_dir}/hello.sh")).to eq(true) + expect(File.read("#{tmp_dir}/hello.sh")).to eq 'echo Hello, H. L. Mencken.' end end # test raw file copying @@ -97,17 +97,17 @@ Dir.mktmpdir do |tmp_dir| # do unarchiving in a temp dir target_file = "#{tmp_dir}/#{File.basename(__FILE__)}" Unpacker.new(@script.generate).unpack_to tmp_dir - File.exists?(target_file).should == true - File.read(target_file).should eq File.read(__FILE__) + expect(File.exists?(target_file)).to eq(true) + expect(File.read(target_file)).to eq File.read(__FILE__) end end # test return values context "when invoked without any output destination" do it "returns the rendered text of the BootScript" do rendered_text = @script.generate - rendered_text.should be_a String - rendered_text.lines.first.chomp.should eq '#!/usr/bin/env bash' - rendered_text.lines.should include("__ARCHIVE_FOLLOWS__\n") + expect(rendered_text).to be_a String + expect(rendered_text.lines.first.chomp).to eq '#!/usr/bin/env bash' + expect(rendered_text.lines).to include("__ARCHIVE_FOLLOWS__\n") end end context "when invoked with a custom output destination" do @@ -116,8 +116,8 @@ File.open('/dev/null', 'w') do |outfile| bytes_written = @script.generate({}, outfile) end - bytes_written.should be_a Fixnum - bytes_written.should == script_size + expect(bytes_written).to be_a Fixnum + expect(bytes_written).to eq(script_size) end end diff --git a/spec/bootscript/uu_writer_spec.rb b/spec/bootscript/uu_writer_spec.rb index 29f0545..2f0942c 100644 --- a/spec/bootscript/uu_writer_spec.rb +++ b/spec/bootscript/uu_writer_spec.rb @@ -6,15 +6,15 @@ #### TEST PUBLIC INSTANCE MEMBER VARIABLES it "exposes a the number of bytes written as an integer" do - UUWriter.new(nil).should respond_to(:bytes_written) - UUWriter.new(nil).bytes_written.should be_a Fixnum + expect(UUWriter.new(nil)).to respond_to(:bytes_written) + expect(UUWriter.new(nil).bytes_written).to be_a Fixnum end #### TEST PUBLIC METHODS describe :initialize do it "sets bytes_written to zero" do - UUWriter.new(nil).bytes_written.should == 0 + expect(UUWriter.new(nil).bytes_written).to eq 0 end end @@ -23,7 +23,7 @@ destination = StringIO.open("", 'w') UUWriter.new(destination).write("Encode me!") destination.close - destination.string.should == ["Encode me!"].pack('m') + expect(destination.string).to eq ["Encode me!"].pack('m') end end diff --git a/spec/bootscript_spec.rb b/spec/bootscript_spec.rb index de2db98..a281187 100644 --- a/spec/bootscript_spec.rb +++ b/spec/bootscript_spec.rb @@ -7,8 +7,8 @@ :create_ramdisk, :ramdisk_size, :ramdisk_mount, :add_script_tags, :inst_pkgs ].each do |required_var| - Bootscript::DEFAULT_VARS.should include(required_var) - Bootscript::DEFAULT_VARS[required_var].should_not be nil + expect(Bootscript::DEFAULT_VARS).to include(required_var) + expect(Bootscript::DEFAULT_VARS[required_var]).to_not eq nil end end end @@ -23,13 +23,13 @@ Bootscript.generate(@template_vars, @data_map) end it "creates a Script with the same data map" do - Bootscript::Script.stub(:new).and_return @script - @script.should_receive(:data_map=).with @data_map + allow(Bootscript::Script).to receive(:new).and_return @script + expect(@script).to receive(:data_map=).with @data_map Bootscript.generate(@template_vars, @data_map) end it "calls generate() on the script, passing the same template vars" do - Bootscript::Script.stub(:new).and_return @script - @script.should_receive(:generate).with(@template_vars, nil) + allow(Bootscript::Script).to receive(:new).and_return @script + expect(@script).to receive(:generate).with(@template_vars, nil) Bootscript.generate(@template_vars, @data_map) end end @@ -39,14 +39,14 @@ [:windows, :WinDoWs, 'windows', 'WINDOWS'].each do |value| context "its Hash argument has :platform => #{value} (#{value.class})" do it "returns true" do - Bootscript.windows?(platform: value).should be true + expect(Bootscript.windows?(platform: value)).to eq(true) end end end [:unix, :OS_X, 'other', 'randomstring0940358'].each do |value| context "its Hash argument has :platform => #{value} (#{value.class})" do it "returns false" do - Bootscript.windows?(platform: value).should be false + expect(Bootscript.windows?(platform: value)).to eq(false) end end end @@ -56,14 +56,14 @@ context "with no arguments" do it "returns a Ruby Logger with LOG_LEVEL set to FATAL" do logger = Bootscript.default_logger - logger.level.should be Logger::FATAL + expect(logger.level).to be Logger::FATAL end end context "with a specific log level" do [Logger::DEBUG, Logger::INFO, Logger::WARN].each do |log_level| it "returns a standard Ruby Logger with level #{log_level}" do logger = Bootscript.default_logger(STDOUT, log_level) - logger.level.should be log_level + expect(logger.level).to be log_level end end end