puppetlabs/puppet · error · Puppet::Module::Plan::InvalidName

puppet.plans/invalid-name

puppet.plans/invalid-name

Error message

Plan names must start with a lowercase letter and be composed of only lowercase letters, numbers, and underscores

What it means

Puppet::Util::Plist.write_plist_file serializes a Ruby object graph to a plist via CFPropertyList and saves it to disk. The method rescues IOError, logs this Puppet.err with the inspected exception, and returns nil - callers get no exception, so a failed write is easy to miss unless logs are read or the file is stat'ed afterwards. It is the standard helper for Puppet's macOS plist writes. Because the rescue matches only IOError (and its subclass EOFError), Errno::* system call errors such as EACCES or ENOSPC are not caught here and propagate to the caller instead.

Source

Thrown at lib/puppet/module/plan.rb:120

    def self.plans_in_module(pup_module)
      # Search e.g. 'modules/<pup_module>/plans' for all plans
      plan_files = Dir.glob(File.join(pup_module.plans_directory, '*'))
                      .keep_if { |f| valid, _ = is_plans_filename?(f); valid }

      plans = plan_files.group_by { |f| plan_name_from_path(f) }

      plans.map do |plan, plan_filenames|
        new_with_files(pup_module, plan, plan_filenames)
      end
    end

    attr_reader :name, :module, :metadata_file

    # file paths must be relative to the modules plan directory
    def initialize(pup_module, plan_name, plan_files)
      valid, reason = Puppet::Module::Plan.is_plans_filename?(plan_files.first)
      unless valid
        raise InvalidName.new(plan_name, reason)
      end

      name = plan_name == "init" ? pup_module.name : "#{pup_module.name}::#{plan_name}"

      @module = pup_module
      @name = name
      @metadata_file = metadata_file
      @plan_files = plan_files || []
    end

    def metadata
      # Nothing to go here unless plans eventually support metadata.
      @metadata ||= {}
    end

    def files
      @files ||= self.class.find_files(@name, @plan_files)
    end

View on GitHub (pinned to e227c27540)

Solutions

  1. Read the %{error} detail - e.inspect names the exact IOError class and message from the save.
  2. Verify privileges and path: run as root for /Library/Preferences, and make sure the parent directory exists and is writable by the current user.
  3. After every write_plist_file call, verify success explicitly (File.exist?, mtime, or re-read with read_plist_file) because the method swallows the failure and returns nil.
  4. If you need real error propagation, bypass the wrapper: File.write(path, Puppet::Util::Plist.dump_plist(data)) - dump_plist does not rescue.
  5. Keep the object graph limited to CFPropertyList-convertible types (String, Integer, true/false, Array, Hash, Time, binary data) so conversion cannot fail regardless of I/O.

Example fix

# before: failure is swallowed - method returns nil, file may not exist
Puppet::Util::Plist.write_plist_file(data, '/Library/Preferences/com.example.app.plist')

# after: dump yourself and write atomically; errors now raise to the caller
require 'fileutils'
tmp = '/Library/Preferences/com.example.app.plist.tmp'
File.write(tmp, Puppet::Util::Plist.dump_plist(data))
FileUtils.mv(tmp, '/Library/Preferences/com.example.app.plist')
Defensive patterns

Strategy: validation

Validate before calling

# Pre-flight the write target before calling write_plist_file
def plist_target_writable?(file_path)
  dir = File.dirname(File.expand_path(file_path))
  File.directory?(dir) && File.writable?(dir) &&
    (!File.exist?(file_path) || File.writable?(file_path))
end

fail 'plist target not writable' unless plist_target_writable?(path)
Puppet::Util::Plist.write_plist_file(data, path)

Try / catch

# The wrapper swallows IOError; propagate errors yourself with dump_plist
begin
  File.write(path, Puppet::Util::Plist.dump_plist(data))
rescue IOError, SystemCallError => e
  raise "plist write failed for #{path}: #{e.message}"
end

Prevention

When it happens

Trigger: CFPropertyList's save raising IOError while writing file_path (closed stream or a write failure surfaced as IOError); writing to paths under root-owned directories like /Library/Preferences from a non-root process when the error surfaces as IOError; a target whose parent directory is missing. Permission and disk-space errors raised as Errno::* bypass this handler entirely.

Common situations: macOS providers and custom facts that write preference plists under restricted privileges; writes toward SIP-protected paths; automation code that assumes write_plist_file raises on failure and never checks the result, so files silently go unwritten.

Related errors


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