diff --git a/lib/puppet/application/module.rb b/lib/puppet/application/module.rb deleted file mode 100644 index 0b1c20ef6..000000000 --- a/lib/puppet/application/module.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'puppet/application/face_base' - -class Puppet::Application::Module < Puppet::Application::FaceBase; end diff --git a/lib/puppet/face/module.rb b/lib/puppet/face/module.rb deleted file mode 100644 index bab553c2f..000000000 --- a/lib/puppet/face/module.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'puppet/face' -require 'puppet/module_tool' - -Puppet::Face.define(:module, '1.0.0') do - copyright "Puppet Labs", 2011 - license "Apache 2 license; see COPYING" - - summary "Creates, installs and searches for modules on the Puppet Forge." - description <<-EOT - Creates, installs and searches for modules on the Puppet Forge. - EOT -end diff --git a/lib/puppet/face/module/list.rb b/lib/puppet/face/module/list.rb new file mode 100644 index 000000000..772990069 --- /dev/null +++ b/lib/puppet/face/module/list.rb @@ -0,0 +1,64 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:list) do + summary "List installed modules" + description <<-HEREDOC + List puppet modules from a specific environment, specified modulepath or + default to listing modules in the default modulepath: + #{Puppet.settings[:modulepath]} + HEREDOC + returns "hash of paths to module objects" + + option "--env ENVIRONMENT" do + summary "Which environments' modules to list" + end + + option "--modulepath MODULEPATH" do + summary "Which directories to look for modules in" + end + + examples <<-EOT + List installed modules: + + $ puppet module list + /etc/puppet/modules + bacula (0.0.2) + /usr/share/puppet/modules + apache (0.0.3) + bacula (0.0.1) + + List installed modules from a specified environment: + + $ puppet module list --env 'test' + /tmp/puppet/modules + rrd (0.0.2) + + List installed modules from a specified modulepath: + + $ puppet module list --modulepath /tmp/facts1:/tmp/facts2 + /tmp/facts1 + stdlib + /tmp/facts2 + nginx (1.0.0) + EOT + + when_invoked do |options| + Puppet[:modulepath] = options[:modulepath] if options[:modulepath] + environment = Puppet::Node::Environment.new(options[:env]) + + environment.modules_by_path + end + + when_rendering :console do |modules_by_path| + output = '' + modules_by_path.each do |path, modules| + output << "#{path}\n" + modules.each do |mod| + version_string = mod.version ? "(#{mod.version})" : '' + output << " #{mod.name} #{version_string}\n" + end + end + output + end + + end +end diff --git a/lib/puppet/face/module/uninstall.rb b/lib/puppet/face/module/uninstall.rb new file mode 100644 index 000000000..19c983ea6 --- /dev/null +++ b/lib/puppet/face/module/uninstall.rb @@ -0,0 +1,50 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:uninstall) do + summary "Uninstall a puppet module." + description <<-EOT + Uninstall a puppet module from the modulepath or a specific + target directory which defaults to + #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).join(', ')}. + EOT + + returns "Array of strings representing paths of uninstalled files." + + examples <<-EOT + Uninstall a module from all directories in the modulepath: + + $ puppet module uninstall ssh + Removed /etc/puppet/modules/ssh + + Uninstall a module from a specific directory: + + $ puppet module uninstall --target-directory /usr/share/puppet/modules ssh + Removed /usr/share/puppet/modules/ssh + EOT + + arguments "" + + option "--target-directory=", "-t=" do + default_to { Puppet.settings[:modulepath].split(File::PATH_SEPARATOR) } + summary "The target directory to search from modules." + description <<-EOT + The target directory to search for modules. + EOT + end + + when_invoked do |name, options| + + if options[:target_directory].is_a?(Array) + options[:target_directories] = options[:target_directory] + else + options[:target_directories] = [ options[:target_directory] ] + end + options.delete(:target_directory) + + Puppet::Module::Tool::Applications::Uninstaller.run(name, options) + end + + when_rendering :console do |removed_modules| + removed_modules.map { |path| "Removed #{path}" }.join('\n') + end + end +end diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index 6f84e077c..6d5c4d461 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,202 +1,198 @@ require 'puppet/util/logging' # Support for modules class Puppet::Module class Error < Puppet::Error; end class MissingModule < Error; end class IncompatibleModule < Error; end class UnsupportedPlatform < Error; end class IncompatiblePlatform < Error; end class MissingMetadata < Error; end class InvalidName < Error; end include Puppet::Util::Logging TEMPLATES = "templates" FILES = "files" MANIFESTS = "manifests" PLUGINS = "plugins" FILETYPES = [MANIFESTS, FILES, TEMPLATES, PLUGINS] - # Return an array of paths by splitting the +modulepath+ config - # parameter. Only consider paths that are absolute and existing - # directories - def self.modulepath(environment = nil) - Puppet::Node::Environment.new(environment).modulepath - end - # Find and return the +module+ that +path+ belongs to. If +path+ is # absolute, or if there is no module whose name is the first component # of +path+, return +nil+ def self.find(modname, environment = nil) return nil unless modname Puppet::Node::Environment.new(environment).module(modname) end attr_reader :name, :environment attr_writer :environment attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page def has_metadata? return false unless metadata_file return false unless FileTest.exist?(metadata_file) metadata = PSON.parse File.read(metadata_file) return metadata.is_a?(Hash) && !metadata.keys.empty? end - def initialize(name, environment = nil) + def initialize(name, options = {}) @name = name + @path = options[:path] assert_validity - if environment.is_a?(Puppet::Node::Environment) - @environment = environment + if options[:environment].is_a?(Puppet::Node::Environment) + @environment = options[:environment] else - @environment = Puppet::Node::Environment.new(environment) + @environment = Puppet::Node::Environment.new(options[:environment]) end load_metadata if has_metadata? validate_puppet_version validate_dependencies end FILETYPES.each do |type| # A boolean method to let external callers determine if # we have files of a given type. define_method(type +'?') do return false unless path return false unless FileTest.exist?(subpath(type)) return true end # A method for returning a given file of a given type. # e.g., file = mod.manifest("my/manifest.pp") # # If the file name is nil, then the base directory for the # file type is passed; this is used for fileserving. define_method(type.to_s.sub(/s$/, '')) do |file| return nil unless path # If 'file' is nil then they're asking for the base path. # This is used for things like fileserving. if file full_path = File.join(subpath(type), file) else full_path = subpath(type) end return nil unless FileTest.exist?(full_path) return full_path end end def exist? ! path.nil? end def license_file return @license_file if defined?(@license_file) return @license_file = nil unless path @license_file = File.join(path, "License") end def load_metadata data = PSON.parse File.read(metadata_file) [:source, :author, :version, :license, :puppetversion].each do |attr| unless value = data[attr.to_s] unless attr == :puppetversion raise MissingMetadata, "No #{attr} module metadata provided for #{self.name}" end end send(attr.to_s + "=", value) end end # Return the list of manifests matching the given glob pattern, # defaulting to 'init.{pp,rb}' for empty modules. def match_manifests(rest) pat = File.join(path, MANIFESTS, rest || 'init') [manifest("init.pp"),manifest("init.rb")].compact + Dir. glob(pat + (File.extname(pat).empty? ? '.{pp,rb}' : '')). reject { |f| FileTest.directory?(f) } end def metadata_file return @metadata_file if defined?(@metadata_file) return @metadata_file = nil unless path @metadata_file = File.join(path, "metadata.json") end # Find this module in the modulepath. def path - environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.directory?(d) } + @path ||= environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.directory?(d) } end # Find all plugin directories. This is used by the Plugins fileserving mount. def plugin_directory subpath("plugins") end - def requires(name, version = nil) - @requires ||= [] - @requires << [name, version] - end - def supports(name, version = nil) @supports ||= [] @supports << [name, version] end def to_s result = "Module #{name}" result += "(#{path})" if path result end def validate_dependencies return unless defined?(@requires) @requires.each do |name, version| unless mod = environment.module(name) raise MissingModule, "Missing module #{name} required by #{self.name}" end if version and mod.version != version raise IncompatibleModule, "Required module #{name} is version #{mod.version} but #{self.name} requires #{version}" end end end def validate_puppet_version return unless puppetversion and puppetversion != Puppet.version raise IncompatibleModule, "Module #{self.name} is only compatible with Puppet version #{puppetversion}, not #{Puppet.version}" end private def subpath(type) return File.join(path, type) unless type.to_s == "plugins" backward_compatible_plugins_dir end def backward_compatible_plugins_dir if dir = File.join(path, "plugins") and FileTest.exist?(dir) Puppet.warning "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" return dir else return File.join(path, "lib") end end def assert_validity raise InvalidName, "Invalid module name; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/ end + + def ==(other) + self.name == other.name && + self.version == other.version && + self.path == other.path && + self.environment == other.environment + end end diff --git a/lib/puppet/module_tool/applications.rb b/lib/puppet/module_tool/applications.rb index e7d54dc00..24bcbe279 100644 --- a/lib/puppet/module_tool/applications.rb +++ b/lib/puppet/module_tool/applications.rb @@ -1,12 +1,13 @@ module Puppet::Module::Tool module Applications require 'puppet/module_tool/applications/application' require 'puppet/module_tool/applications/builder' require 'puppet/module_tool/applications/checksummer' require 'puppet/module_tool/applications/cleaner' require 'puppet/module_tool/applications/generator' require 'puppet/module_tool/applications/installer' require 'puppet/module_tool/applications/searcher' require 'puppet/module_tool/applications/unpacker' + require 'puppet/module_tool/applications/uninstaller' end end diff --git a/lib/puppet/module_tool/applications/uninstaller.rb b/lib/puppet/module_tool/applications/uninstaller.rb new file mode 100644 index 000000000..9cd4d8bd3 --- /dev/null +++ b/lib/puppet/module_tool/applications/uninstaller.rb @@ -0,0 +1,33 @@ +module Puppet::Module::Tool + module Applications + class Uninstaller < Application + + def initialize(name, options = {}) + @name = name + @target_directories = options[:target_directories] + @removed_dirs = [] + end + + def run + uninstall + Puppet.notice "#{@name} is not installed" if @removed_dirs.empty? + @removed_dirs + end + + private + + def uninstall + # TODO: #11803 Check for broken dependencies before uninstalling modules. + # + # Search each path in the target directories for the specified module + # and delete the directory. + @target_directories.each do |target| + if File.directory? target + module_path = File.join(target, @name) + @removed_dirs << FileUtils.rm_rf(module_path).first if File.directory?(module_path) + end + end + end + end + end +end diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb index 4fc314a6a..326809050 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -1,176 +1,189 @@ require 'puppet/util/cacher' require 'monitor' # Just define it, so this class has fewer load dependencies. class Puppet::Node end # Model the environment that a node can operate in. This class just # provides a simple wrapper for the functionality around environments. class Puppet::Node::Environment module Helper def environment Puppet::Node::Environment.new(@environment) end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = env else @environment = env.name end end end include Puppet::Util::Cacher @seen = {} # Return an existing environment instance, or create a new one. def self.new(name = nil) return name if name.is_a?(self) name ||= Puppet.settings.value(:environment) raise ArgumentError, "Environment name must be specified" unless name symbol = name.to_sym return @seen[symbol] if @seen[symbol] obj = self.allocate obj.send :initialize, symbol @seen[symbol] = obj end def self.current Thread.current[:environment] || root end def self.current=(env) Thread.current[:environment] = new(env) end def self.root @root end - # This is only used for testing. def self.clear @seen.clear end attr_reader :name # Return an environment-specific setting. def [](param) Puppet.settings.value(param, self.name) end def initialize(name) @name = name extend MonitorMixin end def known_resource_types # This makes use of short circuit evaluation to get the right thread-safe # per environment semantics with an efficient most common cases; we almost # always just return our thread's known-resource types. Only at the start # of a compilation (after our thread var has been set to nil) or when the # environment has changed do we delve deeper. Thread.current[:known_resource_types] = nil if (krt = Thread.current[:known_resource_types]) && krt.environment != self Thread.current[:known_resource_types] ||= synchronize { if @known_resource_types.nil? or @known_resource_types.require_reparse? @known_resource_types = Puppet::Resource::TypeCollection.new(self) @known_resource_types.import_ast(perform_initial_import, '') end @known_resource_types } end def module(name) - mod = Puppet::Module.new(name, self) + mod = Puppet::Module.new(name, :environment => self) return nil unless mod.exist? mod end # Cache the modulepath, so that we aren't searching through # all known directories all the time. cached_attr(:modulepath, Puppet[:filetimeout]) do dirs = self[:modulepath].split(File::PATH_SEPARATOR) dirs = ENV["PUPPETLIB"].split(File::PATH_SEPARATOR) + dirs if ENV["PUPPETLIB"] validate_dirs(dirs) end # Return all modules from this environment. # Cache the list, because it can be expensive to create. cached_attr(:modules, Puppet[:filetimeout]) do module_names = modulepath.collect { |path| Dir.entries(path) }.flatten.uniq module_names.collect do |path| begin - Puppet::Module.new(path, self) + Puppet::Module.new(path, :environment => self) rescue Puppet::Module::Error => e nil end end.compact end + # Modules broken out by directory in the modulepath + def modules_by_path + modules_by_path = {} + modulepath.each do |path| + Dir.chdir(path) do + module_names = Dir.glob('*').select { |d| FileTest.directory? d } + modules_by_path[path] = module_names.map do |name| + Puppet::Module.new(name, :environment => self, :path => File.join(path, name)) + end + end + end + modules_by_path + end + def to_s name.to_s end def to_sym to_s.to_sym end # The only thing we care about when serializing an environment is its # identity; everything else is ephemeral and should not be stored or # transmitted. def to_zaml(z) self.to_s.to_zaml(z) end def validate_dirs(dirs) dir_regex = Puppet.features.microsoft_windows? ? /^[A-Za-z]:#{File::SEPARATOR}/ : /^#{File::SEPARATOR}/ # REMIND: Dir.getwd on windows returns a path containing backslashes, which when joined with # dir containing forward slashes, breaks our regex matching. In general, path validation needs # to be refactored which will be handled in a future commit. dirs.collect do |dir| if dir !~ dir_regex File.expand_path(File.join(Dir.getwd, dir)) else dir end end.find_all do |p| p =~ dir_regex && FileTest.directory?(p) end end private def perform_initial_import return empty_parse_result if Puppet.settings[:ignoreimport] parser = Puppet::Parser::Parser.new(self) if code = Puppet.settings.uninterpolated_value(:code, name.to_s) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, name.to_s) parser.file = file end parser.parse rescue => detail known_resource_types.parse_failed = true msg = "Could not parse for environment #{self}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end def empty_parse_result # Return an empty toplevel hostclass to use as the result of # perform_initial_import when no file was actually loaded. return Puppet::Parser::AST::Hostclass.new('') end @root = new(:'*root*') end diff --git a/lib/puppet/parser/type_loader.rb b/lib/puppet/parser/type_loader.rb index 68def068d..3cba89515 100644 --- a/lib/puppet/parser/type_loader.rb +++ b/lib/puppet/parser/type_loader.rb @@ -1,174 +1,174 @@ require 'puppet/node/environment' class Puppet::Parser::TypeLoader include Puppet::Node::Environment::Helper # Helper class that makes sure we don't try to import the same file # more than once from either the same thread or different threads. class Helper include MonitorMixin def initialize super # These hashes are indexed by filename @state = {} # :doing or :done @thread = {} # if :doing, thread that's doing the parsing @cond_var = {} # if :doing, condition var that will be signaled when done. end # Execute the supplied block exactly once per file, no matter how # many threads have asked for it to run. If another thread is # already executing it, wait for it to finish. If this thread is # already executing it, return immediately without executing the # block. # # Note: the reason for returning immediately if this thread is # already executing the block is to handle the case of a circular # import--when this happens, we attempt to recursively re-parse a # file that we are already in the process of parsing. To prevent # an infinite regress we need to simply do nothing when the # recursive import is attempted. def do_once(file) need_to_execute = synchronize do case @state[file] when :doing if @thread[file] != Thread.current @cond_var[file].wait end false when :done false else @state[file] = :doing @thread[file] = Thread.current @cond_var[file] = new_cond true end end if need_to_execute begin yield ensure synchronize do @state[file] = :done @thread.delete(file) @cond_var.delete(file).broadcast end end end end end # Import our files. def import(file, current_file = nil) return if Puppet[:ignoreimport] # use a path relative to the file doing the importing if current_file dir = current_file.sub(%r{[^/]+$},'').sub(/\/$/, '') else dir = "." end if dir == "" dir = "." end pat = file modname, files = Puppet::Parser::Files.find_manifests(pat, :cwd => dir, :environment => environment) if files.size == 0 raise Puppet::ImportError.new("No file(s) found for import of '#{pat}'") end loaded_asts = [] files.each do |file| regex = Puppet.features.microsoft_windows? ? /^[A-Za-z]:#{File::SEPARATOR}/ : /^#{File::SEPARATOR}/ unless file =~ regex file = File.join(dir, file) end @loading_helper.do_once(file) do loaded_asts << parse_file(file) end end loaded_asts.inject([]) do |loaded_types, ast| loaded_types + known_resource_types.import_ast(ast, modname) end end def import_all require 'find' module_names = [] # Collect the list of all known modules environment.modulepath.each do |path| Dir.chdir(path) do Dir.glob("*").each do |dir| next unless FileTest.directory?(dir) module_names << dir end end end module_names.uniq! # And then load all files from each module, but (relying on system # behavior) only load files from the first module of a given name. E.g., # given first/foo and second/foo, only files from first/foo will be loaded. module_names.each do |name| - mod = Puppet::Module.new(name, environment) + mod = Puppet::Module.new(name, :environment => environment) Find.find(File.join(mod.path, "manifests")) do |path| if path =~ /\.pp$/ or path =~ /\.rb$/ import(path) end end end end def known_resource_types environment.known_resource_types end def initialize(env) self.environment = env @loading_helper = Helper.new end # Try to load the object with the given fully qualified name. def try_load_fqname(type, fqname) return nil if fqname == "" # special-case main. name2files(fqname).each do |filename| begin imported_types = import(filename) if result = imported_types.find { |t| t.type == type and t.name == fqname } Puppet.debug "Automatically imported #{fqname} from #{filename} into #{environment}" return result end rescue Puppet::ImportError => detail # We couldn't load the item # I'm not convienced we should just drop these errors, but this # preserves existing behaviours. end end # Nothing found. return nil end def parse_file(file) Puppet.debug("importing '#{file}' in environment #{environment}") parser = Puppet::Parser::Parser.new(environment) parser.file = file return parser.parse end private # Return a list of all file basenames that should be tried in order # to load the object with the given fully qualified name. def name2files(fqname) result = [] ary = fqname.split("::") while ary.length > 0 result << ary.join(File::SEPARATOR) ary.pop end return result end end diff --git a/lib/puppet/provider/group/pw.rb b/lib/puppet/provider/group/pw.rb index a054d1ff1..b6c74f766 100644 --- a/lib/puppet/provider/group/pw.rb +++ b/lib/puppet/provider/group/pw.rb @@ -1,34 +1,48 @@ require 'puppet/provider/nameservice/pw' Puppet::Type.type(:group).provide :pw, :parent => Puppet::Provider::NameService::PW do - desc "Group management via `pw`. + desc "Group management via `pw` on FreeBSD." - Only works on FreeBSD. + commands :pw => "pw" + has_features :manages_members - " - - commands :pw => "/usr/sbin/pw" defaultfor :operatingsystem => :freebsd + options :members, :flag => "-M", :method => :mem + verify :gid, "GID must be an integer" do |value| value.is_a? Integer end def addcmd cmd = [command(:pw), "groupadd", @resource[:name]] + if gid = @resource.should(:gid) unless gid == :absent cmd << flag(:gid) << gid end end - # Apparently, contrary to the man page, groupadd does - # not accept -o. - #if @parent[:allowdupe] == :true - # cmd << "-o" - #end + if members = @resource.should(:members) + unless members == :absent + if members.is_a?(Array) + members = members.join(",") + end + cmd << "-M" << members + end + end + + cmd << "-o" if @resource.allowdupe? cmd end + + def modifycmd(param, value) + # members may be an array, need a comma separated list + if param == :members and value.is_a?(Array) + value = value.join(",") + end + super(param, value) + end end diff --git a/lib/puppet/provider/package/pip.rb b/lib/puppet/provider/package/pip.rb index ccc389564..5f6806e6f 100644 --- a/lib/puppet/provider/package/pip.rb +++ b/lib/puppet/provider/package/pip.rb @@ -1,108 +1,108 @@ # Puppet package provider for Python's `pip` package management frontend. # require 'puppet/provider/package' require 'xmlrpc/client' Puppet::Type.type(:package).provide :pip, :parent => ::Puppet::Provider::Package do desc "Python packages via `pip`." has_feature :installable, :uninstallable, :upgradeable, :versionable # Parse lines of output from `pip freeze`, which are structured as # _package_==_version_. def self.parse(line) if line.chomp =~ /^([^=]+)==([^=]+)$/ {:ensure => $2, :name => $1, :provider => name} else nil end end # Return an array of structured information about every installed package # that's managed by `pip` or an empty array if `pip` is not available. def self.instances packages = [] pip_cmd = which('pip') or return [] execpipe "#{pip_cmd} freeze" do |process| process.collect do |line| next unless options = parse(line) packages << new(options) end end packages end # Return structured information about a particular package or `nil` if # it is not installed or `pip` itself is not available. def query self.class.instances.each do |provider_pip| return provider_pip.properties if @resource[:name] == provider_pip.name end return nil end # Ask the PyPI API for the latest version number. There is no local # cache of PyPI's package list so this operation will always have to # ask the web service. def latest client = XMLRPC::Client.new2("http://pypi.python.org/pypi") client.http_header_extra = {"Content-Type" => "text/xml"} result = client.call("package_releases", @resource[:name]) result.first end # Install a package. The ensure parameter may specify installed, # latest, a version number, or, in conjunction with the source # parameter, an SCM revision. In that case, the source parameter # gives the fully-qualified URL to the repository. def install args = %w{install -q} if @resource[:source] args << "-e" if String === @resource[:ensure] args << "#{@resource[:source]}@#{@resource[:ensure]}#egg=#{ @resource[:name]}" else args << "#{@resource[:source]}#egg=#{@resource[:name]}" end else case @resource[:ensure] when String args << "#{@resource[:name]}==#{@resource[:ensure]}" when :latest args << "--upgrade" << @resource[:name] else args << @resource[:name] end end lazy_pip *args end # Uninstall a package. Uninstall won't work reliably on Debian/Ubuntu # unless this issue gets fixed. # def uninstall lazy_pip "uninstall", "-y", "-q", @resource[:name] end def update install end # Execute a `pip` command. If Puppet doesn't yet know how to do so, # try to teach it and if even that fails, raise the error. private def lazy_pip(*args) pip *args rescue NoMethodError => e if pathname = which('pip') self.class.commands :pip => pathname pip *args else - raise e + raise e, 'Could not locate the pip command.' end end end diff --git a/lib/puppet/provider/package/up2date.rb b/lib/puppet/provider/package/up2date.rb index 7c3ad5cd1..ed358105c 100644 --- a/lib/puppet/provider/package/up2date.rb +++ b/lib/puppet/provider/package/up2date.rb @@ -1,40 +1,41 @@ Puppet::Type.type(:package).provide :up2date, :parent => :rpm, :source => :rpm do desc "Support for Red Hat's proprietary `up2date` package update mechanism." commands :up2date => "/usr/sbin/up2date-nox" - defaultfor :osfamily => :redhat, :lsbdistrelease => ["2.1", "3", "4"] + defaultfor :operatingsystem => [:redhat, :oel, :ovm], + :lsbdistrelease => ["2.1", "3", "4"] - confine :osfamily => :redhat + confine :operatingsystem => [:redhat, :oel, :ovm] # Install a package using 'up2date'. def install up2date "-u", @resource[:name] unless self.query raise Puppet::ExecutionFailure.new( "Could not find package #{self.name}" ) end end # What's the latest package version available? def latest #up2date can only get a list of *all* available packages? output = up2date "--showall" if output =~ /^#{Regexp.escape @resource[:name]}-(\d+.*)\.\w+/ return $1 else # up2date didn't find updates, pretend the current # version is the latest return @property_hash[:ensure] end end def update # Install in up2date can be used for update, too self.install end end diff --git a/lib/puppet/provider/service/redhat.rb b/lib/puppet/provider/service/redhat.rb index df59d368e..b600e1b2e 100755 --- a/lib/puppet/provider/service/redhat.rb +++ b/lib/puppet/provider/service/redhat.rb @@ -1,75 +1,75 @@ # Manage Red Hat services. Start/stop uses /sbin/service and enable/disable uses chkconfig Puppet::Type.type(:service).provide :redhat, :parent => :init, :source => :init do desc "Red Hat's (and probably many others') form of `init`-style service management. Uses `chkconfig` for service enabling and disabling. " commands :chkconfig => "/sbin/chkconfig", :service => "/sbin/service" - defaultfor :osfamily => [:redhat, :suse] + defaultfor :operatingsystem => [:redhat, :fedora, :suse, :centos, :sles, :oel, :ovm] def self.instances # this exclude list is all from /sbin/service (5.x), but I did not exclude kudzu self.get_services(['/etc/init.d'], ['functions', 'halt', 'killall', 'single', 'linuxconf']) end def self.defpath superclass.defpath end # Remove the symlinks def disable output = chkconfig(@resource[:name], :off) rescue Puppet::ExecutionFailure raise Puppet::Error, "Could not disable #{self.name}: #{output}" end def enabled? begin output = chkconfig(@resource[:name]) rescue Puppet::ExecutionFailure return :false end # If it's disabled on SuSE, then it will print output showing "off" # at the end if output =~ /.* off$/ return :false end :true end # Don't support them specifying runlevels; always use the runlevels # in the init scripts. def enable output = chkconfig(@resource[:name], :on) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not enable #{self.name}: #{detail}" end def initscript raise Puppet::Error, "Do not directly call the init script for '#{@resource[:name]}'; use 'service' instead" end # use hasstatus=>true when its set for the provider. def statuscmd ((@resource.provider.get(:hasstatus) == true) || (@resource[:hasstatus] == :true)) && [command(:service), @resource[:name], "status"] end def restartcmd (@resource[:hasrestart] == :true) && [command(:service), @resource[:name], "restart"] end def startcmd [command(:service), @resource[:name], "start"] end def stopcmd [command(:service), @resource[:name], "stop"] end end diff --git a/lib/puppet/provider/service/systemd.rb b/lib/puppet/provider/service/systemd.rb index 33ff52d9c..c67a437c5 100755 --- a/lib/puppet/provider/service/systemd.rb +++ b/lib/puppet/provider/service/systemd.rb @@ -1,64 +1,64 @@ # Manage systemd services using /bin/systemctl Puppet::Type.type(:service).provide :systemd, :parent => :base do desc "Manages `systemd` services using `/bin/systemctl`." commands :systemctl => "/bin/systemctl" - #defaultfor :osfamily => [:redhat, :suse] + #defaultfor :operatingsystem => [:redhat, :fedora, :suse, :centos, :sles, :oel, :ovm] def self.instances i = [] output = `systemctl list-units --full --all --no-pager` output.scan(/^(\S+)\s+(loaded|error)\s+(active|inactive)\s+(active|waiting|running|plugged|mounted|dead|exited|listening|elapsed)\s*?(\S.*?)?$/i).each do |m| i << m[0] end return i rescue Puppet::ExecutionFailure return [] end def disable output = systemctl(:disable, @resource[:name]) rescue Puppet::ExecutionFailure raise Puppet::Error, "Could not disable #{self.name}: #{output}" end def enabled? begin systemctl("is-enabled", @resource[:name]) rescue Puppet::ExecutionFailure return :false end :true end def status begin output = systemctl("is-active", @resource[:name]) rescue Puppet::ExecutionFailure return :stopped end return :running end def enable output = systemctl("enable", @resource[:name]) rescue Puppet::ExecutionFailure raise Puppet::Error, "Could not enable #{self.name}: #{output}" end def restartcmd [command(:systemctl), "restart", @resource[:name]] end def startcmd [command(:systemctl), "start", @resource[:name]] end def stopcmd [command(:systemctl), "stop", @resource[:name]] end end diff --git a/lib/puppet/provider/user/pw.rb b/lib/puppet/provider/user/pw.rb index a5988cad1..842397971 100644 --- a/lib/puppet/provider/user/pw.rb +++ b/lib/puppet/provider/user/pw.rb @@ -1,41 +1,95 @@ require 'puppet/provider/nameservice/pw' +require 'open3' Puppet::Type.type(:user).provide :pw, :parent => Puppet::Provider::NameService::PW do desc "User management via `pw` on FreeBSD." commands :pw => "pw" - has_features :manages_homedir, :allows_duplicates + has_features :manages_homedir, :allows_duplicates, :manages_passwords, :manages_expiry defaultfor :operatingsystem => :freebsd options :home, :flag => "-d", :method => :dir options :comment, :method => :gecos options :groups, :flag => "-G" + options :expiry, :method => :expire verify :gid, "GID must be an integer" do |value| value.is_a? Integer end verify :groups, "Groups must be comma-separated" do |value| value !~ /\s/ end def addcmd cmd = [command(:pw), "useradd", @resource[:name]] @resource.class.validproperties.each do |property| - next if property == :ensure + next if property == :ensure or property == :password # the value needs to be quoted, mostly because -c might # have spaces in it if value = @resource.should(property) and value != "" + if property == :expiry + # FreeBSD uses DD-MM-YYYY rather than YYYY-MM-DD + value = value.split("-").reverse.join("-") + end cmd << flag(property) << value end end cmd << "-o" if @resource.allowdupe? cmd << "-m" if @resource.managehome? cmd end + + def modifycmd(param, value) + if param == :expiry + # FreeBSD uses DD-MM-YYYY rather than YYYY-MM-DD + value = value.split("-").reverse.join("-") + end + cmd = super(param, value) + cmd << "-m" if @resource.managehome? + cmd + end + + def create + super + + # Set the password after create if given + self.password = @resource[:password] if @resource[:password] + end + + # use pw to update password hash + def password=(cryptopw) + Puppet.debug "change password for user '#{@resource[:name]}' method called with hash '#{cryptopw}'" + stdin, stdout, stderr = Open3.popen3("pw user mod #{@resource[:name]} -H 0") + stdin.puts(cryptopw) + stdin.close + Puppet.debug "finished password for user '#{@resource[:name]}' method called with hash '#{cryptopw}'" + end + + # get password from /etc/master.passwd + def password + Puppet.debug "checking password for user '#{@resource[:name]}' method called" + current_passline = `getent passwd #{@resource[:name]}` + current_password = current_passline.chomp.split(':')[1] if current_passline + Puppet.debug "finished password for user '#{@resource[:name]}' method called : '#{current_password}'" + current_password + end + + # Get expiry from system and convert to Puppet-style date + def expiry + expiry = self.get(:expiry) + expiry = :absent if expiry == 0 + + if expiry != :absent + t = Time.at(expiry) + expiry = "%4d-%02d-%02d" % [t.year, t.month, t.mday] + end + + expiry + end end diff --git a/lib/puppet/simple_graph.rb b/lib/puppet/simple_graph.rb index 671eef150..6d9365f7e 100644 --- a/lib/puppet/simple_graph.rb +++ b/lib/puppet/simple_graph.rb @@ -1,553 +1,552 @@ require 'puppet/external/dot' require 'puppet/relationship' require 'set' # A hopefully-faster graph class to replace the use of GRATR. class Puppet::SimpleGraph # # All public methods of this class must maintain (assume ^ ensure) the following invariants, where "=~=" means # equiv. up to order: # # @in_to.keys =~= @out_to.keys =~= all vertices # @in_to.values.collect { |x| x.values }.flatten =~= @out_from.values.collect { |x| x.values }.flatten =~= all edges # @in_to[v1][v2] =~= @out_from[v2][v1] =~= all edges from v1 to v2 # @in_to [v].keys =~= vertices with edges leading to v # @out_from[v].keys =~= vertices with edges leading from v # no operation may shed reference loops (for gc) # recursive operation must scale with the depth of the spanning trees, or better (e.g. no recursion over the set # of all vertices, etc.) # # This class is intended to be used with DAGs. However, if the # graph has a cycle, it will not cause non-termination of any of the # algorithms. # def initialize @in_to = {} @out_from = {} @upstream_from = {} @downstream_from = {} end # Clear our graph. def clear @in_to.clear @out_from.clear @upstream_from.clear @downstream_from.clear end # Which resources depend upon the given resource. def dependencies(resource) vertex?(resource) ? upstream_from_vertex(resource).keys : [] end def dependents(resource) vertex?(resource) ? downstream_from_vertex(resource).keys : [] end # Whether our graph is directed. Always true. Used to produce dot files. def directed? true end # Determine all of the leaf nodes below a given vertex. def leaves(vertex, direction = :out) tree_from_vertex(vertex, direction).keys.find_all { |c| adjacent(c, :direction => direction).empty? } end # Collect all of the edges that the passed events match. Returns # an array of edges. def matching_edges(event, base = nil) source = base || event.resource unless vertex?(source) Puppet.warning "Got an event from invalid vertex #{source.ref}" return [] end # Get all of the edges that this vertex should forward events # to, which is the same thing as saying all edges directly below # This vertex in the graph. @out_from[source].values.flatten.find_all { |edge| edge.match?(event.name) } end # Return a reversed version of this graph. def reversal result = self.class.new vertices.each { |vertex| result.add_vertex(vertex) } edges.each do |edge| result.add_edge edge.class.new(edge.target, edge.source, edge.label) end result end # Return the size of the graph. def size vertices.size end def to_a vertices end # This is a simple implementation of Tarjan's algorithm to find strongly # connected components in the graph; this is a fairly ugly implementation, # because I can't just decorate the vertices themselves. # # This method has an unhealthy relationship with the find_cycles_in_graph # method below, which contains the knowledge of how the state object is # maintained. def tarjan(root, s) # initialize the recursion stack we use to work around the nasty lack of a # decent Ruby stack. recur = [{ :node => root }] while not recur.empty? do frame = recur.last vertex = frame[:node] case frame[:step] when nil then s[:index][vertex] = s[:number] s[:lowlink][vertex] = s[:number] s[:number] = s[:number] + 1 s[:stack].push(vertex) s[:seen][vertex] = true frame[:children] = adjacent(vertex) frame[:step] = :children when :children then if frame[:children].length > 0 then child = frame[:children].shift if ! s[:index][child] then # Never seen, need to recurse. frame[:step] = :after_recursion frame[:child] = child recur.push({ :node => child }) elsif s[:seen][child] then s[:lowlink][vertex] = [s[:lowlink][vertex], s[:index][child]].min end else if s[:lowlink][vertex] == s[:index][vertex] then this_scc = [] begin top = s[:stack].pop s[:seen][top] = false this_scc << top end until top == vertex - # NOTE: if we don't reverse we get the components in the opposite - # order to what a human being would expect; reverse should be an - # O(1) operation, without even copying, because we know the length - # of the source, but I worry that an implementation will get this - # wrong. Still, the worst case is O(n) for n vertices as we can't - # possibly put a vertex into two SCCs. - # - # Also, my feeling is that most implementations are going to do - # better with a reverse operation than a string of 'unshift' - # insertions at the head of the array; if they were going to mess - # up the performance of one, it would be unshift. - s[:scc] << this_scc.reverse + s[:scc] << this_scc end recur.pop # done with this node, finally. end when :after_recursion then s[:lowlink][vertex] = [s[:lowlink][vertex], s[:lowlink][frame[:child]]].min frame[:step] = :children else fail "#{frame[:step]} is an unknown step" end end end # Find all cycles in the graph by detecting all the strongly connected # components, then eliminating everything with a size of one as # uninteresting - which it is, because it can't be a cycle. :) # # This has an unhealthy relationship with the 'tarjan' method above, which # it uses to implement the detection of strongly connected components. def find_cycles_in_graph state = { :number => 0, :index => {}, :lowlink => {}, :scc => [], :stack => [], :seen => {} } # we usually have a disconnected graph, must walk all possible roots vertices.each do |vertex| if ! state[:index][vertex] then tarjan vertex, state end end - state[:scc].select { |c| c.length > 1 } + # To provide consistent results to the user, given that a hash is never + # assured to return the same order, and given our graph processing is + # based on hash tables, we need to sort the cycles internally, as well as + # the set of cycles. + # + # Given we are in a failure state here, any extra cost is more or less + # irrelevant compared to the cost of a fix - which is on a human + # time-scale. + state[:scc].select { |c| c.length > 1 }.map do |x| + x.sort_by {|a| a.to_s } + end.sort end # Perform a BFS on the sub graph representing the cycle, with a view to # generating a sufficient set of paths to report the cycle meaningfully, and # ideally usefully, for the end user. # # BFS is preferred because it will generally report the shortest paths # through the graph first, which are more likely to be interesting to the # user. I think; it would be interesting to verify that. --daniel 2011-01-23 def paths_in_cycle(cycle, max_paths = 1) raise ArgumentError, "negative or zero max_paths" if max_paths < 1 # Calculate our filtered outbound vertex lists... adj = {} cycle.each do |vertex| adj[vertex] = adjacent(vertex).select{|s| cycle.member? s} end found = [] # frame struct is vertex, [path] stack = [[cycle.first, []]] while frame = stack.shift do if frame[1].member?(frame[0]) then found << frame[1] + [frame[0]] break if found.length >= max_paths else adj[frame[0]].each do |to| stack.push [to, frame[1] + [frame[0]]] end end end return found end def report_cycles_in_graph cycles = find_cycles_in_graph n = cycles.length # where is "pluralize"? --daniel 2011-01-22 return if n == 0 s = n == 1 ? '' : 's' message = "Found #{n} dependency cycle#{s}:\n" cycles.each do |cycle| paths = paths_in_cycle(cycle) message += paths.map{ |path| '(' + path.join(" => ") + ')'}.join("\n") + "\n" end if Puppet[:graph] then filename = write_cycles_to_graph(cycles) message += "Cycle graph written to #{filename}." else message += "Try the '--graph' option and opening the " message += "resulting '.dot' file in OmniGraffle or GraphViz" end raise Puppet::Error, message end def write_cycles_to_graph(cycles) # This does not use the DOT graph library, just writes the content # directly. Given the complexity of this, there didn't seem much point # using a heavy library to generate exactly the same content. --daniel 2011-01-27 Puppet.settings.use(:graphing) graph = ["digraph Resource_Cycles {"] graph << ' label = "Resource Cycles"' cycles.each do |cycle| paths_in_cycle(cycle, 10).each do |path| graph << path.map { |v| '"' + v.to_s.gsub(/"/, '\\"') + '"' }.join(" -> ") end end graph << '}' filename = File.join(Puppet[:graphdir], "cycles.dot") File.open(filename, "w") { |f| f.puts graph } return filename end # Add a new vertex to the graph. def add_vertex(vertex) @in_to[vertex] ||= {} @out_from[vertex] ||= {} end # Remove a vertex from the graph. def remove_vertex!(v) return unless vertex?(v) @upstream_from.clear @downstream_from.clear (@in_to[v].values+@out_from[v].values).flatten.each { |e| remove_edge!(e) } @in_to.delete(v) @out_from.delete(v) end # Test whether a given vertex is in the graph. def vertex?(v) @in_to.include?(v) end # Return a list of all vertices. def vertices @in_to.keys end # Add a new edge. The graph user has to create the edge instance, # since they have to specify what kind of edge it is. def add_edge(e,*a) return add_relationship(e,*a) unless a.empty? @upstream_from.clear @downstream_from.clear add_vertex(e.source) add_vertex(e.target) @in_to[ e.target][e.source] ||= []; @in_to[ e.target][e.source] |= [e] @out_from[e.source][e.target] ||= []; @out_from[e.source][e.target] |= [e] end def add_relationship(source, target, label = nil) add_edge Puppet::Relationship.new(source, target, label) end # Find all matching edges. def edges_between(source, target) (@out_from[source] || {})[target] || [] end # Is there an edge between the two vertices? def edge?(source, target) vertex?(source) and vertex?(target) and @out_from[source][target] end def edges @in_to.values.collect { |x| x.values }.flatten end def each_edge @in_to.each { |t,ns| ns.each { |s,es| es.each { |e| yield e }}} end # Remove an edge from our graph. def remove_edge!(e) if edge?(e.source,e.target) @upstream_from.clear @downstream_from.clear @in_to [e.target].delete e.source if (@in_to [e.target][e.source] -= [e]).empty? @out_from[e.source].delete e.target if (@out_from[e.source][e.target] -= [e]).empty? end end # Find adjacent edges. def adjacent(v, options = {}) return [] unless ns = (options[:direction] == :in) ? @in_to[v] : @out_from[v] (options[:type] == :edges) ? ns.values.flatten : ns.keys end # Just walk the tree and pass each edge. def walk(source, direction) # Use an iterative, breadth-first traversal of the graph. One could do # this recursively, but Ruby's slow function calls and even slower # recursion make the shorter, recursive algorithm cost-prohibitive. stack = [source] seen = Set.new until stack.empty? node = stack.shift next if seen.member? node connected = adjacent(node, :direction => direction) connected.each do |target| yield node, target end stack.concat(connected) seen << node end end # A different way of walking a tree, and a much faster way than the # one that comes with GRATR. def tree_from_vertex(start, direction = :out) predecessor={} walk(start, direction) do |parent, child| predecessor[child] = parent end predecessor end def downstream_from_vertex(v) return @downstream_from[v] if @downstream_from[v] result = @downstream_from[v] = {} @out_from[v].keys.each do |node| result[node] = 1 result.update(downstream_from_vertex(node)) end result end def direct_dependents_of(v) (@out_from[v] || {}).keys end def upstream_from_vertex(v) return @upstream_from[v] if @upstream_from[v] result = @upstream_from[v] = {} @in_to[v].keys.each do |node| result[node] = 1 result.update(upstream_from_vertex(node)) end result end def direct_dependencies_of(v) (@in_to[v] || {}).keys end # Return an array of the edge-sets between a series of n+1 vertices (f=v0,v1,v2...t=vn) # connecting the two given verticies. The ith edge set is an array containing all the # edges between v(i) and v(i+1); these are (by definition) never empty. # # * if f == t, the list is empty # * if they are adjacent the result is an array consisting of # a single array (the edges from f to t) # * and so on by induction on a vertex m between them # * if there is no path from f to t, the result is nil # # This implementation is not particularly efficient; it's used in testing where clarity # is more important than last-mile efficiency. # def path_between(f,t) if f==t [] elsif direct_dependents_of(f).include?(t) [edges_between(f,t)] elsif dependents(f).include?(t) m = (dependents(f) & direct_dependencies_of(t)).first path_between(f,m) + path_between(m,t) else nil end end # LAK:FIXME This is just a paste of the GRATR code with slight modifications. # Return a DOT::DOTDigraph for directed graphs or a DOT::DOTSubgraph for an # undirected Graph. _params_ can contain any graph property specified in # rdot.rb. If an edge or vertex label is a kind of Hash then the keys # which match +dot+ properties will be used as well. def to_dot_graph (params = {}) params['name'] ||= self.class.name.gsub(/:/,'_') fontsize = params['fontsize'] ? params['fontsize'] : '8' graph = (directed? ? DOT::DOTDigraph : DOT::DOTSubgraph).new(params) edge_klass = directed? ? DOT::DOTDirectedEdge : DOT::DOTEdge vertices.each do |v| name = v.to_s params = {'name' => '"'+name+'"', 'fontsize' => fontsize, 'label' => name} v_label = v.to_s params.merge!(v_label) if v_label and v_label.kind_of? Hash graph << DOT::DOTNode.new(params) end edges.each do |e| params = {'from' => '"'+ e.source.to_s + '"', 'to' => '"'+ e.target.to_s + '"', 'fontsize' => fontsize } e_label = e.to_s params.merge!(e_label) if e_label and e_label.kind_of? Hash graph << edge_klass.new(params) end graph end # Output the dot format as a string def to_dot (params={}) to_dot_graph(params).to_s; end # Call +dotty+ for the graph which is written to the file 'graph.dot' # in the # current directory. def dotty (params = {}, dotfile = 'graph.dot') File.open(dotfile, 'w') {|f| f << to_dot(params) } system('dotty', dotfile) end # Produce the graph files if requested. def write_graph(name) return unless Puppet[:graph] Puppet.settings.use(:graphing) file = File.join(Puppet[:graphdir], "#{name}.dot") File.open(file, "w") { |f| f.puts to_dot("name" => name.to_s.capitalize) } end # This flag may be set to true to use the new YAML serialzation # format (where @vertices is a simple list of vertices rather than a # list of VertexWrapper objects). Deserialization supports both # formats regardless of the setting of this flag. class << self attr_accessor :use_new_yaml_format end self.use_new_yaml_format = false # Stub class to allow graphs to be represented in YAML using the old # (version 2.6) format. class VertexWrapper attr_reader :vertex, :adjacencies def initialize(vertex, adjacencies) @vertex = vertex @adjacencies = adjacencies end def inspect { :@adjacencies => @adjacencies, :@vertex => @vertex.to_s }.inspect end end # instance_variable_get is used by Object.to_zaml to get instance # variables. Override it so that we can simulate the presence of # instance variables @edges and @vertices for serialization. def instance_variable_get(v) case v.to_s when '@edges' then edges when '@vertices' then if self.class.use_new_yaml_format vertices else result = {} vertices.each do |vertex| adjacencies = {} [:in, :out].each do |direction| adjacencies[direction] = {} adjacent(vertex, :direction => direction, :type => :edges).each do |edge| other_vertex = direction == :in ? edge.source : edge.target (adjacencies[direction][other_vertex] ||= Set.new).add(edge) end end result[vertex] = Puppet::SimpleGraph::VertexWrapper.new(vertex, adjacencies) end result end else super(v) end end def to_yaml_properties other_vars = instance_variables. map {|v| v.to_s}. reject { |v| %w{@in_to @out_from @upstream_from @downstream_from}.include?(v) } (other_vars + %w{@vertices @edges}).sort.uniq end def yaml_initialize(tag, var) initialize() vertices = var.delete('vertices') edges = var.delete('edges') if vertices.is_a?(Hash) # Support old (2.6) format vertices = vertices.keys end vertices.each { |v| add_vertex(v) } edges.each { |e| add_edge(e) } var.each do |varname, value| instance_variable_set("@#{varname}", value) end end end diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb index 8f6aa9ad3..f4ced3170 100755 --- a/lib/puppet/type/host.rb +++ b/lib/puppet/type/host.rb @@ -1,73 +1,87 @@ require 'puppet/property/ordered_list' module Puppet newtype(:host) do ensurable newproperty(:ip) do desc "The host's IP address, IPv4 or IPv6." - validate do |value| - unless value =~ /^((([0-9a-fA-F]+:){7}[0-9a-fA-F]+)|(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?::(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?)|((25[0-5]|2[0-4][\d]|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})$/ - raise Puppet::Error, "Invalid IP address" + + def valid_v4?(addr) + if /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ =~ addr + return $~.captures.all? {|i| i = i.to_i; i >= 0 and i <= 255 } end + return false end + def valid_v6?(addr) + # http://forums.dartware.com/viewtopic.php?t=452 + # ...and, yes, it is this hard. Doing it programatically is harder. + return true if addr =~ /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/ + + return false + end + + validate do |value| + return true if valid_v4?(value) or valid_v6?(value) + raise Puppet::Error, "Invalid IP address #{value.inspect}" + end end # for now we use OrderedList to indicate that the order does matter. newproperty(:host_aliases, :parent => Puppet::Property::OrderedList) do desc "Any aliases the host might have. Multiple values must be specified as an array." def delimiter " " end def inclusive? true end validate do |value| raise Puppet::Error, "Host aliases cannot include whitespace" if value =~ /\s/ raise Puppet::Error, "Host alias cannot be an empty string. Use an empty array to delete all host_aliases " if value =~ /^\s*$/ end end newproperty(:comment) do desc "A comment that will be attached to the line with a # character." end newproperty(:target) do desc "The file in which to store service information. Only used by those providers that write to disk. On most systems this defaults to `/etc/hosts`." defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) @resource.class.defaultprovider.default_target else nil end } end newparam(:name) do desc "The host name." isnamevar validate do |value| # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = value.split('.').each do |hostpart| unless hostpart =~ /^([\d\w]+|[\d\w][\d\w\-]+[\d\w])$/ raise Puppet::Error, "Invalid host name" end end end end @doc = "Installs and manages host entries. For most systems, these entries will just be in `/etc/hosts`, but some systems (notably OS X) will have different solutions." end end diff --git a/lib/puppet/util/rdoc/parser.rb b/lib/puppet/util/rdoc/parser.rb index a8996ee9a..05e3aab4d 100644 --- a/lib/puppet/util/rdoc/parser.rb +++ b/lib/puppet/util/rdoc/parser.rb @@ -1,495 +1,495 @@ # Puppet "parser" for the rdoc system # The parser uses puppet parser and traverse the AST to instruct RDoc about # our current structures. It also parses ruby files that could contain # either custom facts or puppet plugins (functions, types...) # rdoc mandatory includes require "rdoc/code_objects" require "puppet/util/rdoc/code_objects" require "rdoc/tokenstream" if ::RUBY_VERSION =~ /1.9/ require "rdoc/markup/preprocess" require "rdoc/parser" else require "rdoc/markup/simple_markup/preprocess" require "rdoc/parsers/parserfactory" end module RDoc class Parser extend ParserFactory unless ::RUBY_VERSION =~ /1.9/ SITE = "__site__" attr_accessor :input_file_name, :top_level # parser registration into RDoc parse_files_matching(/\.(rb|pp)$/) # called with the top level file def initialize(top_level, file_name, content, options, stats) @options = options @stats = stats @input_file_name = file_name @top_level = PuppetTopLevel.new(top_level) @progress = $stderr unless options.quiet end # main entry point def scan environment = Puppet::Node::Environment.new @known_resource_types = environment.known_resource_types unless environment.known_resource_types.watching_file?(@input_file_name) Puppet.info "rdoc: scanning #{@input_file_name}" if @input_file_name =~ /\.pp$/ @parser = Puppet::Parser::Parser.new(environment) @parser.file = @input_file_name @parser.parse.instantiate('').each do |type| @known_resource_types.add type end end end scan_top_level(@top_level) @top_level end # Due to a bug in RDoc, we need to roll our own find_module_named # The issue is that RDoc tries harder by asking the parent for a class/module # of the name. But by doing so, it can mistakenly use a module of same name # but from which we are not descendant. def find_object_named(container, name) return container if container.name == name container.each_classmodule do |m| return m if m.name == name end nil end # walk down the namespace and lookup/create container as needed def get_class_or_module(container, name) # class ::A -> A is in the top level if name =~ /^::/ container = @top_level end names = name.split('::') final_name = names.pop names.each do |name| prev_container = container container = find_object_named(container, name) container ||= prev_container.add_class(PuppetClass, name, nil) end [container, final_name] end # split_module tries to find if +path+ belongs to the module path # if it does, it returns the module name, otherwise if we are sure # it is part of the global manifest path, "__site__" is returned. # And finally if this path couldn't be mapped anywhere, nil is returned. def split_module(path) # find a module fullpath = File.expand_path(path) Puppet.debug "rdoc: testing #{fullpath}" if fullpath =~ /(.*)\/([^\/]+)\/(?:manifests|plugins|lib)\/.+\.(pp|rb)$/ modpath = $1 name = $2 Puppet.debug "rdoc: module #{name} into #{modpath} ?" - Puppet::Module.modulepath.each do |mp| + Puppet::Node::Environment.new.modulepath.each do |mp| if File.identical?(modpath,mp) Puppet.debug "rdoc: found module #{name}" return name end end end if fullpath =~ /\.(pp|rb)$/ # there can be paths we don't want to scan under modules # imagine a ruby or manifest that would be distributed as part as a module # but we don't want those to be hosted under - Puppet::Module.modulepath.each do |mp| + Puppet::Node::Environment.new.modulepath.each do |mp| # check that fullpath is a descendant of mp dirname = fullpath previous = dirname while (dirname = File.dirname(previous)) != previous previous = dirname return nil if File.identical?(dirname,mp) end end end # we are under a global manifests Puppet.debug "rdoc: global manifests" SITE end # create documentation for the top level +container+ def scan_top_level(container) # use the module README as documentation for the module comment = "" readme = File.join(File.dirname(File.dirname(@input_file_name)), "README") comment = File.open(readme,"r") { |f| f.read } if FileTest.readable?(readme) look_for_directives_in(container, comment) unless comment.empty? # infer module name from directory name = split_module(@input_file_name) if name.nil? # skip .pp files that are not in manifests directories as we can't guarantee they're part # of a module or the global configuration. container.document_self = false return end Puppet.debug "rdoc: scanning for #{name}" container.module_name = name container.global=true if name == SITE @stats.num_modules += 1 container, name = get_class_or_module(container,name) mod = container.add_module(PuppetModule, name) mod.record_location(@top_level) mod.comment = comment if @input_file_name =~ /\.pp$/ parse_elements(mod) elsif @input_file_name =~ /\.rb$/ parse_plugins(mod) end end # create documentation for include statements we can find in +code+ # and associate it with +container+ def scan_for_include_or_require(container, code) code = [code] unless code.is_a?(Array) code.each do |stmt| scan_for_include_or_require(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) if stmt.is_a?(Puppet::Parser::AST::Function) and ['include','require'].include?(stmt.name) stmt.arguments.each do |included| Puppet.debug "found #{stmt.name}: #{included}" container.send("add_#{stmt.name}",Include.new(included.to_s, stmt.doc)) end end end end # create documentation for realize statements we can find in +code+ # and associate it with +container+ def scan_for_realize(container, code) code = [code] unless code.is_a?(Array) code.each do |stmt| scan_for_realize(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) if stmt.is_a?(Puppet::Parser::AST::Function) and stmt.name == 'realize' stmt.arguments.each do |realized| Puppet.debug "found #{stmt.name}: #{realized}" container.add_realize(Include.new(realized.to_s, stmt.doc)) end end end end # create documentation for global variables assignements we can find in +code+ # and associate it with +container+ def scan_for_vardef(container, code) code = [code] unless code.is_a?(Array) code.each do |stmt| scan_for_vardef(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) if stmt.is_a?(Puppet::Parser::AST::VarDef) Puppet.debug "rdoc: found constant: #{stmt.name} = #{stmt.value}" container.add_constant(Constant.new(stmt.name.to_s, stmt.value.to_s, stmt.doc)) end end end # create documentation for resources we can find in +code+ # and associate it with +container+ def scan_for_resource(container, code) code = [code] unless code.is_a?(Array) code.each do |stmt| scan_for_resource(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) if stmt.is_a?(Puppet::Parser::AST::Resource) and !stmt.type.nil? begin type = stmt.type.split("::").collect { |s| s.capitalize }.join("::") stmt.instances.each do |inst| title = inst.title.is_a?(Puppet::Parser::AST::ASTArray) ? inst.title.to_s.gsub(/\[(.*)\]/,'\1') : inst.title.to_s Puppet.debug "rdoc: found resource: #{type}[#{title}]" param = [] inst.parameters.children.each do |p| res = {} res["name"] = p.param res["value"] = "#{p.value.to_s}" unless p.value.nil? param << res end container.add_resource(PuppetResource.new(type, title, stmt.doc, param)) end rescue => detail raise Puppet::ParseError, "impossible to parse resource in #{stmt.file} at line #{stmt.line}: #{detail}" end end end end def resource_stmt_to_ref(stmt) type = stmt.type.split("::").collect { |s| s.capitalize }.join("::") title = stmt.title.is_a?(Puppet::Parser::AST::ASTArray) ? stmt.title.to_s.gsub(/\[(.*)\]/,'\1') : stmt.title.to_s param = stmt.params.children.collect do |p| {"name" => p.param, "value" => p.value.to_s} end PuppetResource.new(type, title, stmt.doc, param) end # create documentation for a class named +name+ def document_class(name, klass, container) Puppet.debug "rdoc: found new class #{name}" container, name = get_class_or_module(container, name) superclass = klass.parent superclass = "" if superclass.nil? or superclass.empty? @stats.num_classes += 1 comment = klass.doc look_for_directives_in(container, comment) unless comment.empty? cls = container.add_class(PuppetClass, name, superclass) # it is possible we already encountered this class, while parsing some namespaces # from other classes of other files. But at that time we couldn't know this class superclass # so, now we know it and force it. cls.superclass = superclass cls.record_location(@top_level) # scan class code for include code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::ASTArray) code ||= klass.code unless code.nil? scan_for_include_or_require(cls, code) scan_for_realize(cls, code) scan_for_resource(cls, code) if Puppet.settings[:document_all] end cls.comment = comment rescue => detail raise Puppet::ParseError, "impossible to parse class '#{name}' in #{klass.file} at line #{klass.line}: #{detail}" end # create documentation for a node def document_node(name, node, container) Puppet.debug "rdoc: found new node #{name}" superclass = node.parent superclass = "" if superclass.nil? or superclass.empty? comment = node.doc look_for_directives_in(container, comment) unless comment.empty? n = container.add_node(name, superclass) n.record_location(@top_level) code = node.code.children if node.code.is_a?(Puppet::Parser::AST::ASTArray) code ||= node.code unless code.nil? scan_for_include_or_require(n, code) scan_for_realize(n, code) scan_for_vardef(n, code) scan_for_resource(n, code) if Puppet.settings[:document_all] end n.comment = comment rescue => detail raise Puppet::ParseError, "impossible to parse node '#{name}' in #{node.file} at line #{node.line}: #{detail}" end # create documentation for a define def document_define(name, define, container) Puppet.debug "rdoc: found new definition #{name}" # find superclas if any @stats.num_methods += 1 # find the parent # split define name by :: to find the complete module hierarchy container, name = get_class_or_module(container,name) # build up declaration declaration = "" define.arguments.each do |arg,value| declaration << "\$#{arg}" unless value.nil? declaration << " => " case value when Puppet::Parser::AST::Leaf declaration << "'#{value.value}'" when Puppet::Parser::AST::ASTArray declaration << "[#{value.children.collect { |v| "'#{v}'" }.join(", ")}]" else declaration << "#{value.to_s}" end end declaration << ", " end declaration.chop!.chop! if declaration.size > 1 # register method into the container meth = AnyMethod.new(declaration, name) meth.comment = define.doc container.add_method(meth) look_for_directives_in(container, meth.comment) unless meth.comment.empty? meth.params = "( #{declaration} )" meth.visibility = :public meth.document_self = true meth.singleton = false rescue => detail raise Puppet::ParseError, "impossible to parse definition '#{name}' in #{define.file} at line #{define.line}: #{detail}" end # Traverse the AST tree and produce code-objects node # that contains the documentation def parse_elements(container) Puppet.debug "rdoc: scanning manifest" @known_resource_types.hostclasses.values.sort { |a,b| a.name <=> b.name }.each do |klass| name = klass.name if klass.file == @input_file_name unless name.empty? document_class(name,klass,container) else # on main class document vardefs code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::ASTArray) code ||= klass.code scan_for_vardef(container, code) unless code.nil? end end end @known_resource_types.definitions.each do |name, define| if define.file == @input_file_name document_define(name,define,container) end end @known_resource_types.nodes.each do |name, node| if node.file == @input_file_name document_node(name.to_s,node,container) end end end # create documentation for plugins def parse_plugins(container) Puppet.debug "rdoc: scanning plugin or fact" if @input_file_name =~ /\/facter\/[^\/]+\.rb$/ parse_fact(container) else parse_puppet_plugin(container) end end # this is a poor man custom fact parser :-) def parse_fact(container) comments = "" current_fact = nil File.open(@input_file_name) do |of| of.each do |line| # fetch comments if line =~ /^[ \t]*# ?(.*)$/ comments += $1 + "\n" elsif line =~ /^[ \t]*Facter.add\(['"](.*?)['"]\)/ current_fact = Fact.new($1,{}) look_for_directives_in(container, comments) unless comments.empty? current_fact.comment = comments container.add_fact(current_fact) current_fact.record_location(@top_level) comments = "" Puppet.debug "rdoc: found custom fact #{current_fact.name}" elsif line =~ /^[ \t]*confine[ \t]*:(.*?)[ \t]*=>[ \t]*(.*)$/ current_fact.confine = { :type => $1, :value => $2 } unless current_fact.nil? else # unknown line type comments ="" end end end end # this is a poor man puppet plugin parser :-) # it doesn't extract doc nor desc :-( def parse_puppet_plugin(container) comments = "" current_plugin = nil File.open(@input_file_name) do |of| of.each do |line| # fetch comments if line =~ /^[ \t]*# ?(.*)$/ comments += $1 + "\n" elsif line =~ /^[ \t]*newfunction[ \t]*\([ \t]*:(.*?)[ \t]*,[ \t]*:type[ \t]*=>[ \t]*(:rvalue|:lvalue)\)/ current_plugin = Plugin.new($1, "function") container.add_plugin(current_plugin) look_for_directives_in(container, comments) unless comments.empty? current_plugin.comment = comments current_plugin.record_location(@top_level) comments = "" Puppet.debug "rdoc: found new function plugins #{current_plugin.name}" elsif line =~ /^[ \t]*Puppet::Type.newtype[ \t]*\([ \t]*:(.*?)\)/ current_plugin = Plugin.new($1, "type") container.add_plugin(current_plugin) look_for_directives_in(container, comments) unless comments.empty? current_plugin.comment = comments current_plugin.record_location(@top_level) comments = "" Puppet.debug "rdoc: found new type plugins #{current_plugin.name}" elsif line =~ /module Puppet::Parser::Functions/ # skip else # unknown line type comments ="" end end end end # look_for_directives_in scans the current +comment+ for RDoc directives def look_for_directives_in(context, comment) preprocess = SM::PreProcess.new(@input_file_name, @options.rdoc_include) preprocess.handle(comment) do |directive, param| case directive when "stopdoc" context.stop_doc "" when "startdoc" context.start_doc context.force_documentation = true "" when "enddoc" #context.done_documenting = true #"" throw :enddoc when "main" options = Options.instance options.main_page = param "" when "title" options = Options.instance options.title = param "" when "section" context.set_current_section(param, comment) comment.replace("") # 1.8 doesn't support #clear break else warn "Unrecognized directive '#{directive}'" break end end remove_private_comments(comment) end def remove_private_comments(comment) comment.gsub!(/^#--.*?^#\+\+/m, '') comment.sub!(/^#--.*/m, '') end end end diff --git a/spec/integration/indirector/file_content/file_server_spec.rb b/spec/integration/indirector/file_content/file_server_spec.rb index ca5c7fb96..b4e1c908d 100755 --- a/spec/integration/indirector/file_content/file_server_spec.rb +++ b/spec/integration/indirector/file_content/file_server_spec.rb @@ -1,90 +1,90 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/file_content/file_server' require 'shared_behaviours/file_server_terminus' require 'puppet_spec/files' describe Puppet::Indirector::FileContent::FileServer, " when finding files" do it_should_behave_like "Puppet::Indirector::FileServerTerminus" include PuppetSpec::Files before do @terminus = Puppet::Indirector::FileContent::FileServer.new @test_class = Puppet::FileServing::Content Puppet::FileServing::Configuration.instance_variable_set(:@configuration, nil) end it "should find plugin file content in the environment specified in the request" do path = tmpfile("file_content_with_env") Dir.mkdir(path) modpath = File.join(path, "mod") FileUtils.mkdir_p(File.join(modpath, "lib")) file = File.join(modpath, "lib", "file.rb") File.open(file, "wb") { |f| f.write "1\r\n" } Puppet.settings[:modulepath] = "/no/such/file" env = Puppet::Node::Environment.new("foo") env.stubs(:modulepath).returns [path] result = Puppet::FileServing::Content.indirection.search("plugins", :environment => "foo", :recurse => true) result.should_not be_nil result.length.should == 2 - result[1].should be_instance_of(Puppet::FileServing::Content) - result[1].content.should == "1\r\n" + result.map {|x| x.should be_instance_of(Puppet::FileServing::Content) } + result.find {|x| x.relative_path == 'file.rb' }.content.should == "1\r\n" end it "should find file content in modules" do path = tmpfile("file_content") Dir.mkdir(path) modpath = File.join(path, "mymod") FileUtils.mkdir_p(File.join(modpath, "files")) file = File.join(modpath, "files", "myfile") File.open(file, "wb") { |f| f.write "1\r\n" } Puppet.settings[:modulepath] = path result = Puppet::FileServing::Content.indirection.find("modules/mymod/myfile") result.should_not be_nil result.should be_instance_of(Puppet::FileServing::Content) result.content.should == "1\r\n" end it "should find file content in files when node name expansions are used" do FileTest.stubs(:exists?).returns true FileTest.stubs(:exists?).with(Puppet[:fileserverconfig]).returns(true) @path = tmpfile("file_server_testing") Dir.mkdir(@path) subdir = File.join(@path, "mynode") Dir.mkdir(subdir) File.open(File.join(subdir, "myfile"), "wb") { |f| f.write "1\r\n" } # Use a real mount, so the integration is a bit deeper. @mount1 = Puppet::FileServing::Configuration::Mount::File.new("one") @mount1.stubs(:allowed?).returns true @mount1.path = File.join(@path, "%h") @parser = stub 'parser', :changed? => false @parser.stubs(:parse).returns("one" => @mount1) Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser) path = File.join(@path, "myfile") result = Puppet::FileServing::Content.indirection.find("one/myfile", :environment => "foo", :node => "mynode") result.should_not be_nil result.should be_instance_of(Puppet::FileServing::Content) result.content.should == "1\r\n" end end diff --git a/spec/unit/face/module/build_spec.rb b/spec/unit/face/module/build_spec.rb deleted file mode 100644 index f4dc7f795..000000000 --- a/spec/unit/face/module/build_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' -require 'puppet/face' - -describe "puppet module build" do - subject { Puppet::Face[:module, :current] } - - describe "option validation" do - context "without any options" do - it "should require a path" do - pattern = /wrong number of arguments/ - expect { subject.build }.to raise_error ArgumentError, pattern - end - end - end - - describe "inline documentation" do - subject { Puppet::Face[:module, :current].get_action :build } - - its(:summary) { should =~ /build.*module/im } - its(:description) { should =~ /build.*module/im } - its(:returns) { should =~ /pathname/i } - its(:examples) { should_not be_empty } - - %w{ license copyright summary description returns examples }.each do |doc| - context "of the" do - its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } - end - end - end -end diff --git a/spec/unit/face/module/changes_spec.rb b/spec/unit/face/module/changes_spec.rb deleted file mode 100644 index eefa9d2a6..000000000 --- a/spec/unit/face/module/changes_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' -require 'puppet/face' - -describe "puppet module changes" do - subject { Puppet::Face[:module, :current] } - - describe "option validation" do - context "without any options" do - it "should require a path" do - pattern = /wrong number of arguments/ - expect { subject.changes }.to raise_error ArgumentError, pattern - end - end - end - - describe "inline documentation" do - subject { Puppet::Face[:module, :current].get_action :changes } - - its(:summary) { should =~ /modified.*module/im } - its(:description) { should =~ /modified.*module/im } - its(:returns) { should =~ /array/i } - its(:examples) { should_not be_empty } - - %w{ license copyright summary description returns examples }.each do |doc| - context "of the" do - its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } - end - end - end -end diff --git a/spec/unit/face/module/clean_spec.rb b/spec/unit/face/module/clean_spec.rb deleted file mode 100644 index 708aba155..000000000 --- a/spec/unit/face/module/clean_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' -require 'puppet/face' - -describe "puppet module clean" do - subject { Puppet::Face[:module, :current] } - - describe "option validation" do - context "without any options" do - it "should not require any arguments" do - Puppet::Module::Tool::Applications::Cleaner.expects(:run).once - subject.clean - end - end - end - - describe "inline documentation" do - subject { Puppet::Face[:module, :current].get_action :clean } - - its(:summary) { should =~ /clean.*module/im } - its(:description) { should =~ /clean.*module/im } - its(:returns) { should =~ /hash/i } - its(:examples) { should_not be_empty } - - %w{ license copyright summary description returns examples }.each do |doc| - context "of the" do - its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } - end - end - end -end diff --git a/spec/unit/face/module/generate_spec.rb b/spec/unit/face/module/generate_spec.rb deleted file mode 100644 index 1e5a420b1..000000000 --- a/spec/unit/face/module/generate_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' -require 'puppet/face' - -describe "puppet module generate" do - subject { Puppet::Face[:module, :current] } - - describe "option validation" do - context "without any options" do - it "should require name" do - pattern = /wrong number of arguments/ - expect { subject.generate }.to raise_error ArgumentError, pattern - end - end - end - - describe "inline documentation" do - subject { Puppet::Face[:module, :current].get_action :generate } - - its(:summary) { should =~ /generate.*module/im } - its(:description) { should =~ /generate.*module/im } - its(:returns) { should =~ /array/i } - its(:examples) { should_not be_empty } - - %w{ license copyright summary description returns examples }.each do |doc| - context "of the" do - its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } - end - end - end -end diff --git a/spec/unit/face/module/install_spec.rb b/spec/unit/face/module/install_spec.rb deleted file mode 100644 index 681109793..000000000 --- a/spec/unit/face/module/install_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'spec_helper' -require 'puppet/face' -require 'puppet/module_tool' - -describe "puppet module install" do - subject { Puppet::Face[:module, :current] } - - let(:options) do - {} - end - - describe "option validation" do - let(:expected_options) do - { - :install_dir => File.expand_path("/dev/null/modules"), - :module_repository => "http://forge.puppetlabs.com", - } - end - - context "without any options" do - it "should require a name" do - pattern = /wrong number of arguments/ - expect { subject.install }.to raise_error ArgumentError, pattern - end - - it "should not require any options" do - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once - subject.install("puppetlabs-apache") - end - end - - it "should accept the --force option" do - options[:force] = true - expected_options.merge!(options) - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once - subject.install("puppetlabs-apache", options) - end - - it "should accept the --install-dir option" do - options[:install_dir] = "/foo/puppet/modules" - expected_options.merge!(options) - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once - subject.install("puppetlabs-apache", options) - end - - it "should accept the --module-repository option" do - options[:module_repository] = "http://forge.example.com" - expected_options.merge!(options) - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once - subject.install("puppetlabs-apache", options) - end - - it "should accept the --version option" do - options[:version] = "0.0.1" - expected_options.merge!(options) - Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once - subject.install("puppetlabs-apache", options) - end - end - - describe "inline documentation" do - subject { Puppet::Face[:module, :current].get_action :install } - - its(:summary) { should =~ /install.*module/im } - its(:description) { should =~ /install.*module/im } - its(:returns) { should =~ /pathname/i } - its(:examples) { should_not be_empty } - - %w{ license copyright summary description returns examples }.each do |doc| - context "of the" do - its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } - end - end - end -end diff --git a/spec/unit/face/module/search_spec.rb b/spec/unit/face/module/search_spec.rb deleted file mode 100644 index 250c5e0b1..000000000 --- a/spec/unit/face/module/search_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'spec_helper' -require 'puppet/face' - -describe "puppet module search" do - subject { Puppet::Face[:module, :current] } - - let(:options) do - {} - end - - describe "option validation" do - context "without any options" do - it "should require a search term" do - pattern = /wrong number of arguments/ - expect { subject.search }.to raise_error ArgumentError, pattern - end - end - - it "should accept the --module-repository option" do - options[:module_repository] = "http://forge.example.com" - Puppet::Module::Tool::Applications::Searcher.expects(:run).with("puppetlabs-apache", options).once - subject.search("puppetlabs-apache", options) - end - end - - describe "inline documentation" do - subject { Puppet::Face[:module, :current].get_action :search } - - its(:summary) { should =~ /search.*module/im } - its(:description) { should =~ /search.*module/im } - its(:returns) { should =~ /array/i } - its(:examples) { should_not be_empty } - - %w{ license copyright summary description returns examples }.each do |doc| - context "of the" do - its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ } - end - end - end -end diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index cf7208443..613ad8ebd 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,590 +1,504 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet_spec/files' describe Puppet::Module do include PuppetSpec::Files before do # This is necessary because of the extra checks we have for the deprecated # 'plugins' directory FileTest.stubs(:exist?).returns false end it "should have a class method that returns a named module from a given environment" do env = mock 'module' env.expects(:module).with("mymod").returns "yep" Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should == "yep" end it "should return nil if asked for a named module that doesn't exist" do env = mock 'module' env.expects(:module).with("mymod").returns nil Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should be_nil end it "should support a 'version' attribute" do mod = Puppet::Module.new("mymod") mod.version = 1.09 mod.version.should == 1.09 end it "should support a 'source' attribute" do mod = Puppet::Module.new("mymod") mod.source = "http://foo/bar" mod.source.should == "http://foo/bar" end it "should support a 'project_page' attribute" do mod = Puppet::Module.new("mymod") mod.project_page = "http://foo/bar" mod.project_page.should == "http://foo/bar" end it "should support an 'author' attribute" do mod = Puppet::Module.new("mymod") mod.author = "Luke Kanies " mod.author.should == "Luke Kanies " end it "should support a 'license' attribute" do mod = Puppet::Module.new("mymod") mod.license = "GPL2" mod.license.should == "GPL2" end it "should support a 'summary' attribute" do mod = Puppet::Module.new("mymod") mod.summary = "GPL2" mod.summary.should == "GPL2" end it "should support a 'description' attribute" do mod = Puppet::Module.new("mymod") mod.description = "GPL2" mod.description.should == "GPL2" end it "should support specifying a compatible puppet version" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" mod.puppetversion.should == "0.25" end it "should validate that the puppet version is compatible" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" Puppet.expects(:version).returns "0.25" mod.validate_puppet_version end it "should fail if the specified puppet version is not compatible" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" Puppet.stubs(:version).returns "0.24" lambda { mod.validate_puppet_version }.should raise_error(Puppet::Module::IncompatibleModule) end - describe "when specifying required modules" do - it "should support specifying a required module" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar" - end - - it "should support specifying multiple required modules" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar" - mod.requires "baz" - end - - it "should support specifying a required module and version" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar", 1.0 - end - - it "should fail when required modules are missing" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar" - - mod.environment.expects(:module).with("foobar").returns nil - - lambda { mod.validate_dependencies }.should raise_error(Puppet::Module::MissingModule) - end - - it "should fail when required modules are present but of the wrong version" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar", 1.0 - - foobar = Puppet::Module.new("foobar") - foobar.version = 2.0 - - mod.environment.expects(:module).with("foobar").returns foobar - - lambda { mod.validate_dependencies }.should raise_error(Puppet::Module::IncompatibleModule) - end - - it "should have valid dependencies when no dependencies have been specified" do - mod = Puppet::Module.new("mymod") - - lambda { mod.validate_dependencies }.should_not raise_error - end - - it "should fail when some dependencies are present but others aren't" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar" - mod.requires "baz" - - mod.environment.expects(:module).with("foobar").returns Puppet::Module.new("foobar") - mod.environment.expects(:module).with("baz").returns nil - - lambda { mod.validate_dependencies }.should raise_error(Puppet::Module::MissingModule) - end - - it "should have valid dependencies when all dependencies are met" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar", 1.0 - mod.requires "baz" - - foobar = Puppet::Module.new("foobar") - foobar.version = 1.0 - - baz = Puppet::Module.new("baz") - - mod.environment.expects(:module).with("foobar").returns foobar - mod.environment.expects(:module).with("baz").returns baz - - lambda { mod.validate_dependencies }.should_not raise_error - end - - it "should validate its dependendencies on initialization" do - Puppet::Module.any_instance.expects(:validate_dependencies) - Puppet::Module.new("mymod") - end - end - describe "when managing supported platforms" do it "should support specifying a supported platform" do mod = Puppet::Module.new("mymod") mod.supports "solaris" end it "should support specifying a supported platform and version" do mod = Puppet::Module.new("mymod") mod.supports "solaris", 1.0 end it "should fail when not running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" mod.supports "hpux" lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::UnsupportedPlatform) end it "should fail when supported platforms are present but of the wrong version" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when no supported platforms have been specified" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") lambda { mod.validate_supported_platform }.should_not raise_error end it "should be considered supported when running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when running on any of multiple supported platforms" do pending "Not sure how to send client platform to the module" end it "should validate its platform support on initialization" do pending "Not sure how to send client platform to the module" end end it "should return nil if asked for a module whose name is 'nil'" do Puppet::Module.find(nil, "myenv").should be_nil end it "should provide support for logging" do Puppet::Module.ancestors.should be_include(Puppet::Util::Logging) end it "should be able to be converted to a string" do Puppet::Module.new("foo").to_s.should == "Module foo" end it "should add the path to its string form if the module is found" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a" mod.to_s.should == "Module foo(/a)" end it "should fail if its name is not alphanumeric" do lambda { Puppet::Module.new(".something") }.should raise_error(Puppet::Module::InvalidName) end it "should require a name at initialization" do lambda { Puppet::Module.new }.should raise_error(ArgumentError) end it "should convert an environment name into an Environment instance" do - Puppet::Module.new("foo", "prod").environment.should be_instance_of(Puppet::Node::Environment) + Puppet::Module.new("foo", :environment => "prod").environment.should be_instance_of(Puppet::Node::Environment) end it "should accept an environment at initialization" do - Puppet::Module.new("foo", :prod).environment.name.should == :prod + Puppet::Module.new("foo", :environment => :prod).environment.name.should == :prod end it "should use the default environment if none is provided" do env = Puppet::Node::Environment.new Puppet::Module.new("foo").environment.should equal(env) end it "should use any provided Environment instance" do env = Puppet::Node::Environment.new - Puppet::Module.new("foo", env).environment.should equal(env) + Puppet::Module.new("foo", :environment => env).environment.should equal(env) end - it "should return the path to the first found instance in its environment's module paths as its path" do - dir = tmpdir("deep_path") - first = File.join(dir, "first") - second = File.join(dir, "second") + describe ".path" do + before do + dir = tmpdir("deep_path") - FileUtils.mkdir_p(first) - FileUtils.mkdir_p(second) - Puppet[:modulepath] = "#{first}#{File::PATH_SEPARATOR}#{second}" + @first = File.join(dir, "first") + @second = File.join(dir, "second") + Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" - modpath = File.join(first, "foo") - FileUtils.mkdir_p(modpath) + FileUtils.mkdir_p(@first) + FileUtils.mkdir_p(@second) + end - # Make a second one, which we shouldn't find - FileUtils.mkdir_p(File.join(second, "foo")) + it "should return the path to the first found instance in its environment's module paths as its path" do + modpath = File.join(@first, "foo") + FileUtils.mkdir_p(modpath) - mod = Puppet::Module.new("foo") - mod.path.should == modpath - end + # Make a second one, which we shouldn't find + FileUtils.mkdir_p(File.join(@second, "foo")) + + mod = Puppet::Module.new("foo") + mod.path.should == modpath + end - it "should be able to find itself in a directory other than the first directory in the module path" do - dir = tmpdir("deep_path") - first = File.join(dir, "first") - second = File.join(dir, "second") + it "should be able to find itself in a directory other than the first directory in the module path" do + modpath = File.join(@second, "foo") + FileUtils.mkdir_p(modpath) - FileUtils.mkdir_p(first) - FileUtils.mkdir_p(second) - Puppet[:modulepath] = "#{first}#{File::PATH_SEPARATOR}#{second}" + mod = Puppet::Module.new("foo") + mod.should be_exist + mod.path.should == modpath + end - modpath = File.join(second, "foo") - FileUtils.mkdir_p(modpath) + it "should be able to find itself in a directory other than the first directory in the module path even when it exists in the first" do + environment = Puppet::Node::Environment.new - mod = Puppet::Module.new("foo") - mod.should be_exist - mod.path.should == modpath + first_modpath = File.join(@first, "foo") + FileUtils.mkdir_p(first_modpath) + second_modpath = File.join(@second, "foo") + FileUtils.mkdir_p(second_modpath) + + mod = Puppet::Module.new("foo", :environment => environment, :path => second_modpath) + mod.path.should == File.join(@second, "foo") + mod.environment.should == environment + end end it "should be considered existent if it exists in at least one module path" do mod = Puppet::Module.new("foo") mod.expects(:path).returns "/a/foo" mod.should be_exist end it "should be considered nonexistent if it does not exist in any of the module paths" do mod = Puppet::Module.new("foo") mod.expects(:path).returns nil mod.should_not be_exist end [:plugins, :templates, :files, :manifests].each do |filetype| dirname = filetype == :plugins ? "lib" : filetype.to_s it "should be able to return individual #{filetype}" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should == path end it "should consider #{filetype} to be present if their base directory exists" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s + "?").should be_true end it "should consider #{filetype} to be absent if their base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s + "?").should be_false end it "should consider #{filetype} to be absent if the module base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s + "?").should be_false end it "should return nil if asked to return individual #{filetype} that don't exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return nil when asked for individual #{filetype} if the module does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return the base directory if asked for a nil path" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" base = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(base).returns true mod.send(filetype.to_s.sub(/s$/, ''), nil).should == base end end it "should return the path to the plugin directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.plugin_directory.should == "/a/foo/lib" end it "should throw a warning if plugins are in a 'plugins' directory rather than a 'lib' directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" FileTest.expects(:exist?).with("/a/foo/plugins").returns true mod.plugin_directory.should == "/a/foo/plugins" @logs.first.message.should == "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" @logs.first.level.should == :warning end it "should default to 'lib' for the plugins directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.plugin_directory.should == "/a/foo/lib" end end -describe Puppet::Module, " when building its search path" do - it "should use the current environment's search path if no environment is specified" do - env = mock 'env' - env.expects(:modulepath).returns "eh" - Puppet::Node::Environment.expects(:new).with(nil).returns env - - Puppet::Module.modulepath.should == "eh" - end - - it "should use the specified environment's search path if an environment is specified" do - env = mock 'env' - env.expects(:modulepath).returns "eh" - Puppet::Node::Environment.expects(:new).with("foo").returns env - - Puppet::Module.modulepath("foo").should == "eh" - end -end - describe Puppet::Module, "when finding matching manifests" do before do @mod = Puppet::Module.new("mymod") @mod.stubs(:path).returns "/a" @pq_glob_with_extension = "yay/*.xx" @fq_glob_with_extension = "/a/manifests/#{@pq_glob_with_extension}" end it "should return all manifests matching the glob pattern" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.stubs(:directory?).returns false @mod.match_manifests(@pq_glob_with_extension).should == %w{foo bar} end it "should not return directories" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.expects(:directory?).with("foo").returns false FileTest.expects(:directory?).with("bar").returns true @mod.match_manifests(@pq_glob_with_extension).should == %w{foo} end it "should default to the 'init' file if no glob pattern is specified" do Dir.expects(:glob).with("/a/manifests/init.{pp,rb}").returns(%w{/a/manifests/init.pp}) @mod.match_manifests(nil).should == %w{/a/manifests/init.pp} end it "should return all manifests matching the glob pattern in all existing paths" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{a b}) @mod.match_manifests(@pq_glob_with_extension).should == %w{a b} end it "should match the glob pattern plus '.{pp,rb}' if no extention is specified" do Dir.expects(:glob).with("/a/manifests/yay/foo.{pp,rb}").returns(%w{yay}) @mod.match_manifests("yay/foo").should == %w{yay} end it "should return an empty array if no manifests matched" do Dir.expects(:glob).with(@fq_glob_with_extension).returns([]) @mod.match_manifests(@pq_glob_with_extension).should == [] end end describe Puppet::Module do before do Puppet::Module.any_instance.stubs(:path).returns "/my/mod/path" @module = Puppet::Module.new("foo") end it "should use 'License' in its current path as its metadata file" do @module.license_file.should == "/my/mod/path/License" end it "should return nil as its license file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").license_file.should be_nil end it "should cache the license file" do Puppet::Module.any_instance.expects(:path).once.returns nil mod = Puppet::Module.new("foo") mod.license_file.should == mod.license_file end it "should use 'metadata.json' in its current path as its metadata file" do @module.metadata_file.should == "/my/mod/path/metadata.json" end it "should return nil as its metadata file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").metadata_file.should be_nil end it "should cache the metadata file" do Puppet::Module.any_instance.expects(:path).once.returns nil mod = Puppet::Module.new("foo") mod.metadata_file.should == mod.metadata_file end it "should have metadata if it has a metadata file and its data is not empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should have metadata if it has a metadata file and its data is not empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should not have metadata if has a metadata file and its data is empty" do FileTest.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "/* +-----------------------------------------------------------------------+ | | | ==> DO NOT EDIT THIS FILE! <== | | | | You should edit the `Modulefile` and run `puppet-module build` | | to generate the `metadata.json` file for your releases. | | | +-----------------------------------------------------------------------+ */ {}" @module.should_not be_has_metadata end it "should know if it is missing a metadata file" do FileTest.expects(:exist?).with(@module.metadata_file).returns false @module.should_not be_has_metadata end it "should be able to parse its metadata file" do @module.should respond_to(:load_metadata) end it "should parse its metadata file on initialization if it is present" do Puppet::Module.any_instance.expects(:has_metadata?).returns true Puppet::Module.any_instance.expects(:load_metadata) Puppet::Module.new("yay") end describe "when loading the medatada file", :if => Puppet.features.pson? do before do @data = { :license => "GPL2", :author => "luke", :version => "1.0", :source => "http://foo/", :puppetversion => "0.25" } @text = @data.to_pson @module = Puppet::Module.new("foo") @module.stubs(:metadata_file).returns "/my/file" File.stubs(:read).with("/my/file").returns @text end %w{source author version license}.each do |attr| it "should set #{attr} if present in the metadata file" do @module.load_metadata @module.send(attr).should == @data[attr.to_sym] end it "should fail if #{attr} is not present in the metadata file" do @data.delete(attr.to_sym) @text = @data.to_pson File.stubs(:read).with("/my/file").returns @text lambda { @module.load_metadata }.should raise_error( Puppet::Module::MissingMetadata, "No #{attr} module metadata provided for foo" ) end end it "should set puppetversion if present in the metadata file" do @module.load_metadata @module.puppetversion.should == @data[:puppetversion] end - it "should fail if the discovered name is different than the metadata name" end end diff --git a/spec/unit/module_tool/uninstaller_spec.rb b/spec/unit/module_tool/uninstaller_spec.rb new file mode 100644 index 000000000..abf2db0f8 --- /dev/null +++ b/spec/unit/module_tool/uninstaller_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' +require 'puppet/module_tool' +require 'tmpdir' + +describe Puppet::Module::Tool::Applications::Uninstaller do + include PuppetSpec::Files + + describe "instances" do + let(:tmp_module_path1) { tmpdir("uninstaller_module_path1") } + let(:tmp_module_path2) { tmpdir("uninstaller_module_path2") } + let(:options) do + { :target_directories => [ tmp_module_path1, tmp_module_path2 ] } + end + + it "should return an empty list if the module is not installed" do + described_class.new('foo', options).run.should == [] + end + + it "should uninstall an installed module" do + foo_module_path = File.join(tmp_module_path1, 'foo') + Dir.mkdir(foo_module_path) + described_class.new('foo', options).run.should == [ foo_module_path ] + end + + it "should only uninstall the requested module" do + foo_module_path = File.join(tmp_module_path1, 'foo') + bar_module_path = File.join(tmp_module_path1, 'bar') + Dir.mkdir(foo_module_path) + Dir.mkdir(bar_module_path) + described_class.new('foo', options).run.should == [ foo_module_path ] + end + + it "should uninstall the module from all target directories" do + foo1_module_path = File.join(tmp_module_path1, 'foo') + foo2_module_path = File.join(tmp_module_path2, 'foo') + Dir.mkdir(foo1_module_path) + Dir.mkdir(foo2_module_path) + described_class.new('foo', options).run.should == [ foo1_module_path, foo2_module_path ] + end + + #11803 + it "should check for broken dependencies" + end +end diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index 78d383440..d5d3068a1 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -1,321 +1,339 @@ #!/usr/bin/env rspec require 'spec_helper' require 'tmpdir' require 'puppet/node/environment' require 'puppet/util/execution' describe Puppet::Node::Environment do + let(:env) { Puppet::Node::Environment.new("testing") } + include PuppetSpec::Files after do Puppet::Node::Environment.clear end it "should use the filetimeout for the ttl for the modulepath" do Puppet::Node::Environment.attr_ttl(:modulepath).should == Integer(Puppet[:filetimeout]) end it "should use the filetimeout for the ttl for the module list" do Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout]) end it "should use the default environment if no name is provided while initializing an environment" do Puppet.settings.expects(:value).with(:environment).returns("one") Puppet::Node::Environment.new.name.should == :one end it "should treat environment instances as singletons" do Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one")) end it "should treat an environment specified as names or strings as equivalent" do Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one")) end it "should return its name when converted to a string" do Puppet::Node::Environment.new(:one).to_s.should == "one" end it "should just return any provided environment if an environment is provided as the name" do one = Puppet::Node::Environment.new(:one) Puppet::Node::Environment.new(one).should equal(one) end describe "when managing known resource types" do before do - @env = Puppet::Node::Environment.new("dev") - @collection = Puppet::Resource::TypeCollection.new(@env) - @env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) + @collection = Puppet::Resource::TypeCollection.new(env) + env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) Thread.current[:known_resource_types] = nil end it "should create a resource type collection if none exists" do - Puppet::Resource::TypeCollection.expects(:new).with(@env).returns @collection - @env.known_resource_types.should equal(@collection) + Puppet::Resource::TypeCollection.expects(:new).with(env).returns @collection + env.known_resource_types.should equal(@collection) end it "should reuse any existing resource type collection" do - @env.known_resource_types.should equal(@env.known_resource_types) + env.known_resource_types.should equal(env.known_resource_types) end it "should perform the initial import when creating a new collection" do - @env = Puppet::Node::Environment.new("dev") - @env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) - @env.known_resource_types + env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) + env.known_resource_types end it "should return the same collection even if stale if it's the same thread" do Puppet::Resource::TypeCollection.stubs(:new).returns @collection - @env.known_resource_types.stubs(:stale?).returns true + env.known_resource_types.stubs(:stale?).returns true - @env.known_resource_types.should equal(@collection) + env.known_resource_types.should equal(@collection) end it "should return the current thread associated collection if there is one" do Thread.current[:known_resource_types] = @collection - @env.known_resource_types.should equal(@collection) + env.known_resource_types.should equal(@collection) end it "should give to all threads using the same environment the same collection if the collection isn't stale" do - original_thread_type_collection = Puppet::Resource::TypeCollection.new(@env) - Puppet::Resource::TypeCollection.expects(:new).with(@env).returns original_thread_type_collection - @env.known_resource_types.should equal(original_thread_type_collection) + original_thread_type_collection = Puppet::Resource::TypeCollection.new(env) + Puppet::Resource::TypeCollection.expects(:new).with(env).returns original_thread_type_collection + env.known_resource_types.should equal(original_thread_type_collection) original_thread_type_collection.expects(:require_reparse?).returns(false) - Puppet::Resource::TypeCollection.stubs(:new).with(@env).returns @collection + Puppet::Resource::TypeCollection.stubs(:new).with(env).returns @collection t = Thread.new { - @env.known_resource_types.should equal(original_thread_type_collection) + env.known_resource_types.should equal(original_thread_type_collection) } t.join end it "should generate a new TypeCollection if the current one requires reparsing" do - old_type_collection = @env.known_resource_types + old_type_collection = env.known_resource_types old_type_collection.stubs(:require_reparse?).returns true Thread.current[:known_resource_types] = nil - new_type_collection = @env.known_resource_types + new_type_collection = env.known_resource_types new_type_collection.should be_a Puppet::Resource::TypeCollection new_type_collection.should_not equal(old_type_collection) end end it "should validate the modulepath directories" do real_file = tmpdir('moduledir') path = %W[/one /two #{real_file}].join(File::PATH_SEPARATOR) Puppet[:modulepath] = path - env = Puppet::Node::Environment.new("testing") - env.modulepath.should == [real_file] end it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do Puppet::Util::Execution.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do - env = Puppet::Node::Environment.new("testing") module_path = %w{/one /two}.join(File::PATH_SEPARATOR) env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two} env.expects(:[]).with(:modulepath).returns module_path env.modulepath.should == %w{/l1 /l2 /one /two} end end describe "when validating modulepath or manifestdir directories" do before :each do @path_one = make_absolute('/one') @path_two = make_absolute('/two') end it "should not return non-directories" do - env = Puppet::Node::Environment.new("testing") - FileTest.expects(:directory?).with(@path_one).returns true FileTest.expects(:directory?).with(@path_two).returns false env.validate_dirs([@path_one, @path_two]).should == [@path_one] end it "should use the current working directory to fully-qualify unqualified paths" do FileTest.stubs(:directory?).returns true - env = Puppet::Node::Environment.new("testing") two = File.expand_path(File.join(Dir.getwd, "two")) env.validate_dirs([@path_one, 'two']).should == [@path_one, two] end end describe "when modeling a specific environment" do it "should have a method for returning the environment name" do Puppet::Node::Environment.new("testing").name.should == :testing end it "should provide an array-like accessor method for returning any environment-specific setting" do - env = Puppet::Node::Environment.new("testing") env.should respond_to(:[]) end it "should ask the Puppet settings instance for the setting qualified with the environment name" do Puppet.settings.expects(:value).with("myvar", :testing).returns("myval") - env = Puppet::Node::Environment.new("testing") env["myvar"].should == "myval" end it "should be able to return an individual module that exists in its module path" do - env = Puppet::Node::Environment.new("testing") mod = mock 'module' - Puppet::Module.expects(:new).with("one", env).returns mod + Puppet::Module.expects(:new).with("one", :environment => env).returns mod mod.expects(:exist?).returns true env.module("one").should equal(mod) end it "should return nil if asked for a module that does not exist in its path" do - env = Puppet::Node::Environment.new("testing") mod = mock 'module' - Puppet::Module.expects(:new).with("one", env).returns mod + Puppet::Module.expects(:new).with("one", :environment => env).returns mod mod.expects(:exist?).returns false env.module("one").should be_nil end - it "should be able to return its modules" do - Puppet::Node::Environment.new("testing").should respond_to(:modules) + describe ".modules_by_path" do + before do + dir = tmpdir("deep_path") + + @first = File.join(dir, "first") + @second = File.join(dir, "second") + Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" + + FileUtils.mkdir_p(@first) + FileUtils.mkdir_p(@second) + end + + it "should return an empty list if there are no modules" do + env.modules_by_path.should == { + @first => [], + @second => [] + } + end + + it "should include modules even if they exist in multiple dirs in the modulepath" do + modpath1 = File.join(@first, "foo") + FileUtils.mkdir_p(modpath1) + modpath2 = File.join(@second, "foo") + FileUtils.mkdir_p(modpath2) + + env.modules_by_path.should == { + @first => [Puppet::Module.new('foo', :environment => env, :path => modpath1)], + @second => [Puppet::Module.new('foo', :environment => env, :path => modpath2)] + } + end end describe ".modules" do + it "should return an empty list if there are no modules" do + env.modulepath = %w{/a /b} + Dir.expects(:entries).with("/a").returns [] + Dir.expects(:entries).with("/b").returns [] + + env.modules.should == [] + end + it "should return a module named for every directory in each module path" do - env = Puppet::Node::Environment.new("testing") - env.expects(:modulepath).at_least_once.returns %w{/a /b} + env.modulepath = %w{/a /b} Dir.expects(:entries).with("/a").returns %w{foo bar} Dir.expects(:entries).with("/b").returns %w{bee baz} env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort end it "should remove duplicates" do - env = Puppet::Node::Environment.new("testing") - env.expects(:modulepath).returns( %w{/a /b} ).at_least_once + env.modulepath = %w{/a /b} Dir.expects(:entries).with("/a").returns %w{foo} Dir.expects(:entries).with("/b").returns %w{foo} env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end it "should ignore invalid modules" do - env = Puppet::Node::Environment.new("testing") - env.stubs(:modulepath).returns %w{/a} + env.modulepath = %w{/a} Dir.expects(:entries).with("/a").returns %w{foo bar} Puppet::Module.expects(:new).with { |name, env| name == "foo" }.returns mock("foomod", :name => "foo") Puppet::Module.expects(:new).with { |name, env| name == "bar" }.raises( Puppet::Module::InvalidName, "name is invalid" ) env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end it "should create modules with the correct environment" do - env = Puppet::Node::Environment.new("testing") - env.expects(:modulepath).at_least_once.returns %w{/a} + env.modulepath = %w{/a} Dir.expects(:entries).with("/a").returns %w{foo} env.modules.each {|mod| mod.environment.should == env } end it "should cache the module list" do - env = Puppet::Node::Environment.new("testing") - env.expects(:modulepath).at_least_once.returns %w{/a} + env.modulepath = %w{/a} Dir.expects(:entries).once.with("/a").returns %w{foo} env.modules env.modules end end end describe Puppet::Node::Environment::Helper do before do @helper = Object.new @helper.extend(Puppet::Node::Environment::Helper) end - it "should be able to set and retrieve the environment" do + it "should be able to set and retrieve the environment as a symbol" do @helper.environment = :foo @helper.environment.name.should == :foo end it "should accept an environment directly" do - env = Puppet::Node::Environment.new :foo - @helper.environment = env + @helper.environment = Puppet::Node::Environment.new(:foo) @helper.environment.name.should == :foo end it "should accept an environment as a string" do - env = Puppet::Node::Environment.new "foo" - @helper.environment = env + @helper.environment = 'foo' @helper.environment.name.should == :foo end end describe "when performing initial import" do before do @parser = Puppet::Parser::Parser.new("test") Puppet::Parser::Parser.stubs(:new).returns @parser - @env = Puppet::Node::Environment.new("env") end it "should set the parser's string to the 'code' setting and parse if code is available" do Puppet.settings[:code] = "my code" @parser.expects(:string=).with "my code" @parser.expects(:parse) - @env.instance_eval { perform_initial_import } + env.instance_eval { perform_initial_import } end it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do filename = tmpfile('myfile') File.open(filename, 'w'){|f| } Puppet.settings[:manifest] = filename @parser.expects(:file=).with filename @parser.expects(:parse) - @env.instance_eval { perform_initial_import } + env.instance_eval { perform_initial_import } end it "should pass the manifest file to the parser even if it does not exist on disk" do filename = tmpfile('myfile') Puppet.settings[:code] = "" Puppet.settings[:manifest] = filename @parser.expects(:file=).with(filename).once @parser.expects(:parse).once - @env.instance_eval { perform_initial_import } + env.instance_eval { perform_initial_import } end it "should fail helpfully if there is an error importing" do File.stubs(:exist?).returns true - @env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(@env) + env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) @parser.expects(:file=).once @parser.expects(:parse).raises ArgumentError - lambda { @env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error) + lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error) end it "should not do anything if the ignore_import settings is set" do Puppet.settings[:ignoreimport] = true @parser.expects(:string=).never @parser.expects(:file=).never @parser.expects(:parse).never - @env.instance_eval { perform_initial_import } + env.instance_eval { perform_initial_import } end it "should mark the type collection as needing a reparse when there is an error parsing" do @parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...") - @env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(@env) + env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) - lambda { @env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../) - @env.known_resource_types.require_reparse?.should be_true + lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../) + env.known_resource_types.require_reparse?.should be_true end end end diff --git a/spec/unit/parser/ast/asthash_spec.rb b/spec/unit/parser/ast/asthash_spec.rb index d7fbbfae9..ab1281f91 100755 --- a/spec/unit/parser/ast/asthash_spec.rb +++ b/spec/unit/parser/ast/asthash_spec.rb @@ -1,97 +1,96 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Parser::AST::ASTHash do before :each do @scope = Puppet::Parser::Scope.new end it "should have a merge functionality" do hash = Puppet::Parser::AST::ASTHash.new(:value => {}) hash.should respond_to(:merge) end it "should be able to merge 2 AST hashes" do hash = Puppet::Parser::AST::ASTHash.new(:value => { "a" => "b" }) hash.merge(Puppet::Parser::AST::ASTHash.new(:value => {"c" => "d"})) hash.value.should == { "a" => "b", "c" => "d" } end it "should be able to merge with a ruby Hash" do hash = Puppet::Parser::AST::ASTHash.new(:value => { "a" => "b" }) hash.merge({"c" => "d"}) hash.value.should == { "a" => "b", "c" => "d" } end it "should evaluate each hash value" do key1 = stub "key1" value1 = stub "value1" key2 = stub "key2" value2 = stub "value2" value1.expects(:safeevaluate).with(@scope).returns("b") value2.expects(:safeevaluate).with(@scope).returns("d") operator = Puppet::Parser::AST::ASTHash.new(:value => { key1 => value1, key2 => value2}) operator.evaluate(@scope) end it "should evaluate the hash keys if they are AST instances" do key1 = stub "key1" value1 = stub "value1", :safeevaluate => "one" key2 = stub "key2" value2 = stub "value2", :safeevaluate => "two" key1.expects(:safeevaluate).with(@scope).returns("1") key2.expects(:safeevaluate).with(@scope).returns("2") operator = Puppet::Parser::AST::ASTHash.new(:value => { key1 => value1, key2 => value2}) hash = operator.evaluate(@scope) hash["1"].should == "one" hash["2"].should == "two" end it "should evaluate the hash keys if they are not AST instances" do key1 = "1" value1 = stub "value1", :safeevaluate => "one" key2 = "2" value2 = stub "value2", :safeevaluate => "two" operator = Puppet::Parser::AST::ASTHash.new(:value => { key1 => value1, key2 => value2}) hash = operator.evaluate(@scope) hash["1"].should == "one" hash["2"].should == "two" end it "should return an evaluated hash" do key1 = stub "key1" value1 = stub "value1", :safeevaluate => "b" key2 = stub "key2" value2 = stub "value2", :safeevaluate => "d" operator = Puppet::Parser::AST::ASTHash.new(:value => { key1 => value1, key2 => value2}) operator.evaluate(@scope).should == { key1 => "b", key2 => "d" } end describe "when being initialized without arguments" do it "should evaluate to an empty hash" do hash = Puppet::Parser::AST::ASTHash.new({}) hash.evaluate(@scope).should == {} end it "should support merging" do hash = Puppet::Parser::AST::ASTHash.new({}) hash.merge({"a" => "b"}).should == {"a" => "b"} end end it "should return a valid string with to_s" do hash = Puppet::Parser::AST::ASTHash.new(:value => { "a" => "b", "c" => "d" }) - - hash.to_s.should == '{a => b, c => d}' + ["{a => b, c => d}", "{c => d, a => b}"].should be_include hash.to_s end end diff --git a/spec/unit/property/keyvalue_spec.rb b/spec/unit/property/keyvalue_spec.rb index 821c61799..2bead50e3 100755 --- a/spec/unit/property/keyvalue_spec.rb +++ b/spec/unit/property/keyvalue_spec.rb @@ -1,167 +1,170 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/property/keyvalue' klass = Puppet::Property::KeyValue describe klass do it "should be a subclass of Property" do klass.superclass.must == Puppet::Property end describe "as an instance" do before do # Wow that's a messy interface to the resource. klass.initvars @resource = stub 'resource', :[]= => nil, :property => nil @property = klass.new(:resource => @resource) end it "should have a , as default delimiter" do @property.delimiter.should == ";" end it "should have a = as default separator" do @property.separator.should == "=" end it "should have a :membership as default membership" do @property.membership.should == :key_value_membership end it "should return the same value passed into should_to_s" do @property.should_to_s({:foo => "baz", :bar => "boo"}) == "foo=baz;bar=boo" end - it "should return the passed in array values joined with the delimiter from is_to_s" do - @property.is_to_s({"foo" => "baz" , "bar" => "boo"}).should == "foo=baz;bar=boo" + it "should return the passed in hash values joined with the delimiter from is_to_s" do + s = @property.is_to_s({"foo" => "baz" , "bar" => "boo"}) + + # We can't predict the order the hash is processed in... + ["foo=baz;bar=boo", "bar=boo;foo=baz"].should be_include s end describe "when calling inclusive?" do it "should use the membership method to look up on the @resource" do @property.expects(:membership).returns(:key_value_membership) @resource.expects(:[]).with(:key_value_membership) @property.inclusive? end it "should return true when @resource[membership] == inclusive" do @property.stubs(:membership).returns(:key_value_membership) @resource.stubs(:[]).with(:key_value_membership).returns(:inclusive) @property.inclusive?.must == true end it "should return false when @resource[membership] != inclusive" do @property.stubs(:membership).returns(:key_value_membership) @resource.stubs(:[]).with(:key_value_membership).returns(:minimum) @property.inclusive?.must == false end end describe "when calling process_current_hash" do it "should return {} if hash is :absent" do @property.process_current_hash(:absent).must == {} end it "should set every key to nil if inclusive?" do @property.stubs(:inclusive?).returns(true) @property.process_current_hash({:foo => "bar", :do => "re"}).must == { :foo => nil, :do => nil } end it "should return the hash if !inclusive?" do @property.stubs(:inclusive?).returns(false) @property.process_current_hash({:foo => "bar", :do => "re"}).must == {:foo => "bar", :do => "re"} end end describe "when calling should" do it "should return nil if @should is nil" do @property.should.must == nil end it "should call process_current_hash" do @property.should = ["foo=baz", "bar=boo"] @property.stubs(:retrieve).returns({:do => "re", :mi => "fa" }) @property.expects(:process_current_hash).returns({}) @property.should end it "should return the hashed values of @should and the nilled values of retrieve if inclusive" do @property.should = ["foo=baz", "bar=boo"] @property.expects(:retrieve).returns({:do => "re", :mi => "fa" }) @property.expects(:inclusive?).returns(true) @property.should.must == { :foo => "baz", :bar => "boo", :do => nil, :mi => nil } end it "should return the hashed @should + the unique values of retrieve if !inclusive" do @property.should = ["foo=baz", "bar=boo"] @property.expects(:retrieve).returns({:foo => "diff", :do => "re", :mi => "fa"}) @property.expects(:inclusive?).returns(false) @property.should.must == { :foo => "baz", :bar => "boo", :do => "re", :mi => "fa" } end end describe "when calling retrieve" do before do @provider = mock("provider") @property.stubs(:provider).returns(@provider) end it "should send 'name' to the provider" do @provider.expects(:send).with(:keys) @property.expects(:name).returns(:keys) @property.retrieve end it "should return a hash with the provider returned info" do @provider.stubs(:send).with(:keys).returns({"do" => "re", "mi" => "fa" }) @property.stubs(:name).returns(:keys) @property.retrieve == {"do" => "re", "mi" => "fa" } end it "should return :absent when the provider returns :absent" do @provider.stubs(:send).with(:keys).returns(:absent) @property.stubs(:name).returns(:keys) @property.retrieve == :absent end end describe "when calling hashify" do it "should return the array hashified" do @property.hashify(["foo=baz", "bar=boo"]).must == { :foo => "baz", :bar => "boo" } end end describe "when calling safe_insync?" do before do @provider = mock("provider") @property.stubs(:provider).returns(@provider) @property.stubs(:name).returns(:prop_name) end it "should return true unless @should is defined and not nil" do @property.safe_insync?("foo") == true end it "should return true if the passed in values is nil" do @property.should = "foo" @property.safe_insync?(nil) == true end it "should return true if hashified should value == (retrieved) value passed in" do @provider.stubs(:prop_name).returns({ :foo => "baz", :bar => "boo" }) @property.should = ["foo=baz", "bar=boo"] @property.expects(:inclusive?).returns(true) @property.safe_insync?({ :foo => "baz", :bar => "boo" }).must == true end it "should return false if prepared value != should value" do @provider.stubs(:prop_name).returns({ "foo" => "bee", "bar" => "boo" }) @property.should = ["foo=baz", "bar=boo"] @property.expects(:inclusive?).returns(true) @property.safe_insync?({ "foo" => "bee", "bar" => "boo" }).must == false end end end end diff --git a/spec/unit/provider/group/pw_spec.rb b/spec/unit/provider/group/pw_spec.rb new file mode 100755 index 000000000..3dfc5ec71 --- /dev/null +++ b/spec/unit/provider/group/pw_spec.rb @@ -0,0 +1,81 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +provider_class = Puppet::Type.type(:group).provider(:pw) + +describe provider_class do + let :resource do + Puppet::Type.type(:group).new(:name => "testgroup", :provider => :pw) + end + + let :provider do + resource.provider + end + + describe "when creating groups" do + let :provider do + prov = resource.provider + prov.expects(:exists?).returns nil + prov + end + + it "should run pw with no additional flags when no properties are given" do + provider.addcmd.must == [provider_class.command(:pw), "groupadd", "testgroup"] + provider.expects(:execute).with([provider_class.command(:pw), "groupadd", "testgroup"]) + provider.create + end + + it "should use -o when allowdupe is enabled" do + resource[:allowdupe] = true + provider.expects(:execute).with(includes("-o")) + provider.create + end + + it "should use -g with the correct argument when the gid property is set" do + resource[:gid] = 12345 + provider.expects(:execute).with(all_of(includes("-g"), includes(12345))) + provider.create + end + + it "should use -M with the correct argument when the members property is set" do + resource[:members] = "user1" + provider.expects(:execute).with(all_of(includes("-M"), includes("user1"))) + provider.create + end + + it "should use -M with all the given users when the members property is set to an array" do + resource[:members] = ["user1", "user2"] + provider.expects(:execute).with(all_of(includes("-M"), includes("user1,user2"))) + provider.create + end + end + + describe "when deleting groups" do + it "should run pw with no additional flags" do + provider.expects(:exists?).returns true + provider.deletecmd.must == [provider_class.command(:pw), "groupdel", "testgroup"] + provider.expects(:execute).with([provider_class.command(:pw), "groupdel", "testgroup"]) + provider.delete + end + end + + describe "when modifying groups" do + it "should run pw with the correct arguments" do + provider.modifycmd("gid", 12345).must == [provider_class.command(:pw), "groupmod", "testgroup", "-g", 12345] + provider.expects(:execute).with([provider_class.command(:pw), "groupmod", "testgroup", "-g", 12345]) + provider.gid = 12345 + end + + it "should use -M with the correct argument when the members property is changed" do + resource[:members] = "user1" + provider.expects(:execute).with(all_of(includes("-M"), includes("user2"))) + provider.members = "user2" + end + + it "should use -M with all the given users when the members property is changed with an array" do + resource[:members] = ["user1", "user2"] + provider.expects(:execute).with(all_of(includes("-M"), includes("user3,user4"))) + provider.members = ["user3", "user4"] + end + end +end diff --git a/spec/unit/provider/package/pip_spec.rb b/spec/unit/provider/package/pip_spec.rb index 97b3b5e73..7849ec49c 100755 --- a/spec/unit/provider/package/pip_spec.rb +++ b/spec/unit/provider/package/pip_spec.rb @@ -1,180 +1,187 @@ #!/usr/bin/env rspec require 'spec_helper' provider_class = Puppet::Type.type(:package).provider(:pip) describe provider_class do before do @resource = Puppet::Resource.new(:package, "fake_package") @provider = provider_class.new(@resource) client = stub_everything('client') client.stubs(:call).with('package_releases', 'real_package').returns(["1.3", "1.2.5", "1.2.4"]) client.stubs(:call).with('package_releases', 'fake_package').returns([]) XMLRPC::Client.stubs(:new2).returns(client) end describe "parse" do it "should return a hash on valid input" do provider_class.parse("real_package==1.2.5").should == { :ensure => "1.2.5", :name => "real_package", :provider => :pip, } end it "should return nil on invalid input" do provider_class.parse("foo").should == nil end end describe "instances" do it "should return an array when pip is present" do provider_class.expects(:which).with('pip').returns("/fake/bin/pip") p = stub("process") p.expects(:collect).yields("real_package==1.2.5") provider_class.expects(:execpipe).with("/fake/bin/pip freeze").yields(p) provider_class.instances end it "should return an empty array when pip is missing" do provider_class.expects(:which).with('pip').returns nil provider_class.instances.should == [] end end describe "query" do before do @resource[:name] = "real_package" end it "should return a hash when pip and the package are present" do provider_class.expects(:instances).returns [provider_class.new({ :ensure => "1.2.5", :name => "real_package", :provider => :pip, })] @provider.query.should == { :ensure => "1.2.5", :name => "real_package", :provider => :pip, } end it "should return nil when the package is missing" do provider_class.expects(:instances).returns [] @provider.query.should == nil end end describe "latest" do it "should find a version number for real_package" do @resource[:name] = "real_package" @provider.latest.should_not == nil end it "should not find a version number for fake_package" do @resource[:name] = "fake_package" @provider.latest.should == nil end end describe "install" do before do @resource[:name] = "fake_package" @url = "git+https://example.com/fake_package.git" end it "should install" do @resource[:ensure] = :installed @resource[:source] = nil @provider.expects(:lazy_pip). with("install", '-q', "fake_package") @provider.install end it "should install from SCM" do @resource[:ensure] = :installed @resource[:source] = @url @provider.expects(:lazy_pip). with("install", '-q', '-e', "#{@url}#egg=fake_package") @provider.install end it "should install a particular SCM revision" do @resource[:ensure] = "0123456" @resource[:source] = @url @provider.expects(:lazy_pip). with("install", "-q", "-e", "#{@url}@0123456#egg=fake_package") @provider.install end it "should install a particular version" do @resource[:ensure] = "0.0.0" @resource[:source] = nil @provider.expects(:lazy_pip).with("install", "-q", "fake_package==0.0.0") @provider.install end it "should upgrade" do @resource[:ensure] = :latest @resource[:source] = nil @provider.expects(:lazy_pip). with("install", "-q", "--upgrade", "fake_package") @provider.install end end describe "uninstall" do it "should uninstall" do @resource[:name] = "fake_package" @provider.expects(:lazy_pip). with('uninstall', '-y', '-q', 'fake_package') @provider.uninstall end end describe "update" do it "should just call install" do @provider.expects(:install).returns(nil) @provider.update end end describe "lazy_pip" do it "should succeed if pip is present" do @provider.stubs(:pip).returns(nil) @provider.method(:lazy_pip).call "freeze" end it "should retry if pip has not yet been found" do @provider.expects(:pip).twice.with('freeze').raises(NoMethodError).then.returns(nil) @provider.expects(:which).with('pip').returns("/fake/bin/pip") @provider.method(:lazy_pip).call "freeze" end it "should fail if pip is missing" do @provider.expects(:pip).with('freeze').raises(NoMethodError) @provider.expects(:which).with('pip').returns(nil) expect { @provider.method(:lazy_pip).call("freeze") }.to raise_error(NoMethodError) end + it "should output a useful error message if pip is missing" do + @provider.expects(:pip).with('freeze').raises(NoMethodError) + @provider.expects(:which).with('pip').returns(nil) + expect { @provider.method(:lazy_pip).call("freeze") }. + to raise_error(NoMethodError, 'Could not locate the pip command.') + end + end end diff --git a/spec/unit/provider/package/up2date_spec.rb b/spec/unit/provider/package/up2date_spec.rb deleted file mode 100644 index 17709b3c3..000000000 --- a/spec/unit/provider/package/up2date_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# spec/unit/provider/package/up2date_spec.rb -require 'spec_helper' - -describe 'up2date package provider' do - - # This sets the class itself as the subject rather than - # an instance of the class. - subject do - Puppet::Type.type(:package).provider(:up2date) - end - - osfamily = [ 'redhat' ] - releases = [ '2.1', '3', '4' ] - - osfamily.product(releases).each do |osfamily, release| - it "should be the default provider on #{osfamily} #{release}" do - Facter.expects(:value).with(:osfamily).returns(osfamily) - Facter.expects(:value).with(:lsbdistrelease).returns(release) - subject.default?.should be_true - end - end -end diff --git a/spec/unit/provider/service/redhat_spec.rb b/spec/unit/provider/service/redhat_spec.rb index 919541e93..aabcf8d0f 100755 --- a/spec/unit/provider/service/redhat_spec.rb +++ b/spec/unit/provider/service/redhat_spec.rb @@ -1,132 +1,123 @@ #!/usr/bin/env rspec # # Unit testing for the RedHat service Provider # require 'spec_helper' provider_class = Puppet::Type.type(:service).provider(:redhat) describe provider_class do before :each do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) @class = Puppet::Type.type(:service).provider(:redhat) @resource = stub 'resource' @resource.stubs(:[]).returns(nil) @resource.stubs(:[]).with(:name).returns "myservice" @provider = provider_class.new @resource.stubs(:provider).returns @provider @provider.resource = @resource @provider.stubs(:get).with(:hasstatus).returns false FileTest.stubs(:file?).with('/sbin/service').returns true FileTest.stubs(:executable?).with('/sbin/service').returns true end - osfamily = [ 'redhat', 'suse' ] - - osfamily.each do |osfamily| - it "should be the default provider on #{osfamily}" do - Facter.expects(:value).with(:osfamily).returns(osfamily) - provider_class.default?.should be_true - end - end - # test self.instances describe "when getting all service instances" do before :each do @services = ['one', 'two', 'three', 'four', 'kudzu', 'functions', 'halt', 'killall', 'single', 'linuxconf'] @not_services = ['functions', 'halt', 'killall', 'single', 'linuxconf'] Dir.stubs(:entries).returns @services FileTest.stubs(:directory?).returns(true) FileTest.stubs(:executable?).returns(true) end it "should return instances for all services" do (@services-@not_services).each do |inst| @class.expects(:new).with{|hash| hash[:name] == inst && hash[:path] == '/etc/init.d'}.returns("#{inst}_instance") end results = (@services-@not_services).collect {|x| "#{x}_instance"} @class.instances.should == results end it "should call service status when initialized from provider" do @resource.stubs(:[]).with(:status).returns nil @provider.stubs(:get).with(:hasstatus).returns true @provider.expects(:execute).with{|command, *args| command == ['/sbin/service', 'myservice', 'status']} @provider.send(:status) end end it "should have an enabled? method" do @provider.should respond_to(:enabled?) end it "should have an enable method" do @provider.should respond_to(:enable) end it "should have a disable method" do @provider.should respond_to(:disable) end [:start, :stop, :status, :restart].each do |method| it "should have a #{method} method" do @provider.should respond_to(method) end describe "when running #{method}" do it "should use any provided explicit command" do @resource.stubs(:[]).with(method).returns "/user/specified/command" @provider.expects(:execute).with { |command, *args| command == ["/user/specified/command"] } @provider.send(method) end it "should execute the service script with #{method} when no explicit command is provided" do @resource.stubs(:[]).with("has#{method}".intern).returns :true @provider.expects(:execute).with { |command, *args| command == ['/sbin/service', 'myservice', method.to_s]} @provider.send(method) end end end describe "when checking status" do describe "when hasstatus is :true" do before :each do @resource.stubs(:[]).with(:hasstatus).returns :true end it "should execute the service script with fail_on_failure false" do @provider.expects(:texecute).with(:status, ['/sbin/service', 'myservice', 'status'], false) @provider.status end it "should consider the process running if the command returns 0" do @provider.expects(:texecute).with(:status, ['/sbin/service', 'myservice', 'status'], false) $CHILD_STATUS.stubs(:exitstatus).returns(0) @provider.status.should == :running end [-10,-1,1,10].each { |ec| it "should consider the process stopped if the command returns something non-0" do @provider.expects(:texecute).with(:status, ['/sbin/service', 'myservice', 'status'], false) $CHILD_STATUS.stubs(:exitstatus).returns(ec) @provider.status.should == :stopped end } end describe "when hasstatus is not :true" do it "should consider the service :running if it has a pid" do @provider.expects(:getpid).returns "1234" @provider.status.should == :running end it "should consider the service :stopped if it doesn't have a pid" do @provider.expects(:getpid).returns nil @provider.status.should == :stopped end end end describe "when restarting and hasrestart is not :true" do it "should stop and restart the process with the server script" do @provider.expects(:texecute).with(:stop, ['/sbin/service', 'myservice', 'stop'], true) @provider.expects(:texecute).with(:start, ['/sbin/service', 'myservice', 'start'], true) @provider.restart end end end diff --git a/spec/unit/provider/service/systemd_spec.rb b/spec/unit/provider/service/systemd_spec.rb index 8480c0149..98b6855a6 100755 --- a/spec/unit/provider/service/systemd_spec.rb +++ b/spec/unit/provider/service/systemd_spec.rb @@ -1,33 +1,25 @@ #!/usr/bin/env rspec # # Unit testing for the RedHat service Provider # require 'spec_helper' provider_class = Puppet::Type.type(:service).provider(:systemd) describe provider_class do before :each do @class = Puppet::Type.type(:service).provider(:redhat) @resource = stub 'resource' @resource.stubs(:[]).returns(nil) @resource.stubs(:[]).with(:name).returns "myservice.service" @provider = provider_class.new @resource.stubs(:provider).returns @provider @provider.resource = @resource end - osfamily = [ 'redhat', 'suse' ] - - osfamily.each do |osfamily| - it "should be the default provider on #{osfamily}" do - pending "This test is pending the change in RedHat-related Linuxes to systemd for service management" - end - end - [:enabled?, :enable, :disable, :start, :stop, :status, :restart].each do |method| it "should have a #{method} method" do @provider.should respond_to(method) end end end diff --git a/spec/unit/provider/user/pw_spec.rb b/spec/unit/provider/user/pw_spec.rb new file mode 100755 index 000000000..495fef35b --- /dev/null +++ b/spec/unit/provider/user/pw_spec.rb @@ -0,0 +1,183 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +provider_class = Puppet::Type.type(:user).provider(:pw) + +describe provider_class do + let :resource do + Puppet::Type.type(:user).new(:name => "testuser", :provider => :pw) + end + + describe "when creating users" do + let :provider do + prov = resource.provider + prov.expects(:exists?).returns nil + prov + end + + it "should run pw with no additional flags when no properties are given" do + provider.addcmd.must == [provider_class.command(:pw), "useradd", "testuser"] + provider.expects(:execute).with([provider_class.command(:pw), "useradd", "testuser"]) + provider.create + end + + it "should use -o when allowdupe is enabled" do + resource[:allowdupe] = true + provider.expects(:execute).with(includes("-o")) + provider.create + end + + it "should use -c with the correct argument when the comment property is set" do + resource[:comment] = "Testuser Name" + provider.expects(:execute).with(all_of(includes("-c"), includes("Testuser Name"))) + provider.create + end + + it "should use -e with the correct argument when the expiry property is set" do + resource[:expiry] = "2010-02-19" + provider.expects(:execute).with(all_of(includes("-e"), includes("19-02-2010"))) + provider.create + end + + it "should use -g with the correct argument when the gid property is set" do + resource[:gid] = 12345 + provider.expects(:execute).with(all_of(includes("-g"), includes(12345))) + provider.create + end + + it "should use -G with the correct argument when the groups property is set" do + resource[:groups] = "group1" + provider.expects(:execute).with(all_of(includes("-G"), includes("group1"))) + provider.create + end + + it "should use -G with all the given groups when the groups property is set to an array" do + resource[:groups] = ["group1", "group2"] + provider.expects(:execute).with(all_of(includes("-G"), includes("group1,group2"))) + provider.create + end + + it "should use -d with the correct argument when the home property is set" do + resource[:home] = "/home/testuser" + provider.expects(:execute).with(all_of(includes("-d"), includes("/home/testuser"))) + provider.create + end + + it "should use -m when the managehome property is enabled" do + resource[:managehome] = true + provider.expects(:execute).with(includes("-m")) + provider.create + end + + it "should call the password set function with the correct argument when the password property is set" do + resource[:password] = "*" + provider.expects(:execute) + provider.expects(:password=).with("*") + provider.create + end + + it "should use -s with the correct argument when the shell property is set" do + resource[:shell] = "/bin/sh" + provider.expects(:execute).with(all_of(includes("-s"), includes("/bin/sh"))) + provider.create + end + + it "should use -u with the correct argument when the uid property is set" do + resource[:uid] = 12345 + provider.expects(:execute).with(all_of(includes("-u"), includes(12345))) + provider.create + end + + # (#7500) -p should not be used to set a password (it means something else) + it "should not use -p when a password is given" do + resource[:password] = "*" + provider.addcmd.should_not include("-p") + provider.expects(:password=) + provider.expects(:execute).with(Not(includes("-p"))) + provider.create + end + end + + describe "when deleting users" do + it "should run pw with no additional flags" do + provider = resource.provider + provider.expects(:exists?).returns true + provider.deletecmd.must == [provider_class.command(:pw), "userdel", "testuser"] + provider.expects(:execute).with([provider_class.command(:pw), "userdel", "testuser"]) + provider.delete + end + end + + describe "when modifying users" do + let :provider do + resource.provider + end + + it "should run pw with the correct arguments" do + provider.modifycmd("uid", 12345).must == [provider_class.command(:pw), "usermod", "testuser", "-u", 12345] + provider.expects(:execute).with([provider_class.command(:pw), "usermod", "testuser", "-u", 12345]) + provider.uid = 12345 + end + + it "should use -c with the correct argument when the comment property is changed" do + resource[:comment] = "Testuser Name" + provider.expects(:execute).with(all_of(includes("-c"), includes("Testuser New Name"))) + provider.comment = "Testuser New Name" + end + + it "should use -e with the correct argument when the expiry property is changed" do + resource[:expiry] = "2010-02-19" + provider.expects(:execute).with(all_of(includes("-e"), includes("19-02-2011"))) + provider.expiry = "2011-02-19" + end + + it "should use -g with the correct argument when the gid property is changed" do + resource[:gid] = 12345 + provider.expects(:execute).with(all_of(includes("-g"), includes(54321))) + provider.gid = 54321 + end + + it "should use -G with the correct argument when the groups property is changed" do + resource[:groups] = "group1" + provider.expects(:execute).with(all_of(includes("-G"), includes("group2"))) + provider.groups = "group2" + end + + it "should use -G with all the given groups when the groups property is changed with an array" do + resource[:groups] = ["group1", "group2"] + provider.expects(:execute).with(all_of(includes("-G"), includes("group3,group4"))) + provider.groups = "group3,group4" + end + + it "should use -d with the correct argument when the home property is changed" do + resource[:home] = "/home/testuser" + provider.expects(:execute).with(all_of(includes("-d"), includes("/newhome/testuser"))) + provider.home = "/newhome/testuser" + end + + it "should use -m and -d with the correct argument when the home property is changed and managehome is enabled" do + resource[:home] = "/home/testuser" + resource[:managehome] = true + provider.expects(:execute).with(all_of(includes("-d"), includes("/newhome/testuser"), includes("-m"))) + provider.home = "/newhome/testuser" + end + + it "should call the password set function with the correct argument when the password property is changed" do + resource[:password] = "*" + provider.expects(:password=).with("!") + provider.password = "!" + end + + it "should use -s with the correct argument when the shell property is changed" do + resource[:shell] = "/bin/sh" + provider.expects(:execute).with(all_of(includes("-s"), includes("/bin/tcsh"))) + provider.shell = "/bin/tcsh" + end + + it "should use -u with the correct argument when the uid property is changed" do + resource[:uid] = 12345 + provider.expects(:execute).with(all_of(includes("-u"), includes(54321))) + provider.uid = 54321 + end + end +end diff --git a/spec/unit/ssl/certificate_factory_spec.rb b/spec/unit/ssl/certificate_factory_spec.rb index fc1b3a740..88521b0fb 100755 --- a/spec/unit/ssl/certificate_factory_spec.rb +++ b/spec/unit/ssl/certificate_factory_spec.rb @@ -1,126 +1,126 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/ssl/certificate_factory' describe Puppet::SSL::CertificateFactory do let :serial do OpenSSL::BN.new('12') end let :name do "example.local" end let :x509_name do OpenSSL::X509::Name.new([['CN', name]]) end let :key do Puppet::SSL::Key.new(name).generate end let :csr do csr = Puppet::SSL::CertificateRequest.new(name) csr.generate(key) csr end let :issuer do cert = OpenSSL::X509::Certificate.new cert.subject = OpenSSL::X509::Name.new([["CN", 'issuer.local']]) cert end describe "when generating the certificate" do it "should return a new X509 certificate" do subject.build(:server, csr, issuer, serial).should_not == subject.build(:server, csr, issuer, serial) end it "should set the certificate's version to 2" do subject.build(:server, csr, issuer, serial).version.should == 2 end it "should set the certificate's subject to the CSR's subject" do cert = subject.build(:server, csr, issuer, serial) cert.subject.should eql x509_name end it "should set the certificate's issuer to the Issuer's subject" do cert = subject.build(:server, csr, issuer, serial) cert.issuer.should eql issuer.subject end it "should set the certificate's public key to the CSR's public key" do cert = subject.build(:server, csr, issuer, serial) cert.public_key.should be_public cert.public_key.to_s.should == csr.content.public_key.to_s end it "should set the certificate's serial number to the provided serial number" do cert = subject.build(:server, csr, issuer, serial) cert.serial.should == serial end it "should have 24 hours grace on the start of the cert" do cert = subject.build(:server, csr, issuer, serial) - cert.not_before.should be_within(1).of(Time.now - 24*60*60) + cert.not_before.should be_within(30).of(Time.now - 24*60*60) end it "should set the default TTL of the certificate" do ttl = Puppet::SSL::CertificateFactory.ttl cert = subject.build(:server, csr, issuer, serial) - cert.not_after.should be_within(1).of(Time.now + ttl) + cert.not_after.should be_within(30).of(Time.now + ttl) end it "should respect a custom TTL for the CA" do Puppet[:ca_ttl] = 12 cert = subject.build(:server, csr, issuer, serial) - cert.not_after.should be_within(1).of(Time.now + 12) + cert.not_after.should be_within(30).of(Time.now + 12) end it "should build extensions for the certificate" do cert = subject.build(:server, csr, issuer, serial) cert.extensions.map {|x| x.to_h }.find {|x| x["oid"] == "nsComment" }.should == { "oid" => "nsComment", "value" => "Puppet Ruby/OpenSSL Internal Certificate", "critical" => false } end # See #2848 for why we are doing this: we need to make sure that # subjectAltName is set if the CSR has it, but *not* if it is set when the # certificate is built! it "should not add subjectAltNames from dns_alt_names" do Puppet[:dns_alt_names] = 'one, two' # Verify the CSR still has no extReq, just in case... csr.request_extensions.should == [] cert = subject.build(:server, csr, issuer, serial) cert.extensions.find {|x| x.oid == 'subjectAltName' }.should be_nil end it "should add subjectAltName when the CSR requests them" do Puppet[:dns_alt_names] = '' expect = %w{one two} + [name] csr = Puppet::SSL::CertificateRequest.new(name) csr.generate(key, :dns_alt_names => expect.join(', ')) csr.request_extensions.should_not be_nil csr.subject_alt_names.should =~ expect.map{|x| "DNS:#{x}"} cert = subject.build(:server, csr, issuer, serial) san = cert.extensions.find {|x| x.oid == 'subjectAltName' } san.should_not be_nil expect.each do |name| san.value.should =~ /DNS:#{name}\b/i end end # Can't check the CA here, since that requires way more infrastructure # that I want to build up at this time. We can verify the critical # values, though, which are non-CA certs. --daniel 2011-10-11 { :ca => 'CA:TRUE', :terminalsubca => ['CA:TRUE', 'pathlen:0'], :server => 'CA:FALSE', :ocsp => 'CA:FALSE', :client => 'CA:FALSE', }.each do |name, value| it "should set basicConstraints for #{name} #{value.inspect}" do cert = subject.build(name, csr, issuer, serial) bc = cert.extensions.find {|x| x.oid == 'basicConstraints' } bc.should be bc.value.split(/\s*,\s*/).should =~ Array(value) end end end end diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index caedf4dad..d4df006b3 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -1,1520 +1,1520 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:file) do include PuppetSpec::Files let(:path) { tmpfile('file_testing') } let(:file) { described_class.new(:path => path, :catalog => catalog) } let(:provider) { file.provider } let(:catalog) { Puppet::Resource::Catalog.new } before do @real_posix = Puppet.features.posix? Puppet.features.stubs("posix?").returns(true) end describe "the path parameter" do describe "on POSIX systems", :if => Puppet.features.posix? do it "should remove trailing slashes" do file[:path] = "/foo/bar/baz/" file[:path].should == "/foo/bar/baz" end it "should remove double slashes" do file[:path] = "/foo/bar//baz" file[:path].should == "/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "/foo/bar/baz//" file[:path].should == "/foo/bar/baz" end it "should leave a single slash alone" do file[:path] = "/" file[:path].should == "/" end it "should accept a double-slash at the start of the path" do expect { file[:path] = "//tmp/xxx" # REVISIT: This should be wrong, later. See the next test. # --daniel 2011-01-31 file[:path].should == '/tmp/xxx' }.should_not raise_error end # REVISIT: This is pending, because I don't want to try and audit the # entire codebase to make sure we get this right. POSIX treats two (and # exactly two) '/' characters at the start of the path specially. # # See sections 3.2 and 4.11, which allow DomainOS to be all special like # and still have the POSIX branding and all. --daniel 2011-01-31 it "should preserve the double-slash at the start of the path" end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do it "should remove trailing slashes" do file[:path] = "X:/foo/bar/baz/" file[:path].should == "X:/foo/bar/baz" end it "should remove double slashes" do file[:path] = "X:/foo/bar//baz" file[:path].should == "X:/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "X:/foo/bar/baz//" file[:path].should == "X:/foo/bar/baz" end it "should leave a drive letter with a slash alone", :'fails_on_ruby_1.9.2' => true do file[:path] = "X:/" file[:path].should == "X:/" end it "should not accept a drive letter without a slash", :'fails_on_ruby_1.9.2' => true do lambda { file[:path] = "X:" }.should raise_error(/File paths must be fully qualified/) end describe "when using UNC filenames", :if => Puppet.features.microsoft_windows?, :'fails_on_ruby_1.9.2' => true do before :each do pending("UNC file paths not yet supported") end it "should remove trailing slashes" do file[:path] = "//server/foo/bar/baz/" file[:path].should == "//server/foo/bar/baz" end it "should remove double slashes" do file[:path] = "//server/foo/bar//baz" file[:path].should == "//server/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "//server/foo/bar/baz//" file[:path].should == "//server/foo/bar/baz" end it "should remove a trailing slash from a sharename" do file[:path] = "//server/foo/" file[:path].should == "//server/foo" end it "should not modify a sharename" do file[:path] = "//server/foo" file[:path].should == "//server/foo" end end end end describe "the backup parameter" do [false, 'false', :false].each do |value| it "should disable backup if the value is #{value.inspect}" do file[:backup] = value file[:backup].should == false end end [true, 'true', '.puppet-bak'].each do |value| it "should use .puppet-bak if the value is #{value.inspect}" do file[:backup] = value file[:backup].should == '.puppet-bak' end end it "should use the provided value if it's any other string" do file[:backup] = "over there" file[:backup].should == "over there" end it "should fail if backup is set to anything else" do expect do file[:backup] = 97 end.to raise_error(Puppet::Error, /Invalid backup type 97/) end end describe "the recurse parameter" do it "should default to recursion being disabled" do file[:recurse].should be_false end [true, "true", 10, "inf", "remote"].each do |value| it "should consider #{value} to enable recursion" do file[:recurse] = value file[:recurse].should be_true end end [false, "false", 0].each do |value| it "should consider #{value} to disable recursion" do file[:recurse] = value file[:recurse].should be_false end end it "should warn if recurse is specified as a number" do file[:recurse] = 3 message = /Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit/ @logs.find { |log| log.level == :warning and log.message =~ message}.should_not be_nil end end describe "the recurselimit parameter" do it "should accept integers" do file[:recurselimit] = 12 file[:recurselimit].should == 12 end it "should munge string numbers to number numbers" do file[:recurselimit] = '12' file[:recurselimit].should == 12 end it "should fail if given a non-number" do expect do file[:recurselimit] = 'twelve' end.to raise_error(Puppet::Error, /Invalid value "twelve"/) end end describe "the replace parameter" do [true, :true, :yes].each do |value| it "should consider #{value} to be true" do file[:replace] = value file[:replace].should == :true end end [false, :false, :no].each do |value| it "should consider #{value} to be false" do file[:replace] = value file[:replace].should == :false end end end describe "#[]" do it "should raise an exception" do expect do described_class['anything'] end.to raise_error("Global resource access is deprecated") end end describe ".instances" do it "should return an empty array" do described_class.instances.should == [] end end describe "#asuser" do before :each do # Mocha won't let me just stub SUIDManager.asuser to yield and return, # but it will do exactly that if we're not root. Puppet.features.stubs(:root?).returns false end it "should return the desired owner if they can write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true file.asuser.should == 1001 end it "should return nil if the desired owner can't write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false file.asuser.should == nil end it "should return nil if not managing owner" do file.asuser.should == nil end end describe "#bucket" do it "should return nil if backup is off" do file[:backup] = false file.bucket.should == nil end it "should not return a bucket if using a file extension for backup" do file[:backup] = '.backup' file.bucket.should == nil end it "should return the default filebucket if using the 'puppet' filebucket" do file[:backup] = 'puppet' bucket = stub('bucket') file.stubs(:default_bucket).returns bucket file.bucket.should == bucket end it "should fail if using a remote filebucket and no catalog exists" do file.catalog = nil file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end it "should fail if the specified filebucket isn't in the catalog" do file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end it "should use the specified filebucket if it is in the catalog" do file[:backup] = 'my_bucket' filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') catalog.add_resource(filebucket) file.bucket.should == filebucket.bucket end end describe "#asuser" do before :each do # Mocha won't let me just stub SUIDManager.asuser to yield and return, # but it will do exactly that if we're not root. Puppet.features.stubs(:root?).returns false end it "should return the desired owner if they can write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true file.asuser.should == 1001 end it "should return nil if the desired owner can't write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false file.asuser.should == nil end it "should return nil if not managing owner" do file.asuser.should == nil end end describe "#bucket" do it "should return nil if backup is off" do file[:backup] = false file.bucket.should == nil end it "should return nil if using a file extension for backup" do file[:backup] = '.backup' file.bucket.should == nil end it "should return the default filebucket if using the 'puppet' filebucket" do file[:backup] = 'puppet' bucket = stub('bucket') file.stubs(:default_bucket).returns bucket file.bucket.should == bucket end it "should fail if using a remote filebucket and no catalog exists" do file.catalog = nil file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end it "should fail if the specified filebucket isn't in the catalog" do file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end it "should use the specified filebucket if it is in the catalog" do file[:backup] = 'my_bucket' filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') catalog.add_resource(filebucket) file.bucket.should == filebucket.bucket end end describe "#exist?" do it "should be considered existent if it can be stat'ed" do file.expects(:stat).returns mock('stat') file.must be_exist end it "should be considered nonexistent if it can not be stat'ed" do file.expects(:stat).returns nil file.must_not be_exist end end describe "#eval_generate" do before do @graph = stub 'graph', :add_edge => nil catalog.stubs(:relationship_graph).returns @graph end it "should recurse if recursion is enabled" do resource = stub('resource', :[] => 'resource') file.expects(:recurse).returns [resource] file[:recurse] = true file.eval_generate.should == [resource] end it "should not recurse if recursion is disabled" do file.expects(:recurse).never file[:recurse] = false file.eval_generate.should == [] end end describe "#ancestors" do it "should return the ancestors of the file, in ascending order" do file = described_class.new(:path => make_absolute("/tmp/foo/bar/baz/qux")) pieces = %W[#{make_absolute('/')} tmp foo bar baz] ancestors = file.ancestors ancestors.should_not be_empty ancestors.reverse.each_with_index do |path,i| path.should == File.join(*pieces[0..i]) end end end describe "#flush" do it "should flush all properties that respond to :flush" do file[:source] = File.expand_path(__FILE__) file.parameter(:source).expects(:flush) file.flush end it "should reset its stat reference" do FileUtils.touch(path) stat1 = file.stat file.stat.should equal(stat1) file.flush file.stat.should_not equal(stat1) end end describe "#initialize" do it "should remove a trailing slash from the title to create the path" do title = File.expand_path("/abc/\n\tdef/") file = described_class.new(:title => title) file[:path].should == title end it "should set a desired 'ensure' value if none is set and 'content' is set" do file = described_class.new(:path => path, :content => "/foo/bar") file[:ensure].should == :file end it "should set a desired 'ensure' value if none is set and 'target' is set" do file = described_class.new(:path => path, :target => File.expand_path(__FILE__)) file[:ensure].should == :symlink end end describe "#mark_children_for_purging" do it "should set each child's ensure to absent" do paths = %w[foo bar baz] children = paths.inject({}) do |children,child| children.merge child => described_class.new(:path => File.join(path, child), :ensure => :present) end file.mark_children_for_purging(children) children.length.should == 3 children.values.each do |child| child[:ensure].should == :absent end end it "should skip children which have a source" do child = described_class.new(:path => path, :ensure => :present, :source => File.expand_path(__FILE__)) file.mark_children_for_purging('foo' => child) child[:ensure].should == :present end end describe "#newchild" do it "should create a new resource relative to the parent" do child = file.newchild('bar') child.should be_a(described_class) child[:path].should == File.join(file[:path], 'bar') end { :ensure => :present, :recurse => true, :recurselimit => 5, :target => "some_target", :source => File.expand_path("some_source"), }.each do |param, value| it "should omit the #{param} parameter" do # Make a new file, because we have to set the param at initialization # or it wouldn't be copied regardless. file = described_class.new(:path => path, param => value) child = file.newchild('bar') child[param].should_not == value end end it "should copy all of the parent resource's 'should' values that were set at initialization" do parent = described_class.new(:path => path, :owner => 'root', :group => 'wheel') child = parent.newchild("my/path") child[:owner].should == 'root' child[:group].should == 'wheel' end it "should not copy default values to the new child" do child = file.newchild("my/path") child.original_parameters.should_not include(:backup) end it "should not copy values to the child which were set by the source" do source = File.expand_path(__FILE__) file[:source] = source metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever", :source => source file.parameter(:source).stubs(:metadata).returns metadata file.parameter(:source).copy_source_values file.class.expects(:new).with { |params| params[:group].nil? } file.newchild("my/path") end end describe "#purge?" do it "should return false if purge is not set" do file.must_not be_purge end it "should return true if purge is set to true" do file[:purge] = true file.must be_purge end it "should return false if purge is set to false" do file[:purge] = false file.must_not be_purge end end describe "#recurse" do before do file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do it "should pass the already-discovered resources to recurse_remote" do file[:source] = File.expand_path(__FILE__) file.stubs(:recurse_local).returns(:foo => "bar") file.expects(:recurse_remote).with(:foo => "bar").returns [] file.recurse end end describe "and a target is set" do it "should use recurse_link" do file[:target] = File.expand_path(__FILE__) file.stubs(:recurse_local).returns(:foo => "bar") file.expects(:recurse_link).with(:foo => "bar").returns [] file.recurse end end it "should use recurse_local if recurse is not remote" do file.expects(:recurse_local).returns({}) file.recurse end it "should not use recurse_local if recurse is remote" do file[:recurse] = :remote file.expects(:recurse_local).never file.recurse end it "should return the generated resources as an array sorted by file path" do one = stub 'one', :[] => "/one" two = stub 'two', :[] => "/one/two" three = stub 'three', :[] => "/three" file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) file.recurse.should == [one, two, three] end describe "and purging is enabled" do before do file[:purge] = true end it "should mark each file for removal" do local = described_class.new(:path => path, :ensure => :present) file.expects(:recurse_local).returns("local" => local) file.recurse local[:ensure].should == :absent end it "should not remove files that exist in the remote repository" do file[:source] = File.expand_path(__FILE__) file.expects(:recurse_local).returns({}) remote = described_class.new(:path => path, :source => File.expand_path(__FILE__), :ensure => :present) file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } file.recurse remote[:ensure].should_not == :absent end end end describe "#remove_less_specific_files" do it "should remove any nested files that are already in the catalog" do foo = described_class.new :path => File.join(file[:path], 'foo') bar = described_class.new :path => File.join(file[:path], 'bar') baz = described_class.new :path => File.join(file[:path], 'baz') catalog.add_resource(foo) catalog.add_resource(bar) file.remove_less_specific_files([foo, bar, baz]).should == [baz] end end describe "#remove_less_specific_files" do it "should remove any nested files that are already in the catalog" do foo = described_class.new :path => File.join(file[:path], 'foo') bar = described_class.new :path => File.join(file[:path], 'bar') baz = described_class.new :path => File.join(file[:path], 'baz') catalog.add_resource(foo) catalog.add_resource(bar) file.remove_less_specific_files([foo, bar, baz]).should == [baz] end end describe "#recurse?" do it "should be true if recurse is true" do file[:recurse] = true file.must be_recurse end it "should be true if recurse is remote" do file[:recurse] = :remote file.must be_recurse end it "should be false if recurse is false" do file[:recurse] = false file.must_not be_recurse end end describe "#recurse_link" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do file[:target] = "mylinks" file.expects(:perform_recursion).with("mylinks").returns [@first] file.stubs(:newchild).returns @resource file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." file[:target] = "mylinks" file.expects(:perform_recursion).with("mylinks").returns [@first] file.stubs(:newchild).never file.expects(:[]=).with(:ensure, :directory) file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do file.expects(:perform_recursion).returns [@first, @second] file.expects(:newchild).with(@first.relative_path).returns @resource file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do file.expects(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do file.stubs(:perform_recursion).returns [@first, @second] file.recurse_link("first" => @resource, "second" => file) file[:ensure].should == :link file[:target].should == "/my/second" end it "should :ensure to :directory if the file is a directory" do file.stubs(:perform_recursion).returns [@first, @second] file.recurse_link("first" => file, "second" => @resource) file[:ensure].should == :directory end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do file.expects(:perform_recursion).returns [@first, @second] file.stubs(:newchild).returns file file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} end end describe "#recurse_local" do before do @metadata = stub 'metadata', :relative_path => "my/file" end it "should pass its path to the :perform_recursion method" do file.expects(:perform_recursion).with(file[:path]).returns [@metadata] file.stubs(:newchild) file.recurse_local end it "should return an empty hash if the recursion returns nothing" do file.expects(:perform_recursion).returns nil file.recurse_local.should == {} end it "should create a new child resource with each generated metadata instance's relative path" do file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" file.recurse_local end it "should not create a new child resource for the '.' directory" do @metadata.stubs(:relative_path).returns "." file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).never file.recurse_local end it "should return a hash of the created resources with the relative paths as the hash keys" do file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).with("my/file").returns "fiebar" file.recurse_local.should == {"my/file" => "fiebar"} end it "should set checksum_type to none if this file checksum is none" do file[:checksum] = :none Puppet::FileServing::Metadata.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] file.expects(:newchild).with("my/file").returns "fiebar" file.recurse_local end end describe "#recurse_remote" do before do file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first") @second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] file.stubs(:newchild).returns @resource file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".") data.stubs(:ftype).returns "file" file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] file.expects(:newchild).never file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) file.expects(:perform_recursion).returns [@first] file.stubs(:newchild).returns @resource file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).with("first").returns @resource file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. it "should set the checksum type to :md5 if the remote file is a file" do @first.stubs(:ftype).returns "file" file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) file.recurse_remote("first" => @resource) end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." file.stubs(:perform_recursion).returns [@first] file.parameter(:source).expects(:metadata=).with @first file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do let(:sources) do h = {} - %w{/one /two /three /four}.each do |key| + %w{/a /b /c /d}.each do |key| h[key] = URI.unescape(Puppet::Util.path_to_uri(File.expand_path(key)).to_s) end h end describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") - file[:source] = sources.keys.map { |key| File.expand_path(key) } - file.expects(:perform_recursion).with(sources['/one']).returns nil - file.expects(:perform_recursion).with(sources['/two']).returns [] - file.expects(:perform_recursion).with(sources['/three']).returns [data] - file.expects(:perform_recursion).with(sources['/four']).never + file[:source] = sources.keys.sort.map { |key| File.expand_path(key) } + file.expects(:perform_recursion).with(sources['/a']).returns nil + file.expects(:perform_recursion).with(sources['/b']).returns [] + file.expects(:perform_recursion).with(sources['/c']).returns [data] + file.expects(:perform_recursion).with(sources['/d']).never file.expects(:newchild).with("foobar").returns @resource file.recurse_remote({}) end end describe "and :sourceselect is set to :all" do before do file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata - file[:source] = %w{/one /two /three /four}.map {|f| File.expand_path(f) } + file[:source] = %w{/a /b /c /d}.map {|f| File.expand_path(f) } file.stubs(:newchild).returns @resource - one = [klass.new("/one", :relative_path => "a")] - file.expects(:perform_recursion).with(sources['/one']).returns one + one = [klass.new("/a", :relative_path => "a")] + file.expects(:perform_recursion).with(sources['/a']).returns one file.expects(:newchild).with("a").returns @resource - two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] - file.expects(:perform_recursion).with(sources['/two']).returns two + two = [klass.new("/b", :relative_path => "a"), klass.new("/b", :relative_path => "b")] + file.expects(:perform_recursion).with(sources['/b']).returns two file.expects(:newchild).with("b").returns @resource - three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")] - file.expects(:perform_recursion).with(sources['/three']).returns three + three = [klass.new("/c", :relative_path => "a"), klass.new("/c", :relative_path => "c")] + file.expects(:perform_recursion).with(sources['/c']).returns three file.expects(:newchild).with("c").returns @resource - file.expects(:perform_recursion).with(sources['/four']).returns [] + file.expects(:perform_recursion).with(sources['/d']).returns [] file.recurse_remote({}) end end end end describe "#perform_recursion" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.indirection.expects(:search) file.perform_recursion(file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" } file.perform_recursion("/foo") end it "should return the results of the metadata search" do Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar" file.perform_recursion(file[:path]).should == "foobar" end it "should pass its recursion value to the search" do file[:recurse] = true Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } file.perform_recursion(file[:path]) end it "should pass true if recursion is remote" do file[:recurse] = :remote Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } file.perform_recursion(file[:path]) end it "should pass its recursion limit value to the search" do file[:recurselimit] = 10 Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 } file.perform_recursion(file[:path]) end it "should configure the search to ignore or manage links" do file[:links] = :manage Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage } file.perform_recursion(file[:path]) end it "should pass its 'ignore' setting to the search if it has one" do file[:ignore] = %w{.svn CVS} Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } file.perform_recursion(file[:path]) end end describe "#remove_existing" do it "should do nothing if the file doesn't exist" do file.remove_existing(:file).should == nil end it "should fail if it can't backup the file" do file.stubs(:stat).returns stub('stat') file.stubs(:perform_backup).returns false expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up; will not replace/) end it "should not do anything if the file is already the right type and not a link" do file.stubs(:stat).returns stub('stat', :ftype => 'file') file.remove_existing(:file).should == nil end it "should not remove directories and should not invalidate the stat unless force is set" do # Actually call stat to set @needs_stat to nil file.stat file.stubs(:stat).returns stub('stat', :ftype => 'directory') file.remove_existing(:file) file.instance_variable_get(:@stat).should == nil @logs.should be_any {|log| log.level == :notice and log.message =~ /Not removing directory; use 'force' to override/} end it "should remove a directory if force is set" do file[:force] = true file.stubs(:stat).returns stub('stat', :ftype => 'directory') FileUtils.expects(:rmtree).with(file[:path]) file.remove_existing(:file).should == true end it "should remove an existing file" do file.stubs(:perform_backup).returns true FileUtils.touch(path) file.remove_existing(:directory).should == true File.exists?(file[:path]).should == false end it "should remove an existing link", :unless => Puppet.features.microsoft_windows? do file.stubs(:perform_backup).returns true target = tmpfile('link_target') FileUtils.touch(target) FileUtils.symlink(target, path) file[:target] = target file.remove_existing(:directory).should == true File.exists?(file[:path]).should == false end it "should fail if the file is not a file, link, or directory" do file.stubs(:stat).returns stub('stat', :ftype => 'socket') expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up files of type socket/) end it "should invalidate the existing stat of the file" do # Actually call stat to set @needs_stat to nil file.stat file.stubs(:stat).returns stub('stat', :ftype => 'file') File.stubs(:unlink) file.remove_existing(:directory).should == true file.instance_variable_get(:@stat).should == :needs_stat end end describe "#retrieve" do it "should copy the source values if the 'source' parameter is set" do file[:source] = File.expand_path('/foo/bar') file.parameter(:source).expects(:copy_source_values) file.retrieve end end describe "#should_be_file?" do it "should have a method for determining if the file should be a normal file" do file.must respond_to(:should_be_file?) end it "should be a file if :ensure is set to :file" do file[:ensure] = :file file.must be_should_be_file end it "should be a file if :ensure is set to :present and the file exists as a normal file" do file.stubs(:stat).returns(mock('stat', :ftype => "file")) file[:ensure] = :present file.must be_should_be_file end it "should not be a file if :ensure is set to something other than :file" do file[:ensure] = :directory file.must_not be_should_be_file end it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do file.stubs(:stat).returns(mock('stat', :ftype => "directory")) file[:ensure] = :present file.must_not be_should_be_file end it "should be a file if :ensure is not set and :content is" do file[:content] = "foo" file.must be_should_be_file end it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do file.stubs(:stat).returns(mock("stat", :ftype => "file")) file.must be_should_be_file end it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do file.stubs(:stat).returns(mock("stat", :ftype => "directory")) file.must_not be_should_be_file end end describe "#stat", :unless => Puppet.features.microsoft_windows? do before do target = tmpfile('link_target') FileUtils.touch(target) FileUtils.symlink(target, path) file[:target] = target file[:links] = :manage # so we always use :lstat end it "should stat the target if it is following links" do file[:links] = :follow file.stat.ftype.should == 'file' end it "should stat the link if is it not following links" do file[:links] = :manage file.stat.ftype.should == 'link' end it "should return nil if the file does not exist" do file[:path] = '/foo/bar/baz/non-existent' file.stat.should be_nil end it "should return nil if the file cannot be stat'ed" do dir = tmpfile('link_test_dir') child = File.join(dir, 'some_file') Dir.mkdir(dir) File.chmod(0, dir) file[:path] = child file.stat.should be_nil # chmod it back so we can clean it up File.chmod(0777, dir) end it "should return the stat instance" do file.stat.should be_a(File::Stat) end it "should cache the stat instance" do file.stat.should equal(file.stat) end end describe "#write" do it "should propagate failures encountered when renaming the temporary file" do File.stubs(:open) File.expects(:rename).raises ArgumentError file[:backup] = 'puppet' file.stubs(:validate_checksum?).returns(false) property = stub('content_property', :actual_content => "something", :length => "something".length) file.stubs(:property).with(:content).returns(property) lambda { file.write(:content) }.should raise_error(Puppet::Error) end it "should delegate writing to the content property" do filehandle = stub_everything 'fh' File.stubs(:open).yields(filehandle) File.stubs(:rename) property = stub('content_property', :actual_content => "something", :length => "something".length) file[:backup] = 'puppet' file.stubs(:validate_checksum?).returns(false) file.stubs(:property).with(:content).returns(property) property.expects(:write).with(filehandle) file.write(:content) end describe "when validating the checksum" do before { file.stubs(:validate_checksum?).returns(true) } it "should fail if the checksum parameter and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') file.stubs(:property).with(:content).returns(property) lambda { file.write :NOTUSED }.should raise_error(Puppet::Error) end end describe "when not validating the checksum" do before { file.stubs(:validate_checksum?).returns(false) } it "should not fail if the checksum property and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b') file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') file.stubs(:property).with(:content).returns(property) lambda { file.write :NOTUSED }.should_not raise_error(Puppet::Error) end end end describe "#fail_if_checksum_is_wrong" do it "should fail if the checksum of the file doesn't match the expected one" do expect do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns('wrong!!') fail_if_checksum_is_wrong(self[:path], 'anything!') end end.to raise_error(Puppet::Error, /File written to disk did not match checksum/) end it "should not fail if the checksum is correct" do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns('anything!') fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil end end it "should not fail if the checksum is absent" do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns(nil) fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil end end end describe "#write_content" do it "should delegate writing the file to the content property" do io = stub('io') file[:content] = "some content here" file.property(:content).expects(:write).with(io) file.send(:write_content, io) end end describe "#write_temporary_file?" do it "should be true if the file has specified content" do file[:content] = 'some content' file.send(:write_temporary_file?).should be_true end it "should be true if the file has specified source" do file[:source] = File.expand_path('/tmp/foo') file.send(:write_temporary_file?).should be_true end it "should be false if the file has neither content nor source" do file.send(:write_temporary_file?).should be_false end end describe "#property_fix" do { :mode => 0777, :owner => 'joeuser', :group => 'joeusers', :seluser => 'seluser', :selrole => 'selrole', :seltype => 'seltype', :selrange => 'selrange' }.each do |name,value| it "should sync the #{name} property if it's not in sync" do file[name] = value prop = file.property(name) prop.expects(:retrieve) prop.expects(:safe_insync?).returns false prop.expects(:sync) file.send(:property_fix) end end end describe "when autorequiring" do describe "target" do it "should require file resource when specified with the target property" do file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory) link = described_class.new(:path => File.expand_path("/bar"), :ensure => :symlink, :target => File.expand_path("/foo")) catalog.add_resource file catalog.add_resource link reqs = link.autorequire reqs.size.must == 1 reqs[0].source.must == file reqs[0].target.must == link end it "should require file resource when specified with the ensure property" do file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory) link = described_class.new(:path => File.expand_path("/bar"), :ensure => File.expand_path("/foo")) catalog.add_resource file catalog.add_resource link reqs = link.autorequire reqs.size.must == 1 reqs[0].source.must == file reqs[0].target.must == link end it "should not require target if target is not managed" do link = described_class.new(:path => File.expand_path('/foo'), :ensure => :symlink, :target => '/bar') catalog.add_resource link link.autorequire.size.should == 0 end end describe "directories" do it "should autorequire its parent directory" do dir = described_class.new(:path => File.dirname(path)) catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should autorequire its nearest ancestor directory" do dir = described_class.new(:path => File.dirname(path)) grandparent = described_class.new(:path => File.dirname(File.dirname(path))) catalog.add_resource file catalog.add_resource dir catalog.add_resource grandparent reqs = file.autorequire reqs.length.must == 1 reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire anything when there is no nearest ancestor directory" do catalog.add_resource file file.autorequire.should be_empty end it "should not autorequire its parent dir if its parent dir is itself" do file[:path] = File.expand_path('/') catalog.add_resource file file.autorequire.should be_empty end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do describe "when using UNC filenames" do it "should autorequire its parent directory" do file[:path] = '//server/foo/bar/baz' dir = described_class.new(:path => "//server/foo/bar") catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should autorequire its nearest ancestor directory" do file = described_class.new(:path => "//server/foo/bar/baz/qux") dir = described_class.new(:path => "//server/foo/bar/baz") grandparent = described_class.new(:path => "//server/foo/bar") catalog.add_resource file catalog.add_resource dir catalog.add_resource grandparent reqs = file.autorequire reqs.length.must == 1 reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire anything when there is no nearest ancestor directory" do file = described_class.new(:path => "//server/foo/bar/baz/qux") catalog.add_resource file file.autorequire.should be_empty end it "should not autorequire its parent dir if its parent dir is itself" do file = described_class.new(:path => "//server/foo") catalog.add_resource file puts file.autorequire file.autorequire.should be_empty end end end end end describe "when managing links" do require 'tempfile' if @real_posix describe "on POSIX systems" do before do Dir.mkdir(path) @target = File.join(path, "target") @link = File.join(path, "link") File.open(@target, "w", 0644) { |f| f.puts "yayness" } File.symlink(@target, @link) file[:path] = @link file[:mode] = 0755 catalog.add_resource file end it "should default to managing the link" do catalog.apply # I convert them to strings so they display correctly if there's an error. (File.stat(@target).mode & 007777).to_s(8).should == '644' end it "should be able to follow links" do file[:links] = :follow catalog.apply (File.stat(@target).mode & 007777).to_s(8).should == '755' end end else # @real_posix # should recode tests using expectations instead of using the filesystem end describe "on Microsoft Windows systems" do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should refuse to work with links" end end describe "when using source" do before do file[:source] = File.expand_path('/one') end Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do file[:checksum] = checksum_type end it 'should validate' do lambda { file.validate }.should_not raise_error end end end describe "with checksum 'none'" do before do file[:checksum] = :none end it 'should raise an exception when validating' do lambda { file.validate }.should raise_error(/You cannot specify source when using checksum 'none'/) end end end describe "when using content" do before do file[:content] = 'file contents' end (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do file[:checksum] = checksum_type end it 'should validate' do lambda { file.validate }.should_not raise_error end end end SOURCE_ONLY_CHECKSUMS.each do |checksum_type| describe "with checksum '#{checksum_type}'" do it 'should raise an exception when validating' do file[:checksum] = checksum_type lambda { file.validate }.should raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) end end end end describe "when auditing" do before :each do # to prevent the catalog from trying to write state.yaml Puppet::Util::Storage.stubs(:store) end it "should not fail if creating a new file if group is not set" do file = described_class.new(:path => path, :audit => 'all', :content => 'content') catalog.add_resource(file) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed File.read(path).should == 'content' end it "should not log errors if creating a new file with ensure present and no content" do file[:audit] = 'content' file[:ensure] = 'present' catalog.add_resource(file) catalog.apply File.should be_exist(path) @logs.should_not be_any {|l| l.level != :notice } end end describe "when specifying both source and checksum" do it 'should use the specified checksum when source is first' do file[:source] = File.expand_path('/foo') file[:checksum] = :md5lite file[:checksum].should == :md5lite end it 'should use the specified checksum when source is last' do file[:checksum] = :md5lite file[:source] = File.expand_path('/foo') file[:checksum].should == :md5lite end end describe "when validating" do [[:source, :target], [:source, :content], [:target, :content]].each do |prop1,prop2| it "should fail if both #{prop1} and #{prop2} are specified" do file[prop1] = prop1 == :source ? File.expand_path("prop1 value") : "prop1 value" file[prop2] = "prop2 value" expect do file.validate end.to raise_error(Puppet::Error, /You cannot specify more than one of/) end end end end diff --git a/spec/unit/type/host_spec.rb b/spec/unit/type/host_spec.rb index 602c428af..3cbe56943 100755 --- a/spec/unit/type/host_spec.rb +++ b/spec/unit/type/host_spec.rb @@ -1,129 +1,656 @@ #!/usr/bin/env rspec require 'spec_helper' host = Puppet::Type.type(:host) describe host do before do @class = host @catalog = Puppet::Resource::Catalog.new @provider = stub 'provider' @resource = stub 'resource', :resource => nil, :provider => @provider end it "should have :name be its namevar" do @class.key_attributes.should == [:name] end describe "when validating attributes" do [:name, :provider ].each do |param| it "should have a #{param} parameter" do @class.attrtype(param).should == :param end end [:ip, :target, :host_aliases, :comment, :ensure].each do |property| it "should have a #{property} property" do @class.attrtype(property).should == :property end end it "should have a list host_aliases" do @class.attrclass(:host_aliases).ancestors.should be_include(Puppet::Property::OrderedList) end end describe "when validating values" do it "should support present as a value for ensure" do proc { @class.new(:name => "foo", :ensure => :present) }.should_not raise_error end it "should support absent as a value for ensure" do proc { @class.new(:name => "foo", :ensure => :absent) }.should_not raise_error end it "should accept IPv4 addresses" do proc { @class.new(:name => "foo", :ip => '10.96.0.1') }.should_not raise_error end it "should accept long IPv6 addresses" do # Taken from wikipedia article about ipv6 proc { @class.new(:name => "foo", :ip => '2001:0db8:85a3:08d3:1319:8a2e:0370:7344') }.should_not raise_error end it "should accept one host_alias" do proc { @class.new(:name => "foo", :host_aliases => 'alias1') }.should_not raise_error end it "should accept multiple host_aliases" do proc { @class.new(:name => "foo", :host_aliases => [ 'alias1', 'alias2' ]) }.should_not raise_error end it "should accept shortened IPv6 addresses" do proc { @class.new(:name => "foo", :ip => '2001:db8:0:8d3:0:8a2e:70:7344') }.should_not raise_error proc { @class.new(:name => "foo", :ip => '::ffff:192.0.2.128') }.should_not raise_error proc { @class.new(:name => "foo", :ip => '::1') }.should_not raise_error end it "should not accept malformed IPv4 addresses like 192.168.0.300" do proc { @class.new(:name => "foo", :ip => '192.168.0.300') }.should raise_error end + it "should reject over-long IPv4 addresses" do + expect { @class.new(:name => "foo", :ip => '10.10.10.10.10') }.to raise_error + end + it "should not accept malformed IP addresses like 2001:0dg8:85a3:08d3:1319:8a2e:0370:7344" do proc { @class.new(:name => "foo", :ip => '2001:0dg8:85a3:08d3:1319:8a2e:0370:7344') }.should raise_error end + # Assorted, annotated IPv6 passes. + ["::1", # loopback, compressed, non-routable + "::", # unspecified, compressed, non-routable + "0:0:0:0:0:0:0:1", # loopback, full + "0:0:0:0:0:0:0:0", # unspecified, full + "2001:DB8:0:0:8:800:200C:417A", # unicast, full + "FF01:0:0:0:0:0:0:101", # multicast, full + "2001:DB8::8:800:200C:417A", # unicast, compressed + "FF01::101", # multicast, compressed + # Some more test cases that should pass. + "2001:0000:1234:0000:0000:C1C0:ABCD:0876", + "3ffe:0b00:0000:0000:0001:0000:0000:000a", + "FF02:0000:0000:0000:0000:0000:0000:0001", + "0000:0000:0000:0000:0000:0000:0000:0001", + "0000:0000:0000:0000:0000:0000:0000:0000", + # Assorted valid, compressed IPv6 addresses. + "2::10", + "ff02::1", + "fe80::", + "2002::", + "2001:db8::", + "2001:0db8:1234::", + "::ffff:0:0", + "::1", + "1:2:3:4:5:6:7:8", + "1:2:3:4:5:6::8", + "1:2:3:4:5::8", + "1:2:3:4::8", + "1:2:3::8", + "1:2::8", + "1::8", + "1::2:3:4:5:6:7", + "1::2:3:4:5:6", + "1::2:3:4:5", + "1::2:3:4", + "1::2:3", + "1::8", + "::2:3:4:5:6:7:8", + "::2:3:4:5:6:7", + "::2:3:4:5:6", + "::2:3:4:5", + "::2:3:4", + "::2:3", + "::8", + "1:2:3:4:5:6::", + "1:2:3:4:5::", + "1:2:3:4::", + "1:2:3::", + "1:2::", + "1::", + "1:2:3:4:5::7:8", + "1:2:3:4::7:8", + "1:2:3::7:8", + "1:2::7:8", + "1::7:8", + # IPv4 addresses as dotted-quads + "1:2:3:4:5:6:1.2.3.4", + "1:2:3:4:5::1.2.3.4", + "1:2:3:4::1.2.3.4", + "1:2:3::1.2.3.4", + "1:2::1.2.3.4", + "1::1.2.3.4", + "1:2:3:4::5:1.2.3.4", + "1:2:3::5:1.2.3.4", + "1:2::5:1.2.3.4", + "1::5:1.2.3.4", + "1::5:11.22.33.44", + "fe80::217:f2ff:254.7.237.98", + "::ffff:192.168.1.26", + "::ffff:192.168.1.1", + "0:0:0:0:0:0:13.1.68.3", # IPv4-compatible IPv6 address, full, deprecated + "0:0:0:0:0:FFFF:129.144.52.38", # IPv4-mapped IPv6 address, full + "::13.1.68.3", # IPv4-compatible IPv6 address, compressed, deprecated + "::FFFF:129.144.52.38", # IPv4-mapped IPv6 address, compressed + "fe80:0:0:0:204:61ff:254.157.241.86", + "fe80::204:61ff:254.157.241.86", + "::ffff:12.34.56.78", + "::ffff:192.0.2.128", # this is OK, since there's a single zero digit in IPv4 + "fe80:0000:0000:0000:0204:61ff:fe9d:f156", + "fe80:0:0:0:204:61ff:fe9d:f156", + "fe80::204:61ff:fe9d:f156", + "::1", + "fe80::", + "fe80::1", + "::ffff:c000:280", + + # Additional test cases from http://rt.cpan.org/Public/Bug/Display.html?id=50693 + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "2001:db8:85a3:0:0:8a2e:370:7334", + "2001:db8:85a3::8a2e:370:7334", + "2001:0db8:0000:0000:0000:0000:1428:57ab", + "2001:0db8:0000:0000:0000::1428:57ab", + "2001:0db8:0:0:0:0:1428:57ab", + "2001:0db8:0:0::1428:57ab", + "2001:0db8::1428:57ab", + "2001:db8::1428:57ab", + "0000:0000:0000:0000:0000:0000:0000:0001", + "::1", + "::ffff:0c22:384e", + "2001:0db8:1234:0000:0000:0000:0000:0000", + "2001:0db8:1234:ffff:ffff:ffff:ffff:ffff", + "2001:db8:a::123", + "fe80::", + + "1111:2222:3333:4444:5555:6666:7777:8888", + "1111:2222:3333:4444:5555:6666:7777::", + "1111:2222:3333:4444:5555:6666::", + "1111:2222:3333:4444:5555::", + "1111:2222:3333:4444::", + "1111:2222:3333::", + "1111:2222::", + "1111::", + "1111:2222:3333:4444:5555:6666::8888", + "1111:2222:3333:4444:5555::8888", + "1111:2222:3333:4444::8888", + "1111:2222:3333::8888", + "1111:2222::8888", + "1111::8888", + "::8888", + "1111:2222:3333:4444:5555::7777:8888", + "1111:2222:3333:4444::7777:8888", + "1111:2222:3333::7777:8888", + "1111:2222::7777:8888", + "1111::7777:8888", + "::7777:8888", + "1111:2222:3333:4444::6666:7777:8888", + "1111:2222:3333::6666:7777:8888", + "1111:2222::6666:7777:8888", + "1111::6666:7777:8888", + "::6666:7777:8888", + "1111:2222:3333::5555:6666:7777:8888", + "1111:2222::5555:6666:7777:8888", + "1111::5555:6666:7777:8888", + "::5555:6666:7777:8888", + "1111:2222::4444:5555:6666:7777:8888", + "1111::4444:5555:6666:7777:8888", + "::4444:5555:6666:7777:8888", + "1111::3333:4444:5555:6666:7777:8888", + "::3333:4444:5555:6666:7777:8888", + "::2222:3333:4444:5555:6666:7777:8888", + "1111:2222:3333:4444:5555:6666:123.123.123.123", + "1111:2222:3333:4444:5555::123.123.123.123", + "1111:2222:3333:4444::123.123.123.123", + "1111:2222:3333::123.123.123.123", + "1111:2222::123.123.123.123", + "1111::123.123.123.123", + "::123.123.123.123", + "1111:2222:3333:4444::6666:123.123.123.123", + "1111:2222:3333::6666:123.123.123.123", + "1111:2222::6666:123.123.123.123", + "1111::6666:123.123.123.123", + "::6666:123.123.123.123", + "1111:2222:3333::5555:6666:123.123.123.123", + "1111:2222::5555:6666:123.123.123.123", + "1111::5555:6666:123.123.123.123", + "::5555:6666:123.123.123.123", + "1111:2222::4444:5555:6666:123.123.123.123", + "1111::4444:5555:6666:123.123.123.123", + "::4444:5555:6666:123.123.123.123", + "1111::3333:4444:5555:6666:123.123.123.123", + "::2222:3333:4444:5555:6666:123.123.123.123", + + # Playing with combinations of "0" and "::"; these are all sytactically + # correct, but are bad form because "0" adjacent to "::" should be + # combined into "::" + "::0:0:0:0:0:0:0", + "::0:0:0:0:0:0", + "::0:0:0:0:0", + "::0:0:0:0", + "::0:0:0", + "::0:0", + "::0", + "0:0:0:0:0:0:0::", + "0:0:0:0:0:0::", + "0:0:0:0:0::", + "0:0:0:0::", + "0:0:0::", + "0:0::", + "0::", + + # Additional cases: http://crisp.tweakblogs.net/blog/2031/ipv6-validation-%28and-caveats%29.html + "0:a:b:c:d:e:f::", + "::0:a:b:c:d:e:f", # syntactically correct, but bad form (::0:... could be combined) + "a:b:c:d:e:f:0::", + ].each do |ip| + it "should accept #{ip.inspect} as an IPv6 address" do + expect { @class.new(:name => "foo", :ip => ip) }.not_to raise_error + end + end + + # ...aaaand, some failure cases. + [":", + "02001:0000:1234:0000:0000:C1C0:ABCD:0876", # extra 0 not allowed! + "2001:0000:1234:0000:00001:C1C0:ABCD:0876", # extra 0 not allowed! + "2001:0000:1234:0000:0000:C1C0:ABCD:0876 0", # junk after valid address + "2001:0000:1234: 0000:0000:C1C0:ABCD:0876", # internal space + "3ffe:0b00:0000:0001:0000:0000:000a", # seven segments + "FF02:0000:0000:0000:0000:0000:0000:0000:0001", # nine segments + "3ffe:b00::1::a", # double "::" + "::1111:2222:3333:4444:5555:6666::", # double "::" + "1:2:3::4:5::7:8", # Double "::" + "12345::6:7:8", + # IPv4 embedded, but bad... + "1::5:400.2.3.4", "1::5:260.2.3.4", "1::5:256.2.3.4", "1::5:1.256.3.4", + "1::5:1.2.256.4", "1::5:1.2.3.256", "1::5:300.2.3.4", "1::5:1.300.3.4", + "1::5:1.2.300.4", "1::5:1.2.3.300", "1::5:900.2.3.4", "1::5:1.900.3.4", + "1::5:1.2.900.4", "1::5:1.2.3.900", "1::5:300.300.300.300", "1::5:3000.30.30.30", + "1::400.2.3.4", "1::260.2.3.4", "1::256.2.3.4", "1::1.256.3.4", + "1::1.2.256.4", "1::1.2.3.256", "1::300.2.3.4", "1::1.300.3.4", + "1::1.2.300.4", "1::1.2.3.300", "1::900.2.3.4", "1::1.900.3.4", + "1::1.2.900.4", "1::1.2.3.900", "1::300.300.300.300", "1::3000.30.30.30", + "::400.2.3.4", "::260.2.3.4", "::256.2.3.4", "::1.256.3.4", + "::1.2.256.4", "::1.2.3.256", "::300.2.3.4", "::1.300.3.4", + "::1.2.300.4", "::1.2.3.300", "::900.2.3.4", "::1.900.3.4", + "::1.2.900.4", "::1.2.3.900", "::300.300.300.300", "::3000.30.30.30", + "2001:1:1:1:1:1:255Z255X255Y255", # garbage instead of "." in IPv4 + "::ffff:192x168.1.26", # ditto + "::ffff:2.3.4", + "::ffff:257.1.2.3", + "1.2.3.4:1111:2222:3333:4444::5555", + "1.2.3.4:1111:2222:3333::5555", + "1.2.3.4:1111:2222::5555", + "1.2.3.4:1111::5555", + "1.2.3.4::5555", + "1.2.3.4::", + + # Testing IPv4 addresses represented as dotted-quads Leading zero's in + # IPv4 addresses not allowed: some systems treat the leading "0" in + # ".086" as the start of an octal number Update: The BNF in RFC-3986 + # explicitly defines the dec-octet (for IPv4 addresses) not to have a + # leading zero + "fe80:0000:0000:0000:0204:61ff:254.157.241.086", + "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:1.2.3.4", + "1111:2222:3333:4444:5555:6666:00.00.00.00", + "1111:2222:3333:4444:5555:6666:000.000.000.000", + "1111:2222:3333:4444:5555:6666:256.256.256.256", + + "1111:2222:3333:4444::5555:", + "1111:2222:3333::5555:", + "1111:2222::5555:", + "1111::5555:", + "::5555:", + ":::", + "1111:", + ":", + + ":1111:2222:3333:4444::5555", + ":1111:2222:3333::5555", + ":1111:2222::5555", + ":1111::5555", + ":::5555", + ":::", + + # Additional test cases from http://rt.cpan.org/Public/Bug/Display.html?id=50693 + "123", + "ldkfj", + "2001::FFD3::57ab", + "2001:db8:85a3::8a2e:37023:7334", + "2001:db8:85a3::8a2e:370k:7334", + "1:2:3:4:5:6:7:8:9", + "1::2::3", + "1:::3:4:5", + "1:2:3::4:5:6:7:8:9", + + # Invalid data + "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX", + + # Too many components + "1111:2222:3333:4444:5555:6666:7777:8888:9999", + "1111:2222:3333:4444:5555:6666:7777:8888::", + "::2222:3333:4444:5555:6666:7777:8888:9999", + + # Too few components + "1111:2222:3333:4444:5555:6666:7777", + "1111:2222:3333:4444:5555:6666", + "1111:2222:3333:4444:5555", + "1111:2222:3333:4444", + "1111:2222:3333", + "1111:2222", + "1111", + + # Missing : + "11112222:3333:4444:5555:6666:7777:8888", + "1111:22223333:4444:5555:6666:7777:8888", + "1111:2222:33334444:5555:6666:7777:8888", + "1111:2222:3333:44445555:6666:7777:8888", + "1111:2222:3333:4444:55556666:7777:8888", + "1111:2222:3333:4444:5555:66667777:8888", + "1111:2222:3333:4444:5555:6666:77778888", + + # Missing : intended for :: + "1111:2222:3333:4444:5555:6666:7777:8888:", + "1111:2222:3333:4444:5555:6666:7777:", + "1111:2222:3333:4444:5555:6666:", + "1111:2222:3333:4444:5555:", + "1111:2222:3333:4444:", + "1111:2222:3333:", + "1111:2222:", + "1111:", + ":", + ":8888", + ":7777:8888", + ":6666:7777:8888", + ":5555:6666:7777:8888", + ":4444:5555:6666:7777:8888", + ":3333:4444:5555:6666:7777:8888", + ":2222:3333:4444:5555:6666:7777:8888", + ":1111:2222:3333:4444:5555:6666:7777:8888", + + # ::: + ":::2222:3333:4444:5555:6666:7777:8888", + "1111:::3333:4444:5555:6666:7777:8888", + "1111:2222:::4444:5555:6666:7777:8888", + "1111:2222:3333:::5555:6666:7777:8888", + "1111:2222:3333:4444:::6666:7777:8888", + "1111:2222:3333:4444:5555:::7777:8888", + "1111:2222:3333:4444:5555:6666:::8888", + "1111:2222:3333:4444:5555:6666:7777:::", + + # Double ::", + "::2222::4444:5555:6666:7777:8888", + "::2222:3333::5555:6666:7777:8888", + "::2222:3333:4444::6666:7777:8888", + "::2222:3333:4444:5555::7777:8888", + "::2222:3333:4444:5555:7777::8888", + "::2222:3333:4444:5555:7777:8888::", + + "1111::3333::5555:6666:7777:8888", + "1111::3333:4444::6666:7777:8888", + "1111::3333:4444:5555::7777:8888", + "1111::3333:4444:5555:6666::8888", + "1111::3333:4444:5555:6666:7777::", + + "1111:2222::4444::6666:7777:8888", + "1111:2222::4444:5555::7777:8888", + "1111:2222::4444:5555:6666::8888", + "1111:2222::4444:5555:6666:7777::", + + "1111:2222:3333::5555::7777:8888", + "1111:2222:3333::5555:6666::8888", + "1111:2222:3333::5555:6666:7777::", + + "1111:2222:3333:4444::6666::8888", + "1111:2222:3333:4444::6666:7777::", + + "1111:2222:3333:4444:5555::7777::", + + + # Too many components" + "1111:2222:3333:4444:5555:6666:7777:8888:1.2.3.4", + "1111:2222:3333:4444:5555:6666:7777:1.2.3.4", + "1111:2222:3333:4444:5555:6666::1.2.3.4", + "::2222:3333:4444:5555:6666:7777:1.2.3.4", + "1111:2222:3333:4444:5555:6666:1.2.3.4.5", + + # Too few components + "1111:2222:3333:4444:5555:1.2.3.4", + "1111:2222:3333:4444:1.2.3.4", + "1111:2222:3333:1.2.3.4", + "1111:2222:1.2.3.4", + "1111:1.2.3.4", + + # Missing : + "11112222:3333:4444:5555:6666:1.2.3.4", + "1111:22223333:4444:5555:6666:1.2.3.4", + "1111:2222:33334444:5555:6666:1.2.3.4", + "1111:2222:3333:44445555:6666:1.2.3.4", + "1111:2222:3333:4444:55556666:1.2.3.4", + "1111:2222:3333:4444:5555:66661.2.3.4", + + # Missing . + "1111:2222:3333:4444:5555:6666:255255.255.255", + "1111:2222:3333:4444:5555:6666:255.255255.255", + "1111:2222:3333:4444:5555:6666:255.255.255255", + + # Missing : intended for :: + ":1.2.3.4", + ":6666:1.2.3.4", + ":5555:6666:1.2.3.4", + ":4444:5555:6666:1.2.3.4", + ":3333:4444:5555:6666:1.2.3.4", + ":2222:3333:4444:5555:6666:1.2.3.4", + ":1111:2222:3333:4444:5555:6666:1.2.3.4", + + # ::: + ":::2222:3333:4444:5555:6666:1.2.3.4", + "1111:::3333:4444:5555:6666:1.2.3.4", + "1111:2222:::4444:5555:6666:1.2.3.4", + "1111:2222:3333:::5555:6666:1.2.3.4", + "1111:2222:3333:4444:::6666:1.2.3.4", + "1111:2222:3333:4444:5555:::1.2.3.4", + + # Double :: + "::2222::4444:5555:6666:1.2.3.4", + "::2222:3333::5555:6666:1.2.3.4", + "::2222:3333:4444::6666:1.2.3.4", + "::2222:3333:4444:5555::1.2.3.4", + + "1111::3333::5555:6666:1.2.3.4", + "1111::3333:4444::6666:1.2.3.4", + "1111::3333:4444:5555::1.2.3.4", + + "1111:2222::4444::6666:1.2.3.4", + "1111:2222::4444:5555::1.2.3.4", + + "1111:2222:3333::5555::1.2.3.4", + + # Missing parts + "::.", + "::..", + "::...", + "::1...", + "::1.2..", + "::1.2.3.", + "::.2..", + "::.2.3.", + "::.2.3.4", + "::..3.", + "::..3.4", + "::...4", + + # Extra : in front + ":1111:2222:3333:4444:5555:6666:7777::", + ":1111:2222:3333:4444:5555:6666::", + ":1111:2222:3333:4444:5555::", + ":1111:2222:3333:4444::", + ":1111:2222:3333::", + ":1111:2222::", + ":1111::", + ":::", + ":1111:2222:3333:4444:5555:6666::8888", + ":1111:2222:3333:4444:5555::8888", + ":1111:2222:3333:4444::8888", + ":1111:2222:3333::8888", + ":1111:2222::8888", + ":1111::8888", + ":::8888", + ":1111:2222:3333:4444:5555::7777:8888", + ":1111:2222:3333:4444::7777:8888", + ":1111:2222:3333::7777:8888", + ":1111:2222::7777:8888", + ":1111::7777:8888", + ":::7777:8888", + ":1111:2222:3333:4444::6666:7777:8888", + ":1111:2222:3333::6666:7777:8888", + ":1111:2222::6666:7777:8888", + ":1111::6666:7777:8888", + ":::6666:7777:8888", + ":1111:2222:3333::5555:6666:7777:8888", + ":1111:2222::5555:6666:7777:8888", + ":1111::5555:6666:7777:8888", + ":::5555:6666:7777:8888", + ":1111:2222::4444:5555:6666:7777:8888", + ":1111::4444:5555:6666:7777:8888", + ":::4444:5555:6666:7777:8888", + ":1111::3333:4444:5555:6666:7777:8888", + ":::3333:4444:5555:6666:7777:8888", + ":::2222:3333:4444:5555:6666:7777:8888", + ":1111:2222:3333:4444:5555:6666:1.2.3.4", + ":1111:2222:3333:4444:5555::1.2.3.4", + ":1111:2222:3333:4444::1.2.3.4", + ":1111:2222:3333::1.2.3.4", + ":1111:2222::1.2.3.4", + ":1111::1.2.3.4", + ":::1.2.3.4", + ":1111:2222:3333:4444::6666:1.2.3.4", + ":1111:2222:3333::6666:1.2.3.4", + ":1111:2222::6666:1.2.3.4", + ":1111::6666:1.2.3.4", + ":::6666:1.2.3.4", + ":1111:2222:3333::5555:6666:1.2.3.4", + ":1111:2222::5555:6666:1.2.3.4", + ":1111::5555:6666:1.2.3.4", + ":::5555:6666:1.2.3.4", + ":1111:2222::4444:5555:6666:1.2.3.4", + ":1111::4444:5555:6666:1.2.3.4", + ":::4444:5555:6666:1.2.3.4", + ":1111::3333:4444:5555:6666:1.2.3.4", + ":::2222:3333:4444:5555:6666:1.2.3.4", + + # Extra : at end + "1111:2222:3333:4444:5555:6666:7777:::", + "1111:2222:3333:4444:5555:6666:::", + "1111:2222:3333:4444:5555:::", + "1111:2222:3333:4444:::", + "1111:2222:3333:::", + "1111:2222:::", + "1111:::", + ":::", + "1111:2222:3333:4444:5555:6666::8888:", + "1111:2222:3333:4444:5555::8888:", + "1111:2222:3333:4444::8888:", + "1111:2222:3333::8888:", + "1111:2222::8888:", + "1111::8888:", + "::8888:", + "1111:2222:3333:4444:5555::7777:8888:", + "1111:2222:3333:4444::7777:8888:", + "1111:2222:3333::7777:8888:", + "1111:2222::7777:8888:", + "1111::7777:8888:", + "::7777:8888:", + "1111:2222:3333:4444::6666:7777:8888:", + "1111:2222:3333::6666:7777:8888:", + "1111:2222::6666:7777:8888:", + "1111::6666:7777:8888:", + "::6666:7777:8888:", + "1111:2222:3333::5555:6666:7777:8888:", + "1111:2222::5555:6666:7777:8888:", + "1111::5555:6666:7777:8888:", + "::5555:6666:7777:8888:", + "1111:2222::4444:5555:6666:7777:8888:", + "1111::4444:5555:6666:7777:8888:", + "::4444:5555:6666:7777:8888:", + "1111::3333:4444:5555:6666:7777:8888:", + "::3333:4444:5555:6666:7777:8888:", + "::2222:3333:4444:5555:6666:7777:8888:", + ].each do |ip| + it "should reject #{ip.inspect} as an IPv6 address" do + expect { @class.new(:name => "foo", :ip => ip) }.to raise_error + end + end + it "should not accept spaces in resourcename" do proc { @class.new(:name => "foo bar") }.should raise_error end it "should not accept host_aliases with spaces" do proc { @class.new(:name => "foo", :host_aliases => [ 'well_formed', 'not wellformed' ]) }.should raise_error end it "should not accept empty host_aliases" do proc { @class.new(:name => "foo", :host_aliases => ['alias1','']) }.should raise_error end end describe "when syncing" do it "should send the first value to the provider for ip property" do @ip = @class.attrclass(:ip).new(:resource => @resource, :should => %w{192.168.0.1 192.168.0.2}) @provider.expects(:ip=).with '192.168.0.1' @ip.sync end it "should send the first value to the provider for comment property" do @comment = @class.attrclass(:comment).new(:resource => @resource, :should => %w{Bazinga Notme}) @provider.expects(:comment=).with 'Bazinga' @comment.sync end it "should send the joined array to the provider for host_alias" do @host_aliases = @class.attrclass(:host_aliases).new(:resource => @resource, :should => %w{foo bar}) @provider.expects(:host_aliases=).with 'foo bar' @host_aliases.sync end it "should also use the specified delimiter for joining" do @host_aliases = @class.attrclass(:host_aliases).new(:resource => @resource, :should => %w{foo bar}) @host_aliases.stubs(:delimiter).returns "\t" @provider.expects(:host_aliases=).with "foo\tbar" @host_aliases.sync end it "should care about the order of host_aliases" do @host_aliases = @class.attrclass(:host_aliases).new(:resource => @resource, :should => %w{foo bar}) @host_aliases.insync?(%w{foo bar}).should == true @host_aliases.insync?(%w{bar foo}).should == false end it "should not consider aliases to be in sync if should is a subset of current" do @host_aliases = @class.attrclass(:host_aliases).new(:resource => @resource, :should => %w{foo bar}) @host_aliases.insync?(%w{foo bar anotherone}).should == false end end end diff --git a/spec/unit/util/rdoc/parser_spec.rb b/spec/unit/util/rdoc/parser_spec.rb index 9c8cc7588..892b932b2 100755 --- a/spec/unit/util/rdoc/parser_spec.rb +++ b/spec/unit/util/rdoc/parser_spec.rb @@ -1,568 +1,568 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/resource/type_collection' require 'puppet/util/rdoc/parser' require 'puppet/util/rdoc/code_objects' require 'rdoc/options' require 'rdoc/rdoc' describe RDoc::Parser, :'fails_on_ruby_1.9.2' => true do include PuppetSpec::Files before :each do File.stubs(:stat).with("init.pp") @top_level = stub_everything 'toplevel', :file_relative_name => "init.pp" @parser = RDoc::Parser.new(@top_level, "module/manifests/init.pp", nil, Options.instance, RDoc::Stats.new) end describe "when scanning files" do it "should parse puppet files with the puppet parser" do @parser.stubs(:scan_top_level) parser = stub 'parser' Puppet::Parser::Parser.stubs(:new).returns(parser) parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')).at_least_once parser.expects(:file=).with("module/manifests/init.pp") parser.expects(:file=).with(File.expand_path("/dev/null/manifests/site.pp")) @parser.scan end it "should scan the ast for Puppet files" do parser = stub_everything 'parser' Puppet::Parser::Parser.stubs(:new).returns(parser) parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')).at_least_once @parser.expects(:scan_top_level) @parser.scan end it "should return a PuppetTopLevel to RDoc" do parser = stub_everything 'parser' Puppet::Parser::Parser.stubs(:new).returns(parser) parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')).at_least_once @parser.expects(:scan_top_level) @parser.scan.should be_a(RDoc::PuppetTopLevel) end it "should scan the top level even if the file has already parsed" do known_type = stub 'known_types' env = stub 'env' Puppet::Node::Environment.stubs(:new).returns(env) env.stubs(:known_resource_types).returns(known_type) known_type.expects(:watching_file?).with("module/manifests/init.pp").returns(true) @parser.expects(:scan_top_level) @parser.scan end end describe "when scanning top level entities" do before :each do @resource_type_collection = resource_type_collection = stub_everything('resource_type_collection') @parser.instance_eval { @known_resource_types = resource_type_collection } @parser.stubs(:split_module).returns("module") @topcontainer = stub_everything 'topcontainer' @container = stub_everything 'container' @module = stub_everything 'module' @container.stubs(:add_module).returns(@module) @parser.stubs(:get_class_or_module).returns([@container, "module"]) end it "should read any present README as module documentation" do FileTest.stubs(:readable?).returns(true) File.stubs(:open).returns("readme") @parser.stubs(:parse_elements) @module.expects(:comment=).with("readme") @parser.scan_top_level(@topcontainer) end it "should tell the container its module name" do @parser.stubs(:parse_elements) @topcontainer.expects(:module_name=).with("module") @parser.scan_top_level(@topcontainer) end it "should not document our toplevel if it isn't a valid module" do @parser.stubs(:split_module).returns(nil) @topcontainer.expects(:document_self=).with(false) @parser.expects(:parse_elements).never @parser.scan_top_level(@topcontainer) end it "should set the module as global if we parse the global manifests (ie __site__ module)" do @parser.stubs(:split_module).returns(RDoc::Parser::SITE) @parser.stubs(:parse_elements) @topcontainer.expects(:global=).with(true) @parser.scan_top_level(@topcontainer) end it "should attach this module container to the toplevel container" do @parser.stubs(:parse_elements) @container.expects(:add_module).with(RDoc::PuppetModule, "module").returns(@module) @parser.scan_top_level(@topcontainer) end it "should defer ast parsing to parse_elements for this module" do @parser.expects(:parse_elements).with(@module) @parser.scan_top_level(@topcontainer) end it "should defer plugins parsing to parse_plugins for this module" do @parser.input_file_name = "module/lib/puppet/parser/function.rb" @parser.expects(:parse_plugins).with(@module) @parser.scan_top_level(@topcontainer) end end describe "when finding modules from filepath" do before :each do - Puppet::Module.stubs(:modulepath).returns("/path/to/modules") + Puppet::Node::Environment.any_instance.stubs(:modulepath).returns("/path/to/modules") end it "should return the module name for modulized puppet manifests" do File.stubs(:expand_path).returns("/path/to/module/manifests/init.pp") File.stubs(:identical?).with("/path/to", "/path/to/modules").returns(true) @parser.split_module("/path/to/modules/mymodule/manifests/init.pp").should == "module" end it "should return for manifests not under module path" do File.stubs(:expand_path).returns("/path/to/manifests/init.pp") File.stubs(:identical?).returns(false) @parser.split_module("/path/to/manifests/init.pp").should == RDoc::Parser::SITE end it "should handle windows paths with drive letters", :if => Puppet.features.microsoft_windows? do @parser.split_module("C:/temp/init.pp").should == RDoc::Parser::SITE end end describe "when parsing AST elements" do before :each do @klass = stub_everything 'klass', :file => "module/manifests/init.pp", :name => "myclass", :type => :hostclass @definition = stub_everything 'definition', :file => "module/manifests/init.pp", :type => :definition, :name => "mydef" @node = stub_everything 'node', :file => "module/manifests/init.pp", :type => :node, :name => "mynode" @resource_type_collection = resource_type_collection = Puppet::Resource::TypeCollection.new("env") @parser.instance_eval { @known_resource_types = resource_type_collection } @container = stub_everything 'container' end it "should document classes in the parsed file" do @resource_type_collection.add_hostclass(@klass) @parser.expects(:document_class).with("myclass", @klass, @container) @parser.parse_elements(@container) end it "should not document class parsed in an other file" do @klass.stubs(:file).returns("/not/same/path/file.pp") @resource_type_collection.add_hostclass(@klass) @parser.expects(:document_class).with("myclass", @klass, @container).never @parser.parse_elements(@container) end it "should document vardefs for the main class" do @klass.stubs(:name).returns :main @resource_type_collection.add_hostclass(@klass) code = stub 'code', :is_a? => false @klass.stubs(:name).returns("") @klass.stubs(:code).returns(code) @parser.expects(:scan_for_vardef).with(@container, code) @parser.parse_elements(@container) end it "should document definitions in the parsed file" do @resource_type_collection.add_definition(@definition) @parser.expects(:document_define).with("mydef", @definition, @container) @parser.parse_elements(@container) end it "should not document definitions parsed in an other file" do @definition.stubs(:file).returns("/not/same/path/file.pp") @resource_type_collection.add_definition(@definition) @parser.expects(:document_define).with("mydef", @definition, @container).never @parser.parse_elements(@container) end it "should document nodes in the parsed file" do @resource_type_collection.add_node(@node) @parser.expects(:document_node).with("mynode", @node, @container) @parser.parse_elements(@container) end it "should not document node parsed in an other file" do @node.stubs(:file).returns("/not/same/path/file.pp") @resource_type_collection.add_node(@node) @parser.expects(:document_node).with("mynode", @node, @container).never @parser.parse_elements(@container) end end describe "when documenting definition" do before(:each) do @define = stub_everything 'define', :arguments => [], :doc => "mydoc", :file => "file", :line => 42 @class = stub_everything 'class' @parser.stubs(:get_class_or_module).returns([@class, "mydef"]) end it "should register a RDoc method to the current container" do @class.expects(:add_method).with { |m| m.name == "mydef"} @parser.document_define("mydef", @define, @class) end it "should attach the documentation to this method" do @class.expects(:add_method).with { |m| m.comment = "mydoc" } @parser.document_define("mydef", @define, @class) end it "should produce a better error message on unhandled exception" do @class.expects(:add_method).raises(ArgumentError) lambda { @parser.document_define("mydef", @define, @class) }.should raise_error(Puppet::ParseError, /in file at line 42/) end it "should convert all definition parameter to string" do arg = stub 'arg' val = stub 'val' @define.stubs(:arguments).returns({arg => val}) arg.expects(:to_s).returns("arg") val.expects(:to_s).returns("val") @parser.document_define("mydef", @define, @class) end end describe "when documenting nodes" do before :each do @code = stub_everything 'code' @node = stub_everything 'node', :doc => "mydoc", :parent => "parent", :code => @code, :file => "file", :line => 42 @rdoc_node = stub_everything 'rdocnode' @class = stub_everything 'class' @class.stubs(:add_node).returns(@rdoc_node) end it "should add a node to the current container" do @class.expects(:add_node).with("mynode", "parent").returns(@rdoc_node) @parser.document_node("mynode", @node, @class) end it "should associate the node documentation to the rdoc node" do @rdoc_node.expects(:comment=).with("mydoc") @parser.document_node("mynode", @node, @class) end it "should scan for include and require" do @parser.expects(:scan_for_include_or_require).with(@rdoc_node, @code) @parser.document_node("mynode", @node, @class) end it "should scan for variable definition" do @parser.expects(:scan_for_vardef).with(@rdoc_node, @code) @parser.document_node("mynode", @node, @class) end it "should scan for resources if needed" do Puppet.settings.stubs(:[]).with(:document_all).returns(true) @parser.expects(:scan_for_resource).with(@rdoc_node, @code) @parser.document_node("mynode", @node, @class) end it "should produce a better error message on unhandled exception" do @class.stubs(:add_node).raises(ArgumentError) lambda { @parser.document_node("mynode", @node, @class) }.should raise_error(Puppet::ParseError, /in file at line 42/) end end describe "when documenting classes" do before :each do @code = stub_everything 'code' @class = stub_everything 'class', :doc => "mydoc", :parent => "parent", :code => @code, :file => "file", :line => 42 @rdoc_class = stub_everything 'rdoc-class' @module = stub_everything 'class' @module.stubs(:add_class).returns(@rdoc_class) @parser.stubs(:get_class_or_module).returns([@module, "myclass"]) end it "should add a class to the current container" do @module.expects(:add_class).with(RDoc::PuppetClass, "myclass", "parent").returns(@rdoc_class) @parser.document_class("mynode", @class, @module) end it "should set the superclass" do @rdoc_class.expects(:superclass=).with("parent") @parser.document_class("mynode", @class, @module) end it "should associate the node documentation to the rdoc class" do @rdoc_class.expects(:comment=).with("mydoc") @parser.document_class("mynode", @class, @module) end it "should scan for include and require" do @parser.expects(:scan_for_include_or_require).with(@rdoc_class, @code) @parser.document_class("mynode", @class, @module) end it "should scan for resources if needed" do Puppet.settings.stubs(:[]).with(:document_all).returns(true) @parser.expects(:scan_for_resource).with(@rdoc_class, @code) @parser.document_class("mynode", @class, @module) end it "should produce a better error message on unhandled exception" do @module.stubs(:add_class).raises(ArgumentError) lambda { @parser.document_class("mynode", @class, @module) }.should raise_error(Puppet::ParseError, /in file at line 42/) end end describe "when scanning for includes and requires" do def create_stmt(name) stmt_value = stub "#{name}_value", :to_s => "myclass" Puppet::Parser::AST::Function.new( :name => name, :arguments => [stmt_value], :doc => 'mydoc' ) end before(:each) do @class = stub_everything 'class' @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should also scan mono-instruction code" do @class.expects(:add_include).with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } @parser.scan_for_include_or_require(@class, create_stmt("include")) end it "should register recursively includes to the current container" do @code.stubs(:children).returns([ create_stmt("include") ]) @class.expects(:add_include)#.with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } @parser.scan_for_include_or_require(@class, [@code]) end it "should register requires to the current container" do @code.stubs(:children).returns([ create_stmt("require") ]) @class.expects(:add_require).with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } @parser.scan_for_include_or_require(@class, [@code]) end end describe "when scanning for realized virtual resources" do def create_stmt stmt_value = stub "resource_ref", :to_s => "File[\"/tmp/a\"]" Puppet::Parser::AST::Function.new( :name => 'realize', :arguments => [stmt_value], :doc => 'mydoc' ) end before(:each) do @class = stub_everything 'class' @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should also scan mono-instruction code" do @class.expects(:add_realize).with { |i| i.is_a?(RDoc::Include) and i.name == "File[\"/tmp/a\"]" and i.comment == "mydoc" } @parser.scan_for_realize(@class,create_stmt) end it "should register recursively includes to the current container" do @code.stubs(:children).returns([ create_stmt ]) @class.expects(:add_realize).with { |i| i.is_a?(RDoc::Include) and i.name == "File[\"/tmp/a\"]" and i.comment == "mydoc" } @parser.scan_for_realize(@class, [@code]) end end describe "when scanning for variable definition" do before :each do @class = stub_everything 'class' @stmt = stub_everything 'stmt', :name => "myvar", :value => "myvalue", :doc => "mydoc" @stmt.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(false) @stmt.stubs(:is_a?).with(Puppet::Parser::AST::VarDef).returns(true) @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should recursively register variables to the current container" do @code.stubs(:children).returns([ @stmt ]) @class.expects(:add_constant).with { |i| i.is_a?(RDoc::Constant) and i.name == "myvar" and i.comment == "mydoc" } @parser.scan_for_vardef(@class, [ @code ]) end it "should also scan mono-instruction code" do @class.expects(:add_constant).with { |i| i.is_a?(RDoc::Constant) and i.name == "myvar" and i.comment == "mydoc" } @parser.scan_for_vardef(@class, @stmt) end end describe "when scanning for resources" do before :each do @class = stub_everything 'class' @stmt = Puppet::Parser::AST::Resource.new( :type => "File", :instances => Puppet::Parser::AST::ASTArray.new(:children => [ Puppet::Parser::AST::ResourceInstance.new( :title => Puppet::Parser::AST::Name.new(:value => "myfile"), :parameters => Puppet::Parser::AST::ASTArray.new(:children => []) ) ]), :doc => 'mydoc' ) @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should register a PuppetResource to the current container" do @code.stubs(:children).returns([ @stmt ]) @class.expects(:add_resource).with { |i| i.is_a?(RDoc::PuppetResource) and i.title == "myfile" and i.comment == "mydoc" } @parser.scan_for_resource(@class, [ @code ]) end it "should also scan mono-instruction code" do @class.expects(:add_resource).with { |i| i.is_a?(RDoc::PuppetResource) and i.title == "myfile" and i.comment == "mydoc" } @parser.scan_for_resource(@class, @stmt) end end describe "when parsing plugins" do before :each do @container = stub 'container' end it "should delegate parsing custom facts to parse_facts" do @parser = RDoc::Parser.new(@top_level, "module/manifests/lib/puppet/facter/test.rb", nil, Options.instance, RDoc::Stats.new) @parser.expects(:parse_fact).with(@container) @parser.parse_plugins(@container) end it "should delegate parsing plugins to parse_plugins" do @parser = RDoc::Parser.new(@top_level, "module/manifests/lib/puppet/functions/test.rb", nil, Options.instance, RDoc::Stats.new) @parser.expects(:parse_puppet_plugin).with(@container) @parser.parse_plugins(@container) end end describe "when parsing plugins" do before :each do @container = stub_everything 'container' end it "should add custom functions to the container" do File.stubs(:open).yields("# documentation module Puppet::Parser::Functions newfunction(:myfunc, :type => :rvalue) do |args| File.dirname(args[0]) end end".split("\n")) @container.expects(:add_plugin).with do |plugin| plugin.comment == "documentation\n" #and plugin.name == "myfunc" end @parser.parse_puppet_plugin(@container) end it "should add custom types to the container" do File.stubs(:open).yields("# documentation Puppet::Type.newtype(:mytype) do end".split("\n")) @container.expects(:add_plugin).with do |plugin| plugin.comment == "documentation\n" #and plugin.name == "mytype" end @parser.parse_puppet_plugin(@container) end end describe "when parsing facts" do before :each do @container = stub_everything 'container' File.stubs(:open).yields(["# documentation", "Facter.add('myfact') do", "confine :kernel => :linux", "end"]) end it "should add facts to the container" do @container.expects(:add_fact).with do |fact| fact.comment == "documentation\n" and fact.name == "myfact" end @parser.parse_fact(@container) end it "should add confine to the parsed facts" do ourfact = nil @container.expects(:add_fact).with do |fact| ourfact = fact true end @parser.parse_fact(@container) ourfact.confine.should == { :type => "kernel", :value => ":linux" } end end end