Hooking in a custom provider for an entire chef run

Hi,

is it possible to overwrite the default provider for a built-in resource (say service or package) for an entire chef run?

How would I do that?

Probably I missed existing answers to this question, my apologies if so.

Best

Christopher

It's technically possible, but highly brittle, a severe footgun and begs the question of why you want to do it instead of providing a PR upstream ?

Could you ellaborate on the reasons so we may better help/give advice on how to achieve that ?

Right.

The scenario is as follows:
We use a bunch of cookbooks to configure "normal" machines (say Linux desktops).

We also have nodes that mount their root fs from NFS read-only. Their setup is split in two parts:

  1. The setup of the OS image(s) on the NFS server(s) and
  2. the individual setup of the nodes with NFS root.

It would be nice to configure stand-alone and netboot nodes with the same cookbooks, but:
service { action :start } should be a no-op inside the OS images on the NFS server,
while package will fail on the read-only netboot nodes.

Therefore I was thinking about replacing the affected resources and actions with no-ops.

That's why the action :nothing exists.

You'll need to have a way to differenciate which machine the cookbook is running on, but that could be something like (using an attribute of nfs_server being true or false)

package 'xxx' do
  action node['nfs_server'] ? :install : :nothing
end

service 'xxx' do
  action node['nfs_server'] ? :nothing : :start
end

Recipes are evaluated as ruby code, so you can do logic within the recipe code based on attributes or something else.

Hm.

If I don't want the provider to do :nothing but :something_else the functional equivalent would be:

package 'asdf' do
  provider MyCustomProvider if node['nfs_server']
end

In either case I would need to change dozens, maybe hundreds of resources, hence my question if this could be tweaked centrally.

But your suggestion is indeed very helpful as this may be a promising approach:

Chef::Resource::Package.default_action = %i[nothing] if node['nfs_server']

I'll look further into that direction.

Many thanks
Christopher

I think we'd all need code to understand what you're willing to do here.

Conditional action on resource are pretty common, like for files or services we can use the ternary operator to say which action we want based on attribute as recipes are evaulated as pure ruby, like this:

file '/path/to/file' do
  content 'A content to update'
  action node['my_namespace']['do_not_overwrite'] ? :create_if_missing : :create
end

service 'my_service' so
  action node['my_namespace']['start_enable'] ? [:start, :enable] : :nothing
end

Or for something more complex based on distribution type (pseudo code, just to give an idea):

if node['platform'] == 'redhat'
   package 'httpd'
 
   service 'httpd' do
     action [:start, :enable]
   end

  template '/etc/httpd/....' do
    source 'your_template.erb'
    action xx
    notifies :reload, 'service[httpd]', :immediately
  end
else
  package 'apache2' 
  
  service 'apache2' do
     action [:start, :enable]
  end

  template '/etc/apache2/conf.d/xxx.conf' do
    source "XXX.erb"
    notifies :restart, 'service[apache2]'
  end
end

Hi Tensibai

I think get your point. My use case is to reuse existing cookbooks in this new scenario with as little modification as possible.

For now I found a working solution monkey-patching Chef::Resource.provider in a wrapper-cookbookslibraries/….rb.

This is my custom service provider:

# use systemd provider but turn :start and :restart into no-ops
class Chef::Provider::Service::Netboot < Chef::Provider::Service::Systemd
  def start_service
    Chef::Log.info("Stubbing systemctl start #{new_resource}")    
  end

  def restart_service
    Chef::Log.info("Stubbing systemctl restart #{new_resource}")    
  end
end

And this is the monkey patch:

class Chef::Resource::Service
  def provider(arg = nil)
    set_or_return(:provider, Chef::Provider::Service::Netboot, kind_of: [ Class ])
  end
end

That's nowhere near an optimal solution and it surely breaks

service 'my-service' do
  provider Chef::Provider::Service::Foo
end

A better solution would be to inject my provider into the first place of Chef::ProviderResolver.prioritized_handlers (via Chef.set_provider_priority_array :service, …?) but that seems to be even uglier and I haven't managed to get this to work.

Cheers
Christopher

P.S.: In my use case the custom service provider is required anyhow as Chef does not properly detect systemd inside the server-side chroots but falls back to Chef::Provider::Service::Debian which is completely wrong.