Communication with the underlying system

The available tools

Having completed the previous steps, we now have a clear view on how the interface will look like. The next logical step is to get some data to display there. The only way to access the systemd journal in a Linux system is the journalctl command. Fortunately journalctl is very flexible and supports many output formats, some of them very convenient for automatic processing. For example, this command will print to the standard output all the journal entries generated since the last system's startup in JSON format.

journalctl --no-pager -o json -b 0

As explained in the architecture description found in the YaST documentation, YaST offers a very convenient way to communicate with the underlying Linux system - the SCR component and its agents. Each agent takes care of a very concrete type of system resource and implements a common interface with just four operations: read, write, dir and execute. The SCR component uses YCP paths to identify the agent and the concrete action. A path is a YCP-specific data type designed to denote nodes from tree-like structures using labels and dots. For example, since the SCR agent parsing the file "/etc/sysconfig/clock" is attached to the path ".sysconfig.clock", the following Ruby code can be used to read the value of the TIMEZONE parameter in that file.

path = Yast::Path.new(".sysconfig.clock.TIMEZONE")
Yast::SCR.Read(path)

The most widely used YaST agent is called Target and offers an interface to (shell) commands of the target Linux system. Unsurprisingly, the Target agent is attached to the ".target" path. One of the many commands offered by the Target agent is "bash_output", which executes a command in the target system and returns a structure including "exit" for the exit code, "stdout" for the command output and "stderr" for the error messages.

You can read more about the agents, including a full description of the functionality provided by the Target agent, in the YaST core documentation.

The Journalctl::Entry class

Taking into consideration all the above information, looks like we have in our toolbox everything we need in order to implement a class representing systemd journal entries and offering methods to retrieve them from the target system. Feel free to design and implement your our solution. You can find the proposed one in "src/lib/journalctl/entry.rb" right after executing:

git checkout domain_models

As you can see, the public interface offered by this class is very simple, just a class method called "all" which will return an array of Entry objects and some attribute readers for the relevant information of each entry. The only method containing YaST-specific code is "journalctl_output", which relies on the abovementioned Target SCR agent. The proposed solution includes some handling for known journalctl errors.

JOURNALCTL = "LANG=C journalctl --no-pager -o json"

# Handles the journalctl call
#
# @param args [String] arguments to journalctl
# @return [String] command output
def self.journalctl_output(args)
  cmd = "#{JOURNALCTL} #{args}".strip
  path = Yast::Path.new(".target.bash_output")
  cmd_result = Yast::SCR.Execute(path, cmd)

  if cmd_result["exit"].zero?
    cmd_result["stdout"]
  else
    if cmd_result["stderr"] =~ /^Failed to .* timestamp:/
      # Most likely, journalctl bug when an empty list is found
      ""
    else
      raise "Calling journalctl failed: #{cmd_result["stderr"]}"
    end
  end
end

The Journalctl::Query class

Using the Entry class directly would require to know the syntax of the journalctl arguments. In order to abstract the details and also as a way to provide some introspection capabilities for the user interface, a Journalctl::Query class is implemented in "src/lib/journalctl/query.rb". The usage is again quite simple, as shown by the following example.

require "journalctl/query"

query = Journalctl::Query.new(boot: -1, priority: 5)
puts "First entry's process: #{query.entries.first.process_name}"
puts "Using priority #{query.filters[:priority]}"

This looks like good API to support not only the user interface we designed in the previous steps but also any other Ruby code needing access to the systemd journal. "But is that enough?" you may wonder. After all, YaST is a multi-language beast and maybe you should provide a YCP interface for your journalctl wrapper to be used from other programming languages. Fortunately, the answer is that you don't have to worry about it. YCP is still a core technology for YaST, but it's considered legacy. Ruby is the future of YaST and thus all new code is expected to focus only in Ruby and its ecosystem.

Laying the foundation

We already have a basic user interface and some domain models capable of reading the information we need from the system. But before starting to put both things together we need to ensure that we are standing on solid ground.

First of all, we obviously need to write unit tests for our new models. Moreover, it would be nice to have some tools to check that our code can be properly executed and complies with the YaST standards.

So before we continue writing code, let's take a look to the available YaST development tools in the next step of the tutorial.