Zypper-upgraderepo came in my mind the day I realized to abandon the old method of download and burn the ISO to upgrade my openSUSE Linux distro. There is nothing wrong on reinstalling everything from zero and clean up all the junk accumulated, but the good ability of Yast and Zypper to keep the system in good conditions after several package installations and removals, made me think to take advantage of the dist-upgrade command.
One of the first obstacles in changing my upgrading strategy was a missing feature I would have seen implemented: a way to correctly upgrade the repositories URLs without messing them up with versions and addresses.
Instead of waiting for someone implementing it, I decided to do my part creating a small Bash script to complete that simple task: loop through repository’s URLs and replace the current version with the next.
Soon I realized that this approach might lead to a fundamental error: Not all repositories are upgraded at the same time, some of them might also be abandoned, or their path might slightly change.
That was the time I decided to use curl to check for URL validity, redirections and page not found errors.
The old bash script
That has been the basic concept I brought on zypper-upgraderepo, adding other useful option to create a more complete tool and switching to a language more comfortable than Bash to achieve different operations.
Ruby is excellent to quickly develop algorithms and scripts, being a full general-purpose language I found no limits creating, years ago, my first CLI application with it. Since then I continued to improve my skills transforming small scripts in complex applications that I often use for my daily activities, enjoying the smooth syntax and the great expressivity of this language.
The Ruby source code
Of course like all interpreted language has its limits to reach high performances compared to statically compiled languages, but in this case what makes the difference is the possibility to create well structured and maintainability code with highly readability instructions, and Ruby in this area is the best.
As plus, the whole Ruby ecosystem resulted, year by year, very stable and reliable so I decided to publish some of my applications in the hope to inspire someone else.
Usually applications take advantage of as many external libraries as possible to get the job done with minimal efforts on boring or critical features well implemented by others. My choice has been slightly different, focusing on a more compact code and less external dependencies.
To create the command line interface I used the built-in library optparse, the HTTP requests are handled through the built-in Net module, colored output messages and data displaying are implemented on my own.
Among the external gems bundler is almost mandatory for every project.
To read and write the repository files and the distribution settings stored in /etc/os-release, both in ini format, I discarded inifile in favor of iniparse, which is more stable.
The latest gem worthing a mention is minitar, as the name says it creates tar archives supporting also the Gzip compression, and all that in just a single line of code:
Minitar.pack(RepositoryList::REPOSITORY_PATH, Zlib::GzipWriter.new(File.open(filename, 'wb')))
Software design is the art of transforming the software requirements and analysis into the main structure of our application.
Being Ruby an OOP language we must reason for classes to solve our problems taking advantage of some design patterns.
UML diagram of zypper-upgraderepo
The command-line interface is developed around three classes.
CLI with its static method
#start represent the entry point to run the application and it’s invoked by the executable file set by Bundler. That method mainly calls the OptParseMain class and based on the returned object instantiate and executes the related command provided by Builder.
OptParseMain is in charge of translating the command line arguments to valuable options for the Builder class. It makes use of the built-in OptionParser class which helps to implement the interface switches and their decoding on the final OpenStruct object.
The class which implements the available operations is Builder. Although the name invokes its proper design pattern, its structure is more close to a Facade pattern, why I haven’t refactored it is still a mystery ;)
Repositories live in the Repository class which exposes the methods to read and write all the information stored in their respective ini files. This class is also able to upgrade the data to a given version replacing the version number where available.
Instantiating all the repositories, iterating over them to execute custom code and saving them is in charge of the RepositoryList class. I had to introduce this class to quickly handle the whole collection with ease especially when they have to be initially sorted using custom criteria.
PageRequest is the base class which models a HTTP Get/Head request using the default net/http library. The response is cached and the other methods query the object to understand whether or not a certain URL is valid, fails, or has been redirected elsewhere.
I kept it as generic because I want to reuse it in other projects without creating a new gem.
It is also a SimpleDelegator subclass because through the specialized class RepositoryRequest can be applied as an enhancement to a Repository class through delegation.
The just named RepositoryRequest is specialized in finding URLs alternatives, traversing the URL path back and forth, looking for the wanted version.
There are other aspects of this application I classified to better organize the code and related methods calls.
OsRelease provides all the needed information about the openSUSE Leap releases, recognizing the current, the next, or a past version. The version numbers are hardcoded for a reason: between the 13.2 version and the 15.0, three 42.x versions can’t be ordered in any natural way.
To handle the data visualization a View module contains all the classes involved in the output representation: Table, Report, Quiet, Ini. Their names are quite self-explanatory, the methods are the same in number and header, that way is easy to access to every single implementation just instantiating the right class.
Every error message belongs to a distinct StandardError subclass. Classifying all the errors might look a waste of time at first, but in real:
- avoid assigning different messages to the same error raised in different points of our code;
- having a list of potential errors automatize the research of certain conditions while implementing the methods;
- it’s way easier assigning a custom exit code to different errors.
Last but not least also warnings, errors and status messages display using a common style, that’s why the Messages class exists.
This is the journey inside my zypper-upgraderepo code, at a first sight using an Object-Oriented language to design a small application like that might result in a too long process, but the long-term benefits are matchless.
Less coupled and highly cohesive classes produce code comfortable to extend, easy to fix, facilitating the writing of testing procedures.
However, creating good Object Oriented code is not so immediate, it requires hours in software analysis and design, and other hours of refactoring to better align the code to the ongoing enhancement, especially in an Agile context.
What I like more about the whole process is the opportunity to reuse the single classes in other projects, building time by time a personal catalog of classes to solve specific problems.
Hopefully, some of the choice listed in that article might inspire someone to write their application and consider the use of an Object-Oriented Programming language as Ruby for future projects. This article covers a personal project, further info is provided at the ZypperUpgraderepo project page.
This work is licensed under a Creative Commons Attribution-NoCommercial-ShareAlike 4.0 International License
This article covers a personal project, further info is provided at the ZypperUpgraderepo project page.