Tackling a dynamic list of things, and in this case fonts for Windows

I have an interesting challenge of installing a large amount of Windows Fonts on my node. These fonts are something our application works with and needs to run properly. At first my strategy was fairly straightforward. I would include the fonts as files in my cookbook in files/default/fonts, and I would then use the windows_font resource from the windows cookbook. So that I could work with a dynamic list of fonts, I would work with the cookbook's run_context.

First approach:

cb = run_context.cookbook_collection['cookbook-name']
cb.manifest[:files].each do |font|
next if not font[:path].include? "fonts/"
  windows_font font[:name] do
    source "#{font[:path]}/#{font[:name]}"
  end
end

I had two issues with the above. First, the source path was not happy with looking in the fonts directory I created in files/default/fonts (could be / versus \ or it just didn't like my path). Second, if I tried to include all 400+ fonts we currently have, WinRM gets very grumpy throwing out of memory errors. To work around the first issue, I put the fonts at the files/default folder and remove the references to the source for the font. That works fine for when the number of fonts is small. When we included all of the fonts in the file directory the WinRM problem cropped up. I heard at ChefConf that the 8k limitation with WinRM was going away soon (I don't when or what is required to move past that), but the thought was that including all of these fonts in the cookbook wasn't the best course of action.

Second Approach:
I then tried to put these fonts in a zip file, added it to Artifactory for storage, and bring in the zip file that way. There are multiple ways to bring in the zip file (dsc_resource for one), but we used the seven_zip_archive resource from the seven_zip cookbook.

fonts_path = 'c:\fonts'

directory fonts_path do
  recursive true
  action :create
end

seven_zip_archive 'fonts' do
  path fonts_path
  source 'http://artifacts/artifactory/repository/fonts.zip'
  overwrite true
  action :extract
end

The zip file successfully extracts. I have it sitting, waiting for me to access. All I have to do is iterate over the files in that directory and install each font. Right? Wrong. I get bit by the timing of how things are run.

if Dir.exist?(fonts_path)
  Dir.foreach(fonts_path) do |font|
    next if font == '.' or font == '..'
    puts font
    windows_font font do
      source "#{fonts_source}/#{font}"
    end
  end
else
  puts "Cannot find directory #{fonts_path}!"
end

This works great the 2nd time I do a converge in test-kitchen. That's because the folder already exists from the first run Since the Ruby code executes first followed by the chef resources later. The first run it passes through saying it can't find the folder. I tried putting this inside a ruby_block, but then it fails because it does not know what windows_font means. There may be issues with the test-client stalling as it tries to install fonts on that 2nd converge as well, but I only have limited information there at the moment.

There has to be a way to loop through a dynamic list of things and perform some sort of resource action on them, is there not? Is there some other approach that I should be considering entirely? Some thoughts I heard from others on this was possibly creating a manifest (ugly since it's not very dynamic) or going completely in a different direction exploring Windows Font Management systems (which feels like it is more than what we need). Should I abandon the windows_font resource and try to do all this within a powershell_script?

The thought here would be for future chef runs to detect and install the new fonts added to the zip (and I would explore ways to not have to extract the zip file on every chef run), but right now we're just trying to get something working as a good first iteration.

(insert funny and hilarious quip about a metaphorical chef wanting to throw his pots and pans around his kitchen in a rage of confused frustration here)

I should also add that the fonts_source variable is the same as my fonts path, only with forward slashes instead of backslashes. I hadn’t spent the time to work out way to handle that with a single variable yet just due to trying out other stuff.

fonts_path = 'c:\fonts'
fonts_source = 'c:/fonts'

It is true that the 8k winrm transfer chunk limit will be going away in winrm v2 however if your fonts are relatively large, keeping them in artifactory is definitely the way to go. One thing you can do to avoid the first eun failure is to extract the font archive at compile time:

directory fonts_path do
  recursive true
  action :nothing
end.run_action(:create)

seven_zip_archive 'fonts' do
  path fonts_path
  source 'http://artifacts/artifactory/repository/fonts.zip'
  overwrite true
  action :nothing
end.run_action(:extract)

Now your font folder should be present when building the collection of fonts to install.

1 Like

An alternative to mucking around in the compile phase would be to implement your own lwrp/custom resource that processes the fonts directory and leverages the font resource. Since your custom resource would only fire during the execution phase, you’d be able to avoid the race condition.

I was able to get success with this, thanks! I had to switch to a different cookbook for extracting the zip (using zipfile instead) as the seven_zip_archive was using remote_file to bring down the file and seeming to throw off the timing again.

Now I’m facing an issue where my loop through the files gets about 70 files in (and about 40 of the 70 was not already installed by default) and it hangs without any errors. But at least I’m past my previous block!

Thanks David, I'll definitely look into that as an option as we continue to improve on what we're doing. I haven't written a custom resource yet, so it sounds like a fun thing to try. My current fear as I alluded to in my other recent post is that the windows_font resource is hanging part of the way in, so I need to try to figure out what is going on there as well!