In light of some experiments I’ve been doing with the Raspberry Pi, I’ve committed myself to creating a way of sharing information between services. I needed something that was lightweight, minimal, and most of all, flexible.
Ozone is basically that. Simply speaking, it’s a shared minimal Node.js API for sharing state and intentions between multiple Node instances. It’s goal is to securely allow instances to communicate with one-another on an enclosed internal network, which is extremely useful for sharing state information and handling specific actions.
I’m not ready to start giving elevator pitches for Ozone yet (since it’s still in it’s infancy), but here’s a more practical example of how it works:
A better example…
Say you have multiple concurrent Node instances running in parallel. One Node instance might be in charge of handling and doing file-system “things”, while another handles fetching weather information. These two services need to communicate with one another to do their jobs, but without delegation these services will fail to communicate in structured and organized manner.
This becomes a real issue when you have several instances trying to communicate information to a single node instance. How do you prevent conflicts? What happens when network conditions prevent you from reaching the instance before another one can?
These were the exact problems I was facing during my own experiments, and I needed a way to handle this asynchronously without causing tight coupling. Ozone acts as a bridge, in charge of delegating actions securely and asynchronously.
Securing the sub-system
I’m glad you asked.
Ozone requires the sub-system (basically the directory it runs within) to be owned and only editable by the root user before booting.
If it detects that a sub-system has been potentially compromised, or that unauthorized file-system changes have been detected between init sequences it throws and tells you exactly what’s up.
The core files (and anything within it’s directory) are recursively locked. As a result, any unauthorized changes that effect the SHA integrity of the core, init, services (or even custom directories) will cause a dramatic premature failure to prevent potential code injection, or running of corrupted files.
That’s also why by default, Ozone has no dependencies in order to maintain a expected behavior as well as prevent unwanted security risks.
State management and intentions
The other goal as I’ve mentioned, was a way to efficiently share state and send data to other services in an organized fashion.
Services defined within the
services.json have access to a special attribute called
state, which specifies the unique key on the state that a service is authorized to manipulate. Whenever the global state is modified, each service is then notified and can execute the required events.
The goal of this is to give each service access to a shared structure of information. This is meant to be generally vague but potentially useful data, such as sensor readings, the current orientation of the device, or the current color of the sky.
Intentions on the other hand, are the whole reason I made Ozone. In theory, an intention based model means that services are loosely couped and essentially interchangeable. This is useful for my needs as it allows less interdependence between services, so if one service catches and burns the entire system won’t crumble.
In the future, I plan on adding redundancy features to auto revert services to working states in the event of elevated failure.
notify attribute of a service allows it to receive generic intentions as well as any data attached to it. In example, a service that sends out a
MAKE_ME_A_PENCIL intention will be sending it out to any matching services that have it under it’s
notify attribute. The information included might include the color desired, the type of wood it’s made out of, as well as any preferences that other services would likely interpret.
So each one of those services would receive a message
MAKE_ME_A_PENCIL with some JSON object containing relevant data. It’s up to the specific services to decide what to do with the intentions received. Instead of sending a response to each of those services, it would then update it’s state if the service’s logic warrants it. So if the
pencil-maker service receives the
MAKE_ME_A_PENCIL intention, it can then render a pencil and then update the state to increase the current pencil count.
This is my vision of an ideal system. If you sift through all the abstractions, a single service should essentially be doing one thing and one thing well. Any extra parts that a service needs to abstract data, so be it. But as far as other services are concerned, they only care about the essential data.
While the core itself is written in Node, technically any language or package capable of interfacing with the spec can leverage the state and intentions API. I’m still actively improving the system so it can leverage multi-host services (for clustering), as well as making the spec documentation more coherent and useful.
Feel free to checkout my code on GitHub, it’s BSD-2 Clause. It’s still a work in progress, but people are surely welcomed to come take a look.
I’m looking forward to seeing how far I’m able to take this project, and how the community chooses to use or adapt it. As of right now my target is embedded systems, but as far as I’m concerned it can go much further.