Attila Pinter and Maurizio Galli join the openSUSE Board
We had two candidates for two available seats. As per the board election rules, in such a case, each candidate should receive at least 50% of the total number of votes to be considered as elected.
Both Attila Pinter and Maurizio Galli received good support from the community members. We had 542 eligible voters, out of which 147 voted in the election.
Attila Pinter received 122 votes in his favour which represents 83%, 22 votes against and 3 abstentions.
Maurizio Galli received 120 votes in his favour representing 82%, 21 votes against and 6 abstentions.
Congratulations to both of them! 👏 🎉
27 Years with the Perfect OS
If you are a longtime FreeBSD user, you probably know everything I have to say, and, what’s more, you can probably add a few more points. But hopefully, there will be some Linux or even Windows users among readers who might learn something new!
FreeBSD is not just a kernel but a complete operating system. It has everything to boot and use the system: networking utilities, text editors, development tools and more. Why is that a big deal? Well, because all these components are developed together, they work perfectly together! And a well-polished system is also easier to document. One of my favorite pieces of documentation is the FreeBSD Handbook which covers most of the operating system and is (most of the time) up to date.
Of course, not everything can be integrated into the base operating system, and this is where FreeBSD ports and packages can be useful. The ports system allows a clean separation of the base system and third-party software which allows you to install third-party software on top of a FreeBSD base system. There are tens of thousands ready-to-use software packages to choose from. For example, all the graphical desktop applications are in ports, just as various web servers or more up-to-date development tools.

FreeBSD: the power to serve
You can read the rest of my article in the FreeBSD Journal at https://issue.freebsdfoundation.org/publication/?m=33057&i=733207&p=14&ver=html5
Best Linux Distros for Servers
Noodlings 35 | Light for Christmas
openSUSE Tumbleweed – Review of the week 2021/52
Dear Tumbleweed users and hackers,
The last week of the year, and the last day of the year, are coming to an end. Tumbleweed had a small dip, as the last two snapshots that moved to openQA had to be stopped from being published. Nevertheless, we still managed to publish 6 snapshots before heading out to the new-year celebrations (1223…1228).
The snapshots contained those relevant changes:
- Qemu 6.2.0
- Boost 1.78.0
- gimp 2.10.30
- gpg 2.3.4
- Pango 1.50.3
- sssd 2.6.3
- Default ruby interpreter switched from 2.7 to 3.0
The staging projects are mostly, but not completely, empty. The few ones in use are testing these items:
- Wayland 1.20 (Seems to cause some bugs, see https://bugzilla.opensuse.org/show_bug.cgi?id=1194190
- Linux kernel 5.15.12
- Ruby 3.1. A direct attempt to switch to Ruby 3.1, and removing Ruby 2.7 and Ruby 3.0 in one go. Most likely it will be phased a little bit (e.g. Vagrant is not Ruby 3.1 compatible yet)
- zstd 1.5.1: breaks build of zchunk and libzypp
- Enabling the build of python310-* modules; the move of the default python3 provider to python310 could follow soon after
PDF Quirk Version Update
The good thing about this quiet holidays is that there is time to work on projects. I had a few nice changes of PDF Quirk laying around, and now finalized them to a new version. Please welcome PDF Quirk version 0.95!
What is PDF Quirk?
PDF Quirk is a little desktop utility to create PDF files from images, targeted to non nerdy desktop users.
Sending PDFs is (still) often a requirement in offices where people are asked to transfer PDF files via email, or better by pushing them through their private ownCloud.
The source images can either be loaded from file, or directly scanned with an hardware scanner. For that, PDF Quirk utilizes the tool scanimage from the SANE Projekt, to avoid reinventing the wheel. Configured once, that works like a charm.
Having scanned or picked the source images, they can be deskewed, turned and rearranged, and finally converted to a good quality PDF file with reasonable file size.
New Version 0.95
The new version brings a few features and some fixes:
- Images can deskewed now
- Basic PDF options like margin, paper size and orientation can be set
- The UI got a little cleanup
- Translations were added, first new language is German
- Dependencies were reduced, new Qt based PDF generator was implemented
- Builds with Qt5 and Qt6
Interested?
Check out PDF Quirks Website for more information.
PDF Quirk is free software. Please contribute through the Github repository.
Orient: Life at Sea in the 21st Century
The Titanic could accommodate about 2500 passengers who booked either first, second or third class. Wikipedia tells that
those travelling in first class, most of them the wealthiest passengers on board, included prominent members of the upper class, businessmen, politicians, high-ranking military personnel, industrialists, bankers, entertainers, socialites, and professional athletes. Second-class passengers were predominantly middle-class travellers and included professors, authors, clergymen, and tourists. Third-class or steerage passengers were primarily immigrants moving to the United States and Canada.
Much has changed since then.
Life at Sea in the 21st Century
Purpose and passengers of such mega ships (rather kilo ships :wink:) have changed dramatically since then. The ship is no longer a mean of transport. Passengers from Central Europe fly 5000 km to the Orient for a cruise of 500 km to a nearby harbour and back to the point of departure. Many passengers are rather age group 50+ and have cruised around quite a lot already1. Then, there are few younger families and couples as well. Other single travellers fall rather into the category of widows2.
My ship features 1267 twin cabins for 2534 passengers, but if need be, can host up to 2700 passengers–the 1030 crew members excluded. The other ship in the habour, Costas Firenze, has 2116 cabins for up to 5078 passengers (two times the Titanic) and provides for a crew of about 1300 members.
Due to Covid-19, the ships are far from fully booked. In my case the occupation rate was about 40%, a bit more than 1000 people.
Life at sea on this German-operated ship is best compared to Club holidays in Germany3:
- room service takes care of cabins twice per day
- all inclusive all you can eat and drink
- about 5 restaurants and several bars
- Döner Kebab at the poolside :sweat_smile:
- SPA area
- fitness club with courses, volleyball court and running area
- entertainment program in the lounge, the two theatres or on the open-air stage with shows that involve the dancers and acrobats belonging to the crew and guest artists (stand-up, magician)
- casino (open when in international waters)
- a few shops for shopping (VAT-free!)
- kids club
- organised day trips (bus, bike and boat tours, etc.)
- mostly German-speaking crew
- 99% German-speaking guests/passengers (I spotted two Dutch)
Consequently, a cruise on this ship is the perfect fit for all those who would like to hang out with Germans, have German bread and bread rolls for every meal, enjoy Sauerkraut, Klöße, Currywurst and Döner Kebab, but at the same time rather prefer a more Mediterranean climate than what Germany can typically offer! Kind of German holidays outside of Germany.
Gallery
With 1000 German passengers on board, it is easy to make pictures of the scenic locations without people: At 8 PM, everyone is at dinner! Let me take you on a tour.

Pool area.

Running path (opening hours: 6 to 8 AM).

Salon area.

Salon area II.

Salon area III.

Midnight snackbar: burgers, Belgium fries, cakes, etc.

Theatre of the ship.

Sport court.
I have a few more impressions taken at daytime.

View from Deck 12 to the family pool area.

Spa area.
-
I know because during some show on the deck, the moderator has asked the question who has been on a cruise before and many hands were raised. ↩︎
-
There was a meetup of single travellers on the ship. However, I didn’t use the occasion to ask them whether they were really widowed. :see_no_evil: ↩︎
-
Not that I have ever done club holidays in Germany–but that’s how I imagine it! ↩︎
Playing with Shelly
For xmass I got few Shelly lamps to play with. Shelly lamps are simple IoT devices. Super easy to install, configure and use. The Youtube is full with instructions on what can be done with these smart lamps. Naturally my main motivation was to figure out how to hack these devices and how ready my openSUSE servers are with tools and services (spoiler: they are ready)
Look daddy no cloud
Needless to say that like most smart home automation devices the Shelly lamps can be operated via the Shelly cloud. I may cover that area in the next post. But now I am interested in what can be done without the cloud. After all, one big selling point of the Shelly devices is that they are fully operable and functional even without Internet connection just on a WiFi LAN. It means that if I am concerned about the security of my home infrastructure I have an option not to expose my smart devices.
Home Assistant for a Newbie
Jekyll: Import Disqus comments for Staticman
For some years already, I try to rely for this website on less external resources and avoid ad-powered services to improve the privacy for my dear readers.
Recently, I removed the comments provided by Disqus from this blog, because Disqus introduced too much data sharing with many third parties. Norway just fined this year Disqus 2,5 Mio Euro for tracking without legal basis.
Please find hereafter some tips on how to export comments from Disqus and display them in a privacy-friendly way in your Jekyll blog.
Export Disqus Comments to JSON and YAML
- Disqus documents the export and export format at https://docs.disqus.com/developers/export/
-
Navigate to http://disqus.com/admin/discussions/export/ to export your comments to XML format.
The XML has principally 3 parts: meta data, a list with webpages and a list with comments that are linked each to a webpage (via a Disqus identifier) and possibly a parent comment in case the comment is a reply.
For use within Jekyll, I need to restructure the data and have a list of comments for each webpage by my own identifier (e.g. post slug) and convert everything to a format that Jekyll can handle, hence YAML, JSON, CSV, or TSV. I choose YAML.
-
Install the linux tool
xqto manipulate XML files and export to JSON and the tooljq.xqis basically a wrapper ofjq.pip install xqDownload binaries of jq here: https://stedolan.github.io/jq/download/
-
I convert then the Disqus XML export into a JSON file with the code in
export-disqus-xml2json.sh - Then, I pipe the output through
import-json-yaml.rbto split the list of comments into individual files for easy consumption by Jekyll.
# file: 'export-disqus-xml2json.sh'
#!/usr/bin/env sh
xq '.disqus | .thread as $threads | .post | map(select(.isDeleted == "false")) | map(.thread."@dsq:id" as $id | ($threads[] | select(."@dsq:id" == $id)) as $thread | {id: ("disqus-"+."@dsq:id"), date: .createdAt, slug: ($thread.id | tostring | gsub("/$";"") | split("/") | last), name: (if .author.name == "Robert" then "Robert Riemann" else .author.name end), avatar: .author | (if has("username") and .username != "rriemann" then "https://disqus.com/api/users/avatars/"+.username+".jpg" else null end), email: .author | (if has("username") and .username == "rriemann" then "my@mail.com" else null end), message, origin: ($thread.link | tostring | gsub("^https://blog.riemann.cc";"")), replying_to: (if has("parent") then ("disqus-"+.parent."@dsq:id") else null end)})' "$@"
Example comment from the JSON list:
{
"id": "disqus-4145062197",
"date": "2018-10-14T22:14:58Z",
"slug": "versioning-of-openoffice-libreoffice-documents-using-git",
"name": "Robert Riemann",
"avatar": null,
"email": "my@mail.com",
"message": "<p>I agree, it is not perfect. I have no solution how to keep the noise out of git.</p>",
"origin": "/2013/04/23/versioning-of-openoffice-libreoffice-documents-using-git/",
"replying_to": "disqus-4136593561"
}
The script import-json-yaml.rb takes each comment and puts it in YAML format with a unique filenname in the folder named after the slug.
# file: 'import-json-yaml.rb'
#!/usr/bin/env ruby
require 'json'
require 'yaml'
require 'fileutils'
require 'date'
data = if ARGV.length > 0 then
JSON.load_file(ARGV[0])
else
JSON.parse(ARGF.read)
end
data.each do |comment|
FileUtils.mkdir_p comment['slug']
File.write "#{comment['slug']}/#{comment['id']}-#{Date.parse(comment['date']).strftime('%s')}.yml", comment.to_yaml
end
The output with tree looks like:
_data
├── comments
│ ├── announcing-kubeplayer
│ │ ├── disqus-113988522-1292630400.yml
│ │ └── disqus-1858985256-1424044800.yml
│ ├── requires-owncloud-serverside-backend
│ │ ├── disqus-41270666-1269302400.yml
│ │ ├── disqus-41273219-1269302400.yml
...
Display Comments in Jekyll
Those comments are accessible in jekyll posts/pages via site.data.comments[page.slug]
Most helpful for the integration of comments to Jekyll was the post https://mademistakes.com/mastering-jekyll/static-comments-improved/.
<!-- file: 'my-comments.html' -->
{% assign comments = site.data.comments[page.slug] | sort %}
{% for comment in comments %}
{% assign index = forloop.index %}
{% assign replying_to = comment[1].replying_to | to_integer %}
{% assign avatar = comment[1].avatar %}
{% assign email = comment[1].email %}
{% assign name = comment[1].name %}
{% assign url = comment[1].url %}
{% assign date = comment[1].date %}
{% assign message = comment[1].message %}
{% include comment index=index replying_to=replying_to avatar=avatar email=email name=name url=url date=date message=message %}
{% endfor %}
<!-- file: 'comment' -->
<article id="comment{% unless include.r %}{{ index | prepend: '-' }}{% else %}{{ include.index | prepend: '-' }}{% endunless %}" class="js-comment comment {% if include.name == site.author.name %}admin{% endif %} {% unless include.replying_to == 0 %}child{% endunless %}">
<div class="comment__avatar">
{% if include.avatar %}
<img src="{{ include.avatar }}" alt="{{ include.name | escape }}">
{% elsif include.email %}
<img src="https://www.gravatar.com/avatar/{{ include.email | md5 }}?d=mm&s=60" srcset="https://www.gravatar.com/avatar/{{ include.email | md5 }}?d=mm&s=120 2x" alt="{{ include.name | escape }}">
{% else %}
<img src="/assets/img/avatar-60.jpg" srcset="/assets/img/avatar-120.jpg 2x" alt="{{ include.name | escape }}">
{% endif %}
</div>
<div class="comment__inner">
<header>
<p>
<span class="comment__author-name">
{% unless include.url == blank %}
<a rel="external nofollow" href="{{ include.url }}">
{{ include.name }}
</a>
{% else %}
{{ include.name }}
{% endunless %}
</span>
wrote on
<span class="comment__timestamp">
{% if include.date %}
{% if include.index %}<a href="#comment{% if r %}{{ index | prepend: '-' }}{% else %}{{ include.index | prepend: '-' }}{% endif %}" title="link to this comment">{% endif %}
<time datetime="{{ include.date | date_to_xmlschema }}">{{ include.date | date: '%B %d, %Y' }}</time>
{% if include.index %}</a>{% endif %}
{% endif %}
</span>
</p>
</header>
<div class="comment__content">
{{ include.message | markdownify }}
</div>
</div>
</article>
Receiving New Comments
Like explained in https://mademistakes.com/mastering-jekyll/static-comments/, the software https://staticman.net/ allows to feed POST HTTP requests to Github and Gitlab pull requests, so that comments can be added automatically. Of course, the website requires after each time a rebuild.
I had much trouble to setup Staticman. Eventually, I decided to use a Ruby CGI program that emails me the new comment as an attachment. I like Ruby very much. :wink: Once I figured out how to use the Gitlab API wrapper, I may also use pull requests instead of email attachments.
# file: 'index.rb'
#!/usr/bin/env ruby
Gem.paths = { 'GEM_PATH' => '/var/www/virtual/rriemann/gem' }
require 'cgi'
require 'yaml'
require 'date'
require 'mail'
cgi = CGI.new
# rudimentary validation
unless ENV['HTTP_ORIGIN'] == 'https://blog.riemann.cc' and
ENV['CONTENT_TYPE'] == 'application/x-www-form-urlencoded' and
ENV['REQUEST_METHOD'] == 'POST' and
cgi.params['email']&.first&.strip =~ URI::MailTo::EMAIL_REGEXP and
cgi.params['age']&.first == '' then # age is a bot honeypot
print cgi.http_header("status" => "FORBIDDEN")
print "<p>Error: 403 Forbidden</p>"
exit
end
output = Hash.new
date = DateTime.now
output['id'] = ENV['UNIQUE_ID']
output['date'] = date.iso8601
output['updated'] = date.iso8601
output['origin'] = cgi.params['origin']&.first
output['slug'] = cgi.params['slug']&.first&.gsub(/[^\w-]/, '') # some sanitizing
output['name'] = cgi.params['name']&.first
output['email'] = cgi.params['email']&.first&.downcase&.strip
output['url'] = cgi.params['url']&.first
output['message'] = cgi.params['message']&.join("\n").encode(universal_newline: true)
output['replying_to'] = cgi.params['replying_to']&.first
#Mail.defaults do
# delivery_method :sendmail
#end
Mail.defaults do
delivery_method :smtp, address: "smtp.domain", port: 587, user_name: "smtp_user", password: "smtp_password", enable_starttls_auto: true
end
mail = Mail.new do
from 'no-reply@domain' # 'rriemann'
to 'comments-recipient@domain' # ENV['SERVER_ADMIN']
reply_to output['email']
header['X-Blog-Comment'] = output['slug']
subject "New Comment from #{output['name']} for #{cgi.params['title']&.first}"
body <<~BODY
Hi blog author,
a new comment from #{output['name']} for https://blog.riemann.cc#{output['origin']}:
#{output['message']}
BODY
add_file(filename: "#{output['id']}-#{date.strftime('%s')}.yml", content: output.to_yaml)
end
mail.deliver
if mail.error_status then
print cgi.http_header("status" => "SERVER_ERROR")
cgi.print <<~RESPONSE
<p><b>Error: </b> #{mail.error_status}</p>
<p>An error occured. Please try again later.</p>
<p><a href="javascript:history.back()">Go back</a></p>
RESPONSE
else
print cgi.http_header
cgi.print <<~RESPONSE
<p><b>Thank you</b> for your fedback! Your comment is published after review.</p>
<p><a href="#{output['origin']}">Back to the previous page</a></p>
RESPONSE
end
To make it work with Apache, you may need to add these lines to the Apache configuration (could be a .htaccess file):
DirectoryIndex index.html index.rb
Options +ExecCGI
SetHandler cgi-script
AddHandler cgi-script .rb