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)
endView on GitHub (pinned to e227c27540)
Solutions
- Read the %{error} detail - e.inspect names the exact IOError class and message from the save.
- Verify privileges and path: run as root for /Library/Preferences, and make sure the parent directory exists and is writable by the current user.
- 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.
- If you need real error propagation, bypass the wrapper: File.write(path, Puppet::Util::Plist.dump_plist(data)) - dump_plist does not rescue.
- 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
- Never trust write_plist_file's return value; verify the file changed (mtime or re-read with read_plist_file) afterwards.
- Prefer dump_plist plus your own atomic File.write when you need failures raised.
- Check directory existence and writability before system plist paths, and run as root for /Library/Preferences.
- Remember the built-in rescue covers only IOError; Errno::* errors propagate - handle both in callers.
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
- Unknown plist format #{format}
- Unable to find launchd plist for job: #{label}
- Unable to read overrides plist, too many attempts
- Unable to parse launchd plist at path: #{job_path}
- No disk entities returned by mount at %{path}
AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21).
Data as JSON: /api/errors/84438e28dc9565a3.
Report an issue: GitHub.