diff options
| author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-08-14 06:21:03 +0000 |
|---|---|---|
| committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-08-14 06:21:03 +0000 |
| commit | 12452ee9ca294563f2e2724ff36f179004f9846f (patch) | |
| tree | bee6053e8164f4a8dbf214f1898fafecb1d61f2f /lib/puppet/provider | |
| parent | 4d6120a1f77cfe76fafbe32caa5d853449562376 (diff) | |
| download | puppet-12452ee9ca294563f2e2724ff36f179004f9846f.tar.gz puppet-12452ee9ca294563f2e2724ff36f179004f9846f.tar.xz puppet-12452ee9ca294563f2e2724ff36f179004f9846f.zip | |
Merging r1468 from the implementations branch with r1438 from when the branch was first created.
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@1469 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'lib/puppet/provider')
28 files changed, 2573 insertions, 0 deletions
diff --git a/lib/puppet/provider/group/groupadd.rb b/lib/puppet/provider/group/groupadd.rb new file mode 100644 index 000000000..4c204ddf6 --- /dev/null +++ b/lib/puppet/provider/group/groupadd.rb @@ -0,0 +1,29 @@ +require 'puppet/provider/nameservice/objectadd' + +Puppet::Type.type(:group).provide :groupadd, :parent => Puppet::Provider::NameService::ObjectAdd do + desc "Group management via ``groupadd`` and its ilk. The default + for most platforms" + + commands :add => "groupadd", :delete => "groupdel", :modify => "groupmod" + + verify :gid, "GID must be an integer" do |value| + value.is_a? Integer + end + + def addcmd + cmd = [command(:add)] + if gid = @model[:gid] + unless gid == :absent + cmd << flag(:gid) << "'%s'" % @model[:gid] + end + end + if @model[:allowdupe] == :true + cmd << "-o" + end + cmd << @model[:name] + + return cmd.join(" ") + end +end + +# $Id$ diff --git a/lib/puppet/provider/group/netinfo.rb b/lib/puppet/provider/group/netinfo.rb new file mode 100644 index 000000000..4be1e4f35 --- /dev/null +++ b/lib/puppet/provider/group/netinfo.rb @@ -0,0 +1,15 @@ +# Manage NetInfo POSIX objects. Probably only used on OS X, but I suppose +# it could be used elsewhere. +require 'puppet/provider/nameservice/netinfo' + +Puppet::Type.type(:group).provide :netinfo, :parent => Puppet::Provider::NameService::NetInfo do + desc "Group management using NetInfo." + NIREPORT = binary("nireport") + NIUTIL = binary("niutil") + confine :exists => NIREPORT + confine :exists => NIUTIL + + defaultfor :operatingsystem => :darwin +end + +# $Id$ diff --git a/lib/puppet/provider/group/pw.rb b/lib/puppet/provider/group/pw.rb new file mode 100644 index 000000000..48934a3c5 --- /dev/null +++ b/lib/puppet/provider/group/pw.rb @@ -0,0 +1,31 @@ +require 'puppet/provider/nameservice/pw' + +Puppet::Type.type(:group).provide :pw, :parent => Puppet::Provider::NameService::PW do + desc "Group management via ``pw``. Only works on FreeBSD." + + commands :pw => "/usr/sbin/pw" + defaultfor :operatingsystem => :freebsd + + verify :gid, "GID must be an integer" do |value| + value.is_a? Integer + end + + def addcmd + cmd = [command(:pw), "groupadd", @model[:name]] + if gid = @model[:gid] + unless gid == :absent + cmd << flag(:gid) << "'%s'" % @model[:gid] + end + end + + # Apparently, contrary to the man page, groupadd does + # not accept -o. + #if @parent[:allowdupe] == :true + # cmd << "-o" + #end + + return cmd.join(" ") + end +end + +# $Id$ diff --git a/lib/puppet/provider/nameservice.rb b/lib/puppet/provider/nameservice.rb new file mode 100644 index 000000000..96c1b6f4d --- /dev/null +++ b/lib/puppet/provider/nameservice.rb @@ -0,0 +1,319 @@ +require 'puppet' + +# This is the parent class of all NSS classes. They're very different in +# their backend, but they're pretty similar on the front-end. This class +# provides a way for them all to be as siilar as possible. +class Puppet::Provider::NameService < Puppet::Provider + class << self + + def list + objects = [] + listbyname do |name| + obj = nil + check = model.validstates + if obj = model[name] + obj[:check] = check + else + # unless it exists, create it as an unmanaged object + obj = model.create(:name => name, :check => check) + end + + next unless obj # In case there was an error somewhere + + objects << obj + yield obj if block_given? + end + + objects + end + + def option(name, option) + name = name.intern if name.is_a? String + if defined? @options and @options.include? name and @options[name].include? option + return @options[name][option] + else + return nil + end + end + + def options(name, hash) + unless model.validstate?(name) + raise Puppet::DevError, "%s is not a valid state for %s" % + [name, model.name] + end + @options ||= {} + @options[name] ||= {} + + # Set options individually, so we can call the options method + # multiple times. + hash.each do |param, value| + @options[name][param] = value + end + end + + # List everything out by name. Abstracted a bit so that it works + # for both users and groups. + def listbyname + names = [] + Etc.send("set%sent" % section()) + begin + while ent = Etc.send("get%sent" % section()) + names << ent.name + if block_given? + yield ent.name + end + end + ensure + Etc.send("end%sent" % section()) + end + + return names + end + + # This is annoying, but there really aren't that many options, + # and this *is* built into Ruby. + def section + unless defined? @model + raise Puppet::DevError, + "Cannot determine Etc section without a model" + + end + + if @model.name == :group + "gr" + else + "pw" + end + end + + def disabled_validate(name, value) + name = name.intern if name.is_a? String + if @checks.include? name + block = @checks[name][:block] + unless block.call(value) + raise ArgumentError, "Invalid value %s: %s" % + [value, @checks[name][:error]] + end + end + end + + def verify(name, error, &block) + name = name.intern if name.is_a? String + @checks ||= {} + @checks[name] = {:error => error, :block => block} + end + + private + + def op(state) + @ops[state.name] || ("-" + state.name) + end + end + + # Autogenerate either a uid or a gid. This is hard-coded: we can only + # generate one field type per class. + def autogen + highest = 0 + + group = method = nil + case @model.class.name + when :user: group = :passwd; method = :uid + when :group: group = :group; method = :gid + else + raise Puppet::DevError, "Invalid model name %s" % model + end + + # Make sure we don't use the same value multiple times + if defined? @@prevauto + @@prevauto += 1 + else + Etc.send(group) { |obj| + if obj.gid > highest + unless obj.send(method) > 65000 + highest = obj.send(method) + end + end + } + + @@prevauto = highest + 1 + end + + return @@prevauto + end + + def autogen_gid + autogen(@model.class.name) + end + + def autogen_uid + autogen(@model.class.name) + end + + def create + self.ensure = :present + end + + def delete + self.ensure = :absent + end + + def ensure + if exists? + :present + else + :absent + end + end + + # This is only used when creating or destroying the object. + def ensure=(value) + cmd = nil + event = nil + case value + when :absent + # we need to remove the object... + unless exists? + info "already absent" + # the object already doesn't exist + return nil + end + + # again, needs to be set by the ind. state or its + # parent + cmd = self.deletecmd + type = "delete" + when :present + if exists? + info "already exists" + # The object already exists + return nil + end + + # blah blah, define elsewhere, blah blah + cmd = self.addcmd + type = "create" + end + + begin + execute(cmd) + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not %s %s %s: %s" % + [type, @model.class.name, @model.name, detail] + end + end + + # Does our object exist? + def exists? + if getinfo(true) + return true + else + return false + end + end + + # Retrieve a specific value by name. + def get(param) + if hash = getinfo(false) + return hash[param] + else + return nil + end + end + + # Retrieve what we can about our object + def getinfo(refresh) + if @objectinfo.nil? or refresh == true + @etcmethod ||= ("get" + self.class.section().to_s + "nam").intern + begin + @objectinfo = Etc.send(@etcmethod, @model[:name]) + rescue ArgumentError => detail + @objectinfo = nil + end + end + + # Now convert our Etc struct into a hash. + if @objectinfo + return info2hash(@objectinfo) + else + return nil + end + end + + # The list of all groups the user is a member of. Different + # user mgmt systems will need to override this method. + def groups + groups = [] + + # Reset our group list + Etc.setgrent + + user = @model[:name] + + # Now iterate across all of the groups, adding each one our + # user is a member of + while group = Etc.getgrent + members = group.mem + + if members.include? user + groups << group.name + end + end + + # We have to close the file, so each listing is a separate + # reading of the file. + Etc.endgrent + + groups.join(",") + end + + # Convert the Etc struct into a hash. + def info2hash(info) + hash = {} + self.class.model.validstates.each do |param| + method = posixmethod(param) + if info.respond_to? method + hash[param] = info.send(posixmethod(param)) + end + end + + return hash + end + + def initialize(model) + super + + @objectinfo = nil + end + + # + def method_missing(name, *args) + name = name.to_s + + # Make sure it's a valid state. We go up our class structure instead of + # our model's because the model is fake during testing. + unless self.class.model.validstate?(name.sub("=",'')) + raise Puppet::DevError, "%s is not a valid %s state" % + [name, @model.class.name] + end + + # Each class has to override this manually + if name =~ /=/ + set(name.to_s.sub("=", ''), *args) + else + return get(name.intern) || :absent + end + end + + def set(param, value) + #self.class.validate(param, value) + cmd = modifycmd(param, value) + begin + execute(cmd) + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not set %s on %s[%s]: %s" % + [param, @model.class.name, @model.name, detail] + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/nameservice/netinfo.rb b/lib/puppet/provider/nameservice/netinfo.rb new file mode 100644 index 000000000..d3a51bcf0 --- /dev/null +++ b/lib/puppet/provider/nameservice/netinfo.rb @@ -0,0 +1,211 @@ +# Manage NetInfo POSIX objects. Probably only used on OS X, but I suppose +# it could be used elsewhere. + +require 'puppet' +require 'puppet/provider/nameservice' + +class Puppet::Provider::NameService +class NetInfo < Puppet::Provider::NameService + # Attempt to flush the database, but this doesn't seem to work at all. + def self.flush + begin + output = execute("/usr/sbin/lookupd -flushcache 2>&1") + rescue Puppet::ExecutionFailure + # Don't throw an error; it's just a failed cache flush + Puppet.err "Could not flush lookupd cache: %s" % output + end + end + + # Similar to posixmethod, what key do we use to get data? Defaults + # to being the object name. + def self.netinfodir + if defined? @netinfodir + return @netinfodir + else + return @model.name.to_s + "s" + end + end + + def self.finish + case self.name + when :uid: + noautogen + when :gid: + noautogen + end + end + + # How to add an object. + def addcmd + creatorcmd("-create") + end + + def creatorcmd(arg) + cmd = ["niutil"] + cmd << arg + + cmd << "/" << "/%s/%s" % + [self.class.netinfodir(), @model[:name]] + return cmd.join(" ") + end + + def deletecmd + creatorcmd("-destroy") + end + + def ensure=(arg) + super + + # Because our stupid type can't create the whole thing at once, + # we have to do this hackishness. Yay. + if arg == :present + if @model.class.name == :user + notice modifycmd(:groups, @model[:groups]).inspect + end + # We need to generate the id if it's missing. + @model.class.validstates.each do |name| + next if name == :ensure + unless val = @model[name] + if (@model.class.name == :user and name == :uid) or + (@model.class.name == :group and name == :gid) + val = autogen() + else + # No value, and it's not required, so skip it. + info "No value for %s" % name + next + end + end + self.send(name.to_s + "=", val) + end + end + end + + #def exists? + # if self.report(:name) + # return true + # else + # return false + # end + #end + + # Retrieve a specific value by name. + def get(param) + hash = getinfo(false) + if hash + return hash[param] + else + return :absent + end + end + + # Retrieve everything about this object at once, instead of separately. + def getinfo(refresh = false) + if refresh or (! defined? @infohash or ! @infohash) + states = [:name] + self.class.model.validstates + states.delete(:ensure) if states.include? :ensure + @infohash = report(*states) + end + + return @infohash + end + + def modifycmd(param, value) + cmd = ["niutil"] + + cmd << "-createprop" << "/" << "/%s/%s" % + [self.class.netinfodir, @model[:name]] + + if key = netinfokey(param) + cmd << key << "'%s'" % value + else + raise Puppet::DevError, + "Could not find netinfokey for state %s" % + self.class.name + end + cmd.join(" ") + end + + # Determine the flag to pass to our command. + def netinfokey(name) + name = symbolize(name) + self.class.option(name, :key) || name + end + + # Retrieve the data, yo. + # FIXME This should retrieve as much information as possible, + # rather than retrieving it one at a time. + def report(*params) + dir = self.class.netinfodir() + cmd = ["nireport", "/", "/%s" % dir] + + # We require the name in order to know if we match. There's no + # way to just report on our individual object, we have to get the + # whole list. + params.unshift :name unless params.include? :name + + params.each do |param| + if key = netinfokey(param) + cmd << key.to_s + else + raise Puppet::DevError, + "Could not find netinfokey for state %s" % + self.class.name + end + end + self.debug "Executing %s" % cmd.join(" ").inspect + + %x{#{cmd.join(" ")} 2>&1}.split("\n").each { |line| + values = line.split(/\t/) + + hash = {} + params.zip(values).each do |param, value| + next if value == '#NoValue#' + hash[param] = if value =~ /^[-0-9]+$/ + Integer(value) + else + value + end + end + + if hash[:name] == @model[:name] + return hash + else + next + end + +# +# if line =~ /^(\w+)\s+(.+)$/ +# name = $1 +# value = $2.sub(/\s+$/, '') +# +# if name == @model[:name] +# if value =~ /^[-0-9]+$/ +# return Integer(value) +# else +# return value +# end +# end + } + + return nil + end + + def retrieve + raise "wtf?" + @is = report() || :absent + end + + def setuserlist(group, list) + cmd = "niutil -createprop / /groups/%s users %s" % + [group, list.join(",")] + begin + output = execute(cmd) + rescue Puppet::Execution::Failure => detail + raise Puppet::Error, "Failed to set user list on %s: %s" % + [group, detail] + end + end +end +end + +# $Id$ diff --git a/lib/puppet/provider/nameservice/objectadd.rb b/lib/puppet/provider/nameservice/objectadd.rb new file mode 100644 index 000000000..056ac5e84 --- /dev/null +++ b/lib/puppet/provider/nameservice/objectadd.rb @@ -0,0 +1,45 @@ +require 'puppet/provider/nameservice' + +class Puppet::Provider::NameService +class ObjectAdd < Puppet::Provider::NameService + # Does the object already exist? + def self.exists?(obj) + if obj.getinfo(true) + return true + else + return false + end + end + + def deletecmd + [command(:delete), @model[:name]].join(" ") + end + + # Determine the flag to pass to our command. + def flag(name) + name = name.intern if name.is_a? String + self.class.option(name, :flag) || "-" + name.to_s[0,1] + end + + def modifycmd(param, value) + cmd = [command(:modify), + flag(param), + "'%s'" % value] + if @model[:allowdupe] == :true + cmd << "-o" + end + cmd << @model[:name] + + return cmd.join(" ") + end + + def posixmethod(name) + name = name.intern if name.is_a? String + method = self.class.option(name, :method) || name + + return method + end +end +end + +# $Id$ diff --git a/lib/puppet/provider/nameservice/pw.rb b/lib/puppet/provider/nameservice/pw.rb new file mode 100644 index 000000000..e151a258d --- /dev/null +++ b/lib/puppet/provider/nameservice/pw.rb @@ -0,0 +1,22 @@ +require 'puppet/provider/nameservice/objectadd' + +class Puppet::Provider::NameService +class PW < ObjectAdd + def deletecmd + "#{command(:pw)} #{@model.class.name.to_s}del %s" % @model[:name] + end + + def modifycmd(param, value) + cmd = [ + command(:pw), + "#{@model.class.name.to_s}mod", + @model[:name], + flag(param), + "'%s'" % value + ] + return cmd.join(" ") + end +end +end + +# $Id$ diff --git a/lib/puppet/provider/package/apple.rb b/lib/puppet/provider/package/apple.rb new file mode 100755 index 000000000..a9a86652d --- /dev/null +++ b/lib/puppet/provider/package/apple.rb @@ -0,0 +1,55 @@ +# OS X Packaging sucks. We can install packages, but that's about it. +Puppet::Type.type(:package).provide :apple do + desc "Package management based on OS X's builtin packaging system. This is + essentially the simplest and least functional package system in existence -- + it only supports installation; no deletion or upgrades." + + confine :exists => "/Library/Receipts" + confine :exists => "/usr/sbin/installer" + + defaultfor :operatingsystem => :darwin + + def self.listbyname + Dir.entries("/Library/Receipts").find { |f| + f =~ /\.pkg$/ + }.collect { |f| + name = f.sub(/\.pkg/, '') + yield name if block_given? + + name + } + end + + def self.list + listbyname.collect do |name| + Puppet.type(:package).installedpkg( + :name => name, + :provider => :apple, + :ensure => :installed + ) + end + end + + def query + if FileTest.exists?("/Library/Receipts/#{@model[:name]}.pkg") + return {:name => @model[:name], :ensure => :present} + else + return nil + end + end + + def install + source = nil + unless source = @model[:source] + self.fail "Mac OS X packages must specify a package source" + end + + begin + output = execute("/usr/sbin/installer -pkg #{source} -target / 2>&1") + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/apt.rb b/lib/puppet/provider/package/apt.rb new file mode 100755 index 000000000..7f2cef375 --- /dev/null +++ b/lib/puppet/provider/package/apt.rb @@ -0,0 +1,112 @@ +Puppet::Type.type(:package).provide :apt, :parent => :dpkg do + desc "Package management via ``apt-get``." + + APT = "/usr/bin/apt-get" + + confine :exists => APT + + defaultfor :operatingsystem => :debian + + ENV['DEBIAN_FRONTEND'] = "noninteractive" + + # A derivative of DPKG; this is how most people actually manage + # Debian boxes, and the only thing that differs is that it can + # install packages from remote sites. + + def checkforcdrom + unless defined? @@checkedforcdrom + if FileTest.exists? "/etc/apt/sources.list" + if File.read("/etc/apt/sources.list") =~ /^[^#]*cdrom:/ + @@checkedforcdrom = true + else + @@checkedforcdrom = false + end + else + # This is basically a pathalogical case, but we'll just + # ignore it + @@checkedforcdrom = false + end + end + + if @@checkedforcdrom and @model[:allowcdrom] != :true + raise Puppet::Error, + "/etc/apt/sources.list contains a cdrom source; not installing. Use 'allowcdrom' to override this failure." + end + end + + # Install a package using 'apt-get'. This function needs to support + # installing a specific version. + def install + should = @model.should(:ensure) + + checkforcdrom() + + str = @model[:name] + case should + when true, false, Symbol + # pass + else + # Add the package version + str += "=%s" % should + end + cmd = "/usr/bin/apt-get -q -y install %s" % str + + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end + + # What's the latest package version available? + def latest + cmd = "/usr/bin/apt-cache showpkg %s" % @model[:name] + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + + if output =~ /Versions:\s*\n((\n|.)+)^$/ + versions = $1 + version = versions.split(/\n/).collect { |version| + if version =~ /^([^\(]+)\(/ + $1 + else + self.warning "Could not match version '%s'" % version + nil + end + }.reject { |vers| vers.nil? }.sort[-1] + + unless version + self.debug "No latest version" + if Puppet[:debug] + print output + end + end + + return version + else + self.err "Could not match string" + end + end + + def update + self.install + end + + def uninstall + cmd = "/usr/bin/apt-get -y -q remove %s" % @model[:name] + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end + + def versionable? + true + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/blastwave.rb b/lib/puppet/provider/package/blastwave.rb new file mode 100755 index 000000000..b6c449415 --- /dev/null +++ b/lib/puppet/provider/package/blastwave.rb @@ -0,0 +1,138 @@ +# Packaging using Blastwave's pkg-get program. +Puppet::Type.type(:package).provide :blastwave, :parent => :sun do + desc "Package management using Blastwave.org's ``pkg-get`` command on Solaris." + if FileTest.executable?("/opt/csw/bin/pkg-get") + PKGGET = "/opt/csw/bin/pkg-get" + elsif pkgget = binary("pkg-get") + PKGGET = pkgget + else + PKGGET = nil + end + + confine :exists => PKGGET + + # This is so stupid, but then, so is blastwave. + ENV["PAGER"] = "/usr/bin/cat" + + def self.extended(mod) + unless PKGGET + raise Puppet::Error, + "The pkg-get command is missing; blastwave packaging unavailable" + end + + unless FileTest.exists?("/var/pkg-get/admin") + Puppet.notice "It is highly recommended you create '/var/pkg-get/admin'." + Puppet.notice "See /var/pkg-get/admin-fullauto" + end + end + + def self.list(hash = {}) + blastlist(hash).each do |bhash| + bhash.delete(:avail) + Puppet::Type.type(:package).installedpkg(bhash) + end + end + + # Turn our blastwave listing into a bunch of hashes. + def self.blastlist(hash) + command = "#{PKGGET} -c" + + if hash[:justme] + command += " " + hash[:justme] + end + + begin + output = execute(command) + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not get package listing: %s" % + detail + end + + list = output.split("\n").collect do |line| + next if line =~ /^#/ + next if line =~ /^WARNING/ + next if line =~ /localrev\s+remoterev/ + + blastsplit(line) + end.reject { |h| h.nil? } + + if hash[:justme] + return list[0] + else + list.reject! { |h| + h[:ensure] == :absent + } + return list + end + + end + + # Split the different lines into hashes. + def self.blastsplit(line) + if line =~ /\s*(\S+)\s+((\[Not installed\])|(\S+))\s+(\S+)/ + hash = {} + hash[:name] = $1 + hash[:ensure] = if $2 == "[Not installed]" + :absent + else + $2 + end + hash[:avail] = $5 + + if hash[:avail] == "SAME" + hash[:avail] = hash[:ensure] + end + hash[:provider] = :blastwave + + return hash + else + Puppet.warning "Cannot match %s" % line + return nil + end + end + + def install + begin + execute("#{PKGGET} -f install #{@model[:name]}") + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, + "Could not install %s: %s" % + [@model[:name], detail] + end + end + + # Retrieve the version from the current package file. + def latest + hash = self.class.blastlist(:justme => @model[:name]) + hash[:avail] + end + + def query + hash = self.class.blastlist(:justme => @model[:name]) + + {:ensure => hash[:ensure]} + end + + # Remove the old package, and install the new one + def update + begin + execute("#{PKGGET} -f upgrade #{@model[:name]}") + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, + "Could not upgrade %s: %s" % + [@model[:name], detail] + end + end + + def uninstall + begin + execute("#{PKGGET} -f remove #{@model[:name]}") + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, + "Could not remove %s: %s" % + [@model[:name], detail] + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/darwinport.rb b/lib/puppet/provider/package/darwinport.rb new file mode 100755 index 000000000..ccf0ae82d --- /dev/null +++ b/lib/puppet/provider/package/darwinport.rb @@ -0,0 +1,95 @@ +Puppet::Type.type(:package).provide :darwinport do + desc "Package management using DarwinPorts on OS X." + + PORT = "/opt/local/bin/port" + confine :exists => PORT, :operatingsystem => "Darwin" + + def self.eachpkgashash + # list out all of the packages + open("| #{PORT} list installed") { |process| + regex = %r{(\S+)\s+@(\S+)\s+(\S+)} + fields = [:name, :version, :location] + hash = {} + + # now turn each returned line into a package object + process.each { |line| + hash.clear + + if match = regex.match(line) + fields.zip(match.captures) { |field,value| + hash[field] = value + } + + hash.delete :location + hash[:ensure] = hash[:version] + yield hash.dup + else + raise Puppet::DevError, + "Failed to match dpkg line %s" % line + end + } + } + end + + def self.list + packages = [] + + eachpkgashash do |hash| + pkg = Puppet.type(:package).installedpkg(hash) + packages << pkg + end + + return packages + end + + def install + should = @model[:ensure] + + # Seems like you can always say 'upgrade' + cmd = "#{PORT} upgrade #{@model[:name]}" + + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end + + def query + version = nil + self.class.eachpkgashash do |hash| + if hash[:name] == @model[:name] + return hash + end + end + + return nil + end + + def latest + info = %x{#{PORT} search '^#{@model[:name]}$' 2>/dev/null} + + if $? != 0 or info =~ /^Error/ + return nil + end + + ary = info.split(/\s+/) + version = ary[2].sub(/^@/, '') + + return version + end + + def uninstall + cmd = "#{PORT} uninstall #{@model[:name]}" + output = %x{#{cmd} 2>&1} + if $? != 0 + raise Puppet::PackageError.new(output) + end + end + + def update + return install() + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/dpkg.rb b/lib/puppet/provider/package/dpkg.rb new file mode 100755 index 000000000..a747a884c --- /dev/null +++ b/lib/puppet/provider/package/dpkg.rb @@ -0,0 +1,115 @@ +Puppet::Type.type(:package).provide :dpkg do + desc "Package management via ``dpkg``. Because this only uses ``dpkg`` + and not ``apt``, you must specify the source of any packages you want + to manage." + + DPKG = "/usr/bin/dpkg" + + confine :exists => DPKG + + def self.list + packages = [] + + # dpkg only prints as many columns as you have available + # which means we don't get all of the info + # stupid stupid + oldcol = ENV["COLUMNS"] + ENV["COLUMNS"] = "500" + + # list out all of the packages + open("| #{DPKG} -l") { |process| + # our regex for matching dpkg output + regex = %r{^(\S+)\s+(\S+)\s+(\S+)\s+(.+)$} + fields = [:status, :name, :version, :description] + hash = {} + + 5.times { process.gets } # throw away the header + + # now turn each returned line into a package object + process.each { |line| + if match = regex.match(line) + hash.clear + + fields.zip(match.captures) { |field,value| + hash[field] = value + } + + hash[:provider] = self.name + + packages.push Puppet.type(:package).installedpkg(hash) + else + raise Puppet::DevError, + "Failed to match dpkg line %s" % line + end + } + } + ENV["COLUMNS"] = oldcol + + return packages + end + + def query + packages = [] + + # dpkg only prints as many columns as you have available + # which means we don't get all of the info + # stupid stupid + oldcol = ENV["COLUMNS"] + ENV["COLUMNS"] = "500" + fields = [:desired, :status, :error, :name, :version, :description] + + hash = {} + # list out our specific package + open("| #{DPKG} -l %s 2>/dev/null" % @model[:name]) { |process| + # our regex for matching dpkg output + regex = %r{^(.)(.)(.)\s(\S+)\s+(\S+)\s+(.+)$} + + # we only want the last line + lines = process.readlines + # we've got four header lines, so we should expect all of those + # plus our output + if lines.length < 5 + return nil + end + + line = lines[-1] + + if match = regex.match(line) + fields.zip(match.captures) { |field,value| + hash[field] = value + } + #packages.push Puppet.type(:package).installedpkg(hash) + else + raise Puppet::DevError, + "failed to match dpkg line %s" % line + end + } + ENV["COLUMNS"] = oldcol + + if hash[:error] != " " + raise Puppet::PackageError.new( + "Package %s, version %s is in error state: %s" % + [hash[:name], hash[:version], hash[:error]] + ) + end + + if hash[:status] == "i" + hash[:ensure] = :present + else + hash[:ensure] = :absent + end + + return hash + end + + def uninstall + cmd = "#{DPKG} -r %s" % @model[:name] + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/freebsd.rb b/lib/puppet/provider/package/freebsd.rb new file mode 100755 index 000000000..cd484d47a --- /dev/null +++ b/lib/puppet/provider/package/freebsd.rb @@ -0,0 +1,52 @@ +Puppet::Type.type(:package).provide :freebsd, :parent => :openbsd do + desc "The specific form of package management on FreeBSD. This is an + extremely quirky packaging system, in that it freely mixes between + ports and packages. Apparently all of the tools are written in Ruby, + so there are plans to rewrite this support to directly use those + libraries." + + commands :info => "/usr/sbin/pkg_info", + :add => "/usr/sbin/pkg_add", + :delete => "/usr/sbin/pkg_delete" + + def self.listcmd + command(:info) + end + + def install + should = @model[:ensure] + + if @model[:source] + return super + end + + cmd = command(:add) + " -r " + @model[:name] + + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end + + def query + self.class.list + + if @model[:version] + return :listed + else + return nil + end + end + + def uninstall + cmd = "#{command(:delete)} %s-%s" % [@model[:name], @model[:version]] + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/gem.rb b/lib/puppet/provider/package/gem.rb new file mode 100755 index 000000000..fba8b4702 --- /dev/null +++ b/lib/puppet/provider/package/gem.rb @@ -0,0 +1,108 @@ +# Ruby gems support. +Puppet::Type.type(:package).provide :gem do + desc "Ruby Gem support. By default uses remote gems, but you can specify + the path to a local gem via ``source``." + GEM = binary("gem") + + confine :exists => GEM + + def self.gemlist(hash) + command = "#{GEM} list " + + if hash[:local] + command += "--local " + else + command += "--remote " + end + + if name = hash[:justme] + command += name + end + begin + list = execute(command).split("\n\n").collect do |set| + if gemhash = gemsplit(set) + gemhash[:provider] = :gem + gemhash[:ensure] = gemhash[:version][0] + gemhash + else + nil + end + end.reject { |p| p.nil? } + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not list gems: %s" % detail + end + + if hash[:justme] + return list.shift + else + return list + end + end + + def self.gemsplit(desc) + case desc + when /^\*\*\*/: return nil + when /^(\S+)\s+\((.+)\)\n/ + name = $1 + version = $2.split(/,\s*/) + return { + :name => name, + :version => version + } + else + Puppet.warning "Could not match %s" % desc + nil + end + end + + def self.list(justme = false) + gemlist(:local => true).each do |hash| + Puppet::Type.type(:package).installedpkg(hash) + end + end + + def install(useversion = true) + command = "#{GEM} install " + if @model[:version] and useversion + command += "-v %s " % @model[:version] + end + if source = @model[:source] + command += source + else + command += @model[:name] + end + begin + execute(command) + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not install %s: %s" % + [@model[:name], detail] + end + end + + def latest + # This always gets the latest version available. + hash = self.class.gemlist(:justme => @model[:name]) + + return hash[:version][0] + end + + def query + self.class.gemlist(:justme => @model[:name], :local => true) + end + + def uninstall + begin + # Remove everything, including the binaries. + execute("#{GEM} uninstall -x -a #{@model[:name]}") + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not uninstall %s: %s" % + [@model[:name], detail] + end + end + + def update + self.install(false) + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/openbsd.rb b/lib/puppet/provider/package/openbsd.rb new file mode 100755 index 000000000..05dee8656 --- /dev/null +++ b/lib/puppet/provider/package/openbsd.rb @@ -0,0 +1,110 @@ +# Packaging on OpenBSD. Doesn't work anywhere else that I know of. +Puppet::Type.type(:package).provide :openbsd do + desc "OpenBSD's form of ``pkg_add`` support." + + commands :info => "pkg_info", :add => "pkg_add", :delete => "pkg_delete" + + defaultfor :operatingsystem => :openbsd + + def self.list + packages = [] + + begin + execpipe(listcmd()) do |process| + # our regex for matching pkg_info output + regex = %r{^(\S+)-([^-\s]+)\s+(.+)} + fields = [:name, :version, :description] + hash = {} + + # now turn each returned line into a package object + process.each { |line| + hash.clear + if match = regex.match(line) + fields.zip(match.captures) { |field,value| + hash[field] = value + } + yup = nil + name = hash[:name] + hash[:ensure] = :present + + hash[:provider] = self.name + + pkg = Puppet.type(:package).installedpkg(hash) + packages << pkg + else + # Print a warning on lines we can't match, but move + # on, since it should be non-fatal + warning("Failed to match line %s" % line) + end + } + end + # Mark as absent any packages we didn't find + Puppet.type(:package).each do |pkg| + unless packages.include? pkg + pkg.is = [:ensure, :absent] + end + end + + return packages + rescue Puppet::ExecutionFailure + return nil + end + end + + def self.listcmd + "#{command(:info)} -a" + end + + def install + should = @model[:ensure] + + unless @model[:source] + raise Puppet::Error, + "You must specify a package source for BSD packages" + end + + cmd = command(:add) + " " + @model[:source] + + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end + + def query + hash = {} + begin + # list out our specific package + info = execute("#{command(:info)} #{@model[:name]}") + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(info) + end + + # Search for the version info + if info =~ /Information for #{@model[:name]}-(\S+)/ + hash[:version] = $1 + hash[:ensure] = :present + else + return nil + end + + # And the description + if info =~ /Comment:\s*\n(.+)/ + hash[:description] = $1 + end + + return hash + end + + def uninstall + cmd = "#{command(:delete)} %s" % @model[:name] + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/ports.rb b/lib/puppet/provider/package/ports.rb new file mode 100755 index 000000000..6a46dcc03 --- /dev/null +++ b/lib/puppet/provider/package/ports.rb @@ -0,0 +1,104 @@ +Puppet::Type.type(:package).provide :ports, :parent => :freebsd do + desc "Support for FreeBSD's ports. Again, this still mixes packages + and ports." + commands :upgrade => "/usr/local/sbin/portupgrade", + :version => "/usr/local/sbin/portversion", + :uninstall => "/usr/local/sbin/pkg_deinstall", + :info => "/usr/sbin/pkg_info" + + defaultfor :operatingsystem => :freebsd + + # I hate ports + %w{INTERACTIVE UNAME}.each do |var| + if ENV.include?(var) + ENV.delete(var) + end + end + + def install + # -p: create a package + # -N: install if the package is missing, otherwise upgrade + # -P: prefer binary packages + cmd = "#{command(:upgrade)} -p -N -P #{@model[:name]}" + + begin + output = execute(cmd) + if output =~ /\*\* No such / + raise Puppet::PackageError, "Could not find package %s" % @model[:name] + end + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + end + + # If there are multiple packages, we only use the last one + def latest + cmd = "#{command(:version)} -v #{@model[:name]}" + + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + line = output.split("\n").pop + + unless line =~ /^(\S+)\s+(\S)\s+(.+)$/ + # There's no "latest" version, so just return a placeholder + return :latest + end + + pkgstuff = $1 + match = $2 + info = $3 + + unless pkgstuff =~ /^(\w+)-([0-9].+)$/ + raise Puppet::PackageError, + "Could not match package info '%s'" % pkgstuff + end + + name, version = $1, $2 + + if match == "=" or match == ">" + # we're up to date or more recent + return version + end + + # Else, we need to be updated; we need to pull out the new version + + unless info =~ /\((\w+) has (.+)\)/ + raise Puppet::PackageError, + "Could not match version info '%s'" % info + end + + source, newversion = $1, $2 + + debug "Newer version in %s" % source + return newversion + end + + def query + self.class.list + + if @model[:version] and @model.is(:ensure) != :absent + return :listed + else + return nil + end + end + + def uninstall + cmd = "#{command(:uninstall)} #{@model[:name]}" + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::PackageError.new(output) + end + + end + + def update + install() + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/rpm.rb b/lib/puppet/provider/package/rpm.rb new file mode 100755 index 000000000..25ae0385b --- /dev/null +++ b/lib/puppet/provider/package/rpm.rb @@ -0,0 +1,120 @@ +# RPM packaging. Should work anywhere that has rpm installed. +Puppet::Type.type(:package).provide :rpm do + desc "RPM packaging support; should work anywhere with a working ``rpm`` + binary." + + VERSIONSTRING = "%{VERSION}-%{RELEASE}" + + commands :rpm => "rpm" + + def self.list + packages = [] + + # list out all of the packages + begin + execpipe("#{command(:rpm)} -q -a --qf '%{NAME} #{VERSIONSTRING}\n'") { |process| + # our regex for matching dpkg output + regex = %r{^(\S+)\s+(\S+)} + fields = [:name, :ensure] + hash = {} + + # now turn each returned line into a package object + process.each { |line| + if match = regex.match(line) + hash.clear + + fields.zip(match.captures) { |field,value| + hash[field] = value + } + hash[:provider] = self.name + packages.push Puppet.type(:package).installedpkg(hash) + else + raise "failed to match rpm line %s" % line + end + } + } + rescue Puppet::ExecutionFailure + raise Puppet::Error, "Failed to list packages" + end + + return packages + end + + def query + fields = { + :name => "NAME", + :version => "VERSION", + :description => "DESCRIPTION" + } + + cmd = "#{command(:rpm)} -q #{@model[:name]} --qf '%s\n'" % + "%{NAME} #{VERSIONSTRING}" + + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + return nil + end + + regex = %r{^(\S+)\s+(\S+)} + #fields = [:name, :ensure, :description] + fields = [:name, :version] + hash = {} + if match = regex.match(output) + fields.zip(match.captures) { |field,value| + hash[field] = value + } + else + raise Puppet::DevError, + "Failed to match rpm output '%s'" % + output + end + + hash[:ensure] = :present + + return hash + end + + # Here we just retrieve the version from the file specified in the source. + def latest + unless source = @model[:source] + @model.fail "RPMs must specify a package source" + end + + cmd = "#{command(:rpm)} -q --qf '#{VERSIONSTRING}' -p #{@model[:source]}" + version = execfail(cmd, Puppet::Error) + + return version + end + + def install + source = nil + unless source = @model[:source] + @model.fail "RPMs must specify a package source" + end + + flag = "-i" + if @model.is(:ensure) != :absent + flag = "-U" + end + output = %x{#{command(:rpm)} #{flag} #{source} 2>&1} + + unless $? == 0 + raise Puppet::PackageError.new(output) + end + end + + def uninstall + cmd = "#{command(:rpm)} -e %s" % @model[:name] + output = %x{#{cmd}} + if $? != 0 + raise output + end + end + + def update + self.install + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/sun.rb b/lib/puppet/provider/package/sun.rb new file mode 100755 index 000000000..590e8f84a --- /dev/null +++ b/lib/puppet/provider/package/sun.rb @@ -0,0 +1,179 @@ +# Sun packaging. No one else uses these package tools, AFAIK. + +Puppet::Type.type(:package).provide :sun do + desc "Sun's packaging system. Requires that you specify the source for + the packages you're managing." + commands :info => "/usr/bin/pkginfo", + :add => "/usr/sbin/pkgadd", + :rm => "/usr/sbin/pkgrm" + + defaultfor :operatingsystem => :solaris + + def self.list + packages = [] + hash = {} + names = { + "PKGINST" => :name, + "NAME" => nil, + "CATEGORY" => :category, + "ARCH" => :platform, + "VERSION" => :ensure, + "BASEDIR" => :root, + "HOTLINE" => nil, + "EMAIL" => nil, + "VENDOR" => :vendor, + "DESC" => :description, + "PSTAMP" => nil, + "INSTDATE" => nil, + "STATUS" => nil, + "FILES" => nil + } + + cmd = "#{command(:info)} -l" + + # list out all of the packages + execpipe(cmd) { |process| + # we're using the long listing, so each line is a separate + # piece of information + process.each { |line| + case line + when /^$/: + hash[:provider] = :sun + + packages.push Puppet.type(:package).installedpkg(hash) + hash.clear + when /\s*(\w+):\s+(.+)/: + name = $1 + value = $2 + if names.include?(name) + unless names[name].nil? + hash[names[name]] = value + end + else + raise "Could not find %s" % name + end + when /\s+\d+.+/: + # nothing; we're ignoring the FILES info + end + } + } + return packages + end + + # Get info on a package, optionally specifying a device. + def info2hash(device = nil) + names = { + "PKGINST" => :name, + "NAME" => nil, + "CATEGORY" => :category, + "ARCH" => :platform, + "VERSION" => :ensure, + "BASEDIR" => :root, + "HOTLINE" => nil, + "EMAIL" => nil, + "VSTOCK" => nil, + "VENDOR" => :vendor, + "DESC" => :description, + "PSTAMP" => nil, + "INSTDATE" => nil, + "STATUS" => nil, + "FILES" => nil + } + + hash = {} + cmd = "#{command(:info)} -l" + if device + cmd += " -d #{device}" + end + cmd += " #{@model[:name]}" + + begin + # list out all of the packages + execpipe(cmd) { |process| + # we're using the long listing, so each line is a separate + # piece of information + process.each { |line| + case line + when /^$/: # ignore + when /\s*([A-Z]+):\s+(.+)/: + name = $1 + value = $2 + if names.include?(name) + unless names[name].nil? + hash[names[name]] = value + end + end + when /\s+\d+.+/: + # nothing; we're ignoring the FILES info + end + } + } + return hash + rescue Puppet::ExecutionFailure + return nil + end + end + + def install + unless @model[:source] + raise Puppet::Error, "Sun packages must specify a package source" + end + cmd = [command(:add)] + + if @model[:adminfile] + cmd << " -a " + @model[:adminfile] + end + + if @model[:responsefile] + cmd << " -r " + @model[:responsefile] + end + + cmd += ["-d", @model[:source]] + cmd += ["-n", @model[:name]] + cmd = cmd.join(" ") + + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure => detail + raise Puppet::PackageError.new(output) + end + end + + # Retrieve the version from the current package file. + def latest + hash = info2hash(@model[:source]) + hash[:ensure] + end + + def query + info2hash() + end + + def uninstall + command = "#{command(:rm)} -n " + + if @model[:adminfile] + command += " -a " + @model[:adminfile] + end + + command += " " + @model[:name] + begin + execute(command) + rescue ExecutionFailure => detail + raise Puppet::Error, + "Could not uninstall %s: %s" % + [@model[:name], detail] + end + end + + # Remove the old package, and install the new one. This will probably + # often fail. + def update + if @model.is(:ensure) != :absent + self.uninstall + end + self.install + end +end + +# $Id$ diff --git a/lib/puppet/provider/package/sunfreeware.rb b/lib/puppet/provider/package/sunfreeware.rb new file mode 100755 index 000000000..e6c3870f7 --- /dev/null +++ b/lib/puppet/provider/package/sunfreeware.rb @@ -0,0 +1,15 @@ +# At this point, it's an exact copy of the Blastwave stuff. +Puppet::Type.type(:package).provide :sunfreeware, :parent => :blastwave do + desc "Package management using sunfreeware.com's ``pkg-get`` command on Solaris. + At this point, support is exactly the same as ``blastwave`` support and + has not actually been tested." + if pkgget = binary("pkg-get") + PKGGET = pkgget + else + PKGGET = nil + end + + confine :exists => PKGGET +end + +# $Id$ diff --git a/lib/puppet/provider/package/yum.rb b/lib/puppet/provider/package/yum.rb new file mode 100755 index 000000000..587b1911e --- /dev/null +++ b/lib/puppet/provider/package/yum.rb @@ -0,0 +1,53 @@ +Puppet::Type.type(:package).provide :yum, :parent => :rpm do + desc "Support via ``yum``." + commands :yum => "yum", :rpm => "rpm" + + defaultfor :operatingsystem => :fedora + + # Install a package using 'yum'. + def install + cmd = "#{command(:yum)} -y install %s" % @model[:name] + + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure => detail + raise Puppet::PackageError.new(detail) + end + + unless self.query + raise Puppet::PackageError.new( + "Could not find package %s" % self.name + ) + end + end + + # What's the latest package version available? + def latest + cmd = "#{command(:yum)} list available %s" % @model[:name] + + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure => detail + raise Puppet::PackageError.new(detail) + end + + if output =~ /#{@model[:name]}\S+\s+(\S+)\s/ + return $1 + else + # Yum didn't find updates, pretend the current + # version is the latest + return @model[:version] + end + end + + def update + # Install in yum can be used for update, too + self.install + end + + def versionable? + false + end +end + +# $Id$ diff --git a/lib/puppet/provider/service/base.rb b/lib/puppet/provider/service/base.rb new file mode 100755 index 000000000..eedcac4ae --- /dev/null +++ b/lib/puppet/provider/service/base.rb @@ -0,0 +1,135 @@ +Puppet::Type.type(:service).provide :base do + desc "The simplest form of service support. You have to specify + enough about your service for this to work; the minimum you can specify + is a binary for starting the process, and this same binary will be searched + for in the process table to stop the service. + + It is preferable to specify start, stop, and status commands, akin + to how you would do so using ``init``." + + # Execute a command. Basically just makes sure it exits with a 0 + # code. + def execute(type, cmd) + self.debug "Executing %s" % cmd.inspect + output = %x(#{cmd} 2>&1) + unless $? == 0 + @model.fail "Could not %s %s: %s" % + [type, self.name, output.chomp] + end + end + + # Get the process ID for a running process. Requires the 'pattern' + # parameter. + def getpid + unless @model[:pattern] + @model.fail "Either a stop command or a pattern must be specified" + end + ps = Facter["ps"].value + warning ps.inspect + unless ps and ps != "" + @model.fail( + "You must upgrade Facter to a version that includes 'ps'" + ) + end + regex = Regexp.new(@model[:pattern]) + self.debug "Executing '#{ps}'" + IO.popen(ps) { |table| + table.each { |line| + if regex.match(line) + ary = line.sub(/^\s+/, '').split(/\s+/) + return ary[1] + end + } + } + + return nil + end + + # Basically just a synonym for restarting. Used to respond + # to events. + def refresh + self.restart + end + + # How to restart the process. + def restart + if @model[:restart] or self.respond_to?(:restartcmd) + cmd = @model[:restart] || self.restartcmd + self.execute("restart", cmd) + else + self.stop + self.start + end + end + + # Check if the process is running. Prefer the 'status' parameter, + # then 'statuscmd' method, then look in the process table. We give + # the object the option to not return a status command, which might + # happen if, for instance, it has an init script (and thus responds to + # 'statuscmd') but does not have 'hasstatus' enabled. + def status + if @model[:status] or ( + self.respond_to?(:statuscmd) and self.statuscmd + ) + cmd = @model[:status] || self.statuscmd + self.debug "Executing %s" % cmd.inspect + output = %x(#{cmd} 2>&1) + self.debug "%s status returned %s" % + [self.name, output.inspect] + if $? == 0 + return :running + else + return :stopped + end + elsif pid = self.getpid + self.debug "PID is %s" % pid + return :running + else + return :stopped + end + end + + # Run the 'start' parameter command, or the specified 'startcmd'. + def start + cmd = @model[:start] || self.startcmd + self.execute("start", cmd) + end + + # The command used to start. Generated if the 'binary' argument + # is passed. + def startcmd + if @model[:binary] + return @model[:binary] + else + raise Puppet::Error, + "Services must specify a start command or a binary" + end + end + + # Stop the service. If a 'stop' parameter is specified, it + # takes precedence; otherwise checks if the object responds to + # a 'stopcmd' method, and if so runs that; otherwise, looks + # for the process in the process table. + # This method will generally not be overridden by submodules. + def stop + if @model[:stop] + return @model[:stop] + elsif self.respond_to?(:stopcmd) + self.execute("stop", self.stopcmd) + else + pid = getpid + unless pid + self.info "%s is not running" % self.name + return false + end + output = %x(kill #{pid} 2>&1) + if $? != 0 + @model.fail "Could not kill %s, PID %s: %s" % + [self.name, pid, output] + end + return true + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/service/debian.rb b/lib/puppet/provider/service/debian.rb new file mode 100755 index 000000000..b8acbf326 --- /dev/null +++ b/lib/puppet/provider/service/debian.rb @@ -0,0 +1,52 @@ +# Manage debian services. Start/stop is the same as InitSvc, but enable/disable +# is special. +Puppet::Type.type(:service).provide :debian, :parent => :init do + desc "Debian's form of ``init``-style management. The only difference + is that this supports service enabling and disabling via ``update-rc.d``." + + commands :update => "/usr/sbin/update-rc.d" + defaultfor :operatingsystem => :debian + + # Remove the symlinks + def disable + cmd = %{#{command(:update)} -f #{@model[:name]} remove 2>&1} + self.debug "Executing '%s'" % cmd + output = %x{#{cmd}} + + unless $? == 0 + raise Puppet::Error, "Could not disable %s: %s" % + [self.name, output] + end + end + + def enabled? + cmd = %{#{command(:update)} -n -f #{@model[:name]} remove 2>&1} + self.debug "Executing 'enabled' test: '%s'" % cmd + output = %x{#{cmd}} + unless $? == 0 + raise Puppet::Error, "Could not check %s: %s" % + [self.name, output] + end + + # If it's enabled, then it will print output showing removal of + # links. + if output =~ /etc\/rc[\dS].d|Nothing to do\./ + return :true + else + return :false + end + end + + def enable + cmd = %{#{command(:update)} #{@model[:name]} defaults 2>&1} + self.debug "Executing '%s'" % cmd + output = %x{#{cmd}} + + unless $? == 0 + raise Puppet::Error, "Could not enable %s: %s" % + [self.name, output] + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/service/init.rb b/lib/puppet/provider/service/init.rb new file mode 100755 index 000000000..76cac2763 --- /dev/null +++ b/lib/puppet/provider/service/init.rb @@ -0,0 +1,151 @@ +# The standard init-based service type. Many other service types are +# customizations of this module. +Puppet::Type.type(:service).provide :init, :parent => :base do + desc "Standard init service management. This provider assumes that the + init script has not ``status`` command, because so few scripts do, + so you need to either provide a status command or specify via ``hasstatus`` + that one already exists in the init script." + + class << self + attr_accessor :defpath + end + + case Facter["operatingsystem"].value + when "FreeBSD": + @defpath = "/etc/rc.d" + else + @defpath = "/etc/init.d" + end + + #confine :exists => @defpath + + if self.suitable? + # Add it to the search paths + Puppet.type(:service).newpath(:init, defpath()) + + # Set the default init directory. + Puppet.type(:service).attrclass(:path).defaultto defpath() + end + + # List all services of this type. + def self.list(name) + # We need to find all paths specified for our type or any parent types + paths = Puppet.type(:service).paths(name) + + # Now see if there are any included modules + included_modules.each do |mod| + next unless mod.respond_to? :name + + mname = mod.name + + if mpaths = Puppet.type(:service).paths(mname) and ! mpaths.empty? + paths += mpaths + end + end + + paths.each do |path| + unless FileTest.directory?(path) + Puppet.notice "Service path %s does not exist" % path + next + end + + check = [:ensure] + + if public_method_defined? :enabled? + check << :enable + end + + Dir.entries(path).reject { |e| + fullpath = File.join(path, e) + e =~ /^\./ or ! FileTest.executable?(fullpath) + }.each do |name| + if obj = Puppet::Type.type(:service)[name] + obj[:check] = check + else + Puppet::Type.type(:service).create( + :name => name, :check => check, :path => path + ) + end + end + end + end + + # Mark that our init script supports 'status' commands. + def hasstatus=(value) + case value + when true, "true": @parameters[:hasstatus] = true + when false, "false": @parameters[:hasstatus] = false + else + raise Puppet::Error, "Invalid 'hasstatus' value %s" % + value.inspect + end + end + + # it'd be nice if i didn't throw the output away... + # this command returns true if the exit code is 0, and returns + # false otherwise + def initcmd(cmd) + script = self.initscript + + self.debug "Executing '%s %s' as initcmd for '%s'" % + [script,cmd,self] + + rvalue = Kernel.system("%s %s" % + [script,cmd]) + + self.debug "'%s' ran with exit status '%s'" % + [cmd,rvalue] + + + rvalue + end + + # Where is our init script? + def initscript + if defined? @initscript + return @initscript + else + @initscript = self.search(@model[:name]) + end + end + + def search(name) + @model[:path].each { |path| + fqname = File.join(path,name) + begin + stat = File.stat(fqname) + rescue + # should probably rescue specific errors... + self.debug("Could not find %s in %s" % [name,path]) + next + end + + # if we've gotten this far, we found a valid script + return fqname + } + raise Puppet::Error, "Could not find init script for '%s'" % name + end + + # The start command is just the init scriptwith 'start'. + def startcmd + self.initscript + " start" + end + + # If it was specified that the init script has a 'status' command, then + # we just return that; otherwise, we return false, which causes it to + # fallback to other mechanisms. + def statuscmd + if @model[:hasstatus] + return self.initscript + " status" + else + return false + end + end + + # The stop command is just the init script with 'stop'. + def stopcmd + self.initscript + " stop" + end +end + +# $Id$ diff --git a/lib/puppet/provider/service/redhat.rb b/lib/puppet/provider/service/redhat.rb new file mode 100755 index 000000000..e599e63f1 --- /dev/null +++ b/lib/puppet/provider/service/redhat.rb @@ -0,0 +1,45 @@ +# Manage debian services. Start/stop is the same as InitSvc, but enable/disable +# is special. +Puppet::Type.type(:service).provide :redhat, :parent => :init do + desc "Red Hat's (and probably many others) form of ``init``-style service + management; uses ``chkconfig`` for service enabling and disabling." + + confine :exists => "/sbin/chkconfig" + + defaultfor :operatingsystem => [:redhat, :fedora] + + # Remove the symlinks + def disable + begin + output = util_execute("/sbin/chkconfig #{@model[:name]} off 2>&1") + output += util_execute("/sbin/chkconfig --del #{@model[:name]} 2>&1") + rescue Puppet::ExecutionFailure + raise Puppet::Error, "Could not disable %s: %s" % + [self.name, output] + end + end + + def enabled? + begin + output = util_execute("/sbin/chkconfig #{@model[:name]} 2>&1").chomp + rescue Puppet::ExecutionFailure + return :false + end + + return :true + end + + # Don't support them specifying runlevels; always use the runlevels + # in the init scripts. + def enable + begin + output = util_execute("/sbin/chkconfig --add #{@model[:name]} 2>&1") + output += util_execute("/sbin/chkconfig #{@model[:name]} on 2>&1") + rescue Puppet::ExecutionFailure + raise Puppet::Error, "Could not enable %s: %s" % + [self.name, output] + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/service/smf.rb b/lib/puppet/provider/service/smf.rb new file mode 100755 index 000000000..becd6fa3f --- /dev/null +++ b/lib/puppet/provider/service/smf.rb @@ -0,0 +1,84 @@ +# Solaris 10 SMF-style services. +Puppet::Type.type(:service).provide :smf, :parent => :base do + desc "Support for Sun's new Service Management Framework. Starting a service + is effectively equivalent to enabling it, so there is only support + for starting and stopping services, which also enables and disables them, + respectively." + + defaultfor :operatingsystem => :solaris + + commands :adm => "/usr/sbin/svcadm", :svcs => "/usr/bin/svcs" + + def enable + self.start + end + + def enabled? + case self.status + when :running: + return :true + else + return :false + end + end + + def disable + self.stop + end + + def restartcmd + "#{command(:adm)} restart %s" % @model[:name] + end + + def startcmd + "#{command(:adm)} enable %s" % @model[:name] + end + + def status + if @model[:status] + super + return + end + begin + output = Puppet::Util.execute("#{command(:svcs)} -l #{@model[:name]}") + rescue Puppet::ExecutionFailure + warning "Could not get status on service %s" % self.name + return :stopped + end + + output.split("\n").each { |line| + var = nil + value = nil + if line =~ /^(\w+)\s+(.+)/ + var = $1 + value = $2 + else + Puppet.err "Could not match %s" % line.inspect + end + case var + when "state": + case value + when "online": + #self.warning "matched running %s" % line.inspect + return :running + when "offline", "disabled", "uninitialized" + #self.warning "matched stopped %s" % line.inspect + return :stopped + when "legacy_run": + raise Puppet::Error, + "Cannot manage legacy services through SMF" + else + raise Puppet::Error, + "Unmanageable state '%s' on service %s" % + [value, self.name] + end + end + } + end + + def stopcmd + "#{command(:adm)} disable %s" % @model[:name] + end +end + +# $Id$ diff --git a/lib/puppet/provider/user/netinfo.rb b/lib/puppet/provider/user/netinfo.rb new file mode 100644 index 000000000..e453c4582 --- /dev/null +++ b/lib/puppet/provider/user/netinfo.rb @@ -0,0 +1,91 @@ +# Manage NetInfo POSIX objects. Probably only used on OS X, but I suppose +# it could be used elsewhere. +require 'puppet/provider/nameservice/netinfo' + +Puppet::Type.type(:user).provide :netinfo, :parent => Puppet::Provider::NameService::NetInfo do + desc "User management in NetInfo." + + NIREPORT = binary("nireport") + NIUTIL = binary("niutil") + confine :exists => NIREPORT + confine :exists => NIUTIL + + options :comment, :key => "realname" + + defaultfor :operatingsystem => :darwin + + # The list of all groups the user is a member of. Different + # user mgmt systems will need to override this method. + def groups + groups = [] + + user = @model[:name] + # Retrieve them all from netinfo + open("| nireport / /groups name users") do |file| + file.each do |line| + name, members = line.split(/\s+/) + next unless members + next if members =~ /NoValue/ + members = members.split(",") + + if members.include? user + groups << name + end + end + end + + groups.join(",") + end + + # This is really lame. We have to iterate over each + # of the groups and add us to them. + def groups=(groups) + groups = groups.split(/\s*,\s*/) + # Get just the groups we need to modify + diff = groups - (@is || []) + + data = {} + open("| nireport / /groups name users") do |file| + file.each do |line| + name, members = line.split(/\s+/) + + if members.nil? or members =~ /NoValue/ + data[name] = [] + else + # Add each diff group's current members + data[name] = members.split(/,/) + end + end + end + + user = @model[:name] + data.each do |name, members| + if members.include? user and groups.include? name + # I'm in the group and should be + next + elsif members.include? user + # I'm in the group and shouldn't be + setuserlist(name, members - [user]) + elsif groups.include? name + # I'm not in the group and should be + setuserlist(name, members + [user]) + else + # I'm not in the group and shouldn't be + next + end + end + end + + def setuserlist(group, list) + cmd = "niutil -createprop / /groups/%s users %s" % + [group, list.join(",")] + begin + output = execute(cmd) + rescue Puppet::ExecutionFailure + raise Puppet::Error, + "Failed to set groups: %s" % output + end + end +end + +# $Id$ diff --git a/lib/puppet/provider/user/pw.rb b/lib/puppet/provider/user/pw.rb new file mode 100644 index 000000000..7342c42fe --- /dev/null +++ b/lib/puppet/provider/user/pw.rb @@ -0,0 +1,41 @@ +require 'puppet/provider/nameservice/pw' + +Puppet::Type.type(:user).provide :pw, :parent => Puppet::Provider::NameService::PW do + desc "User management via ``pw`` on FreeBSD." + + commands :pw => "pw" + + defaultfor :operatingsystem => :freebsd + + options :home, :flag => "-d", :method => :dir + options :comment, :method => :gecos + options :groups, :flag => "-G" + + 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", @model[:name]] + @model.class.validstates.each do |state| + next if name == :ensure + # the value needs to be quoted, mostly because -c might + # have spaces in it + if value = @model[state] and value != "" + cmd << flag(state) << "'%s'" % @model[state] + end + end + + if @model[:allowdupe] == :true + cmd << "-o" + end + + return cmd.join(" ") + end +end + +# $Id$ diff --git a/lib/puppet/provider/user/useradd.rb b/lib/puppet/provider/user/useradd.rb new file mode 100644 index 000000000..edd5ec6e3 --- /dev/null +++ b/lib/puppet/provider/user/useradd.rb @@ -0,0 +1,46 @@ +require 'puppet/provider/nameservice/objectadd' + +Puppet::Type.type(:user).provide :useradd, :parent => Puppet::Provider::NameService::ObjectAdd do + desc "User management via ``useradd`` and its ilk." + + commands :add => "useradd", :delete => "userdel", :modify => "usermod" + + options :home, :flag => "-d", :method => :dir + options :comment, :method => :gecos + options :groups, :flag => "-G" + + 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(:add)] + @model.class.validstates.each do |state| + next if name == :ensure + # the value needs to be quoted, mostly because -c might + # have spaces in it + if value = @model[state] and value != "" + cmd << flag(state) << "'%s'" % @model[state] + end + end + # stupid fedora + case Facter["operatingsystem"].value + when "Fedora", "RedHat": + cmd << "-M" + else + end + if @model[:allowdupe] == :true + cmd << "-o" + end + + cmd << @model[:name] + + cmd.join(" ") + end +end + +# $Id$ |
