<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Blog on Taavi Väänänen</title><link>https://taavi.wtf/posts/</link><description>Recent content in Blog on Taavi Väänänen</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>taavi@majava.org (Taavi Väänänen)</managingEditor><webMaster>taavi@majava.org (Taavi Väänänen)</webMaster><lastBuildDate>Wed, 18 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://taavi.wtf/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Wikimedia Hackathon Northwestern Europe 2026</title><link>https://taavi.wtf/posts/wikimedia-hackathon-nwe-2026/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/wikimedia-hackathon-nwe-2026/</guid><description>&lt;p>&lt;a href="https://meta.wikimedia.org/wiki/Wikimedia_Nederland">Wikimedia Nederland&lt;/a> organised a new type of event this year, the
&lt;a href="https://www.mediawiki.org/wiki/Wikimedia_Hackathon_Northwestern_Europe_2026">Wikimedia Hackathon Northwestern Europe 2026&lt;/a>, which was held last
weekend in &lt;a href="https://en.wikipedia.org/wiki/Arnhem">Arnhem&lt;/a>, the Netherlands. And I'm very happy they did,
since &lt;a href="https://taavi.wtf/posts/wikimedia-hackathon-athens-2023/">unlike&lt;/a> &lt;a href="https://taavi.wtf/posts/wikimedia-hackathon-tallinn-2024/">last&lt;/a> &lt;a href="https://taavi.wtf/posts/wikimedia-hackathon-istanbul-2025/">years&lt;/a>, I will unfortunately be missing from the
&amp;quot;main&amp;quot; Wikimedia Hackathon (which is happening in Milan at the start of
May).&lt;/p>
&lt;p>I continue to believe the primary reason for these events existing is
the ability to connect with old and new friends in person. That being
said, I did get a bit of &lt;em>technical tinkering&lt;/em> done during the weekend
as well. These include a &lt;a href="https://gerrit.wikimedia.org/r/c/mediawiki/extensions/Echo/+/1251308">dark mode fix&lt;/a> to MediaWiki's notification
interface, fixes to some visual bugs in MediaWiki's &lt;a href="https://gerrit.wikimedia.org/r/c/mediawiki/extensions/OATHAuth/+/1251526">two-factor
authentication&lt;/a> and &lt;a href="https://gerrit.wikimedia.org/r/c/mediawiki/extensions/OAuth/+/1251887">OAuth&lt;/a> functionality. I did also get an older
patch of mine about &lt;a href="https://phabricator.wikimedia.org/T416518">disabling Composer's new auditing functionality&lt;/a>
merged. And, as usual, I spent a bunch of time helping various people
use with the various infrastructure pieces I'm familiar with (or at
least had to suddenly get familiar with) and approved a bunch of OAuth
consumers and other requests.&lt;/p>
&lt;p>We also managed to continue the tradition from the past two Wikimedia
Hackathons of nominating more people to receive &lt;a href="https://www.mediawiki.org/wiki/+2">+2 access&lt;/a> to
&lt;code>mediawiki/*&lt;/code>. That request is still open as of writing, as those have
to run for at least a week, but looks very likely to pass at this
point.&lt;/p>
&lt;p>Overall, the event was very well-organized: the venue was great, except
that the number of stairs was described in a rather misleading way,
food was great, and the atmosphere was amazing. The pressure that you
must Just Get Things Done to justify your attendance that the main
hackathon seems to have recently gained was clearly missing here which
was great. Also, I will clearly need to bring more Finnish chocolate
next time.&lt;/p>
&lt;p>The timing of Friday and Saturday works great for us with other things
(like university for me) during the week, as it takes full advantage of
the weekend but still only eats workdays from a single calendar week.
My main gripe with the logistics was the focus on a &lt;a href="https://en.wikipedia.org/wiki/Telegram_(software)">single sketchy
non-free messaging platform&lt;/a> for all event-related communications with
the IRC bridge used on the main hackathon channel notably missing.&lt;/p>
&lt;hr>
&lt;p>ps. Like &lt;a href="https://lucaswerkmeister.de/posts/2026/03/17/wikimedia-hackathon-northwestern-europe-2026/">Lucas&lt;/a>, I do have Opinions about so many proudly mentioning
they've used &amp;quot;vibe coding&amp;quot; tools during the introduction and showcase.
Those opinions are best left for an another time, but I do want to note
that all of my work and mistakes have still been lovingly handcrafted.&lt;/p></description></item><item><title>How to import a new Wikipedia language edition (in hard mode)</title><link>https://taavi.wtf/posts/tokwiki/</link><pubDate>Sat, 06 Dec 2025 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/tokwiki/</guid><description>&lt;p>I &lt;a href="https://sal.toolforge.org/log/wyDvwJoB8tZ8Ohr0_A6d">created&lt;/a> the latest Wikipedia language edition, the &lt;a href="https://tok.wikipedia.org/">Toki Pona Wikipedia&lt;/a>, last month.
Unlike most other wikis which start their lives in the &lt;a href="https://incubator.wikimedia.org/">Wikimedia Incubator&lt;/a>
before the full wiki is created, in this case the community had been using a
completely external MediaWiki site to build the wiki before it &lt;a href="https://meta.wikimedia.org/wiki/Requests_for_new_languages/Wikipedia_Toki_Pona_2">was approved&lt;/a> as
a &amp;quot;proper&amp;quot; Wikipedia wiki,&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> and now that external wiki needed to be imported
to the newly created Wikimedia-hosted wiki. (As far as I'm aware, the last and previously
only time an external wiki has been imported to a Wikimedia project was in 2013 when
&lt;a href="https://en.wikipedia.org/wiki/Wikitravel#Community_fork_in_2012">Wikitravel was forked&lt;/a> as Wikivoyage.)&lt;/p>
&lt;p>&lt;a href="https://wikitech.wikimedia.org/wiki/Add_a_wiki">Creating a Wikimedia wiki&lt;/a> these days is actually pretty straightforward, at least
when compared to what it used to be like a couple of years ago. Today the process
mostly involves using a script to generate two &lt;a href="https://wikitech.wikimedia.org/wiki/Wikimedia_site_requests">configuration changes&lt;/a>, one to add
the basic configuration for a wiki to operate and an another to add the wiki to the
list of all wikis that exist, and then running a script to create the wiki database
in between of deploying those two configuration changes. And then you wait half an
hour while the script to tell all &lt;a href="https://www.wikidata.org/">Wikidata&lt;/a> client wikis about the new wiki runs on
one wiki at a time.&lt;/p>
&lt;p>The primary technical challenge in importing a third-party wiki is that there's
no &lt;a href="https://meta.wikimedia.org/wiki/Help:Unified_login">SUL&lt;/a> making sure that a single username maps to the same account on both
wikis. This means that the usual strategy of using the functionality I wrote in
&lt;a href="https://www.mediawiki.org/wiki/Extension:CentralAuth">CentralAuth&lt;/a> to manually create local accounts can't be used as is, and so we
needed to come up with a new way of matching everyone's contributions to their
existing Wikimedia accounts.&lt;/p>
&lt;p>(Side note: While the user-facing interface tries to present a single &amp;quot;global&amp;quot;
user account that can be used on all public Wikimedia wikis, in reality the
account management layer in CentralAuth is mostly just a glue layer to link
together individual &amp;quot;local&amp;quot; accounts on each wiki that the user has ever
visited. These local accounts have independent user ID numbers — for example I
am user #35938993 on the English Wikipedia but #4 on the new Toki Pona
Wikipedia — and are what most of MediaWiki code interacts with except for a few
features specifically designed with cross-wiki usage in mind. This distinction
is also still very much present and visible in the various administrative and
anti-abuse workflows.)&lt;/p>
&lt;p>The approach we ended up choosing was to re-write the dump file before importing,
so that a hypothetical account called &lt;code>$Name&lt;/code> would be turned &lt;code>$Name~wikipesija.org&lt;/code>
after the import.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> We also created empty user accounts that would take
ownership of the edits to be imported so that we could use the standard account
management tools on them later on. MediaWiki supports importing contributions
without a local account to attribute them to, but it doesn't seem to be possible
to convert an imported actor&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> to a regular user later on which we wanted
to keep as a possibility, even with the minor downside of creating a few hundred
users that'll likely never get touched again later.&lt;/p>
&lt;p>We also made specific decisions to add the username suffix to everyone, not to
just those names that'd conflicted with existing SUL accounts, and to deal with
renaming users that wanted their contributions linked to an existing SUL account
only after the import. This both reduced complexity and thus risk from the
import phase, which already had much more unknowns compared to the rest of the
process, but also were much better options ethically as well: suffixing all names
meant we would not imply that those people chose to be Wikimedians with those
specific usernames (when in reality it was us choosing to import those edits to
the Wikimedia universe), and doing renames using the standard MediaWiki account
management tooling meant that it produced the normal public log entries that
all other MediaWiki administrative actions create.&lt;/p>
&lt;p>With all of the edits imported, the only major thing remaining was doing those merges
I mentioned earlier to attribute imported edits to people's existing SUL accounts.
Thankfully, the local account -based system makes it actually pretty simple. Usually
CentralAuth prevents renaming individual local accounts that are attached to a global
account, but that check can be bypassed with a maintenance script or a privileged
enough account. Renaming the user automatically detached it from the previous global
account, after which an another maintenance script could be used to attach the
user to the correct global account.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>That external site was a fork of a fork of the original Toki Pona Wikipedia
that was closed in 2005. And because &lt;a href="https://www.w3.org/Provider/Style/URI">cool URIs don't change&lt;/a>,
we made the the URLs that the old Wikipedia was using work again. Try it: &lt;a href="https://art-tokipona.wikipedia.org">https://art-tokipona.wikipedia.org&lt;/a>.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>&lt;code>wikipesija.org&lt;/code> was the domain where the old third-party wiki was hosted on, and
&lt;code>~&lt;/code> was used as a separator character in usernames during the
&lt;a href="https://www.mediawiki.org/wiki/SUL_finalisation">SUL finalization&lt;/a> in the early
2010s so using it here felt appropriate as well.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>An &lt;a href="https://www.mediawiki.org/wiki/Manual:Actor_table">actor&lt;/a> is a MediaWiki term and a
database table referring to anything that can do edits or logged actions. Usually an actor
is a user account or an IP address, but an imported user name in a specific format can
also be represented as an actor.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Tracking my train travel by parsing tickets in emails</title><link>https://taavi.wtf/posts/parsing-train-ticket-emails/</link><pubDate>Sat, 05 Jul 2025 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/parsing-train-ticket-emails/</guid><description>&lt;p>Rumour has it that I might be a bit of a train nerd. At least I want to
collect various nerdy data about my travels. Historically that data has
lived in manual form in several places,&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> but over the past year and
a half I've been working on a toy project to collect most of that
information into a custom tool.&lt;/p>
&lt;p>That toy project&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> uses various sources to get information about
trains to fill up its database: for example, in Finland Fintraffic, the
organization responsible for railway traffic management, publishes very
comprehensive &lt;a href="https://www.digitraffic.fi/en/railway-traffic/">open data&lt;/a> about almost everything that's moving on the
Finnish railway network. Unfortunately, I cannot be on all of the
trains.&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup> Thus I need to tell the system details about my journeys.&lt;/p>
&lt;p>The obvious solution is to make a form that lets me save that data.
Which I did, but I got very quickly bored of filling out that form, and
as regular readers of this blog know, there is no reason to settle for
a simple but boring solution when the alternative is to make something
that is ridiculously overengineered.&lt;/p>
&lt;h2 id="parsing-data-out-of-my-train-tickets">Parsing data out of my train tickets&lt;/h2>
&lt;p>Finnish long-distance trains generally require train-specific seat
reservations, which means &lt;a href="https://en.wikipedia.org/wiki/VR_Group">VR&lt;/a> (the train company) knows which trains
I am on. We just need to find a way to extract that information in some
machine-readable format. So my plan for the ridiculously overengineered
solution was to parse the booking emails to get the details I need.&lt;/p>
&lt;p>Now, VR ticket emails include the data I want in a couple of different
formats: they're included as text in the HTML email body, they're in
the embedded calendar invite, as text in the included PDF ticket, and
encoded in the &lt;a href="https://en.wikipedia.org/wiki/Aztec_Code">Aztec Code&lt;/a> in the included PDF ticket. I chose to
parse the last option with the hopes of building something that could
be ported to parse other operators' tickets with relative ease.&lt;/p>
&lt;p>&lt;p>
&lt;div class="img-realsize-wrapper" style="max-width: 148px;">
&lt;img
src="https://taavi.wtf/img/aztec-example.png"
alt="Example Aztec code"
loading="lazy"
class="img-realsize"
/>
&lt;/div>
&lt;/p>
&lt;div class="img-caption">Example Aztec code&lt;/div>
&lt;/p>
&lt;p>After a bit of digging (thank you to the &lt;a href="https://apps.kde.org/itinerary/">KDE Itinerary&lt;/a> people for
&lt;a href="https://community.kde.org/KDE_PIM/KItinerary/vr.fi_Barcode">documenting this&lt;/a>!) I stumbled upon an &lt;a href="https://en.wikipedia.org/wiki/European_Union_Agency_for_Railways">European Union Agency for
Railways&lt;/a> &lt;a href="https://xkcd.com/2304/">PDF&lt;/a> titled &lt;a href="https://www.era.europa.eu/system/files/2022-11/tap-tsi-technical_document_tap_b_6_0.pdf">ELECTRONIC SEAT/BERTH RESERVATION AND ELECTRONIC
PRODUCTION OF TRANSPORT DOCUMENTS - TRANSPORT DOCUMENTS (RCT2
STANDARD)&lt;/a> which, in its Appendix C.1, describes how the information is
encoded in the code.&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup> (As a side note, various sources call these
codes SSB version 1 codes, although that term isn't used in this
specification. So maybe there are more specifications about the format
that I haven't discovered yet!)&lt;/p>
&lt;p>I then wrote &lt;a href="https://git.majava.org/software/go-ticketparser/">a parser&lt;/a> in Go for the binary data embedded in these
codes. So far it works, although I wouldn't be surprised if there are
some edge cases that it doesn't handle. In particular, the spec
specifies a custom lookup table for converting between text and binary
data, and that only has support for characters 0-9 and A-Z. But Finnish
railway station codes can also use &lt;code>Ä&lt;/code> and &lt;code>Ö&lt;/code>.. maybe I need to buy a
ticket to a station with one of those.&lt;/p>
&lt;h2 id="extracting-barcodes-out-of-emails">Extracting barcodes out of emails&lt;/h2>
&lt;p>A parser just for the binary format isn't enough here if the intended
source input is the emails that VR sends upon making a booking. Time to
write a single-purpose email server! In short, the logic in the server,
again written in Go and with the help of &lt;a href="https://github.com/emersion/go-smtp">go-smtp&lt;/a> and &lt;a href="https://github.com/emersion/go-message">go-message&lt;/a>,
is:&lt;/p>
&lt;ul>
&lt;li>Accept any mail with a reasonable body size&lt;/li>
&lt;li>Process through all body parts&lt;/li>
&lt;li>For all PDF parts, extract all images&lt;/li>
&lt;li>For all images, run them through &lt;a href="https://github.com/zxing-cpp/zxing-cpp">ZXing&lt;/a>&lt;/li>
&lt;li>For all decoded barcodes, try to parse them with my new ticket
parsing library I mentioned earlier&lt;/li>
&lt;li>If any tickets are found, send the data from them and any metadata
to the main backend, which will save them to a database&lt;/li>
&lt;/ul>
&lt;p>The custom mail server exposes an &lt;a href="https://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol">LMTP&lt;/a> interface over TCP for my
internet-facing mail servers to forward to. I chose LMTP for this
because it seemed like a better fit in theory than normal (E)&lt;a href="https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol">SMTP&lt;/a>.
I've since discovered that &lt;code>curl&lt;/code> doesn't support LMTP which makes
development much harder, and in practice there's no benefit of LMTP
here as all mails are being sent to the backend in a single request
regardless of the number of recipients, so maybe I'll migrate it to
regular SMTP at some point.&lt;/p>
&lt;h2 id="side-quest-time">Side quest time&lt;/h2>
&lt;p>The last missing part is automatically forwarding the ticket mails to
the new service. I've routed a dedicated subdomain to the new service,
and the backend is configured to allocate addresses like
&lt;code>i2v44g2pygkcth64stjgyuqz@somedomain.example&lt;/code> for each user. That's
great if we wanted to manually forward mails to the service, but we can
go one step above that. I created a dedicated email alias in my mail
server config that routes both to my regular mailbox and the service
address. That way I can update my VR account to use the alias and have
mails automatically processed while still receiving backup copies of
the tickets (and any other &lt;del>important&lt;/del> mail that VR might send me).&lt;/p>
&lt;p>Unfortunately that last part turns out something that's easier said
than done. Logging in on the website, I'm greeted by this text stating
I need to contact customer service by phone to change the address
associated with my account.&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup> After a bit of digging, I noticed that
the mobile app suggests filling out a feedback form in order to change
the address. So I filled that, and after a day or two I got a &amp;quot;confirm
you want to change your email&amp;quot; mail. Success!&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Including (but not limited to): a page of this website, the notes
app on my phone, and an &lt;a href="https://umap-project.org/">uMap&lt;/a> map.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Which I'm not directly naming here because I still think it needs
a lot more work before being presentable, but if you're really
interested it's not that hard to find out.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Someone should invent human cloning so that we can fix this.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>People who know much more about railway ticketing than I do were
surprised when I told them this format is still in use somewhere.
So, uh, sorry if you were expecting a nice universal worldwide
standard!&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>In case you have not guessed yet, I do not like making phone
calls.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>lua entry thread aborted: runtime error: bad request</title><link>https://taavi.wtf/posts/nginx-lua-runtime-error/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/nginx-lua-runtime-error/</guid><description>&lt;p>The &lt;a href="https://wikitech.wikimedia.org/wiki/Portal:Cloud_VPS">Wikimedia Cloud VPS&lt;/a> &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Using_a_web_proxy_to_reach_Cloud_VPS_servers_from_the_internet">shared web proxy&lt;/a> has an interesting
architecture: the management API writes an entry for each proxy to a
&lt;a href="https://redis.io/">Redis&lt;/a> database, and the web server in use (&lt;a href="https://nginx.org/">Nginx&lt;/a> with Lua support
from &lt;code>ngx_http_lua_module&lt;/code>) looks up the backend server URL from Redis
for each request. This is maybe not how I would design this today, but
the basic design &lt;a href="https://taavi.wtf/posts/cloud-vps-custom-domains/">dates back to 2013&lt;/a> and has served us well ever
since.&lt;/p>
&lt;p>However, with a recent operating system upgrade to Debian 12 (we run
Nginx from the packages in Debian's repositories), we started seeing
mysterious errors that looked like this:&lt;/p>
&lt;pre tabindex="0">&lt;code>2025/04/30 07:24:25 [error] 82656#82656: *5612 lua entry thread aborted: runtime error: /etc/nginx/lua/domainproxy.lua:32: bad request
stack traceback:
coroutine 0:
[C]: in function &amp;#39;set_keepalive&amp;#39;
/etc/nginx/lua/domainproxy.lua:32: in function &amp;#39;redis_shutdown&amp;#39;
/etc/nginx/lua/domainproxy.lua:48: in main chunk, client: [redacted], server: *.wmcloud.org, request: &amp;#34;GET [redacted] HTTP/2.0&amp;#34;, host: &amp;#34;codesearch.wmcloud.org&amp;#34;, referrer: &amp;#34;https://codesearch.wmcloud.org/search/&amp;#34;
&lt;/code>&lt;/pre>&lt;p>The code in question seems straightforward enough:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">function&lt;/span> &lt;span class="nf">redis_shutdown&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">-- Use a connection pool of 256 connections with a 32s idle timeout&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">-- This also closes the current redis connection.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">red&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">set_keepalive&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1000&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">32&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">256&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">-- line 32&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When searching for this error online, you'll end up finding advice like
&amp;quot;the &lt;code>resty.redis&lt;/code> object instance cannot be stored in a Lua variable
at the Lua module level&amp;quot;. However, our code already stores it as a
&lt;code>local&lt;/code> variable:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">redis&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">require&lt;/span> &lt;span class="s1">&amp;#39;nginx.redis&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="n">red&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">redis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">new&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">red&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">set_timeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">red&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">connect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;127.0.0.1&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6379&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Turns out the issue was with the function definition: functions can
also be defined as &lt;code>local&lt;/code>. Without that, something somewhere in some
situations seems to reference the variables from other requests,
instead of using the Redis connection for the current request. (Don't
ask me what changed between Debian 12 and 13 making this only break
now.) So we needed to change our function definition to this instead:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">local&lt;/span> &lt;span class="kr">function&lt;/span> &lt;span class="nf">redis_shutdown&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">-- Use a connection pool of 256 connections with a 32s idle timeout&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">-- This also closes the current redis connection.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">red&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">set_keepalive&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1000&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">32&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">256&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I spent almost an entire workday looking for this, ultimately making a
&lt;a href="https://gerrit.wikimedia.org/r/plugins/gitiles/operations/puppet/+/4cc07a5fedef5776dd7c88d704a5298a0cc4fa78%5E%21/#F0">two-line patch&lt;/a> to fix the issue. Hopefully by publishing this post I
can save that time from everyone else stumbling upon the same problem
after myself.&lt;/p></description></item><item><title>Wikimedia Hackathon Istanbul 2025</title><link>https://taavi.wtf/posts/wikimedia-hackathon-istanbul-2025/</link><pubDate>Sat, 10 May 2025 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/wikimedia-hackathon-istanbul-2025/</guid><description>&lt;p>It's that time of the year again: the &lt;a href="https://www.mediawiki.org/wiki/Wikimedia_Hackathon_2025">Wikimedia Hackathon 2025&lt;/a>
happened last weekend in Istanbul. This year was my third time
attending what has quickly become one of my favourite events of the
year simply due to the concentration of friends and other like-minded
nerds in a single location.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>&lt;img src="https://taavi.wtf/img/wmhack2025-hacking.jpg" alt="Valerio, Lucas, me and a shark.">
&lt;div class="img-caption">&lt;a href="https://commons.wikimedia.org/wiki/File:Wikimedia_Hackathon_2025_-_Day_2_-_CAA_(4).jpg">Image&lt;/a> by &lt;a href="https://commons.wikimedia.org/wiki/User:Chlod">Chlod Alejandro&lt;/a> is licensed under &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/deed.en">CC BY-SA 4.0&lt;/a>.&lt;/div>
&lt;/p>
&lt;p>This year I did &lt;a href="https://phabricator.wikimedia.org/T391137">a short presentation about the MediaWiki packages in
Debian&lt;/a> (&lt;a href="https://people.wikimedia.org/~taavi/presentations/2025-hackathon-debian.pdf">slides&lt;/a>), which is something I do but I suspect is fairly
obscure to most people in the MediaWiki community. I was hoping to do
some work on &lt;a href="https://phabricator.wikimedia.org/T392349">reproducibility of MediaWiki releases&lt;/a>, but other
interests (plus lack of people involved in the release process at the
hackathon) meant that I didn't end up getting any work done on that
(assuming &lt;a href="https://gitlab.wikimedia.org/repos/releng/release/-/merge_requests/169">this&lt;/a> does not count).&lt;/p>
&lt;p>Other long-standing projects did end up getting some work done!
&lt;a href="https://www.mediawiki.org/wiki/User:MusikAnimal">MusikAnimal&lt;/a> and I ended up fixing the &lt;a href="https://meta.wikimedia.org/wiki/Community_Tech/Commons_deletion_notification_bot">Commons deletion notification
bot&lt;/a>, which had been broken for well over two years at that point (and
was at some point in the hackathon plans for &lt;em>last&lt;/em> year for both of us).
Other projects that I made progress on include &lt;a href="https://phabricator.wikimedia.org/T242031">supporting multiple
types of two-factor devices&lt;/a>, and &lt;a href="https://libup.wmcloud.org/">LibraryUpgrader&lt;/a> which gained
support for rebasing and updating existing patches&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>In addition to hacking, the other highlight of these events is the
&lt;a href="https://en.wiktionary.org/wiki/hallway_track">hallway track&lt;/a>. Some of the crowd is people who I've seen at previous
events and/or interact very frequently with, but there are also
significant parts of the community and the Foundation that I don't
usually get to interact with outside of these events. (Although it
still feels extremely weird to heard from various mostly-WMF people
with whom I haven't spoken with before that they've heard various
(usually positive) &lt;del>rumours&lt;/del> stories about me.)&lt;/p>
&lt;p>Unfortunately we did not end up having a &lt;a href="https://meta.wikimedia.org/wiki/Wikimedia_Cuteness_Association">Cuteness Association&lt;/a> meetup
this year, but we had an impromptu PGP key signing party which is
basically almost as good, right?&lt;/p>
&lt;p>However, I did continue a tradition from last year: I ended up
nominating &lt;a href="https://www.mediawiki.org/wiki/User:Chlod">Chlod&lt;/a>, a friend of mine, to receive &lt;a href="https://www.mediawiki.org/wiki/+2">+2 access&lt;/a> to
&lt;code>mediawiki/*&lt;/code> during the hackathon. The request is due to be closed
sometime tomorrow.&lt;/p>
&lt;p>(Usual disclosure: My travel was funded by the Wikimedia Foundation.
Thank you! This is my personal blog and these are my own opinions.)&lt;/p>
&lt;p>Now that you've read this post, maybe check out posts &lt;a href="https://www.mediawiki.org/wiki/Wikimedia_Hackathon_2025/Documentation#Blog_posts">from others&lt;/a>?&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Unfortunately you can never have absolutely everyone attending :(&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Amir, I still have not forgiven you about this.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Writing a custom rsync server to automatically update a static site</title><link>https://taavi.wtf/posts/custom-rsync-server-static-site/</link><pubDate>Wed, 09 Apr 2025 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/custom-rsync-server-static-site/</guid><description>&lt;p>Inspired by some friends,&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> I too wanted to make a tiny website telling which
event I am at this exact moment. Thankfully I already had an another toy project
with that information easily available, so generating the web page was a matter
of just querying that project's API and feeding that data to a HTML template.&lt;/p>
&lt;p>Now the obvious way to host that would be to hook up the HTML-generating code
to a web server, maybe add some caching for the API calls, and then route
external HTTPS traffic to it. However, that'd a) require that server to be
constantly available to serve traffic, and b) be boring.&lt;/p>
&lt;p>For context: I have an existing setup called &lt;code>staticweb&lt;/code>, which is effectively
a fancy name for a couple of &lt;a href="https://en.wikipedia.org/wiki/Puppet_(software)">Puppet&lt;/a>-managed servers that run Apache httpd to
serve static web pages and have a bunch of systemd timers running &lt;code>rsync&lt;/code> to
ensure they're serving the same content. It works really well and I use it for
things ranging from &lt;a href="https://taavi.wtf">my website&lt;/a> or &lt;a href="https://whyisbetabroken.com">whyisbetabroken.com&lt;/a> to things like my
&lt;a href="https://apt.majava.org">internal apt repository&lt;/a>.&lt;/p>
&lt;p>Now, there are two ways to get new content into that mechanism: it can be
manually pushed in from e.g. a CI job, or the system can be configured to
periodically pull it from a separate server. The latter mechanism was initially
created so that I could pull the Debian packages from my separate &lt;a href="https://wiki.debian.org/DebianRepository/SetupWithReprepro">reprepro&lt;/a>
server into the staticweb setup. It turns out that the latter makes a really
neat method for handling other dynamically-generated static sites as well.&lt;/p>
&lt;p>So, for my &amp;quot;&lt;a href="https://miss%C3%A4on.taaviv%C3%A4%C3%A4n%C3%A4nen.fi/">where is Taavi at&lt;/a>&amp;quot; site, I ended up writing the &lt;a href="https://git.majava.org/software/thereis/">server part&lt;/a> in
Go, and included an rsync server using the &lt;a href="https://github.com/gokrazy/rsync">gokrazy/rsync&lt;/a> package. Initially I
just implemented a static temporary directory with a timer to regularly update
the HTML file in it, but then I got an even more cursed idea: what if the HTML
was dynamically generated when an rsync client connected to the server? So I
did just that.&lt;/p>
&lt;p>For deployment, I slapped the entire server part in a container and deployed it
to my Kubernetes cluster. The rsync server is exposed directly as &lt;a href="https://git.majava.org/config/k8s-deploy/tree/charts/thereis/templates/service-rsync.yaml">a service&lt;/a>
to my internal network with no authentication or encryption - I think that's
fine since that's a read-only service in a private LAN and the resulting HTML
is going to be publicly exposed anyway. (Thanks to some &lt;a href="https://taavi.wtf/posts/hetzner-auto-revdns/#generating-dns-zones-the-hard-way">DNS magic&lt;/a>, just
creating a &lt;code>LoadBalancer&lt;/code> Service object with a special annotation is enough to
have a DNS name provisioned for the assigned IP address, which is neat.)&lt;/p>
&lt;p>Overall the setup works nice, at least for now. I need to add some sort of a
cache to not fetch unchanged information from the API since for every update.
And I guess I could write some cursed rsyncd reverse proxy with per-module
rules if I end up creating more sites like this to avoid creating new
LoadBalancer services for each of them.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Mostly from Sammy's &lt;a href="https://where.fops.at">where.fops.at&lt;/a>.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Automatically updating reverse DNS entries for my Hetzner servers</title><link>https://taavi.wtf/posts/hetzner-auto-revdns/</link><pubDate>Fri, 03 Jan 2025 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/hetzner-auto-revdns/</guid><description>&lt;p>Some parts of my infrastructure run on &lt;a href="https://www.hetzner.com/">Hetzner&lt;/a> dedicated servers.
Hetzner's management console has an interface to update reverse DNS
entries, and I wanted to automate that. Unfortunately there's no option
to just delegate the zones to my own authoritative DNS servers. So I
did the next best thing, which is updating the Hetzner-managed records
with data from my own authoritative DNS servers.&lt;/p>
&lt;h2 id="generating-dns-zones-the-hard-way">Generating DNS zones the hard way&lt;/h2>
&lt;p>The first step of automating DNS record provisioning is, well, figuring
out which records need to be provisioned. I wanted to re-use my
existing automation for generating the record data, instead of coming
up with a new system for these records. The basic summary is that
there's a Go program creatively named &lt;a href="https://git.majava.org/software/dnsgen/">&lt;code>dnsgen&lt;/code>&lt;/a> that's in charge of
generating zone file snippets from various sources (these include
Netbox, Kubernetes, PuppetDB and my custom reverse web proxy setup).&lt;/p>
&lt;p>Those snippets are combined with &lt;a href="https://git.majava.org/config/dns/tree/zones">Jinja templates&lt;/a> to generate full
zone files to be loaded to a hidden primary running Bind9 (like all
other DNS servers I run). The zone files are then transferred to a
fleet of internal authoritative servers as well as my public
authoritative DNS server, which in turn transfers them to various other
authoritative DNS servers (like &lt;a href="https://ns-global.zone/">ns-global&lt;/a> and &lt;a href="https://www.traficom.fi/en/anycast-dns-registrars">Traficom anycast&lt;/a>) for
redundancy.&lt;/p>
&lt;p>There's also a bunch of other smaller features, like using Bind views
to server different data to internal and external clients, and
resolving external records during record generation time to be used on
apex records that would use &lt;a href="https://en.wikipedia.org/wiki/CNAME_record">CNAME&lt;/a> records if they could. (The latter
is a workaround for &lt;a href="https://masto.host">Masto.host&lt;/a>, the hosting provider we use for
&lt;a href="https://wikis.world">Wikis World&lt;/a>, not having a stable IPv6 address.) Overall it's a really
nice system, and I've spent quite a bit of time on it.&lt;/p>
&lt;h2 id="updating-records-on-hetzner-managed-space">Updating records on Hetzner-managed space&lt;/h2>
&lt;p>As mentioned above, Hetzner unfortunately does not support custom DNS
servers for reverse records on IP space rented from them. But I wanted
to use my existing, perfectly working DNS record generation setup since
that works perfectly fine. So the obvious answer is to (ab)use &lt;a href="https://en.wikipedia.org/wiki/DNS_zone_transfer">DNS zone
file transfers&lt;/a>.&lt;/p>
&lt;p>I quickly wrote a &lt;a href="https://git.majava.org/software/hetzner-auto-revdns/">few hundred lines of Go&lt;/a> to request the zone data
and then use the Hetzner robot API to ensure the reverse entries are in
sync. The main obstacle hit here was the Hetzner API somehow requiring
an &amp;quot;update&amp;quot; call (instead of a &amp;quot;create&amp;quot; one) to create a new record, as
the create endpoint was returning an HTTP 400 response no matter what.
Once I sorted that out, the script started working fine and created the
few dozen missing records. Finally I added a CronJob in my Kubernetes
cluster to run the script once in a while.&lt;/p>
&lt;p>Overall this is a big improvement over doing things by hand and didn't
require that much effort. The obvious next step would be to expand the
script to a tiny DNS server capable of receiving zone update NOTIFYs to
make the updates happen real-time. Unfortunately there's now no hiding
of the records revealing my &lt;del>ugly hacks&lt;/del> clever networking solutions
:(&lt;/p></description></item><item><title>Custom domains on the Wikimedia Cloud VPS web proxy</title><link>https://taavi.wtf/posts/cloud-vps-custom-domains/</link><pubDate>Fri, 01 Nov 2024 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/cloud-vps-custom-domains/</guid><description>&lt;p>The &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Using_a_web_proxy_to_reach_Cloud_VPS_servers_from_the_internet">shared web proxy&lt;/a> used on &lt;a href="https://wikitech.wikimedia.org/wiki/Portal:Cloud_VPS">Wikimedia Cloud VPS&lt;/a> now has technical
support for &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Using_a_web_proxy_to_reach_Cloud_VPS_servers_from_the_internet#Vanity_domains">using arbitrary domains&lt;/a> (and not just &lt;code>wmcloud.org&lt;/code>
subdomains) in proxy names. I think this is a good example of how
software slowly evolves over time as new requirements emerge, with each
new addition building on top of the previous ones.&lt;/p>
&lt;p>According to the &lt;a href="https://wikitech.wikimedia.org/w/index.php?title=Help:Using_a_web_proxy_to_reach_Cloud_VPS_servers_from_the_internet&amp;amp;action=history">edit history&lt;/a> on Wikitech, the web proxy service has
its origins in 2012, although the current idea where you create a proxy
and map it to a specific instance and port was only introduced a year
later. (Before that, it just directly mapped the subdomain to the VPS
instance with the same name).&lt;/p>
&lt;p>There were some smaller changes in the coming years like the migration
to &lt;a href="https://wikitech.wikimedia.org/wiki/Acme-chief">acme-chief&lt;/a> for TLS certificate management, but the overall logic
stayed very similar until 2020 when the &lt;code>wmcloud.org&lt;/code> domain was
introduced. That was implemented by adding a config option listing all
possible domains, so future domain additions would be as simple as
adding the new domain to that list in the configuration.&lt;/p>
&lt;p>Then the changes start becoming more frequent:&lt;/p>
&lt;ul>
&lt;li>In 2022, for my &lt;a href="https://taavi.wtf/posts/cloud-vps-terraform/">Terraform support project&lt;/a>, a bunch of logic,
including the list of supported backend domains was moved from the
frontend code to the backend. This also made it possible to
dynamically change which projects can use which domains suffixes for
their proxies.&lt;/li>
&lt;li>Then, early this year, I added support for zones restricted to a
single project, because we wanted to use the proxy for the
&lt;code>*.svc.toolforge.org&lt;/code> Toolforge infrastructure domains instead of
coming up with a new system for that use case. This also added suport
for using different TLS certificates for different domains so that we
would not have to have a single giant certificate with all the names.&lt;/li>
&lt;li>Finally, the last step was to add two new features to the proxy
system: support for adding a proxy at the &lt;a href="https://en.wikipedia.org/wiki/Subdomain">apex&lt;/a> of a domain, as well
as support for domains that are not managed in Designate (the Cloud
VPS/OpenStack auth DNS service). In addition, we needed a bit of
config to ensure &lt;a href="https://letsencrypt.org/docs/challenge-types/#http-01-challenge">http-01&lt;/a> challenges get routed to the acme-chief
instance.&lt;/li>
&lt;/ul></description></item><item><title>Bulk downloading Wikimedia Commons categories</title><link>https://taavi.wtf/posts/comload/</link><pubDate>Sun, 13 Oct 2024 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/comload/</guid><description>&lt;p>&lt;a href="https://commons.wikimedia.org/">Wikimedia Commons&lt;/a>, the Wikimedia project for freely licensed media
files, also contains a bunch of photos by me and photos of me at
various events. While I don't think Commons is going away anytime soon,
I would still like to have a local copy of those images available on my
own storage hardware.&lt;/p>
&lt;p>Obviously this requires some way to query for photos you want to
download. I'm using Commons categories for this, since that's easy to
implement and works for both use cases. The Commons community tends to
come up with &lt;a href="https://commons.wikimedia.org/wiki/Category:Number_456_on_trams">very specific categories&lt;/a> that you can use, and if not, you
can usually categorize the files yourself.&lt;/p>
&lt;p>&lt;p>
&lt;div class="img-realsize-wrapper" style="max-width: 400px;">
&lt;img
src="https://taavi.wtf/img/commons-coi-shh.png"
alt="Me replying &amp;#39;shh&amp;#39; to a Discord message showing myself categorizing photos about me and accusing me of COI editing"
loading="lazy"
class="img-realsize"
/>
&lt;/div>
&lt;/p>
&lt;div class="img-caption">thankfully Commons has no such thing as a Conflict of interest (COI) policy&lt;/div>
&lt;/p>
&lt;p>There is almost an existing tool for this: &lt;a href="https://samwilson.id.au/">Sam Wilson&lt;/a>'s mwcli project
has support for &lt;a href="https://samwilson.id.au/Backing_up_(my)_Commons_files">exporting images one has uploaded to Commons&lt;/a>. However
I couldn't use that to upload photos of me others have uploaded, plus
it's written in PHP and I don't exactly want to deal with the problem
of figuring out how to package it in a way I could neatly install it on
my NAS.&lt;/p>
&lt;p>So I wrote my own tool for it, called &lt;a href="https://git.majava.org/software/comload/about/.">&lt;code>comload&lt;/code>&lt;/a>. It's written in Python
because Python is easy to deploy (I can just throw it in a &lt;code>.deb&lt;/code> and
upload it to &lt;a href="https://apt.majava.org/">my internal repository&lt;/a>), and because I did not find a Go
library to handle Action API pagination for me. The basic usage is
like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell-session" data-lang="shell-session">&lt;span class="line">&lt;span class="cl">&lt;span class="gp">$&lt;/span> comload --subcats &lt;span class="s2">&amp;#34;Taavi Väänänen&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This will download any files in &lt;a href="https://commons.wikimedia.org/wiki/Category:Taavi_V%C3%A4%C3%A4n%C3%A4nen">Category:Taavi Väänänen&lt;/a> and its
sub-categories to the current directory. Former image versions, as well
as the image description and &lt;a href="https://commons.wikimedia.org/wiki/Commons:Structured_data">SDC data&lt;/a>, if any, is also included. And
it's smart enough to not download any files that are already there on
future runs, so you can just throw it in a &lt;a href="https://wiki.archlinux.org/title/Systemd/Timers">systemd timer&lt;/a> to get any
future files. I'd still like it to handle moved files without creating
a duplicate copy, but otherwise I'm really happy with the current
state.&lt;/p>
&lt;p>&lt;code>comload&lt;/code> is available from &lt;a href="https://pypi.org/project/comload/">PyPI&lt;/a> and from &lt;a href="https://git.majava.org/software/comload/">my Git server&lt;/a> directly,
and is licensed under the GPLv3.&lt;/p></description></item><item><title>Wikimedia Hackathon Tallinn 2024</title><link>https://taavi.wtf/posts/wikimedia-hackathon-tallinn-2024/</link><pubDate>Tue, 14 May 2024 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/wikimedia-hackathon-tallinn-2024/</guid><description>&lt;p>&lt;a href="https://www.mediawiki.org/wiki/Wikimedia_Hackathon_2024">This year's Wikimedia Hackathon&lt;/a> was held in early May in Tallinn,
Estonia. Like &lt;a href="https://taavi.wtf/posts/wikimedia-hackathon-athens-2023/">last year&lt;/a>, it was a great opportunity to both see
people I work with regularly, including people in &lt;a href="https://www.mediawiki.org/wiki/Wikimedia_Cloud_Services_team">my own team&lt;/a> that I
had not seen in person before, and to work with and help people that I
have had very limited interactions with before.&lt;/p>
&lt;p>&lt;img src="https://taavi.wtf/img/wmhack2024-hacking.jpg" alt="Me talking with Addshore at the Wikimedia Hackathon 2024 hacking room.">
&lt;div class="img-caption">&lt;a href="https://commons.wikimedia.org/wiki/File:Wikimedia_Hackathon_2024-30-.jpg">Image&lt;/a> by &lt;a href="https://commons.wikimedia.org/wiki/User:OlariP">Olari Pilnik&lt;/a> is licensed under &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/deed.en">CC BY-SA 4.0&lt;/a>.&lt;/div>
&lt;/p>
&lt;p>I presented &lt;a href="https://phabricator.wikimedia.org/T361309">a session about Puppet&lt;/a> (&lt;a href="https://people.wikimedia.org/~taavi/presentations/2024-hackathon-puppet.pdf">slides&lt;/a>), the configuration
management tool used on Wikimedia infrastructure (and some other
projects I've been involved on) which I think went quite well. I
also organized (read: picked a spot for in the schedule) &lt;a href="https://phabricator.wikimedia.org/T363870">the
cuteness meetup&lt;/a>.&lt;/p>
&lt;p>In addition to the sessions, the main focus of the event was, of
course, hacking. As usual, I didn't make any major plans beforehand,
and instead ended up working on several smaller projects as they popped
up.&lt;/p>
&lt;p>Here is a list of things I can remember working on:&lt;/p>
&lt;ul>
&lt;li>I fixed several small issues in &lt;a href="https://libraryupgrader2.wmcloud.org/">LibUp&lt;/a> that makes it pass on more
MediaWiki repositories (including &lt;code>core.git&lt;/code>). James and I also
migrated the LibUp configuration to GitLab.&lt;/li>
&lt;li>I finished up &lt;a href="https://gitlab.wikimedia.org/repos/ci-tools/banana-checker/-/merge_requests/10">an MR to grunt-banana-checker&lt;/a> to add support for
automatically fixing some common issues that were causing LibUp
failures and to fix some minor bugs.&lt;/li>
&lt;li>I worked with &lt;a href="https://www.mediawiki.org/wiki/User:PMiazga_(WMF)">Piotr&lt;/a> to get some of my patches to the &lt;a href="https://www.mediawiki.org/wiki/Extension:OATHAuth">OATHAuth&lt;/a> and
&lt;a href="https://www.mediawiki.org/wiki/Extension:WebAuthn">WebAuthn&lt;/a> MediaWiki extensions merged. This is a part of my project
to add support for more than one two-factor authentication device at
a time that I was also working on during the &lt;a href="https://taavi.wtf/posts/wikimania-singapore-2023/">Wikimania 2023&lt;/a>
hackathon. Next up on this project is writing some UI code.&lt;/li>
&lt;li>I fixed &lt;a href="https://www.mediawiki.org/wiki/Gerrit">Wikimedia Gerrit&lt;/a> twice after it had some issues that needed
SRE intervention.&lt;/li>
&lt;li>I sent a patch to Wikimedia's Phabricator/Phorge fork to &lt;a href="https://phabricator.wikimedia.org/T364239">add a new
fox token&lt;/a>. This ended up being deployed on Sunday and I got to
showcase this during the hackathon showcase.&lt;/li>
&lt;li>Reedy and I implemented support for foxes in &lt;a href="https://www.mediawiki.org/wiki/Extension:WikiLove">WikiLove&lt;/a>. I also wrote
&lt;a href="https://toolsadmin.wikimedia.org/tools/id/fox-bot">a bot to spam foxes&lt;/a> to Sammy's talk pages on the beta cluster.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>
(This also involved a fun side quest to get a working thumbnail for
the fox image we used to show up on Beta since the thumbnailing there
&lt;a href="https://whyisbetabroken.com/">is broken&lt;/a>.)&lt;/li>
&lt;li>I removed some deprecated code from core to earn the MediaWiki track
T-shirt. I also reviewed a bunch of patches by others trying to earn
that T-shirt.&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>&lt;/li>
&lt;li>I found and reported &lt;a href="https://phabricator.wikimedia.org/T364228">some&lt;/a> &lt;a href="https://phabricator.wikimedia.org/T364229">bugs&lt;/a> relating to Parsoid read views on
Commons.&lt;/li>
&lt;li>I processed some &lt;a href="https://toolforge.org/">Toolforge&lt;/a> account approval requests and
&lt;a href="https://wmcloud.org/">Cloud VPS&lt;/a> project requests. I also helped some people debug some
Cloud VPS issues.&lt;/li>
&lt;li>I helped Bryan debug and fix &lt;a href="https://phabricator.wikimedia.org/T363877">an issue with HTTP/1.1 streams&lt;/a> through
the Toolforge front proxy.&lt;/li>
&lt;li>I made some queries on the &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Wiki_Replicas">Wiki Replicas&lt;/a> accidentally very slow and
then &lt;a href="https://phabricator.wikimedia.org/T364151">fixed them to be fast again&lt;/a> on the next day.&lt;/li>
&lt;li>Got a 100% helpful, harmless, useful, etc. patch merged to something.
I will provide no more details on this one.&lt;/li>
&lt;/ul>
&lt;p>Finally, a conversation I had at the hackathon resulted in me
&lt;a href="https://phabricator.wikimedia.org/T364531">nominating&lt;/a> Novem Linguae for mediawiki/* +2 access a few days after
the hackathon.&lt;/p>
&lt;p>I had a great time, and the ferry trip to Tallinn was much nicer than
the very early flight I had last year. I can't wait to see you all
again :-)&lt;/p>
&lt;p>Disclosure: I am currently a Wikimedia Foundation contractor, and the
Foundation did pay for my travel to Tallinn. This is my personal blog
and these are my own opinions.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Since backporting this change felt too risky to do on the weekend, and also I have a feeling I'd get in troble if I ran an unapproved bot that edited on random wikis on our production wiki farm.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Anyone who got 5 or more patches to &lt;code>core.git&lt;/code> merged during the Hackathon got a cool MediaWiki T-shirt.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Wikimania 2023</title><link>https://taavi.wtf/posts/wikimania-singapore-2023/</link><pubDate>Mon, 28 Aug 2023 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/wikimania-singapore-2023/</guid><description>&lt;p>I've returned from (very hot and humid) Singapore where &lt;a href="https://wikimania.wikimedia.org/wiki/2023:Wikimania">Wikimania&lt;/a> was
held this year. The primary reason I was there on-site was the
&lt;a href="https://en.wikipedia.org/wiki/Wikimedian_of_the_Year">Wikimedian of the Year&lt;/a> stuff (I was &lt;a href="https://diff.wikimedia.org/2022/08/14/celebrating-the-2022-wikimedians-of-the-year/">awarded&lt;/a> the tech award last
year), but the rest of the conference was great too.&lt;/p>
&lt;p>&lt;img src="https://taavi.wtf/img/wikimania-2023-awards.jpg" alt="Past Technical Contributors of the Year at Wikimania 2023">
&lt;div class="img-caption">&lt;a href="https://meta.wikimedia.org/wiki/User:Jayprakash12345">Jay&lt;/a> and I on-stage for the awards ceremony. &lt;a href="https://commons.wikimedia.org/wiki/File:Technical_Contributors_of_the_year_Wikimania_2023.jpg">Image&lt;/a> by &lt;a href="https://commons.wikimedia.org/wiki/User:ZMcCune_(WMF)">Zack McCune&lt;/a>, &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/deed.en">CC BY-SA 4.0&lt;/a>.&lt;/div>
&lt;/p>
&lt;p>Unlike the main Wikimedia Hackathon (which &lt;a href="https://taavi.wtf/posts/wikimedia-hackathon-athens-2023/">I attended too this year&lt;/a>),
Wikimania is primarly not a technical event. This resulted in several
unexpected situations where I had to explain what &lt;a href="https://wmcloud.org">Cloud Services&lt;/a> is
and I actually do,&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> and also gave me a much better understanding
about all of the other background work going on to improve the wikis.
We also had a &lt;a href="https://pretalx.com/wm2023/talk/KNXDBK/">panel about how we run WMCS collaboratively&lt;/a>.&lt;/p>
&lt;h2 id="wikimedian-of-the-year-ceremony">Wikimedian of the Year ceremony&lt;/h2>
&lt;p>The award ceremony was definitely the highlight of my trip. The stage
was much larger than I expected, thankfully you couldn't really see the
size of the audience from up there. Everything went mostly well, except
that there was some confusion on which microphone(s) to use and I
almost tripped on the stairs when walking up.&lt;/p>
&lt;p>Congrats to this year's winners! The surprise was mostly ruined for me,
thanks to a calendar invite that leaked the winners a few days earlier
than planned. At least the rehearsal schedule kept changing so you
still had a mystery (whether you were in the right place at the right
time or not) to be excited about. And then there was the challenge of
not being suspicious when leaving the hackathon table to go to a
rehearsal without revealing the other, not-yet-announced winner at the
same table.&lt;/p>
&lt;p>It was quite funny to see people editing the &lt;a href="https://en.wikipedia.org/wiki/Wikimedian_of_the_Year">Wikipedia article about
the award&lt;/a> live during the ceremony. (I believe this was shown on
&lt;a href="https://en.wikipedia.org/wiki/Depths_of_Wikipedia">Depths of Wikipedia&lt;/a> too, it was me who showed it to Annie. And on the
same subject, the &amp;quot;Depths of Wikimania&amp;quot; spin-off at the closing
ceremony was great!)&lt;/p>
&lt;p>And yes, my name was mispronounced a few times. I'm kind of used to
that by now.&lt;/p>
&lt;h2 id="hackathon">Hackathon&lt;/h2>
&lt;p>As expected I spent a large portition of my time during the conference
somewhere around the hackathon room. I didn't have any plans on what I
wanted to do beforehand (as those never end up matching reality), and
instead planned to just show up and find something to do after talking
to people. This ended up working nicely, and my primary project during
the hackathon was to pick up my work from some months ago on improving
the &lt;a href="https://www.mediawiki.org/wiki/Extension:OATHAuth">OATHAuth extension&lt;/a> by making it possible to have &lt;a href="https://phabricator.wikimedia.org/T242031">multiple
different two-factor devices&lt;/a>.&lt;/p>
&lt;p>I did &lt;a href="https://gerrit.wikimedia.org/r/c/mediawiki/extensions/OATHAuth/+/948990/">some&lt;/a> &lt;a href="https://gerrit.wikimedia.org/r/c/mediawiki/extensions/OATHAuth/+/949188/">fixes&lt;/a> to make it possible to run the already-existing
migration script&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> in Wikimedia production, and after those were
reviewed and merged I changed the &lt;a href="https://gerrit.wikimedia.org/r/c/operations/mediawiki-config/+/949161/">first&lt;/a> &lt;a href="https://gerrit.wikimedia.org/r/c/operations/mediawiki-config/+/949629/">wikis&lt;/a> to &lt;code>WRITE_BOTH&lt;/code> mode.
I'm expecting to continue work on this even after I return back home.
It even got included in the &lt;a href="https://commons.wikimedia.org/wiki/File:Wikimania_Singapore_2023_illustration.jpg">very cute Wikimania illustration&lt;/a> as did
all other projects shown during the hackathon showcase.&lt;/p>
&lt;p>In addition I helped several people with several other projects and
reviewed some patches. I'm also taking credit for the slide layout used
on the hackathon showcase which ensured that each project had at least
one slide (to avoid confusion) and that all projects used differently
colored slides than projects before and after it.&lt;/p>
&lt;p>Unfortunately my impression was that there weren't that many people at
the hackathon who were completely new to the technical scene; I did
meet a bunch of people that weren't in Athens but most of those still
seemed like people who were already involved in one way or another.
Although there were some folks who were mostly active on user scripts
and other on-wiki activities, and I hope I managed to convince some of
those folks to try working on MediaWiki itself too. In my view getting
existing (power) users involved is one of the best ways to recruit new
long-term contributors and maintainers.&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>&lt;/p>
&lt;p>I also used the opportunity to have a chat with with &lt;a href="https://meta.wikimedia.org/wiki/User:SDeckelmann-WMF">Selena
Deckelmann&lt;/a> (the WMF's current CPTO). I'm not going to go into detail
here, but I'm quite excited about some of the plans we talked about.&lt;/p>
&lt;h2 id="other">Other&lt;/h2>
&lt;p>Travelling there and back was very smooth:&lt;/p>
&lt;ul>
&lt;li>I definitely &lt;a href="https://wikis.world/@taavi/110881544057143416">was way too early&lt;/a> at the airport when flying there.
When you have a morning flight you can just sleep longer and not be
that early, but I had an evening flight this time (the last
departure of the evening, in fact) and I just couldn't wait at home
doing nothing. I guess I could at least have taken the train the
wrong way around the loop and wasted time that way.&lt;/li>
&lt;li>Helsinki Airport was great as usual, although I wish there would have
been more places to eat on the non-Schengen side. Singapore (Changi)
was nice too, although the security-at-the-gate model was a bit
unusual and the arrival security checks for the gate next to ours was
causing quite a bit of confusion.&lt;/li>
&lt;li>The ~13 hour flights were quite long, and about double the length of
the previous longest flight for me. I did survive, though, partially
thanks to the WiFi that was at the same time very slow by modern
standards but still very impressive for a metal tube that high and
fast.&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>&lt;/li>
&lt;li>I still had a direct flight, which was definitely appreciated
especially after seeing several people from Western Europe connect
here in Helsinki.&lt;/li>
&lt;li>Singapore border control was much simpler than I expected. (A benefit
of being an EU citizen, it seems.) I'm pretty sure it took me more
time to leave the Schengen zone than it took for me to enter
Singapore as the gates here in Helsinki require you to take your hat
and glasses off but the ones in Singapore do not.&lt;/li>
&lt;li>The day I was flying back happened to be my birthday. So naturally we
found some cake at the airport. :-P&lt;/li>
&lt;/ul>
&lt;p>I'm pretty sure this was the closest I've ever been to a &lt;a href="https://wikitech.wikimedia.org/wiki/Data_centers">Wikimedia
data center&lt;/a>. We didn't get a tour of it, but at least logged-out page
views were incredibly fast.&lt;/p>
&lt;p>I continued &lt;a href="https://www.wikidata.org/w/index.php?title=Q118521440&amp;amp;oldid=1899877464">the trend&lt;/a> of getting other people to do &lt;a href="https://en.wikipedia.org/wiki/Wikipedia:Conflict_of_interest">COI&lt;/a> editing for
me, this time with a &lt;a href="https://commons.wikimedia.org/wiki/Category:Taavi_V%C3%A4%C3%A4n%C3%A4nen">Commons category&lt;/a>.&lt;/p>
&lt;p>Thank you to the Wikimedia Foundation for providing me a scholarship to
attend this year, especially for the no-application-required process
as a last year's Wikimedian of the Year winner.&lt;/p>
&lt;p>Wikimania will be in Kraków &lt;a href="https://wikimania.wikimedia.org/wiki/2024:Wikimania">next year&lt;/a>. I believe I won't be able to
attend due to IRL stuff going on at the same time, but there should be
more Wikimanias after that :-P. And there are Wikimedia Hackathons and
other conferences of course.&lt;/p>
&lt;p>Now I need a next event/trip to be excited about as I currently have
nothing planned. &lt;a href="https://en.wikipedia.org/wiki/FOSDEM">FOSDEM&lt;/a> in February of next year maybe? It should
even be possible to travel there by land, which would be nice
considering how much I've flown over the last half a year or so.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>A good reminder about living in a bubble if you only hang out in the technical channels, I guess.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>Which was already included included in a MediaWiki release before being run on Wikimedia production. Oops.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Currently &lt;a href="https://www.mediawiki.org/wiki/New_Developers">[[New Developers]]&lt;/a> and related pages seem to mostly focus on completely new contributors that don't have an idea what they want to work on, so I might need to do some marketing for my view at some point.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Although a speedtest became suspiciously slower several orders of magnitude slower after enabling a VPN.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Finding files not managed by Puppet</title><link>https://taavi.wtf/posts/puppet-unmanaged/</link><pubDate>Sat, 01 Jul 2023 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/puppet-unmanaged/</guid><description>&lt;p>With &lt;a href="https://www.puppet.com/docs/puppet/">Puppet&lt;/a>, it's fairly typical
to have a directory where all files not managed by Puppet will be
automatically purged, as that helps to ensure consistency between
servers. For example, you could do something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-puppet" data-lang="puppet">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># @summary manages the ferm firewall frontend&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">class&lt;/span> &lt;span class="na">ferm&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="c">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"> # install package
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"> # manage main ferm.conf file&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="k">file&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s">&amp;#39;/etc/ferm/ferm.d&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="na">ensure&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">directory&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="na">owner&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s">&amp;#39;root&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="na">group&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s">&amp;#39;adm&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="na">mode&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="s">&amp;#39;0755&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="na">recurse&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="na">force&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="na">purge&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">true&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="k">notify&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">Service&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">&amp;#39;ferm&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="p">}&lt;/span>&lt;span class="c">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"> # manage service, etc&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>However, sometimes you end up with a directory that's not managed this
way and you want convert it to a fully managed directory. This is a bit
risky: there's a possibility that some hosts have stale files in that
directory and removing those could break things. And you couldn't use
PuppetDB to search for unmanaged files, either, as the unmanaged files
are by definition not in the Puppet state stored there.&lt;/p>
&lt;p>To solve this, I wrote a tiny Python script that compares a directory
on disk with the Puppet state file. On it's own it's not very useful,
as it only works with the local system, but it really becomes useful
when paired with a tool like &lt;a href="https://wikitech.wikimedia.org/wiki/Cumin">Cumin&lt;/a> to run it on many servers. Cumin
can be integrated with PuppetDB too, so you can run the command on
exact set of servers a potential change would affect.&lt;/p>
&lt;h2 id="the-script">The script&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="ch">#!/usr/bin/python3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># SPDX-License-Identifier: Apache-2.0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Copyright (c) 2023 Taavi Väänänen &amp;lt;hi@taavi.wtf&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;puppet-unmanaged - detect files that are not managed by Puppet
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">This script determines which files in the given directory are not
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">managed by Puppet. It is intended to be used to detect stale unmanaged
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">files when converting directories to purge mode. The script only
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">interacts with the local system, for a full picture you should run it
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">on all affected hosts via Cumin.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">Usage example:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> $ sudo cumin &amp;#34;C:ferm&amp;#34; &amp;#34;locate-unmanaged /etc/ferm/ferm.d/&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">argparse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pathlib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">sys&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">exit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">yaml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">report_constructor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">loader&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">node&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">loader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">construct_mapping&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">node&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parser&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">argparse&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ArgumentParser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="vm">__doc__&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_argument&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;directory&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_argument&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;--report&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">default&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/var/lib/puppet/state/last_run_report.yaml&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">help&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Puppet report to work on&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">args&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parse_args&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># parse the special report tag as a plain object&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">yaml&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_constructor&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;!ruby/object:Puppet::Transaction::Report&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">report_constructor&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Loader&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">yaml&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">SafeLoader&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">report&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">yaml&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">safe_load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">report&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_text&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">all_managed_paths&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">resource&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;path&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">resource&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;title&amp;#34;&lt;/span>&lt;span class="p">]))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">resource&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">report&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;resource_statuses&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">resource&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;resource_type&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;File&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">sorted&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">directory&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">glob&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;**/*&amp;#34;&lt;/span>&lt;span class="p">)):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">all_managed_paths&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">file&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;__main__&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">exit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">main&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Wikimedia Hackathon Athens 2023</title><link>https://taavi.wtf/posts/wikimedia-hackathon-athens-2023/</link><pubDate>Thu, 01 Jun 2023 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/wikimedia-hackathon-athens-2023/</guid><description>&lt;p>I attended my first in-person technical event last month: the
&lt;a href="https://www.mediawiki.org/wiki/Wikimedia_Hackathon_2023">2023 Wikimedia Hackathon&lt;/a> in Athens, Greece! After initially getting
involved &lt;a href="https://www.mediawiki.org/wiki/Google_Code-in/2019">just before certain major world events&lt;/a> it was really nice to
finally get an opportunity to meet both people I'd consider friends and
that I've never worked with before.&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>&lt;img src="https://taavi.wtf/img/wmhack2023-hacking.jpg" alt="Me hacking at the hackathon">
&lt;div class="img-caption">I'm the one in the middle here, with a green t-shirt and a hat. &lt;a href="https://commons.wikimedia.org/wiki/File:Wikimedia_Hackathon_2023_-_Hacking_Room_-_Day_1_-_3.jpg">Image&lt;/a> credit: &lt;a href="https://creativecommons.org/licenses/by/4.0/deed.en">CC BY 4.0&lt;/a> by &lt;a href="https://commons.wikimedia.org/wiki/User:TiagoLubiana">Tiago Lubiana&lt;/a>.&lt;/div>
&lt;/p>
&lt;p>&lt;a href="https://www.mediawiki.org/w/index.php?title=Wikimedia_Hackathon_2023/Connect&amp;amp;diff=prev&amp;amp;oldid=5750504">Back in February&lt;/a> I predicted that I'd be working on &amp;quot;Likely something
WMCS related? Or MW auth stuff (CA/OATHAuth)&amp;quot;. In the end I ended up
hopping between multiple projects. To summarize:&lt;/p>
&lt;ul>
&lt;li>I worked with &lt;a href="https://en.wikipedia.org/wiki/User:Dreamy_Jazz">Dreamy Jazz&lt;/a> on &lt;a href="https://gerrit.wikimedia.org/r/c/mediawiki/extensions/CheckUser/+/921243">this CheckUser patch&lt;/a>. I really
enjoyed getting a perspective on how the CheckUser tool is actually
used day-to-day and what kinds of features the CUs find useful.&lt;/li>
&lt;li>&lt;a href="https://blog.legoktm.com/2023/05/31/2023-wikimedia-hackathon-recap.html">Kunal&lt;/a> and I &lt;a href="https://phabricator.wikimedia.org/T324535">deployed&lt;/a> the &lt;a href="https://www.mediawiki.org/wiki/Extension:RealMe">RealMe&lt;/a> extension to Wikimedia sites. I
think I was the first person to verify their Wikimedia user page on
a Mastodon profile, even though I &lt;a href="https://social.cologne/@Raymond/110396164044936279">was beaten by Raymond&lt;/a> on
announcing the new feature to the public. This was certainly one of
my more interesting experiences deploying stuff to production,
considering it was a Friday evening&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> and there was a very real
chance of missing dinner if something went wrong.&lt;/li>
&lt;li>I helped &lt;a href="https://ral-arturo.org/2023/05/31/hackathon.html">Arturo&lt;/a> with using &lt;a href="https://gitlab.wikimedia.org/repos/cloud/toolforge/toolforge-weld">toolforge-weld&lt;/a>, which is a new helper
library project I started a few weeks before the event.&lt;/li>
&lt;li>I helped a bunch of people with using Toolforge and Cloud VPS, and
processed access and quota requests to unblock people.&lt;/li>
&lt;li>I used the opportunity to get some signatures on my GPG key as those
are still useful when working on &lt;a href="https://www.debian.org/">Debian&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>I got a &amp;quot;I broke Wikipedia and then I fixed it&amp;quot; sticker. Apparently
this was one of only three given out this event so I've been really
good at breaking (and then fixing) stuff. I think my go-to story for
this is the time when &lt;a href="https://phabricator.wikimedia.org/T292779">I broke the ability to be logged in&lt;/a>, including
the possibility to log in or out for anyone with an existing session.
Unfortunately they don't hand out &lt;a href="https://web.archive.org/web/20230601185427/https://twitter.com/depthsofwiki/status/1660631815982399489">t-shirts&lt;/a> for breaking stuff
anymore.&lt;/p>
&lt;p>I ended up attending a couple of sessions that I found interesting. I
wish I could've attended more, but as expected I couldn't consentrate
on the presentations anymore after attending a couple, and I slept over
one that I really wanted to attend :(. I still managed to have very
interesting conversations with lots of different people.&lt;/p>
&lt;p>Thank you to the WMF for providing me a scholarship to attend the event.&lt;/p>
&lt;p>About the travel experience itself:&lt;/p>
&lt;ul>
&lt;li>This was my first time travelling internationally alone. Everything
went well, which was nice.&lt;/li>
&lt;li>I liked the Copenhagen and Stockholm-Arlanda airports, everything
worked as expected and there were not too many people there. I've
been in Stockholm and Copenhagen before many times, but never flown
via either of those.&lt;/li>
&lt;li>I flew with two new aircraft types I have not flown on before: &lt;a href="https://en.wikipedia.org/wiki/Boeing_737_Next_Generation#737-800">B738&lt;/a>
and &lt;a href="https://en.wikipedia.org/wiki/Bombardier_CRJ700_series#CRJ900">CRJ900&lt;/a>. I already dislike the CRJ family as they are clearly
designed for shorter people.&lt;/li>
&lt;/ul>
&lt;p>I can't wait to meet everyone again, whether that's at Wikimania in
Singapore later this year, next year's hackathon or something else.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Although most of the attendees had at least heard of my name, which made this a really weird experience meeting new people.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>We did undeploy an another extension just before deploying this one, so technically we didn't change anything.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Wikimedia needs to re-think MediaWiki staging environments</title><link>https://taavi.wtf/posts/deployment-prep-needs-a-replacement/</link><pubDate>Fri, 18 Nov 2022 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/deployment-prep-needs-a-replacement/</guid><description>&lt;p>Wikimedia's &lt;a href="https://wikitech.wikimedia.org/wiki/Nova_Resource:Deployment-prep">Beta Cluster&lt;/a> (aka &lt;code>deployment-prep&lt;/code>) needs to be replaced
with something competely different.&lt;/p>
&lt;p>The &lt;a href="https://wikitech.wikimedia.org/wiki/Nova_Resource:Deployment-prep">Beta Cluster Wikitech page&lt;/a> describes the projects' ambitions like
this:&lt;/p>
&lt;blockquote>
&lt;p>The &lt;strong>Beta Cluster&lt;/strong> aims to provide a staging area that closely
resembles the Wikimedia production environment. It runs MediaWiki
and extensions from their master branch, allowing developers and
power users to test new code before it goes live on Wikimedia
websites.&lt;/p>
&lt;/blockquote>
&lt;p>This was written in &lt;a href="https://wikitech.wikimedia.org/w/index.php?title=Nova_Resource:Deployment-prep/Documentation&amp;amp;diff=64898&amp;amp;oldid=63311&amp;amp;diffmode=source">early 2013&lt;/a>, nearly a decade ago. Back then, the
Wikimedia technical community and the WMF were much smaller. The Beta
Cluster was one of the first projects on Wikimedia Labs (which is these
days known as Wikimedia Cloud Services).&lt;/p>
&lt;p>The Beta Cluster has from the very beginning attempted to re-use the
same &lt;a href="https://wikitech.wikimedia.org/wiki/Puppet">Puppet&lt;/a> code used in production, with the intention being that
&lt;a href="https://diff.wikimedia.org/2011/09/19/ever-wondered-how-the-wikimedia-servers-are-configured/">Beta could be used by community members to test changes&lt;/a>. This hasn't
always been easy as a large part of the code was not designed to run
outside production; there even was an &lt;a href="https://phabricator.wikimedia.org/T88702">attempt in 2015&lt;/a> to build a
&amp;quot;stabler Beta Cluster&amp;quot; with an explicit goal of having Puppet do all of
the provisioning.&lt;/p>
&lt;p>To summarize: The original intention of the Beta Cluster was to allow
testing changes to &lt;em>both&lt;/em> MediaWiki and the underlying infrastructure.&lt;/p>
&lt;h2 id="the-infrastructure-is-developed-elsewhere">The infrastructure is developed elsewhere&lt;/h2>
&lt;p>As far as I can tell, the Beta Cluster was never maintained by the same
people taking care of the equivalent production infrastructure. The
people maintaining production infrastructure (originally called
TechOps, these days known as the &lt;a href="https://wikitech.wikimedia.org/wiki/SRE">SRE team&lt;/a>) have different needs than
what the MediaWiki developer and testers do.&lt;/p>
&lt;p>The nature of the Beta Cluster made it very inflexible for the
infrastructure people: for example it was hard to test multiple changes
for the same component at the same time, and you needed to be very
careful to not break the cluster entirely because that would be
disruptive to the MediaWiki developers.&lt;/p>
&lt;p>Over time, the SRE team developed other systems for testing the
infrastructure. Today the main way used to test infrastructure changes
in a production-like environment is &lt;a href="https://wikitech.wikimedia.org/wiki/Puppet/Pontoon">Pontoon&lt;/a>. Pontoon's primary aim is
to simplify starting disposable 'stacks' that are largely independent
of each other and are much closer to the actual production environment
than what standard Cloud VPS are. Cloud VPS itself has also &lt;a href="https://phabricator.wikimedia.org/T285539">moved from
its original use case&lt;/a> of being a development environment for services
that were either currently living or planned to live in production.&lt;/p>
&lt;p>A staging cluster that's trying to emulate production as closely as
possible should be maintained by the same people maintaining
production. Otherwise it's going to be impossible to keep up with
all the changes and code that for whatever reason can't be easily used
outside the environment it was originally written for.&lt;/p>
&lt;p>Rather unsuprisingly this kind of environment hasn't been very stable.
Even worse, since the people responding to outages are usually not
familiar with the system, most fixes end up being hacks that are
decreasing the long-term reliability of the entire platform.&lt;/p>
&lt;h2 id="there-is-a-demand-for-a-better-mediawiki-testing-environment">There is a demand for a better MediaWiki testing environment&lt;/h2>
&lt;p>Beta Cluster outages get noticed very quickly, which suggests that
people rely on the Beta Cluster working at least somewhat. However, not
everyone needs it for the same reason. Common reasons seem to include:&lt;/p>
&lt;ul>
&lt;li>Demoing new features to other people&lt;/li>
&lt;li>Testing features that need restricted rights in production&lt;/li>
&lt;li>Testing changes in a more 'production-like environment', for example
on
&lt;ul>
&lt;li>a wiki running the same software versions as the production cluster
does&lt;/li>
&lt;li>a multi-wiki setup (a wiki farm) using &lt;a href="https://www.mediawiki.org/wiki/Extension:CentralAuth">CentralAuth&lt;/a>&lt;/li>
&lt;li>a wiki running a similar configuration compared to production&lt;/li>
&lt;li>a wiki that uses &lt;a href="https://wikitech.wikimedia.org/wiki/Swift">Swift&lt;/a> for media storage&lt;/li>
&lt;li>a wiki with a working VisualEditor&lt;/li>
&lt;li>a wiki that's integrated with &lt;a href="https://www.wikidata.org/wiki/Wikidata:Main_Page">Wikidata&lt;/a>&lt;/li>
&lt;li>a wiki with a statsd stack&lt;/li>
&lt;li>a wiki using &lt;a href="https://www.mediawiki.org/wiki/Extension:CirrusSearch">CirrusSearch&lt;/a>&lt;/li>
&lt;li>a wiki with a proper job queue setup&lt;/li>
&lt;li>... and the list goes on. You get the point.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Testing with a wider range of real world data&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://bash.toolforge.org/quip/AWZYhzBIfM03vZ1oSYM9">Some features are hard to configure.&lt;/a> Others need specialized
dependencies. Either way, considering the Beta Cluster is (&lt;a href="https://phabricator.wikimedia.org/T278666">at least
currently&lt;/a>) only for code already merged to the master branch, we
should instead focus on making it easier to run those features locally
or make the relevant interfaces safer so we can be more confident in
for example something storing files on the local disk also working
properly with a Swift backend.&lt;/p>
&lt;h2 id="going-forward">Going forward&lt;/h2>
&lt;p>The current model for Beta Cluster maintenance has been unsustainable
for years, and it shows. The current model doesn't work unless it's
maintained by the SRE team directly, which is not optimal for the SRE
team. Therefore I think it's reasonable to make the conclusion that
we need to replace the current Beta Cluster with a different solution
(well, solutions) that are more sustainable to maintain and solve the
same problems more efficiently.&lt;/p>
&lt;p>What might that solution look like? Honestly, I'm not completely sure.
What I do know is that &lt;strong>we need to drop the requirement to be as close
as possible to production&lt;/strong>, and instead need to focus on what we need
to work on MediaWiki as efficiently as possible.&lt;/p>
&lt;p>There are a couple of promising projects I'd like to showcase:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://gitlab.wikimedia.org/repos/releng/cli">mwcli&lt;/a> is a command-line tool which supports managing other services
running in Docker.&lt;/li>
&lt;li>&lt;a href="https://patchdemo.wmflabs.org/">Patch demo&lt;/a> can be used to spin up a MediaWiki instance running a
particular patch from Gerrit.&lt;/li>
&lt;/ul>
&lt;h2 id="acknowledgements">Acknowledgements&lt;/h2>
&lt;p>Thanks to &lt;a href="https://www.mediawiki.org/wiki/User:TCipriani_(WMF)">Tyler Cipriani&lt;/a> for providing me access to the 2018 Beta
Cluster Survey, which provided helpful insights on how people use the
Beta Cluster.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>I'll admit that I didn't even think of that, thanks cscott for &lt;a href="https://wikis.world/@cscott@kolektiva.social/109365914666385932">pointing it out&lt;/a>.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Introducing Terraform support on Wikimedia Cloud VPS</title><link>https://taavi.wtf/posts/cloud-vps-terraform/</link><pubDate>Mon, 24 Oct 2022 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/cloud-vps-terraform/</guid><description>&lt;p>I've been working for a while to make it possible to use &lt;a href="https://terraform.io">Terraform&lt;/a> to
manage &lt;a href="https://wikitech.wikimedia.org/wiki/Portal:Cloud_VPS">Wikimedia Cloud VPS&lt;/a>, and I'm finally happy to announce that
for the most part, it's now possible&lt;span class="asterisk" title="with some caveats">*&lt;/span>. Terraform is a
popular open-source &lt;a href="https://en.wikipedia.org/wiki/Infrastructure_as_code">Infrastructure as Code&lt;/a> tool that lets you manage
your infrastructure configuration (such as Cloud VPS &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Cloud_VPS_Instances">instances&lt;/a>) with
a special coding language/framework. You can then manage and review
that code with familiar tools, such as &lt;a href="https://en.wikipedia.org/wiki/Git">Git&lt;/a>.&lt;/p>
&lt;p>&lt;a href="https://asciinema.org/a/528227">&lt;img src="https://taavi.wtf/img/terraform-asciinema.svg" alt="Using Terraform to attach a volume to an existing instance">&lt;/a>
&lt;div class="img-caption">Using Terraform to attach a volume to an existing instance.&lt;/div>
&lt;/p>
&lt;p>If you want to just get started with Terraform on your own project,
&lt;a href="https://wikitech.wikimedia.org/wiki/Help:Using_Terraform_on_Cloud_VPS">read the docs&lt;/a>. Otherwise keep reading to learn about the technical
challenges of making it all possible.&lt;/p>
&lt;h2 id="opening-the-openstack-apis">Opening the OpenStack APIs&lt;/h2>
&lt;p>The core Cloud VPS platform is powered by &lt;a href="https://en.wikipedia.org/wiki/OpenStack">OpenStack&lt;/a>, an open-source
cloud computing platform. OpenStack consists of various separate
services, some of which are used on our deployment. These services
already expose HTTP APIs, and for example the web-based dashboard
(Horizon) uses it internally. However, until now these APIs had always
been firewalled off from the public internet, and only some specific
accounts were allowed to log in from the internal Cloud VPS network
without the 2-factor authentication code that we require from all users
by default.&lt;/p>
&lt;p>Since Cloud VPS uses &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Create_a_Wikimedia_developer_account">Wikimedia developer accounts&lt;/a>, the passwords used
to log in to the dashboard can also be used to log in to other critical
tools. For this reason, we don't want to encourage our users to store
these passwords as plain text on their computers. Thankfully,
OpenStack's Identity service, Keystone, contains a solution that works
for this use case: Application Credentials. These are essentially &lt;a href="https://en.wikipedia.org/wiki/API_key">API
keys&lt;/a> that are tied to a specific user and a specific project. As a
part of this project, we've enabled the use of Application Credentials
in our configuration and wrote &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Using_OpenStack_APIs">some documentation&lt;/a> on how to use them
properly.&lt;/p>
&lt;p>The second major change needed on our setup was to open up the firewall
rules that previously restricted API access to Wikimedia networks. It's
now possible to reach the APIs from anywhere from the internet. As a
part of this, we've also updated our load balancer configuration to
make it easier to limit or block misbehaving clients.&lt;/p>
&lt;h2 id="integrating-custom-services-with-openstack-authentication">Integrating custom services with OpenStack authentication&lt;/h2>
&lt;p>Not everything on Cloud VPS uses an upstream OpenStack projects. Some
components, most notably the current &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Using_a_web_proxy_to_reach_Cloud_VPS_servers_from_the_internet">web proxy service&lt;/a> and the
&lt;a href="https://wikitech.wikimedia.org/wiki/Help:Puppet">Puppet integration&lt;/a> (internally called the Puppet ENC API), are
powered by custom code that's mostly been written using Python and the
Flask framework. Historically they didn't have any proper access
control, and instead we simply had configured our firewalls to block
access to the APIs from everything except the Cloud VPS control plane
servers.&lt;/p>
&lt;p>Since this model doesn't let external users use the APIs directly, we
had to come up with a new model. I ended up &lt;a href="https://taavi.wtf/posts/flask-keystone/">updating both of the
affected services to use the Keystone API&lt;/a>. After those changes, we've
made the web proxy API publicly available like the vanilla OpenStack
services, but the Puppet API is still private until it's &lt;a href="https://phabricator.wikimedia.org/T317478">fixed to work
properly&lt;/a> with external consumers.&lt;/p>
&lt;h2 id="writing-a-custom-terraform-provider">Writing a custom Terraform provider&lt;/h2>
&lt;p>Just having the web proxy API accessible on the internet doesn't mean
that you can directly use it with Terraform. Instead, you need
something called a &amp;quot;Terraform provider&amp;quot;. Providers are programs that
interact with Terraform and the external service (the Cloud VPS web
proxy API in this case). There's an existing provider for OpenStack,
which works great for anything that uses the vanilla OpenStack APIs,
but I ended up writing &lt;a href="https://gitlab.wikimedia.org/repos/cloud/cloud-vps/terraform-cloudvps">a custom provider&lt;/a> to work with our custom
features. Since Terraform and providers are written in Go, I also ended
up writing &lt;a href="https://pkg.go.dev/gitlab.wikimedia.org/repos/cloud/cloud-vps/go-cloudvps">a Go library&lt;/a> to work with the web proxy API. Support for
the Puppet ENC API is planned once it's been updated to support
external clients.&lt;/p>
&lt;p>Since the &lt;a href="https://registry.terraform.io/">official Terraform module registry&lt;/a> (where Terraform
downloads the modules your code uses) is heavily built around GitHub,
a propiertary platform, I ended up deploying a self-hosted registry on
&lt;code>terraform.wmcloud.org&lt;/code> to host the new provider. The registry is based
on &lt;a href="https://hugomartins.io/essays/2021/01/build-a-terraform-private-registry/">the rekisteri project by Hugo Martins&lt;/a> and has been lightly
customized to work for this use case.&lt;/p>
&lt;h2 id="whats-next">What's next&lt;/h2>
&lt;p>It's now possible to do most things via Terraform that you can do via
horizon.wikimedia.org. However, there are still a few major exceptions:&lt;/p>
&lt;ul>
&lt;li>You can't manage the Puppet ENC data, as mentioned above.&lt;/li>
&lt;li>You can't &lt;a href="https://phabricator.wikimedia.org/T320750">manage project membership&lt;/a> due to some
upstream limitations.&lt;/li>
&lt;/ul>
&lt;p>It'd be nice to get those fixed. In addition, I'm planning on working
to make the entire system more streamlined with the Puppet setup we use
to provision instances. Most WMCS managed projects use a &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Standalone_puppetmaster">standalone
Puppetmaster&lt;/a> to manage secrets. There are a few manual steps when
provisioning or decomissioning instances to sign and revoke the TLS
certificates Puppet uses internally, and I want to eventually make
Terraform do that for you.&lt;/p>
&lt;p>If this sounds interesting: &lt;a href="https://wikitech.wikimedia.org/wiki/Help:Cloud_Services_communication">get involved!&lt;/a> The entire stack is
licensed under free licenses and welcomes new contributors, and in my
experience it's a great way to experiment with technology that might
be otherwise hard be able toto play with.&lt;/p>
&lt;blockquote>
&lt;p>This post was originally published on the &lt;a href="https://techblog.wikimedia.org/2022/10/24/introducing-terraform-support-on-wikimedia-cloud-vps/">Wikimedia Technical Blog&lt;/a>.&lt;/p>
&lt;/blockquote></description></item><item><title>Adding OpenStack Keystone authentication to a Flask application</title><link>https://taavi.wtf/posts/flask-keystone/</link><pubDate>Sat, 01 Jan 2022 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/flask-keystone/</guid><description>&lt;p>The &lt;a href="https://wmcloud.org">Wikimedia Cloud VPS&lt;/a> platform is powered by
the &lt;a href="https://openstack.org">OpenStack&lt;/a> platform. In addition to the
standard OpenStack services, we've needed to build our own services and
APIs that add features specific to our installation.&lt;/p>
&lt;p>Historically our OpenStack APIs have not been open to direct use and
we've required everyone to use the OpenStack dashboard (Horizon) to
manage their VPS project. Our custom APIs have been &amp;quot;secured&amp;quot; by just
firewalling access from everywhere else except the OpenStack control
hosts, which has worked fine for us in the past. However, we are
planning on letting our users directly access the APIs to build their
own integrations, and as a part of this work we needed to replace the
firewall-based security model with something more flexible. The natural
solution is to integrate our custom APIs with OpenStack's Identity
service (&lt;a href="https://docs.openstack.org/keystone/latest/">Keystone&lt;/a>).&lt;/p>
&lt;p>Our services use the &lt;a href="https://flask.palletsprojects.com/">Flask&lt;/a>
framework. Thankfully this means that we can just use the code that
&lt;a href="https://www.rackspace.com/">Rackspace&lt;/a> has written: the
&lt;a href="https://github.com/Rackspace-DOT/flask_keystone">flask_keystone&lt;/a>
package does exactly what we want. The documentation is a bit lacking
though.&lt;/p>
&lt;p>flask_keystone makes heavy use of OpenStack's shared library
&lt;a href="https://docs.openstack.org/oslo/latest/">Oslo&lt;/a> and uses Rackspace's
&lt;a href="https://github.com/Rackspace-DOT/flask_oslolog">flask_oslolog&lt;/a> too,
so you need to install them. At Wikimedia we built our own Debian
packages for both Rackspace libraries (Oslo is already packaged for
on the official Debian repositories).&lt;/p>
&lt;p>A minimal code example looks something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">flask&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">flask_keystone&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">FlaskKeystone&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">flask_oslolog&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">OsloLog&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">oslo_config&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">cfg&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">oslo_context&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">context&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FlaskKeystone&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">log&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OsloLog&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">CONF&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">default_config_files&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;/etc/path-to-your/config.ini&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">flask&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Flask&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__name__&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">log&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">init_app&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">key&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">init_app&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">get_oslo_context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rule&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">project_id&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># headers in a specific format that oslo.context wants&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s1">&amp;#39;HTTP_&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">upper&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;-&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;_&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">value&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">flask&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">headers&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">items&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RequestContext&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">from_environ&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">headers&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@app.route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">hello&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">get_oslo_context&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s1">&amp;#39;Hello, &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1"> on project &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">project_id&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">!&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;__main__&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You will also need a config file in the path you gave to oslo_config.
A sample configuration looks something like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="k">[DEFAULT]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">auth_strategy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">keystone&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[keystone_authtoken]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">identity_uri&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">https://openstack.example.org:25000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">www_authenticate_uri&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">https://openstack.example.org:25000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">http_connect_timeout&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">interface&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">public&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">auth_version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">3.14&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">auth_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">password&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">auth_url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">https://openstack.example.org:25000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">user_domain_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">default&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">project_domain_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">default&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">username&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">some-username&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">password&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">some-password&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">project_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">some-project&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">service_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">some-keystone-service-type&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">delay_auth_decision&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">True&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">[flask_keystone]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">roles&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In the config file, you need to add credentials for an OpenStack
account that has access to the &lt;code>identity:validate_token&lt;/code> rule. We
&lt;a href="https://gerrit.wikimedia.org/r/c/operations/puppet/+/739902">added a new role&lt;/a>
for that purpose and granted our service account that role domain-wide.&lt;/p>
&lt;h2 id="authenticating-your-requests">Authenticating your requests&lt;/h2>
&lt;p>Now that your service is secured with Keystone authentication, you
probably want to make some requests to it. This can be done using the
keystoneclient python package:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">keystoneauth1&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">session&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">keystone_session&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">keystoneauth1.identity&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">v3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">keystoneclient.v3&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">client&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">auth&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">v3&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Password&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">auth_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;https://openstack.example.org:25000&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">username&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;some-username&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">password&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;some-password&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">project_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;some-project&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_domain_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;default&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">project_domain_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;default&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">session&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">keystone_session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Session&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">auth&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">auth&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_agent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;some-app&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Client&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">session&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">interface&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;public&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">timeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">service&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">services&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">list&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;some-keystone-service-type&amp;#39;&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">endpoint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">endpoints&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">list&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">service&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">service&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">interface&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;public&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">enabled&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">endpoint&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">url&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">url&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">/test&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="policy-based-access-control">Policy-based access control&lt;/h2>
&lt;p>We can also use OpenStack oslo.policy for policy-based access control,
similar to other OpenStack services.&lt;/p>
&lt;p>First, create an &lt;code>Enforcer&lt;/code> object for your policy rules:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">oslo_config&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">cfg&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">oslo_policy&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">policy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># you should already have this&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">CONF&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">default_config_files&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;/etc/path-to-your/config.ini&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">enforcer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">policy&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Enforcer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cfg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">CONF&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">enforcer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">register_defaults&lt;/span>&lt;span class="p">([&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">policy&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RuleDefault&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;admin&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;role:admin&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">policy&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RuleDefault&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;proxy:index&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">policy&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RuleDefault&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;proxy:view&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">policy&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RuleDefault&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;proxy:create&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;rule:admin&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">policy&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RuleDefault&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;proxy:update&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;rule:admin&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">policy&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RuleDefault&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;proxy:delete&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;rule:admin&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then you can enforce the policies in your route handlers:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@app.route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/v1/proxy&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">all_proxies&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">get_oslo_context&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">enforcer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">authorize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;proxy:index&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{},&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">flask&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;forbidden&amp;#39;&lt;/span>&lt;span class="p">}),&lt;/span> &lt;span class="mi">403&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">flask&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">get_proxies&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">project_id&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Google Code-In 2019</title><link>https://taavi.wtf/posts/gci-2019-20/</link><pubDate>Fri, 24 Jan 2020 00:00:00 +0000</pubDate><author>taavi@majava.org (Taavi Väänänen)</author><guid>https://taavi.wtf/posts/gci-2019-20/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>UPDATE&lt;/strong>: I &lt;a href="https://opensource.googleblog.com/2020/02/announcing-our-google-code-in-2019.html">was a Runner-Up&lt;/a> for Wikimedia!&lt;/p>
&lt;/blockquote>
&lt;p>I participated in &lt;a href="https://codein.withgoogle.com">Google Code-In&lt;/a> for the first time this winter. Looks like &lt;a href="https://www.mediawiki.org/wiki/Google_Code-in/2019#Wrap-up_blog_posts">other people&lt;/a> are starting to blog about so here's my blog post so I do not look like a weird antisocial hermit. To be honest, I had a lot of fun participating.&lt;/p>
&lt;p>In the end, I completed 31 tasks and had done little under 4.5% of all completed &lt;a href="https://wikimedia.org">Wikimedia&lt;/a> tasks.&lt;/p>
&lt;p>&lt;p>
&lt;div class="img-realsize-wrapper" >
&lt;img
src="https://taavi.wtf/img/gci-completed-tasks.png"
alt="31 completed tasks"
loading="lazy"
class="img-realsize"
/>
&lt;/div>
&lt;/p>
&lt;div class="img-caption">such a taboo talking about task counts&lt;/div>
&lt;/p>
&lt;p>All of the tasks I did were in Wikimedia. I've contributed some small patches there before, and I have about &lt;a href="https://xtools.wmflabs.org/ec/en.wikipedia.org/Majavah">2,000 edits&lt;/a> in the English Wikipedia, so I wasn't a newcomer to the environment.&lt;/p>
&lt;p>My first task was &amp;quot;&lt;a href="https://codein.withgoogle.com/dashboard/task-instances/5392401758158848/">Convert two plain text files in MediaWiki core in /doc to Markdown format (III)&lt;/a>&amp;quot;. It was a fairly simple task, but it was my &lt;a href="https://gerrit.wikimedia.org/r/#/c/mediawiki/core/+/554261/">first patch&lt;/a> to the MediaWiki core.&lt;/p>
&lt;p>When I claimed my second task, &amp;quot;&lt;a href="https://codein.withgoogle.com/dashboard/task-instances/6133879677648896/">Add a namespace filter&lt;/a>&amp;quot;, I a message in IRC from &lt;a href="https://meta.wikimedia.org/wiki/User:Martin_Urbanec">Martin&lt;/a>, who I had worked with before (they are a &lt;a href="https://wikitech.wikimedia.org/wiki/SWAT">SWAT deployer&lt;/a>) asking if I was really this young.
Martin also mentored multiple tasks that I completed, so thanks for those.&lt;/p>
&lt;p>For the first ~2 weeks, I had completed at least one task every day. Unfortunately then there were some school stuff that required my attention and my streak was cut short.
After the streak, I did not do any tasks for about some time due to IRL stuff (school, etc) and lack of motivation. After sometime the new years I returned to work.&lt;/p>
&lt;p>I claimed my last task (&lt;a href="https://codein.withgoogle.com/dashboard/task-instances/4808316513943552/">[Tracker] Create maintenance script to force re-queueing MediaWiki communication methods for specified list of tickets&lt;/a>) about two hours before task claiming was closed.&lt;/p>
&lt;p>I personally feel that there weren't enough tasks in MW Core or extensions as they are the most interesting projects on
(excluding the &lt;a href="https://codein.withgoogle.com/dashboard/task-instances/6747800197398528/">jsonlint&lt;/a> &lt;a href="https://codein.withgoogle.com/dashboard/task-instances/5717089717846016/">or&lt;/a> &lt;a href="https://codein.withgoogle.com/dashboard/task-instances/4646299207467008/">jshint&lt;/a> &lt;a href="https://codein.withgoogle.com/dashboard/task-instances/5001623999348736/">to&lt;/a> &lt;a href="https://codein.withgoogle.com/dashboard/task-instances/6293757419323392/">eslint&lt;/a> converting tasks and extension manifest schema upgrading tasks), most actual coding tasks were on Toolforge tools. I ended working with Python most as it's really common on Toolforge for some reason.&lt;/p>
&lt;p>I want to thank &lt;a href="https://meta.wikimedia.org/wiki/User:Florianschmidtwelzow">Florian&lt;/a> for making the notable expections and mentoring a couple of tasks for Core
(&lt;a href="https://codein.withgoogle.com/dashboard/task-instances/6713578032201728/">Create tests for UploadFromUrl::isAllowedHost&lt;/a>) and for the Translate extension
(&lt;a href="https://codein.withgoogle.com/dashboard/task-instances/6387459487694848/">GettextFFS should implement isContentEqual&lt;/a>).&lt;/p>
&lt;p>GCI was definitely a great experience. The mentors knew what they were doing and I had a ton of fun. I am definitely looking for being a part of it in some form next year. Thanks to everyone who was involved this year!&lt;/p>
&lt;p>&lt;a href="https://www.mediawiki.org/wiki/Google_Code-in/2019#Wrap-up_blog_posts">Other people&lt;/a> have also written about this years GCI. Check them out after reading my one.&lt;/p></description></item></channel></rss>