Cpanel::OS - Abstract interface to the OS to obviate the need for if-this-os-do-this-elsif-elsif-else tech debt
my $os = Cpanel::OS->instance;
my $pm = Cpanel::PackMan->instance;
$pm->sys->install( $os->packages("bootstrap") );
as opposed to crufty tech debt version we’ve come to know and not love:
if ($x eq "centos" && $y < 7) {
@packages = …;
}
elsif ($x eq "cloudlinux" && $y > 6) {
@packages = …;
}
elsif (…
…
$x eq "unbuntu" ? call_apt_to_install(@packages) : $y < 8 ? call_yum(@packages) : call_dnf(@packages);
The goal is to make an abstract interface to OS specific things so our code can be as OS agnostic as possible.
In other words, there should be very few places normal code should be doing things based on the OS information.
Such places should contain an explanation justifying why it must be done that way.
It was historically very difficult to add support in cPanel for a new OS. The reason why is because OS variations are handled by logic checking the os name and version to determine what the thing in question needs.
That is undesirable for a number of reasons, including but not limited to:
- We have to update hundreds if not thousands of places arbitrarily strewn through our code base
- As we learn we need to make another adjustment we repeat the mass find and edit
- It adds more complexity that needs tested and documented
These problems are expensive to address, result in bad UX, and limit us due to ever increasing tech debt.
Instead of code asking about os names and numbers to figuring out what it needs depending on the OS, have it ask Cpanel::OS for it.
Put another way: In code like that we are really asking “What is it we need here?” so just ask that question.
We are rarely interested in the distro and version numbers; instead we are interested in finding the information I need, information that can change between OSs.
Cpanel::OS aims to let code ask the question we’re really asking: hey, what foo should I use? Without needing to understand anything about the OS.
For example, take this simple need:
sub get_foo {
my $foo;
if ($dist eq "centos" && $major > 6) {
$foo = "x";
}
elsif ($dist eq "centos" && $major < 6) {
$foo = "y";
}
return $foo;
}
…
my $foo = get_foo();
process_foo($foo);
Did you see any of the problems? There are at least 8:
- Bug: does not set $foo for CentOS 6
- Would get more fragile is we needed a different foo on CentOS 8.2 and 8.3
- Does not differentiate between cloudinux and centos
- more code needs added to add a new OS, e.g. ubuntu
- we have to find this code when adding a new OS
- it can be undefined leading to unexpected result
- it contains logic for unsupported OSs
- ever more complex doc and tests need updated as
get_foo()
changes
Now take the Cpanel::OS version:
my $foo = $os->value("foo");
process_foo($foo);
All 8 (and the others) are non-issues now.
Some of the benefits:
- A new OS can be added by defining its data.
- An old OS can removed by removing its data.
- An OS can define data that differs within itself
- It results in far less code to write, test, and document.
- As we switch code to support ubuntu we can remove systems that support the undesireable approach, for example, Cpanel::GenSysInfo and similar. Less code and more consistency.
Make it easy to define an OS, in git, w/ the ability to differentiate from generic (v42) to specific (v42.2020.99)
Use as low level systems as possible.
Cache results in memory and on disk for other processes.
Rely on a minimum of dependencies.
Only do heavy calculations/deps once when needed and rely on cache after that.
-
A system command lookup
Typically system commands should be encapsulated in a subclass for the system in question.
For example, instead of defining package management commands in the OS, the package management class that implements functionality for the OS’s package system. e.g. for ubuntu Cpanel::PackMan needs an apt “driver” that it loads based on
$os->package_manager
. -
A best OS path lookup for OS specific code.
Similar to system commands. Since we are really asking what foo to use and not how do I do this on $dist $major $minor …
If a need for it does exist and can be demonstarted we can re-instate that behavior.