puppetlabs/puppet · error · Errno::EISDIR

Is a directory: %{directory}

Error message

Is a directory: %{directory}

What it means

Puppet::Util.replace_file writes content to a tempfile then atomically renames it over the destination; on POSIX (the non-Windows branch) it explicitly checks whether the destination exists and is a directory and raises Errno::EISDIR with this message. MRI Ruby's own File.rename would raise EISDIR anyway; Puppet raises it early so JRuby (which would delete the directory) behaves the same. Nearly every atomic config write in Puppet (puppet.conf, agent state files, SSL certs) funnels through this helper.

Source

Thrown at lib/puppet/util.rb:677

      end

      if Puppet::Util::Platform.windows?
        # Windows ReplaceFile needs a file to exist, so touch handles this
        unless Puppet::FileSystem.exist?(file)
          Puppet::FileSystem.touch(file)
          if mode
            Puppet::Util::Windows::Security.set_mode(mode, Puppet::FileSystem.path_string(file))
          end
        end
        # Yes, the arguments are reversed compared to the rename in the rest
        # of the world.
        Puppet::Util::Windows::File.replace_file(FileSystem.path_string(file), tempfile.path)

      else
        # MRI Ruby checks for this and raises an error, while JRuby removes the directory
        # and replaces it with a file. This makes the our version of replace_file() consistent
        if Puppet::FileSystem.exist?(file) && Puppet::FileSystem.directory?(file)
          raise Errno::EISDIR, _("Is a directory: %{directory}") % { directory: file }
        end

        File.rename(tempfile.path, Puppet::FileSystem.path_string(file))
      end
    ensure
      # in case an error occurred before we renamed the temp file, make sure it
      # gets deleted
      if tempfile
        tempfile.close!
      end
    end

    # Ideally, we would now fsync the directory as well, but Ruby doesn't
    # have support for that, and it doesn't matter /that/ much...

    # Return something true, and possibly useful.
    file
  end

View on GitHub (pinned to e227c27540)

Solutions

  1. Remove or rename the offending directory: `rm -rf /etc/puppetlabs/puppet/puppet.conf` (verify it is truly a directory with `ls -ld` first)
  2. Audit manifests/provisioning for mkdir -p applied to file paths
  3. If calling replace_file yourself, pre-check File.directory?(target) and raise a clear application error

Example fix

# before
Puppet::Util.replace_file('/etc/puppetlabs/puppet/puppet.conf', 0o644) { |f| f.write(data) }
# raises Errno::EISDIR when puppet.conf is a directory

# after
require 'fileutils'
target = '/etc/puppetlabs/puppet/puppet.conf'
FileUtils.rm_rf(target) if File.directory?(target)
Puppet::Util.replace_file(target, 0o644) { |f| f.write(data) }
Defensive patterns

Strategy: validation

Validate before calling

raise Errno::EISDIR.new(target) if File.directory?(target)  # fail fast with your own context
FileUtils.rm_rf(target) if File.directory?(target)            # or clear it deliberately

Try / catch

begin
  Puppet::Util.replace_file(target, 0o644) { |f| f.write(data) }
rescue Errno::EISDIR => e
  raise Puppet::Error, "#{target} is a directory; remove it (rm -rf) and rerun: #{e.message}"
end

Prevention

When it happens

Trigger: Any Puppet write path where the target file path is occupied by a directory: someone ran mkdir /etc/puppetlabs/puppet/puppet.conf; a state file like last_run_summary.yaml replaced by a directory; calling Puppet::Util.replace_file(dir_path, mode) directly in custom code.

Common situations: Broken provisioning that mkdir -p's file paths; artifacts left by interrupted installs; operators 'protecting' a file by turning it into a directory; manifests that declare file { path: ensure => directory } colliding with Puppet's internal writes.

Related errors


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/c1bb06ecf02d23dc. Report an issue: GitHub.