Custom providers

369 views
Skip to first unread message

pl

unread,
Jan 6, 2011, 12:01:47 AM1/6/11
to Puppet Users
Hi,

I have a custom provider that seems to run ok in isolation by itself.
In addition, puppet does not complain at all when it runs. However,
when I ensure => installed a package the desired package is never
installed.

There are a few differences between this and something such as the gem
provider, most importantly, I couldn't find anyway to get the provider
to run as a different user without doing direct system call.

Here's the provider

Puppet::Type.type(:package).provide :npm, :parent =>
Puppet::Provider::Package do
desc "node.js package management with npm"

commands :npm_cmd => "/home/node/opt/bin/npm"
raise Puppet::Error, "The npm provider can only be used as root" if
Process.euid != 0

def self.npm_list(hash)
begin
s = `sudo -u node sh -c \"export PATH=/home/node/opt/bin:$
{PATH}; npm ls installed\"`
list = s.split("\n").collect do |set|
if npm_hash = npm_split(set)
npm_hash[:provider] = :npm
if npm_hash[:name] == hash[:justme]
npm_hash
else
nil
end
else
nil
end
end.compact
rescue Puppet::ExecutionFailure => detail
raise Puppet::Error, "Could not list npm packages: #{detail}"
end

list.shift
end

def self.npm_split(desc)
split_desc = desc.split(/ /)
installed = split_desc[0]
name = installed.split(/@/)[0]
version = installed.split(/@/)[1]
if (name.nil? || version.nil?)
Puppet.warning "Could not match #{desc}"
nil
else
return {
:name => name,
:ensure => version
}
end
end

def install
output = `sudo -u node sh -c \"export PATH=/home/node/opt/bin:$
{PATH}; npm install http-console\"`
if output =~ /npm not ok/
raise Puppet::ExecutionFailure, "Failed to install
#{resource[:name]}"
end
end

def uninstall
output = `sudo -u node sh -c \"export PATH=/home/node/opt/bin:$
{PATH}; npm uninstall #{resource[:name]}\"`
if output =~ /npm not ok/
raise Puppet::ExecutionFailure, "Failed to uninstall
#{resource[:name]}"
end
end

def query
version = nil
self.class.npm_list(:justme => resource[:name])
end

end

Does anybody have an idea what's going wrong?

The other problem I'm having is that I have to manually copy the
provider to puppet lib from the module lib.

Thanks.

pl

unread,
Jan 6, 2011, 12:10:34 AM1/6/11
to Puppet Users


On Jan 6, 4:01 pm, pl <patrick.b....@gmail.com> wrote:

>   def install
>     output = `sudo -u node sh -c \"export PATH=/home/node/opt/bin:$
> {PATH}; npm install http-console\"`
>     if output =~ /npm not ok/
>       raise Puppet::ExecutionFailure, "Failed to install
> #{resource[:name]}"
>     end
>   end

Note that this was a hard-coded package (http-console) I added just to
see if it would install. It didn't.

pl

unread,
Jan 10, 2011, 12:29:00 AM1/10/11
to Puppet Users
Hi,

So, after setting pluginsync = true I manage to get this working, but
only executing correctly as puppetd --test

Except that it only functions correctly when run as puppetd --test,
when I run it as the puppet service the custom provider does not do
anything.

This seems odd.

The only notice I have in /var/log/messages is

"Provider npm has not defined the 'instances' class method"

Is that required?

Stefan Schulte

unread,
Jan 10, 2011, 2:23:05 AM1/10/11
to Puppet Users
On Sun, Jan 09, 2011 at 09:29:00PM -0800, pl wrote:
> Hi,
>
> So, after setting pluginsync = true I manage to get this working, but
> only executing correctly as puppetd --test
>
> Except that it only functions correctly when run as puppetd --test,
> when I run it as the puppet service the custom provider does not do
> anything.
>
> This seems odd.
>
> The only notice I have in /var/log/messages is
>
> "Provider npm has not defined the 'instances' class method"
>
> Is that required?

Puppet::Provider::Package defines the prefetch class method. This is
called at the beginning of the puppet run to find matches between existing
packages and the one you described in your manifest. Prefetch calls
instances which should return an array of provider instances. One
provider instance for each package with the appropiate property_hash
{ensure => "whatever-version"}. If you dont know how to implement it try
with returning an empty array like hpux.rb does.

The reason for instances is you can use the resource type to purge
not managed packages and prefetech is useful when you can run one
command to query all packages. When the user specified 200 packages you
dont want to run the same query command 200 times to check if the packges is
installed or not when you can easily parse ONE command at the beginning that
lists all packages.

-Stefan

-Stefan

pl

unread,
Jan 10, 2011, 8:06:04 PM1/10/11
to Puppet Users
Hi,

> Puppet::Provider::Package defines the prefetch class method. This is
> called at the beginning of the puppet run to find matches between existing
> packages and the one you described in your manifest. Prefetch calls
> instances which should return an array of provider instances. One
> provider instance for each package with the appropiate property_hash
> {ensure => "whatever-version"}. If you dont know how to implement it try
> with returning an empty array like hpux.rb does.

Thanks.

I still have this problem that I can't run puppet as a daemon and
install packages as any other user. Trying to sudo inside the custom
provider fails if puppet is running as a daemon.

Is there no way around that?

Stefan Schulte

unread,
Jan 11, 2011, 2:41:08 AM1/11/11
to Puppet Users

Yes there is. And you will probably dont want to use backticks.
You can browse the functions in puppet/lib/util.rb for alternatives.
There are different ways to run commands. If you have defined a command
like you already did:

commands :npm_cmd => "/home/node/opt/bin/npm"

This will mean that puppet will only use the provider if the command is
there and you will get a method for free that will run that command.

npm_cmd('argument1','argument2')

I'm not sure if this will have the output as a return value or just
success/failure. To parse output I find execpipe convenient. (You can
use command(:my_own_defined_command) to return your command as a string
value)

execpipe("#{command(:npm_cmd)} list_or_whatever_argument") do |output|
output.each_line do |line|
# do parsing here
end
end

To run all these as a different user you should (although I never tried
it) wrap them in

Puppet::Util::SUIDManager.asuser("username", "group") do
# do stuff here as different user
end

Just grep for SUIDManager in the sources and you will find examples. You
may have to use userids and groupids for that to work i dont know.

Hope this helps.

-Stefan

pl

unread,
Jan 16, 2011, 11:00:33 PM1/16/11
to Puppet Users
Hi,

> Puppet::Util::SUIDManager.asuser("username", "group") do
> # do stuff here as different user
> end

Not sure this works how you suggested as this

Puppet::Util::SUIDManager.asuser("userx", "userx") do
execpipe("whoami") do |output|

Will report root, not userx.

I'll check the source.

Daniel Pittman

unread,
Jan 17, 2011, 3:25:46 PM1/17/11
to puppet...@googlegroups.com
On Sun, Jan 16, 2011 at 20:00, pl <patric...@gmail.com> wrote:

>>     Puppet::Util::SUIDManager.asuser("username", "group") do
>>       # do stuff here as different user
>>     end
>
> Not sure this works how you suggested as this

In the current codebase, it will silently ignore the request if you
are either on Windows, or not running as root.

Otherwise it juts delegates everything except the extended group list
operations to the Ruby Process class, and should follow the semantics
of that; we also verify that we can find the user by name or UID on
the way and raise an exception if it doesn't work.

So, yeah: I don't doubt you are seeing that behaviour, but I can't
understand how from code inspection. Can you instrument your code to
dump Process.uid and Process.gid from Ruby-space in the block? That
way we can help narrow down where things are going wrong.

Regards,
Daniel
--
✉ Daniel Pittman <dan...@rimspace.net>
dan...@rimspace.net (XMPP)
+1 503 893 2285
♻ made with 100 percent post-consumer electrons

pl

unread,
Jan 17, 2011, 10:26:36 PM1/17/11
to Puppet Users
Hi,

> Can you instrument your code to
> dump Process.uid and Process.gid from Ruby-space in the block?  That
> way we can help narrow down where things are going wrong.

Process.uid was telling me 500, which was the intended user. whoami
inside the execpipe was root though.

I've ended up doing this

def self.exec_as_user(op, pkg)

Puppet::Util::SUIDManager.asuser("node", "node") do
s = execute ["my", "command", "and", "args"]
s.split("\n").collect do | line |
yield line
end
end
end


which is doing what I want

Daniel Pittman

unread,
Jan 17, 2011, 10:29:39 PM1/17/11
to puppet...@googlegroups.com
On Mon, Jan 17, 2011 at 19:26, pl <patric...@gmail.com> wrote:

>> Can you instrument your code to
>> dump Process.uid and Process.gid from Ruby-space in the block?  That
>> way we can help narrow down where things are going wrong.
>
> Process.uid was telling me 500, which was the intended user. whoami
> inside the execpipe was root though.

Hmmmm. I wonder what was convincing whoami that you were root, not
UID 500, then.

> I've ended up doing this
>
>  def self.exec_as_user(op, pkg)
>
>    Puppet::Util::SUIDManager.asuser("node", "node") do
>      s = execute ["my", "command", "and", "args"]
>      s.split("\n").collect do | line |
>        yield line
>      end
>    end
>  end

It might be worth looking at puppet/util.rb to see the handful of "run
this command" helpers hidden away in there, just to make sure you are
not duplicating effort. Anyway, sounds like you got it working, so
glad to hear that.

Stefan Schulte

unread,
Jan 18, 2011, 2:39:02 AM1/18/11
to Puppet Users
On Mon, Jan 17, 2011 at 07:26:36PM -0800, pl wrote:
> Process.uid was telling me 500, which was the intended user. whoami
> inside the execpipe was root though.
>
> I've ended up doing this
>
> def self.exec_as_user(op, pkg)
>
> Puppet::Util::SUIDManager.asuser("node", "node") do
> s = execute ["my", "command", "and", "args"]
> s.split("\n").collect do | line |
> yield line
> end
> end
> end
>
>
> which is doing what I want

You can also pass arguments to execute to run the command as a different
user. It should work like that

output = execute(['command','arg1], {uid => 'node', gid => 'node'})

What operatingsystem are you running? Looking at the source asuser will
just change euid and egid and from the manpage of whoami it should
return the euid. So I dont really understand why whoami is reporting
root and I dont really see why execute should behave
differently from execpipe.

-Stefan

Reply all
Reply to author
Forward
0 new messages