Target Mode with Serial Devices

Thumbnail

Target Mode with Serial Devices

Usually, you will work with SSH or WinRM to connect to remote nodes and configure them. Those standard protocols bring along all the perks of a modern network connection: Encryption, Authentication, File transfers, etc

But what if you have a device without network connectivity?

In a recent project I was working with the management console of some devices not (yet) supported by Chef, because the cabling was still an open issue and I wanted to start with my tasks instead of waiting for patches to be finished.

As I loved the introduction of Train plugins, this was an ideal opportunity to write a custom transport and I took it.

train-serial

The result of that evening is my Train transport to configure devices via serial/USB, which is not yet on rubygems.org but can be easily built locally. It does not require any native libraries, because it makes use of the rubyserial Gem, which maps against local OS methods.

If you want to use this to connect to serial/USB devices, the syntax within your credentials file is as follows:

['nodename']
device = "/dev/ttyUSB0"

This defaults to a connection with 9600 bit per second, 8 data bits, no parity and 1 stop bit (also known as “9600 8N1”), but you can set the parameters individually if you require:

['nodename']
device = "/dev/ttyUSB0"
baud = 9600
data_bits = 8
parity = "none"
stop_bits = 1

As some devices need to get into privileged modes first, you also have the possibility to add setup and teardown commands, which will wrap your actual operations on the start and the end of a session. To allow reporting of errors from the device, you can also specify a regex error_pattern to match those. If any line like this is detected in the response, it will result in the exit code 1 and all matching messages being returned in standard error.

['nodename']
device = "/dev/ttyUSB0"
enable_password = "Passw0rd"
setup = """
enable
#{transport.options[:enable_password]}
"""
teardown = "disable\n"

Client Config

To enable all this, you currently have to specify the protocol in your Chef client.rb, like I wrote in my first post of this series:

train.protocol = "serial"

If you have multiple serially-attached clients, it might be a good idea to have a separate client configuration for this and then call Chef runs with that one.

chef-client -z -c /etc/chef/serial_clients.rb -t nodename -r 'recipe[mycookbook::default]'

Limitations

This driver is still not finished and it requires a bit of sneaky input/output manipulation. I have tried to make the regular expressions good enough so you only get the result of your commands back and not the leading/trailing prompts, but this is far from perfect. If you have any problems, please open a ticket on Github and I will try to incorporate your feedback. For now, you can adjust this with the raw_output and prompt_pattern settings.

Any file upload/download methods will not be available with this driver, at least not in a driver-independant way. It is probably feasible to write device specific forks with support for these operations though.

A least until version 15.2.29, Chef will not cleanly close underlying Transport connections after a run. While this is not relevant for classic SSH connections, most serial devices will need to be reset into a stable state at the end. So far, the following inside your recipe will handle this logic:

Chef::Client.when_run_completes_successfully { |_run_state| run_context.transport_connection.close }
Chef::Client.when_run_fails { |_run_state| run_context.transport_connection.close }