<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>knktc&#39;s Notes</title>
  
  <subtitle>python, cloud, linux...</subtitle>
  <link href="https://knktc.com/en/atom.xml" rel="self"/>
  
  <link href="https://knktc.com/en/"/>
  <updated>2026-05-21T05:59:59.087Z</updated>
  <id>https://knktc.com/en/</id>
  
  <author>
    <name>knktc</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>I Built Myself a Handy Toolbox Site with Codex Vibe Coding: tools.knktc.com</title>
    <link href="https://knktc.com/en/2026/05/21/tools-knktc-codex-vibe-coding-intro/"/>
    <id>https://knktc.com/en/2026/05/21/tools-knktc-codex-vibe-coding-intro/</id>
    <published>2026-05-21T06:05:00.000Z</published>
    <updated>2026-05-21T05:59:59.087Z</updated>
    
    <content type="html"><![CDATA[<p>For a while I had one very ordinary problem: during day-to-day debugging, I kept opening the same category of tiny online tools over and over again.</p><p>Check my current public IP. Convert a timestamp. Look at browser timezone info. Pretty-print a piece of JSON. None of these tasks are big, but together they somehow turn into a part-time job inside the browser.</p><p>So this time I tried a different approach: instead of collecting more bookmarks, I used Codex for a bit of vibe coding and built a small toolbox site that feels nice to use and stays out of the way.</p><p>It is live here:</p><p><a href="https://tools.knktc.com/">https://tools.knktc.com</a></p><span id="more"></span><p>Here is the homepage:</p><p><img src="home-zh.png" alt="tools.knktc.com homepage"></p><p>The goal is not to make the biggest toolbox on the internet. It is more like a drawer of utilities that I actually expect to keep using myself. The guiding ideas were pretty simple:</p><ol><li>Put common utilities in one place so I do less tab-hopping.</li><li>Keep things local in the browser whenever that is enough.</li><li>Favor “open and use” over flashy presentation.</li></ol><p>Right now the site includes tools like:</p><ul><li>Client IP</li><li>IP &amp; Mask Check</li><li>Base64 Converter</li><li>JSON Pretty</li><li>URL Encoder / Decoder</li><li>Timestamp Converter</li><li>Clock Drift Check</li><li>Browser Info &amp; Timezone</li><li>UUID Generator</li></ul><p>A couple of them are especially fun.</p><p>The first one is the clock drift checker. It compares your local device time with the server reference time, which is handy for problems that look random at first but are actually time-related, such as login failures, signed-request issues, or certificate oddities.</p><p><img src="clock-skew.png" alt="Clock drift check tool"></p><p>This is exactly the kind of utility you may not need every day, but when you do need it, it saves you from wandering in circles.</p><p>Another one I like is the browser info and timezone page. Whenever someone says “the time looks wrong here” or “the page behaves differently on my side,” checking timezone, language, user agent, and screen details is often a very good first move.</p><p><img src="browser-info.png" alt="Browser info and timezone tool"></p><p>I also tucked <code>Clock drift</code> into that view, so it does not only show what the browser reports, but also how far it appears to be from the server reference time. It is a small detail, but a useful one.</p><p>Building this with Codex was also a genuinely pleasant experience. Compared with the more traditional “write a full spec first, then implement everything step by step” rhythm, vibe coding feels more like this:</p><ul><li>explain the idea clearly;</li><li>let Codex build and revise quickly;</li><li>keep iterating until the UI, behavior, and details feel right.</li></ul><p>It ends up feeling a bit like pair programming with someone who never gets tired. You keep steering with comments like “this should feel smoother” or “this part needs to be clearer,” and the code keeps catching up.</p><p>The site is definitely not a finished monument, and that is part of the appeal. I would rather treat it as a small toolbox that can keep growing whenever I run into another oddly specific but genuinely useful little problem.</p><p>If this sounds like your kind of thing, you can bookmark it here:</p><p><a href="https://tools.knktc.com/">https://tools.knktc.com</a></p><p>At the very least, the next time you need to check an IP, convert a timestamp, or confirm whether your local clock has drifted, you will have one less tab to hunt for.</p>]]></content>
    
    
    <summary type="html">A lightweight introduction to tools.knktc.com, a small toolbox site I built with Codex vibe coding for everyday development and debugging tasks.</summary>
    
    
    
    
    <category term="codex" scheme="https://knktc.com/en/tags/codex/"/>
    
    <category term="vibe-coding" scheme="https://knktc.com/en/tags/vibe-coding/"/>
    
    <category term="tools" scheme="https://knktc.com/en/tags/tools/"/>
    
    <category term="nextjs" scheme="https://knktc.com/en/tags/nextjs/"/>
    
    <category term="golang" scheme="https://knktc.com/en/tags/golang/"/>
    
  </entry>
  
  <entry>
    <title>Disable VS Code&#39;s Automatic GitHub Copilot Co-authored-by Line</title>
    <link href="https://knktc.com/en/2026/05/06/disable-vscode-copilot-ai-coauthor/"/>
    <id>https://knktc.com/en/2026/05/06/disable-vscode-copilot-ai-coauthor/</id>
    <published>2026-05-06T14:00:00.000Z</published>
    <updated>2026-05-06T04:09:21.153Z</updated>
    
    <content type="html"><![CDATA[<p>Recently I noticed a small but slightly surprising change. After committing code through VS Code with GitHub Copilot involved, my commit message contained this extra line:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Co-authored-by: Copilot &lt;copilot@github.com&gt;</span><br></pre></td></tr></table></figure><span id="more"></span><p>It looked like this:</p><p><img src="info_info_commint.png" alt="Copilot co-author line added to the commit message"></p><p>My first reaction was basically: Microsoft has quietly added another little default.</p><p>What this setting does is simple: when Copilot Chat or Agent participates in a code change, VS Code can automatically append an AI co-author trailer to the commit message. In some team workflows, that may be useful as a transparency signal. For everyday personal commits, though, it can make the history noisier than expected, especially because the default behavior is easy to miss.</p><p>To turn it off:</p><ol><li>Open VS Code Settings.</li><li>Search for <code>Add AI Co Author</code>.</li><li>Find <code>Git: Add AI Co Author</code>.</li><li>Change the default value from <code>chatAndAgent</code> to <code>off</code>.</li></ol><p>The setting looks like this:</p><p><img src="turnoffaicoauthor.png" alt="Turn off VS Code Git Add AI Co Author setting"></p><p>After that, commits made from VS Code should stop automatically adding:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Co-authored-by: Copilot &lt;copilot@github.com&gt;</span><br></pre></td></tr></table></figure><p>If your team wants to keep AI involvement visible in commit history, leaving it enabled is reasonable. If you prefer cleaner commit messages, switching it to <code>off</code> does the job.</p>]]></content>
    
    
    <summary type="html">VS Code can automatically add a Co-authored-by line for GitHub Copilot to commit messages. Here is how to turn it off.</summary>
    
    
    
    
    <category term="vscode" scheme="https://knktc.com/en/tags/vscode/"/>
    
    <category term="github" scheme="https://knktc.com/en/tags/github/"/>
    
    <category term="copilot" scheme="https://knktc.com/en/tags/copilot/"/>
    
    <category term="git" scheme="https://knktc.com/en/tags/git/"/>
    
  </entry>
  
  <entry>
    <title>tiny-requestbin: a lightweight HTTP request debugging tool written in Go</title>
    <link href="https://knktc.com/en/2026/03/07/tiny-requestbin-introduction/"/>
    <id>https://knktc.com/en/2026/03/07/tiny-requestbin-introduction/</id>
    <published>2026-03-07T04:25:23.000Z</published>
    <updated>2026-04-22T01:14:27.717Z</updated>
    
    <content type="html"><![CDATA[<p>While revisiting some of my open-source projects recently, I took another look at the GitHub page for <a href="https://github.com/knktc/tiny-requestbin">tiny-requestbin</a>. It has clearly grown beyond being “just a tiny request catcher” and feels like a very practical tool for local debugging, webhook integration testing, and quick HTTP request inspection.</p><p>The project is written in Go and keeps its goals simple: <strong>minimal dependencies, fast startup, and a straightforward way to inspect incoming HTTP requests</strong>.</p><p>If you often need to verify third-party callbacks, debug inbound requests to your own service, or just capture a few HTTP requests to see what is actually being sent, this tool is quite handy.</p><span id="more"></span><h2 id="What-this-project-does"><a href="#What-this-project-does" class="headerlink" title="What this project does"></a>What this project does</h2><p>tiny-requestbin is a <strong>lightweight HTTP request inspection and debugging tool</strong>. Once started, it captures HTTP requests sent to the service and presents them in a way that is easy to inspect, including:</p><ul><li>request method</li><li>request path</li><li>request headers</li><li>request body</li><li>and the full request details</li></ul><p>The repository README also highlights a few core features clearly:</p><ul><li><strong>Lightweight and fast</strong>: simple implementation with very few dependencies</li><li><strong>Request inspection</strong>: view detailed information about incoming HTTP requests</li><li><strong>Web UI</strong>: inspect captured requests in the browser</li><li><strong>CLI mode</strong>: print request details directly in the terminal</li><li><strong>Local-only runtime</strong>: everything stays on your machine without depending on an external service</li></ul><p>I personally like tools with this kind of sharp scope. It does not try to be a huge platform. It just focuses on doing one thing well: helping you inspect requests.</p><h2 id="Several-easy-installation-options"><a href="#Several-easy-installation-options" class="headerlink" title="Several easy installation options"></a>Several easy installation options</h2><p>The repository homepage offers multiple installation methods, which cover most common local debugging and deployment scenarios.</p><h3 id="1-Docker"><a href="#1-Docker" class="headerlink" title="1. Docker"></a>1. Docker</h3><p>If you just want to get it running quickly, the README recommends using Docker directly:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -p 8282:8282 knktc/tiny-requestbin</span><br></pre></td></tr></table></figure><p>You can also pass custom parameters such as the listen address, port, and maximum number of stored requests:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -p 8282:8282 knktc/tiny-requestbin -port 8282 -listen 0.0.0.0 -max 1000</span><br></pre></td></tr></table></figure><p>The repository also ships with a <code>docker-compose.yml</code>, so <code>docker-compose up -d</code> works as well.</p><h3 id="2-Helm"><a href="#2-Helm" class="headerlink" title="2. Helm"></a>2. Helm</h3><p>If you already have a Kubernetes environment, the project also provides a Helm chart:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">helm install my-requestbin helm/tiny-requestbin/</span><br></pre></td></tr></table></figure><p>The README includes examples for customizing <code>config.max</code>, <code>service.type</code>, and enabling HTTPRoute, which makes the Kubernetes path pretty convenient.</p><h3 id="3-go-install"><a href="#3-go-install" class="headerlink" title="3. go install"></a>3. <code>go install</code></h3><p>Since the project itself is written in Go, installing it that way is also very natural:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go install github.com/knktc/tiny-requestbin@latest</span><br></pre></td></tr></table></figure><h3 id="4-Build-from-source"><a href="#4-Build-from-source" class="headerlink" title="4. Build from source"></a>4. Build from source</h3><p>If you prefer compiling it yourself, that is straightforward too:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/knktc/tiny-requestbin.git</span><br><span class="line"><span class="built_in">cd</span> tiny-requestbin</span><br><span class="line">go build</span><br></pre></td></tr></table></figure><h2 id="How-to-use-it"><a href="#How-to-use-it" class="headerlink" title="How to use it"></a>How to use it</h2><p>The default startup command is intentionally simple:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tiny-requestbin</span><br></pre></td></tr></table></figure><p>Default flags:</p><ul><li><code>-port</code>: listening port, default <code>8282</code></li><li><code>-listen</code>: listening address, default <code>127.0.0.1</code></li><li><code>-max</code>: maximum number of stored requests, default <code>100</code></li><li><code>-cli</code>: whether to enable CLI output mode, default <code>false</code></li></ul><p>For example, the following command makes the service listen on <code>0.0.0.0:9000</code>, keeps up to 1000 requests, and also prints them to the terminal:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tiny-requestbin -port 9000 -listen 0.0.0.0 -max 1000 -cli</span><br></pre></td></tr></table></figure><p>Its basic workflow is very simple:</p><ol><li>Start the service.</li><li>Send requests to <code>http://[listen-address]:[port]/any/path</code>.</li><li>Visit the root path in the browser to inspect captured requests.</li><li>If <code>-cli</code> is enabled, the terminal will print them as well.</li></ol><p>That makes it useful both for temporary local troubleshooting and as a small internal request sink inside a private network.</p><h2 id="Two-ways-to-inspect-requests-terminal-and-web"><a href="#Two-ways-to-inspect-requests-terminal-and-web" class="headerlink" title="Two ways to inspect requests: terminal and web"></a>Two ways to inspect requests: terminal and web</h2><p>The repository homepage includes two screenshots, and I also placed them here so the actual experience is easier to picture.</p><h3 id="CLI-mode-screenshot"><a href="#CLI-mode-screenshot" class="headerlink" title="CLI mode screenshot"></a>CLI mode screenshot</h3><p>When <code>-cli</code> is enabled, incoming HTTP requests are printed directly in the terminal in a formatted way:</p><p><img src="run_in_cmd.png" alt="CLI mode screenshot"></p><p>This mode is especially convenient when working on a server, in a container, or through a remote shell where opening a browser is less convenient.</p><h3 id="Web-UI-screenshot"><a href="#Web-UI-screenshot" class="headerlink" title="Web UI screenshot"></a>Web UI screenshot</h3><p>If you prefer a graphical view, you can simply open the web page and inspect request history there:</p><p><img src="webpage.png" alt="Web UI screenshot"></p><p>From the screenshot, the UI looks intentionally minimal. The focus is on showing request contents clearly without unnecessary distractions.</p><h2 id="Good-use-cases"><a href="#Good-use-cases" class="headerlink" title="Good use cases"></a>Good use cases</h2><p>Based on the repository itself, I think tiny-requestbin is particularly useful for:</p><ul><li>debugging webhook callbacks from third-party platforms</li><li>verifying what an application is actually sending over HTTP</li><li>quickly spinning up a local request receiver during development</li><li>temporarily observing request traffic in containers or Kubernetes</li><li>inspecting request details directly in the terminal with <code>-cli</code></li></ul><p>The fact that it is explicitly <strong>local only</strong> is also reassuring for many internal debugging scenarios, because the captured data stays on your machine.</p><h2 id="A-few-extra-points-worth-mentioning"><a href="#A-few-extra-points-worth-mentioning" class="headerlink" title="A few extra points worth mentioning"></a>A few extra points worth mentioning</h2><p>There are also a few details on the GitHub page that I think are nice touches:</p><ul><li>the container image supports <strong>multiple architectures</strong>, including <code>linux/amd64</code> and <code>linux/arm64</code></li><li>multi-architecture images are built and published automatically with GitHub Actions</li><li>the repository offers multiple entry points: Docker, Helm, and source builds</li><li>the project uses the <strong>MIT License</strong>, which makes adoption and redistribution straightforward</li></ul><p>The README also mentions that the project initially started with Gemini and was later iterated on with GitHub Copilot. That somehow fits the project’s overall character as well: lightweight, direct, and quick to evolve.</p><h2 id="Final-thoughts"><a href="#Final-thoughts" class="headerlink" title="Final thoughts"></a>Final thoughts</h2><p>If you are looking for a <strong>lightweight, Go-based, locally runnable HTTP request debugging tool that supports both web and CLI usage</strong>, I think <a href="https://github.com/knktc/tiny-requestbin">tiny-requestbin</a> is worth trying.</p><p>Project link:</p><ul><li>GitHub: <a href="https://github.com/knktc/tiny-requestbin">https://github.com/knktc/tiny-requestbin</a></li></ul><p>Feel free to star the repository, and PRs or issues are always welcome.</p>]]></content>
    
    
    <summary type="html">A lightweight Go tool for local HTTP request debugging, webhook testing, and quick inspection with both web and CLI views.</summary>
    
    
    
    
    <category term="go" scheme="https://knktc.com/en/tags/go/"/>
    
    <category term="tiny-requestbin" scheme="https://knktc.com/en/tags/tiny-requestbin/"/>
    
    <category term="requestbin" scheme="https://knktc.com/en/tags/requestbin/"/>
    
    <category term="open-source" scheme="https://knktc.com/en/tags/open-source/"/>
    
  </entry>
  
  <entry>
    <title>Don&#39;t forget to set ignore_exc when using pymemcache</title>
    <link href="https://knktc.com/en/2025/01/26/pymemcache-ignore-exc/"/>
    <id>https://knktc.com/en/2025/01/26/pymemcache-ignore-exc/</id>
    <published>2025-01-26T13:07:38.000Z</published>
    <updated>2026-04-22T01:14:27.769Z</updated>
    
    <content type="html"><![CDATA[<p>After upgrading Django to 4.2, we started replacing <code>python-memcached</code> with <code>pymemcache</code>.</p><p>Once the switch was done, we noticed that the default setup was no longer behaving well in a high-availability scenario. If multiple memcached backends were configured and one of them went down, any cache-related code could fail with an exception instead of degrading gracefully.</p><p>After checking the official documentation, the reason became clear: by default, <code>pymemcache</code> raises exceptions on connection failures unless you explicitly set <code>ignore_exc = True</code>.</p><p>So the fix is simply to add that option to your Django <code>CACHES</code> settings:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">CACHES = &#123;</span><br><span class="line">    <span class="string">&#x27;default&#x27;</span>: &#123;</span><br><span class="line">        <span class="string">&#x27;BACKEND&#x27;</span>: <span class="string">&#x27;django.core.cache.backends.memcached.PyMemcacheCache&#x27;</span>,</span><br><span class="line">        <span class="string">&#x27;LOCATION&#x27;</span>: [</span><br><span class="line">            <span class="string">&#x27;host1:11211&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;host2:11211&#x27;</span>,</span><br><span class="line">        ],</span><br><span class="line">        <span class="string">&#x27;OPTIONS&#x27;</span>: &#123;</span><br><span class="line">            <span class="string">&#x27;ignore_exc&#x27;</span>: <span class="literal">True</span>,</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>If you expect memcached nodes to fail independently and still want your application to continue running, this setting is easy to miss but important to have.</p>]]></content>
    
    
    <summary type="html">If you use Django with pymemcache and want cache failures to degrade gracefully, make sure ignore_exc is set to True.</summary>
    
    
    
    
    <category term="python" scheme="https://knktc.com/en/tags/python/"/>
    
    <category term="django" scheme="https://knktc.com/en/tags/django/"/>
    
    <category term="memcache" scheme="https://knktc.com/en/tags/memcache/"/>
    
    <category term="pymemcache" scheme="https://knktc.com/en/tags/pymemcache/"/>
    
    <category term="ignore_exc" scheme="https://knktc.com/en/tags/ignore-exc/"/>
    
    <category term="exception" scheme="https://knktc.com/en/tags/exception/"/>
    
  </entry>
  
  <entry>
    <title>Install pandas on Kylin V10 for the SW64 Architecture</title>
    <link href="https://knktc.com/en/2023/05/06/install-pandas-on-kylin-v10-with-sw64-cpu/"/>
    <id>https://knktc.com/en/2023/05/06/install-pandas-on-kylin-v10-with-sw64-cpu/</id>
    <published>2023-05-06T06:08:14.000Z</published>
    <updated>2026-04-29T01:58:35.404Z</updated>
    
    <content type="html"><![CDATA[<p>I recently did some adaptation work for domestic platforms, trying to make one of my systems run on a Sunway CPU.</p><p>Compared with ARM, the Sunway architecture is much more niche. In practice, support seems limited mainly to UOS and Kylin. The customer provided a SW64 build of Kylin V10, so this post records the issues I ran into while installing pandas in that environment.</p><span id="more"></span><h2 id="Create-a-virtual-environment"><a href="#Create-a-virtual-environment" class="headerlink" title="Create a virtual environment"></a>Create a virtual environment</h2><p>Kylin V10 comes with Python 3.7.4. Start by creating a venv:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">python3 -m venv --copies knktc-env</span><br><span class="line"><span class="built_in">source</span> knktc-env/bin/activate</span><br></pre></td></tr></table></figure><h2 id="Install-numpy"><a href="#Install-numpy" class="headerlink" title="Install numpy"></a>Install numpy</h2><p>My first instinct was to install numpy directly with pip:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install numpy</span><br></pre></td></tr></table></figure><p>That failed with an error like this:</p><blockquote><p>error: #error Unknown CPU, please report this to numpy maintainers with information about your platform (OS, CPU and compiler)</p></blockquote><p>So yes, the architecture was niche enough that numpy’s build logic did not recognize it.</p><p>After some searching, I found discussions on the UOS forums saying that building with pip did not work, and that the only practical route was using a prebuilt package from the OS vendor. Kylin indeed provides one:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum search numpy</span><br></pre></td></tr></table></figure><p>Example output:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python3-numpy.sw_64 : A fast multidimensional array facility for Python</span><br></pre></td></tr></table></figure><p>So install that package first:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install python3-numpy</span><br></pre></td></tr></table></figure><p>After installation, copy the vendor-provided numpy package from the system site-packages directory into the virtual environment manually:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cp -r /usr/lib/python3.7/site-packages/numpy* knktc-env/lib/python3.7/site-packages</span><br></pre></td></tr></table></figure><p>Then check the version:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&gt;&gt;&gt; </span><span class="keyword">import</span> numpy</span><br><span class="line"><span class="meta">&gt;&gt;&gt; </span>numpy.__version__</span><br><span class="line"><span class="string">&#x27;1.16.5&#x27;</span></span><br></pre></td></tr></table></figure><p>That takes care of numpy.</p><h2 id="Install-pandas"><a href="#Install-pandas" class="headerlink" title="Install pandas"></a>Install pandas</h2><p>For pandas, the remaining workable route was to build from source.</p><p>Install the dependencies first:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">pip install setuptools --upgrade -i https://pypi.tuna.tsinghua.edu.cn/simple</span><br><span class="line">pip install wheel --upgrade -i https://pypi.tuna.tsinghua.edu.cn/simple</span><br><span class="line">pip install Cython --upgrade -i https://pypi.tuna.tsinghua.edu.cn/simple</span><br><span class="line">pip install pytz==2018.7 -i https://pypi.tuna.tsinghua.edu.cn/simple</span><br><span class="line">pip install python-dateutil -i https://pypi.tuna.tsinghua.edu.cn/simple</span><br></pre></td></tr></table></figure><p>Then download the source package from:</p><p><a href="https://github.com/pandas-dev/pandas/releases">https://github.com/pandas-dev/pandas/releases</a></p><p>We were still using pandas <code>1.1.5</code>, so I used:</p><p><a href="https://github.com/pandas-dev/pandas/releases/download/v1.1.5/pandas-1.1.5.tar.gz">https://github.com/pandas-dev/pandas/releases/download/v1.1.5/pandas-1.1.5.tar.gz</a></p><p>After extracting the source, install it the old-fashioned way:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> pandas-1.1.5/</span><br><span class="line">python setup.py install</span><br></pre></td></tr></table></figure><p>This step takes a while, so it is a good idea to run it inside <code>screen</code>.</p><p>Thankfully, even though the build was slow, it finished without major errors.</p><p>Finally, verify the version:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&gt;&gt;&gt; </span><span class="keyword">import</span> pandas</span><br><span class="line"><span class="meta">&gt;&gt;&gt; </span>pandas.__version__</span><br><span class="line"><span class="string">&#x27;1.1.5&#x27;</span></span><br></pre></td></tr></table></figure><p>At that point, pandas was working.</p>]]></content>
    
    
    <summary type="html">Notes on getting numpy and pandas working inside a Python 3.7 virtual environment on Kylin V10 running on a Sunway SW64 CPU.</summary>
    
    
    
    
    <category term="sunway" scheme="https://knktc.com/en/tags/sunway/"/>
    
    <category term="localization" scheme="https://knktc.com/en/tags/localization/"/>
    
    <category term="sw64" scheme="https://knktc.com/en/tags/sw64/"/>
    
    <category term="kylin" scheme="https://knktc.com/en/tags/kylin/"/>
    
    <category term="pandas" scheme="https://knktc.com/en/tags/pandas/"/>
    
    <category term="numpy" scheme="https://knktc.com/en/tags/numpy/"/>
    
  </entry>
  
  <entry>
    <title>Fix Git SSH Permission Denied After Upgrading to macOS 13 Ventura</title>
    <link href="https://knktc.com/en/2022/10/27/fix-mac-os-13-ventura-git-ssh-permission-denied/"/>
    <id>https://knktc.com/en/2022/10/27/fix-mac-os-13-ventura-git-ssh-permission-denied/</id>
    <published>2022-10-27T04:41:08.000Z</published>
    <updated>2026-04-29T01:41:14.877Z</updated>
    
    <content type="html"><![CDATA[<p>After upgrading to macOS 13 Ventura, I suddenly found that <code>git push</code> stopped working and failed with:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Permission denied (publickey).</span><br><span class="line">fatal: Could not read from remote repository.</span><br></pre></td></tr></table></figure><span id="more"></span><p>After repeatedly confirming that my public key was still present on GitLab, I found discussions مثل this one:</p><p><a href="https://superuser.com/questions/1749364/git-ssh-permission-denied-in-macos-13-ventura">https://superuser.com/questions/1749364/git-ssh-permission-denied-in-macos-13-ventura</a></p><p>The likely cause is that the bundled <code>OpenSSH_9.0p1</code> in macOS 13 disables RSA/SHA-1 by default.</p><p>There are two main ways to fix it.</p><h2 id="Option-1-Generate-a-new-key"><a href="#Option-1-Generate-a-new-key" class="headerlink" title="Option 1: Generate a new key"></a>Option 1: Generate a new key</h2><p>This is the cleaner option, though it can be more expensive if you need to update the key in multiple places:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen -t ed25519</span><br></pre></td></tr></table></figure><h2 id="Option-2-Re-enable-RSA-in-SSH-config"><a href="#Option-2-Re-enable-RSA-in-SSH-config" class="headerlink" title="Option 2: Re-enable RSA in SSH config"></a>Option 2: Re-enable RSA in SSH config</h2><p>Edit <code>~/.ssh/config</code> and add:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Host *</span><br><span class="line">    HostkeyAlgorithms +ssh-rsa</span><br><span class="line">    PubkeyAcceptedAlgorithms +ssh-rsa</span><br></pre></td></tr></table></figure><p>This can also help when the remote server is too old to support newer algorithms.</p>]]></content>
    
    
    <summary type="html">macOS Ventura&#39;s newer OpenSSH defaults can reject older RSA/SHA-1 server setups, causing git push to fail with Permission denied (publickey).</summary>
    
    
    
    
    <category term="git" scheme="https://knktc.com/en/tags/git/"/>
    
    <category term="macos" scheme="https://knktc.com/en/tags/macos/"/>
    
    <category term="ventura" scheme="https://knktc.com/en/tags/ventura/"/>
    
    <category term="ssh" scheme="https://knktc.com/en/tags/ssh/"/>
    
    <category term="permission" scheme="https://knktc.com/en/tags/permission/"/>
    
    <category term="denied" scheme="https://knktc.com/en/tags/denied/"/>
    
  </entry>
  
  <entry>
    <title>Fixing SyntaxWarning in python-memcached on Python 3.8</title>
    <link href="https://knktc.com/en/2022/08/10/fix-python-memcached-3-8-syntax-warning/"/>
    <id>https://knktc.com/en/2022/08/10/fix-python-memcached-3-8-syntax-warning/</id>
    <published>2022-08-10T13:57:45.000Z</published>
    <updated>2026-04-22T01:34:32.361Z</updated>
    
    <content type="html"><![CDATA[<p>We had been using the <code>python-memcached</code> library for memcached access for a long time, and recently noticed warnings like the following under Python 3.8:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">memcache.py:1303: SyntaxWarning: &quot;is&quot; with a literal. Did you mean &quot;==&quot;?</span><br><span class="line">  if key is &#x27;&#x27;:</span><br><span class="line">memcache.py:1304: SyntaxWarning: &quot;is&quot; with a literal. Did you mean &quot;==&quot;?</span><br><span class="line">  if key_extra_len is 0:</span><br></pre></td></tr></table></figure><span id="more"></span><p>After checking the <code>python-memcached</code> source code, which is basically a single file, the problematic lines were easy to find:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> key <span class="keyword">is</span> <span class="string">&#x27;&#x27;</span>:</span><br><span class="line">    <span class="keyword">if</span> key_extra_len <span class="keyword">is</span> <span class="number">0</span>:</span><br></pre></td></tr></table></figure><p>That pattern is no longer acceptable in Python 3.8.</p><p>Even though it is “just” a warning, it is still noisy and unpleasant in logs, so I wanted to fix it.</p><p>I then checked the official GitHub repository and found that the project has not really been maintained for quite a while. Even though there is already a PR for this issue, it has not been merged:</p><p><a href="https://github.com/linsomniac/python-memcached/issues/176">https://github.com/linsomniac/python-memcached/issues/176</a></p><p>So there are basically two practical options:</p><ol><li>Patch <code>memcache.py</code> directly in your environment and replace <code>is</code> with <code>==</code> in those lines.</li><li>Since we distribute a <code>requirements.txt</code>, I published a fixed package to PyPI, which you can install directly if needed:</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install python-memcached-py38fix</span><br></pre></td></tr></table></figure><p>At this point, moving to <code>pymemcache</code> is probably the more sensible long-term direction anyway, especially since Django has already dropped support for <code>python-memcached</code>.</p>]]></content>
    
    
    <summary type="html">How to deal with the is-vs-== SyntaxWarning raised by python-memcached under Python 3.8, and a practical workaround.</summary>
    
    
    
    
    <category term="python" scheme="https://knktc.com/en/tags/python/"/>
    
    <category term="memcached" scheme="https://knktc.com/en/tags/memcached/"/>
    
    <category term="SyntaxWarning" scheme="https://knktc.com/en/tags/SyntaxWarning/"/>
    
  </entry>
  
  <entry>
    <title>Ignore 404 not found logs in Nginx</title>
    <link href="https://knktc.com/en/2022/08/10/nginx-not-found-log-off/"/>
    <id>https://knktc.com/en/2022/08/10/nginx-not-found-log-off/</id>
    <published>2022-08-10T13:42:44.000Z</published>
    <updated>2026-04-22T01:34:32.348Z</updated>
    
    <content type="html"><![CDATA[<p>I recently used Nginx to serve a few static JSON files as lightweight configuration endpoints. The simplest configuration looks like this:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">location</span> <span class="regexp">~ ^/myconf/(?&lt;filename&gt;.*)$</span> &#123;</span><br><span class="line">    <span class="attribute">alias</span> /home/knktc/myconf/$filename;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><span id="more"></span><p>The problem is that when the requested file does not exist, Nginx returns its default 404 page. To make this easier for frontend code to handle, I changed the configuration so that missing files return an empty JSON object instead:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">location</span> <span class="regexp">~ ^/myconf/(?&lt;filename&gt;.*)$</span> &#123;</span><br><span class="line">    <span class="attribute">alias</span> /home/knktc/myconf/$filename;</span><br><span class="line">    <span class="attribute">error_page</span> <span class="number">404</span> = @not_found_fallback;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="attribute">location</span> @not_found_fallback &#123;</span><br><span class="line">    <span class="attribute">add_header</span> Content-Type text/plain;</span><br><span class="line">    <span class="attribute">return</span> <span class="number">200</span> <span class="string">&quot;&#123;&#125;&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Now, if the file is missing, the response is an empty JSON object.</p><p>But there is still another issue: Nginx will log the 404 event in <code>error.log</code> even though the client gets the fallback response.</p><p>So the configuration needs one more change to suppress the not-found log:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">location</span> <span class="regexp">~ ^/myconf/(?&lt;filename&gt;.*)$</span> &#123;</span><br><span class="line">    <span class="attribute">alias</span> /home/knktc/myconf/$filename;</span><br><span class="line">    <span class="attribute">error_page</span> <span class="number">404</span> = @not_found_fallback;</span><br><span class="line">    <span class="attribute">log_not_found</span> <span class="literal">off</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="attribute">location</span> @not_found_fallback &#123;</span><br><span class="line">    <span class="attribute">add_header</span> Content-Type text/plain;</span><br><span class="line">    <span class="attribute">return</span> <span class="number">200</span> <span class="string">&quot;&#123;&#125;&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>That does the trick. Using Nginx’s static file handling for simple lightweight interfaces can be quite convenient.</p>]]></content>
    
    
    <summary type="html">How to return an empty JSON object for missing static files in Nginx and suppress the related not-found error log entries.</summary>
    
    
    
    
    <category term="log" scheme="https://knktc.com/en/tags/log/"/>
    
    <category term="nginx" scheme="https://knktc.com/en/tags/nginx/"/>
    
    <category term="404" scheme="https://knktc.com/en/tags/404/"/>
    
    <category term="json" scheme="https://knktc.com/en/tags/json/"/>
    
  </entry>
  
  <entry>
    <title>Automating HTTPS certificates with Certbot on Ubuntu, Cloudflare, and Nginx</title>
    <link href="https://knktc.com/en/2022/06/16/certbot-on-ubuntu-cheatsheet/"/>
    <id>https://knktc.com/en/2022/06/16/certbot-on-ubuntu-cheatsheet/</id>
    <published>2022-06-16T13:45:36.000Z</published>
    <updated>2026-04-22T02:02:38.195Z</updated>
    
    <content type="html"><![CDATA[<p>There is already plenty of documentation online for generating Let’s Encrypt certificates with Certbot, but I still wanted to keep my own notes here for future reference.</p><p>In this setup, the operating system is Ubuntu 20.04, DNS is managed with Cloudflare, and Nginx is used as the web server.</p><p>Following the steps below is enough to get it working.</p><span id="more"></span><h2 id="Install-Certbot"><a href="#Install-Certbot" class="headerlink" title="Install Certbot"></a>Install Certbot</h2><p>You can install it with the following commands. In some environments, <code>snap install</code> can be slow and may need to be retried once or twice:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">apt install snapd</span><br><span class="line">snap install core</span><br><span class="line">snap refresh core</span><br><span class="line">snap install --classic certbot</span><br></pre></td></tr></table></figure><p>After installation, run <code>certbot --version</code>. If it prints a version number, the installation is successful.</p><h2 id="Install-the-Cloudflare-plugin"><a href="#Install-the-Cloudflare-plugin" class="headerlink" title="Install the Cloudflare plugin"></a>Install the Cloudflare plugin</h2><p>To renew certificates automatically, you also need the plugin for your DNS provider. Since I use Cloudflare for DNS, I installed the Cloudflare plugin like this:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">snap set certbot trust-plugin-with-root=ok</span><br><span class="line"></span><br><span class="line">snap install certbot-dns-cloudflare</span><br></pre></td></tr></table></figure><h2 id="Create-a-Cloudflare-API-token"><a href="#Create-a-Cloudflare-API-token" class="headerlink" title="Create a Cloudflare API token"></a>Create a Cloudflare API token</h2><p>To use the Cloudflare plugin, first log in to Cloudflare and create an API token that is only allowed to manage DNS records.</p><p>You can create it here after signing in:</p><p><a href="https://dash.cloudflare.com/profile/api-tokens">https://dash.cloudflare.com/profile/api-tokens</a></p><p>Once the token is created, save it somewhere safe and then create the Certbot credentials file:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mkdir -p ~/.secrets/certbot</span><br><span class="line"></span><br><span class="line">vim ~/.secrets/certbot/cloudflare.ini</span><br></pre></td></tr></table></figure><p>The file should contain:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">dns_cloudflare_api_token</span> = YOUR_API_TOKEN</span><br></pre></td></tr></table></figure><p>After saving it, tighten the file permissions. Otherwise Certbot will warn that the credentials file is insecure:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">chmod 600 ~/.secrets/certbot/cloudflare.ini</span><br></pre></td></tr></table></figure><h2 id="Request-the-certificate"><a href="#Request-the-certificate" class="headerlink" title="Request the certificate"></a>Request the certificate</h2><p>Now you can generate the certificate Nginx will use:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">certbot certonly \</span><br><span class="line">  --dns-cloudflare \</span><br><span class="line">  --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \</span><br><span class="line">  --dns-cloudflare-propagation-seconds 60 \</span><br><span class="line">  -d knktc.com</span><br></pre></td></tr></table></figure><p>Note the use of <code>--dns-cloudflare-propagation-seconds 60</code>. I increased the wait time to 60 seconds because the default 10 seconds sometimes caused validation failures.</p><p>The issued certificate and private key will be placed under:</p><p><code>/etc/letsencrypt/live/YOUR_HOST/</code></p><p>For reference, here is a related Nginx configuration snippet:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">listen</span> <span class="number">443</span> ssl http2;</span><br><span class="line"><span class="attribute">server_name</span> knktc.com;</span><br><span class="line"><span class="attribute">ssl_certificate</span> /etc/letsencrypt/live/knktc.com/fullchain.pem; <span class="comment"># managed by Certbot</span></span><br><span class="line"><span class="attribute">ssl_certificate_key</span> /etc/letsencrypt/live/knktc.com/privkey.pem; <span class="comment"># managed by Certbot</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">ssl_protocols</span>       TLSv1 TLSv1.<span class="number">1</span> TLSv1.<span class="number">2</span>;</span><br><span class="line"><span class="attribute">ssl_ciphers</span>         HIGH:!aNULL:!MD5;</span><br><span class="line"><span class="attribute">keepalive_timeout</span>   <span class="number">70</span>;</span><br><span class="line"><span class="attribute">ssl_session_timeout</span>  <span class="number">5m</span>;</span><br></pre></td></tr></table></figure><h2 id="Scheduled-renewal"><a href="#Scheduled-renewal" class="headerlink" title="Scheduled renewal"></a>Scheduled renewal</h2><p>You can verify whether Certbot’s scheduled renewal task has been added successfully with:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl list-timers</span><br></pre></td></tr></table></figure><p>If you want Nginx to reload automatically whenever a new certificate is issued, add a renewal hook script:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim /etc/letsencrypt/renewal-hooks/post/reload-nginx</span><br></pre></td></tr></table></figure><p>Put the following into the file:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash">!/bin/sh</span></span><br><span class="line"></span><br><span class="line">systemctl reload nginx</span><br></pre></td></tr></table></figure><p>Then make it executable:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">chmod +x /etc/letsencrypt/renewal-hooks/post/reload-nginx</span><br></pre></td></tr></table></figure><p>With that in place, Nginx will automatically reload after every successful certificate renewal.</p>]]></content>
    
    
    <summary type="html">A practical walkthrough for issuing and renewing Let&#39;s Encrypt certificates on Ubuntu with Certbot, Cloudflare DNS, and Nginx.</summary>
    
    
    
    
    <category term="certbot" scheme="https://knktc.com/en/tags/certbot/"/>
    
    <category term="ubuntu" scheme="https://knktc.com/en/tags/ubuntu/"/>
    
    <category term="ssl" scheme="https://knktc.com/en/tags/ssl/"/>
    
    <category term="https" scheme="https://knktc.com/en/tags/https/"/>
    
    <category term="letsencrypt" scheme="https://knktc.com/en/tags/letsencrypt/"/>
    
    <category term="cloudflare" scheme="https://knktc.com/en/tags/cloudflare/"/>
    
    <category term="nginx" scheme="https://knktc.com/en/tags/nginx/"/>
    
  </entry>
  
  <entry>
    <title>Using a mirror to speed up Poetry installation</title>
    <link href="https://knktc.com/en/2022/04/22/set-mirror-when-installing-poerty/"/>
    <id>https://knktc.com/en/2022/04/22/set-mirror-when-installing-poerty/</id>
    <published>2022-04-22T05:52:52.000Z</published>
    <updated>2026-04-22T01:34:32.334Z</updated>
    
    <content type="html"><![CDATA[<p>First, configure a global pip mirror:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip config --user <span class="built_in">set</span> global.index-url https://pypi.tuna.tsinghua.edu.cn/simple</span><br></pre></td></tr></table></figure><p>Then download the Poetry installer script locally, for example as <code>install-poetry.py</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -sSL https://install.python-poetry.org -o install-poetry.py</span><br></pre></td></tr></table></figure><p>Open the script and find this function:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">pip</span>(<span class="params">self, *args, **kwargs</span>) -&gt; subprocess.CompletedProcess:</span></span><br><span class="line">        <span class="keyword">return</span> self.python(<span class="string">&quot;-m&quot;</span>, <span class="string">&quot;pip&quot;</span>, <span class="string">&quot;--isolated&quot;</span>, *args, **kwargs)</span><br></pre></td></tr></table></figure><p>Remove <code>&quot;--isolated&quot;</code> from that call. After that, when the installer invokes pip, it will use the mirror you configured earlier.</p><p>Then run the installer:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python3 install-poetry.py</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">A simple workaround to make the Poetry installer use a configured pip mirror instead of isolated mode.</summary>
    
    
    
    
    <category term="mirror" scheme="https://knktc.com/en/tags/mirror/"/>
    
    <category term="poetry" scheme="https://knktc.com/en/tags/poetry/"/>
    
    <category term="pip" scheme="https://knktc.com/en/tags/pip/"/>
    
  </entry>
  
  <entry>
    <title>Using Celery broadcast tasks</title>
    <link href="https://knktc.com/en/2022/03/26/use-celery-broadcast-queue/"/>
    <id>https://knktc.com/en/2022/03/26/use-celery-broadcast-queue/</id>
    <published>2022-03-26T13:35:13.000Z</published>
    <updated>2026-04-22T02:02:38.222Z</updated>
    
    <content type="html"><![CDATA[<p>I recently wanted to add an Agent component to one of my systems for tasks such as configuration updates and monitoring data collection.</p><p>Because the system is deployed with multiple instances, those agents may need to run the same task at the same time. Since we were already using Celery, its broadcast feature turned out to be a good fit: the same task can be delivered to every worker.</p><span id="more"></span><p>To test the idea, I first wrote a simple task:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># filename: agent.py</span></span><br><span class="line"><span class="keyword">import</span> datetime</span><br><span class="line"><span class="keyword">from</span> celery <span class="keyword">import</span> shared_task</span><br><span class="line"></span><br><span class="line"><span class="meta">@shared_task(<span class="params">bind=<span class="literal">True</span></span>)</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">test</span>(<span class="params">self</span>):</span></span><br><span class="line">    name = self.request.hostname</span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">f&#x27;/tmp/<span class="subst">&#123;name&#125;</span>&#x27;</span>, <span class="string">&#x27;w&#x27;</span>) <span class="keyword">as</span> f:</span><br><span class="line">        f.write(<span class="built_in">str</span>(datetime.datetime.now()))</span><br></pre></td></tr></table></figure><p>This task reads the worker hostname from <code>self.request.hostname</code>, then creates a file under <code>/tmp</code> with that hostname as the filename and the execution time as the content.</p><p>In other words, if the broadcast works correctly, multiple files should appear in <code>/tmp</code> at the same time, one for each worker bound to the queue.</p><p>Then I added the following Celery app configuration:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> kombu.common <span class="keyword">import</span> Broadcast</span><br><span class="line"></span><br><span class="line">app.conf.task_queues = (Broadcast(<span class="string">&#x27;broadcast_tasks&#x27;</span>),)</span><br><span class="line">app.conf.task_routes = &#123;</span><br><span class="line">    <span class="string">&#x27;agent.test&#x27;</span>: &#123;</span><br><span class="line">        <span class="string">&#x27;queue&#x27;</span>: <span class="string">&#x27;broadcast_tasks&#x27;</span>,</span><br><span class="line">        <span class="string">&#x27;exchange&#x27;</span>: <span class="string">&#x27;broadcast_tasks&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This defines a queue named <code>broadcast_tasks</code>, and the <code>agent.test</code> task is routed through the <code>broadcast_tasks</code> exchange.</p><p>On the worker side, I started two workers bound to <code>broadcast_tasks</code>, one named <code>agent1</code> and the other <code>agent2</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">celery -A boss2_manager worker --pidfile=/var/run/celeryworker_agent1.pid -Q broadcast_tasks -n agent1@%h</span><br><span class="line">celery -A boss2_manager worker --pidfile=/var/run/celeryworker_agent2.pid -Q broadcast_tasks -n agent2@%h</span><br></pre></td></tr></table></figure><p>If RabbitMQ is used as the broker, you can also see this in its management UI: a Celery broadcast queue is effectively a <code>fanout</code> exchange plus one queue per worker bound to it.</p><p>Then I simply triggered the task with <code>delay()</code>:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> agent <span class="keyword">import</span> test</span><br><span class="line"></span><br><span class="line">test.delay()</span><br></pre></td></tr></table></figure><p>Checking <code>/tmp</code> afterward, I found two files:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">agent1@knktc-rmbp.local</span><br><span class="line">agent2@knktc-rmbp.local</span><br></pre></td></tr></table></figure><p>The timestamps inside them were almost the same.</p><p>That was enough to confirm the approach, so I ended up using this method for the Agent feature.</p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul><li>Celery broadcast documentation: <a href="https://docs.celeryq.dev/en/stable/userguide/routing.html#broadcast">https://docs.celeryq.dev/en/stable/userguide/routing.html#broadcast</a></li></ul>]]></content>
    
    
    <summary type="html">How to use Celery broadcast queues so multiple workers can receive and run the same task at the same time.</summary>
    
    
    
    
    <category term="celery" scheme="https://knktc.com/en/tags/celery/"/>
    
    <category term="rabbitmq" scheme="https://knktc.com/en/tags/rabbitmq/"/>
    
    <category term="broadcast" scheme="https://knktc.com/en/tags/broadcast/"/>
    
  </entry>
  
  <entry>
    <title>How to import SNMP MIB files on Ubuntu</title>
    <link href="https://knktc.com/en/2022/03/23/where-to-place-mib-files-in-ubuntu/"/>
    <id>https://knktc.com/en/2022/03/23/where-to-place-mib-files-in-ubuntu/</id>
    <published>2022-03-23T14:39:41.000Z</published>
    <updated>2026-04-22T01:34:32.314Z</updated>
    
    <content type="html"><![CDATA[<p>One of my systems uses Telegraf’s <code>snmp_trap</code> input plugin to collect SNMP trap alerts and then forward them as HTTP for the next stage of processing.</p><p>The plugin itself is easy enough to configure, but translating OIDs into readable names depends on <code>snmptranslate</code>, and <code>snmptranslate</code> in turn depends on having the correct MIB files installed and configured properly.</p><p>I recently used this setup to collect alerts from H3C switches, so I thought it would be useful to record how to import MIB files on Ubuntu.</p><span id="more"></span><h2 id="Before-configuration"><a href="#Before-configuration" class="headerlink" title="Before configuration"></a>Before configuration</h2><p>Before changing anything, try running <code>snmptranslate</code> first. The <code>-L n</code> option simply suppresses error output:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">snmptranslate -L n .1.3.6.1.4.1.25506.2.38.1.6.3.0.1</span><br></pre></td></tr></table></figure><p>This example OID comes from an H3C switch. If the MIB files are not installed, the result will only look like this:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SNMPv2-SMI::enterprises.25506.2.38.1.6.3.0.1</span><br></pre></td></tr></table></figure><p>That is obviously not very helpful if you want to know what the device actually sent.</p><h2 id="Import-and-configure"><a href="#Import-and-configure" class="headerlink" title="Import and configure"></a>Import and configure</h2><p>First, obtain the MIB files from H3C and extract them into a directory. In this example, I put them under:</p><p><code>/usr/share/mibs/h3c_mibs</code></p><p>After placing the MIB files there, edit the <code>snmp.conf</code> file:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim /etc/snmp/snmp.conf</span><br></pre></td></tr></table></figure><p>If the file does not exist, you may need to install the required package first:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">apt install snmp</span><br></pre></td></tr></table></figure><p>The original configuration usually looks like this:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># As the snmp packages come without MIB files due to license reasons, loading</span><br><span class="line"># of MIBs is disabled by default. If you added the MIBs you can reenable</span><br><span class="line"># loading them by commenting out the following line.</span><br><span class="line">mibs :</span><br></pre></td></tr></table></figure><p>To enable your custom MIB files, change it like this:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># mibs :</span><br><span class="line"></span><br><span class="line">mibdirs +/usr/share/mibs/h3c_mibs</span><br><span class="line">mibdirs +/usr/share/mibs/another_mib_dir  # for example</span><br><span class="line"></span><br><span class="line">mibs +ALL</span><br></pre></td></tr></table></figure><p>A quick explanation:</p><ul><li>Comment out the original <code>mibs :</code> line so the system is allowed to load third-party MIBs.</li><li>Use <code>mibdirs</code> to specify directories that contain MIB files. You can use it multiple times if needed.</li><li>Use <code>mibs +ALL</code> to search through all available MIB files.</li></ul><p>Once the file is updated, save and exit. No service restart is required.</p><h2 id="Test"><a href="#Test" class="headerlink" title="Test"></a>Test</h2><p>Now try translating the OID again. If the MIB files are placed correctly and the config is right, it should work:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">root@knktc.com:/root# snmptranslate -L n .1.3.6.1.4.1.25506.2.38.1.6.3.0.1</span><br><span class="line">HH3C-TRAP-MIB::hh3cPeriodicalTrap</span><br></pre></td></tr></table></figure><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li>Telegraf <code>snmp_trap</code> plugin: <a href="https://github.com/influxdata/telegraf/tree/master/plugins/inputs/snmp_trap">https://github.com/influxdata/telegraf/tree/master/plugins/inputs/snmp_trap</a></li><li><code>snmp.conf</code> man page: <a href="https://linux.die.net/man/5/snmp.conf">https://linux.die.net/man/5/snmp.conf</a></li></ul>]]></content>
    
    
    <summary type="html">How to place and enable third-party SNMP MIB files on Ubuntu so tools like snmptranslate can resolve vendor-specific OIDs correctly.</summary>
    
    
    
    
    <category term="ubuntu" scheme="https://knktc.com/en/tags/ubuntu/"/>
    
    <category term="mib" scheme="https://knktc.com/en/tags/mib/"/>
    
    <category term="snmp" scheme="https://knktc.com/en/tags/snmp/"/>
    
    <category term="mibs" scheme="https://knktc.com/en/tags/mibs/"/>
    
    <category term="telegraf" scheme="https://knktc.com/en/tags/telegraf/"/>
    
  </entry>
  
  <entry>
    <title>Using a different SSH key for Git commands</title>
    <link href="https://knktc.com/en/2022/03/21/git-use-different-ssh-key/"/>
    <id>https://knktc.com/en/2022/03/21/git-use-different-ssh-key/</id>
    <published>2022-03-21T14:55:38.000Z</published>
    <updated>2026-04-22T01:34:32.297Z</updated>
    
    <content type="html"><![CDATA[<p>I recently needed to add a tagging step to an automated build, but for some reason the default deploy key that had been configured before was no longer available, possibly due to a GitLab bug.</p><p>So I ended up creating a new SSH key pair on the build server and configuring Git-related SSH access to use that new key explicitly.</p><span id="more"></span><p>In the examples below, assume the Git server hostname is <code>gitlab.knktc.com</code>.</p><p>First, create a new SSH key pair:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mkdir -p /home/knktc/gitlab_ssh_keys</span><br><span class="line">ssh-keygen -f /home/knktc/gitlab_ssh_keys/id_rsa</span><br></pre></td></tr></table></figure><p>In this example, the new private key is stored at:</p><p><code>/home/knktc/gitlab_ssh_keys/id_rsa</code></p><p>Then create or edit the SSH config file under the current user’s home directory:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim ~/.ssh/config</span><br></pre></td></tr></table></figure><p>Add the following configuration:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Host gitlab.knktc.com</span><br><span class="line">    IdentityFile &#x2F;home&#x2F;knktc&#x2F;gitlab_ssh_keys&#x2F;id_rsa</span><br></pre></td></tr></table></figure><p>This means that when connecting to <code>gitlab.knktc.com</code> over SSH, the newly created key will be used.</p><p>Finally, test cloning the repository:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> git@gitlab.knktc.com:blog/knktc-com.git</span><br></pre></td></tr></table></figure><p>That should work.</p><p>For more details about SSH config, see:</p><p><a href="https://linux.die.net/man/5/ssh_config">https://linux.die.net/man/5/ssh_config</a></p>]]></content>
    
    
    <summary type="html">How to make Git use a separate SSH key for a specific Git server by adding an entry to your SSH config.</summary>
    
    
    
    
    <category term="git" scheme="https://knktc.com/en/tags/git/"/>
    
    <category term="ssh" scheme="https://knktc.com/en/tags/ssh/"/>
    
    <category term="private" scheme="https://knktc.com/en/tags/private/"/>
    
    <category term="public" scheme="https://knktc.com/en/tags/public/"/>
    
    <category term="key" scheme="https://knktc.com/en/tags/key/"/>
    
  </entry>
  
  <entry>
    <title>Add a Standby Mode to Celery Beat with a Monkey Patch</title>
    <link href="https://knktc.com/en/2022/03/12/celery-beat-standby-mode/"/>
    <id>https://knktc.com/en/2022/03/12/celery-beat-standby-mode/</id>
    <published>2022-03-12T09:24:03.000Z</published>
    <updated>2026-04-29T01:36:04.137Z</updated>
    
    <content type="html"><![CDATA[<p>I have used Celery Beat for scheduled tasks for a long time. It is simple and useful, but it has one persistent problem: if multiple Beat instances run at the same time, tasks get scheduled more than once.</p><p>We previously relied on uWSGI legion mode to make sure only one Beat instance was active at a time, but that depends on having a reliable network connection. Recently I ran into a case where the network between two Beat nodes could be unstable, so uWSGI legion no longer felt safe enough. That led me to look for a way to make Celery Beat enter a “standby” mode: the service stays up, but it stops generating scheduled tasks.</p><span id="more"></span><p>After reading the Celery Beat source code, I found that the <code>celery</code> CLI eventually calls the <code>start</code> method on the <code>Service</code> class from <code>celery.beat</code>. So the easiest approach seemed to be patching that method.</p><p>Here is a simple monkey patch:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"><span class="keyword">from</span> celery.beat <span class="keyword">import</span> info, debug, humanize_seconds, signals, platforms</span><br><span class="line"></span><br><span class="line">STANDBY_CHECK_INTERVAL = <span class="number">5</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">is_standby</span>():</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">True</span> <span class="keyword">if</span> os.path.isfile(<span class="string">&#x27;/tmp/standby.flag&#x27;</span>) <span class="keyword">else</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">patched_start</span>(<span class="params">self, embedded_process=<span class="literal">False</span></span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot; monkey patched start method, will skip tasks when standby flag is set &quot;&quot;&quot;</span></span><br><span class="line">    info(<span class="string">&#x27;beat: Starting...&#x27;</span>)</span><br><span class="line">    debug(<span class="string">&#x27;beat: Ticking with max interval-&gt;%s&#x27;</span>,</span><br><span class="line">          humanize_seconds(self.scheduler.max_interval))</span><br><span class="line"></span><br><span class="line">    signals.beat_init.send(sender=self)</span><br><span class="line">    <span class="keyword">if</span> embedded_process:</span><br><span class="line">        signals.beat_embedded_init.send(sender=self)</span><br><span class="line">        platforms.set_process_title(<span class="string">&#x27;celery beat&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">while</span> <span class="keyword">not</span> self._is_shutdown.is_set():</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> is_standby():</span><br><span class="line">                debug(<span class="string">f&#x27;beat: in standby mode, all tasks will be skipped, &#x27;</span></span><br><span class="line">                      <span class="string">f&#x27;will check in [<span class="subst">&#123;STANDBY_CHECK_INTERVAL&#125;</span>] seconds&#x27;</span>)</span><br><span class="line">                time.sleep(STANDBY_CHECK_INTERVAL)</span><br><span class="line">                <span class="keyword">continue</span></span><br><span class="line"></span><br><span class="line">            interval = self.scheduler.tick()</span><br><span class="line">            <span class="keyword">if</span> interval <span class="keyword">and</span> interval &gt; <span class="number">0.0</span>:</span><br><span class="line">                debug(<span class="string">&#x27;beat: Waking up %s.&#x27;</span>,</span><br><span class="line">                      humanize_seconds(interval, prefix=<span class="string">&#x27;in &#x27;</span>))</span><br><span class="line">                time.sleep(interval)</span><br><span class="line">                <span class="keyword">if</span> self.scheduler.should_sync():</span><br><span class="line">                    self.scheduler._do_sync()</span><br><span class="line">    <span class="keyword">except</span> (KeyboardInterrupt, SystemExit):</span><br><span class="line">        self._is_shutdown.<span class="built_in">set</span>()</span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        self.sync()</span><br></pre></td></tr></table></figure><p>A quick explanation:</p><ul><li>This example checks whether <code>/tmp/standby.flag</code> exists.</li><li>If the file exists, Beat is considered to be in standby mode.</li><li>Inside the main loop, Beat checks for that file before scheduling tasks.</li><li>If the file is present, it sleeps for 5 seconds and checks again.</li></ul><p>To use it, just apply the patch at your startup entry point:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> celery.beat <span class="keyword">import</span> Service</span><br><span class="line"></span><br><span class="line">Service.start = patched_start</span><br></pre></td></tr></table></figure><p>After that, once Celery Beat has started, you can run <code>touch /tmp/standby.flag</code> to put it into standby mode. It will stop generating scheduled tasks. Remove the file, wait a few seconds, and Beat will resume normal scheduling.</p>]]></content>
    
    
    <summary type="html">Monkey-patch Celery Beat so it stays running but temporarily stops generating scheduled tasks when a standby flag is present.</summary>
    
    
    
    
    <category term="celery" scheme="https://knktc.com/en/tags/celery/"/>
    
    <category term="beat" scheme="https://knktc.com/en/tags/beat/"/>
    
    <category term="standby" scheme="https://knktc.com/en/tags/standby/"/>
    
    <category term="monkey" scheme="https://knktc.com/en/tags/monkey/"/>
    
    <category term="patch" scheme="https://knktc.com/en/tags/patch/"/>
    
  </entry>
  
  <entry>
    <title>Submit a Hexo Sitemap to Baidu with GitHub Actions</title>
    <link href="https://knktc.com/en/2022/02/27/submit-baidu-sitemap-by-github-actions/"/>
    <id>https://knktc.com/en/2022/02/27/submit-baidu-sitemap-by-github-actions/</id>
    <published>2022-02-27T13:35:07.000Z</published>
    <updated>2026-04-29T00:35:33.380Z</updated>
    
    <content type="html"><![CDATA[<p>When I looked at this blog’s rather modest page views, I started paying more attention to SEO. That was also the first time I logged into <a href="https://ziyuan.baidu.com/">Baidu Search Resource Platform</a>, only to discover that Baidu had indexed just 8 pages from my site.</p><p>No wonder almost all of my traffic was coming from Google and Bing. Baidu had barely indexed anything. Since Baidu does not provide a sitemap submission API like Google does, the only practical option is to submit URLs directly. So I put together a small workflow that lets this Hexo blog submit sitemap URLs to Baidu automatically through GitHub Actions.</p><span id="more"></span><h1 id="Prepare-the-script"><a href="#Prepare-the-script" class="headerlink" title="Prepare the script"></a>Prepare the script</h1><p>First, write a small Python script that downloads a <code>sitemap.xml</code>, extracts the URLs, and submits them to Baidu:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/usr/bin/env python3</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">Script for submitting sitemap URLs to Baidu</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">visit: https://knktc.com</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">@author:knktc</span></span><br><span class="line"><span class="string">@contact:me@knktc.com</span></span><br><span class="line"><span class="string">@create:2022-02-12 22:49</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"><span class="keyword">import</span> argparse</span><br><span class="line"><span class="keyword">from</span> urllib <span class="keyword">import</span> request</span><br><span class="line"><span class="keyword">from</span> urllib.parse <span class="keyword">import</span> urljoin</span><br><span class="line"><span class="keyword">import</span> xml.etree.ElementTree <span class="keyword">as</span> ET</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">chunker</span>(<span class="params">seq, size</span>):</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot; iterate list by chunk &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">return</span> (seq[pos:pos + size] <span class="keyword">for</span> pos <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">0</span>, <span class="built_in">len</span>(seq), size))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">BaiduSubmitter</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span>(<span class="params">self, site: <span class="built_in">str</span>, token: <span class="built_in">str</span>, sitemap: <span class="built_in">str</span></span>):</span></span><br><span class="line">        self.submit_url = self.gen_submit_url(site, token)</span><br><span class="line">        self.sitemap_url = self.gen_sitemap_url(site, sitemap)</span><br><span class="line"></span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">gen_submit_url</span>(<span class="params">site: <span class="built_in">str</span>, token: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot; generate url to submit to &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">f&#x27;http://data.zz.baidu.com/urls?site=<span class="subst">&#123;site&#125;</span>&amp;token=<span class="subst">&#123;token&#125;</span>&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">gen_sitemap_url</span>(<span class="params">site: <span class="built_in">str</span>, sitemap: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot; generate url path to get sitemap &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">return</span> urljoin(site, sitemap)</span><br><span class="line"></span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">get_links_from_sitemap</span>(<span class="params">sitemap_url</span>) -&gt; <span class="built_in">list</span>:</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot; download sitemap, parse and get urls &quot;&quot;&quot;</span></span><br><span class="line">        <span class="keyword">with</span> request.urlopen(sitemap_url) <span class="keyword">as</span> resp:</span><br><span class="line">            data = resp.read()</span><br><span class="line"></span><br><span class="line">        root = ET.fromstring(data)</span><br><span class="line">        <span class="keyword">return</span> [_.text <span class="keyword">for</span></span><br><span class="line">                _ <span class="keyword">in</span> root.findall(<span class="string">&#x27;./&#123;http://www.sitemaps.org/schemas/sitemap/0.9&#125;url/&#123;http://www.sitemaps.org/schemas/sitemap/0.9&#125;loc&#x27;</span>)]</span><br><span class="line"></span><br><span class="line"><span class="meta">    @staticmethod</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">submit</span>(<span class="params">submit_url: <span class="built_in">str</span>, links: <span class="built_in">list</span></span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot; submit to baidu &quot;&quot;&quot;</span></span><br><span class="line">        data = <span class="string">&#x27;\n&#x27;</span>.join(links).encode(<span class="string">&#x27;utf8&#x27;</span>)</span><br><span class="line">        req = request.Request(submit_url, data=data)</span><br><span class="line">        <span class="keyword">return</span> request.urlopen(req).read().decode()</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">run</span>(<span class="params">self, chunk_size=<span class="number">20</span>, sleep_time=<span class="number">0.1</span></span>):</span></span><br><span class="line">        <span class="string">&quot;&quot;&quot; submit process &quot;&quot;&quot;</span></span><br><span class="line">        links = self.get_links_from_sitemap(self.sitemap_url)</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&#x27;Get <span class="subst">&#123;<span class="built_in">len</span>(links)&#125;</span> links from sitemap: [<span class="subst">&#123;self.sitemap_url&#125;</span>]&#x27;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> chunk <span class="keyword">in</span> chunker(links, chunk_size):</span><br><span class="line">            resp = self.submit(self.submit_url, chunk)</span><br><span class="line">            <span class="built_in">print</span>(resp)</span><br><span class="line">            <span class="keyword">if</span> sleep_time:</span><br><span class="line">                time.sleep(sleep_time)</span><br><span class="line"></span><br><span class="line">            time.sleep(<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_args</span>():</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot; get cli args &quot;&quot;&quot;</span></span><br><span class="line">    parser = argparse.ArgumentParser(description=<span class="string">&#x27;Submit sitemap to Baidu&#x27;</span>)</span><br><span class="line">    parser.add_argument(<span class="string">&#x27;--site&#x27;</span>, <span class="string">&#x27;-s&#x27;</span>, <span class="built_in">type</span>=<span class="built_in">str</span>, dest=<span class="string">&#x27;site&#x27;</span>, required=<span class="literal">True</span>,</span><br><span class="line">                        <span class="built_in">help</span>=<span class="string">&#x27;your site, eg: https://knktc.com&#x27;</span>)</span><br><span class="line">    parser.add_argument(<span class="string">&#x27;--token&#x27;</span>, <span class="string">&#x27;-t&#x27;</span>, <span class="built_in">type</span>=<span class="built_in">str</span>, dest=<span class="string">&#x27;token&#x27;</span>, required=<span class="literal">True</span>,</span><br><span class="line">                        <span class="built_in">help</span>=<span class="string">&#x27;baidu ziyuan token, you may find your token in https://ziyuan.baidu.com/linksubmit&#x27;</span>)</span><br><span class="line">    parser.add_argument(<span class="string">&#x27;--sitemap&#x27;</span>, <span class="string">&#x27;-p&#x27;</span>, <span class="built_in">type</span>=<span class="built_in">str</span>, dest=<span class="string">&#x27;sitemap&#x27;</span>, default=<span class="string">&#x27;sitemap.xml&#x27;</span>,</span><br><span class="line">                        <span class="built_in">help</span>=<span class="string">&#x27;url path to get sitemap.xml file, default: sitemap.xml&#x27;</span>)</span><br><span class="line">    parser.add_argument(<span class="string">&#x27;--chunk&#x27;</span>, <span class="string">&#x27;-c&#x27;</span>, <span class="built_in">type</span>=<span class="built_in">int</span>, dest=<span class="string">&#x27;chunk_size&#x27;</span>, default=<span class="number">100</span>,</span><br><span class="line">                        <span class="built_in">help</span>=<span class="string">&#x27;how many urls should be submitted each time&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    args = parser.parse_args()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> args</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">main</span>():</span></span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    main process</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    args = get_args()</span><br><span class="line">    site = args.site</span><br><span class="line">    token = args.token</span><br><span class="line">    sitemap_path = args.sitemap</span><br><span class="line">    chunk_size = args.chunk_size</span><br><span class="line"></span><br><span class="line">    submitter = BaiduSubmitter(site, token, sitemap_path)</span><br><span class="line">    submitter.run(chunk_size=chunk_size)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&#x27;__main__&#x27;</span>:</span><br><span class="line">    main()</span><br></pre></td></tr></table></figure><p>This script only uses Python’s standard library, so there is no extra dependency to install.</p><p>Save it locally, get your Baidu submission token from <a href="https://ziyuan.baidu.com/linksubmit">Baidu Search Resource Platform</a>, and run it like this:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python3 baidu_submit.py --site https://knktc.com --token AABBCCDD --sitemap sitemap.xml --chunk 100</span><br></pre></td></tr></table></figure><p>A quick explanation of the arguments:</p><ul><li><code>--site</code> or <code>-s</code>: your blog URL</li><li><code>--token</code> or <code>-t</code>: the submission token from Baidu</li><li><code>--sitemap</code> or <code>-p</code>: the sitemap path; for example, if your sitemap is at <code>https://knktc.com/sitemap.xml</code>, then <code>sitemap.xml</code> is enough here</li><li><code>--chunk</code> or <code>-c</code>: how many URLs to send in each request; the default is <code>100</code></li></ul><p>The output looks like this:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Get 235 links from sitemap: [https://knktc.com/sitemap.xml]</span><br><span class="line">&#123;&quot;remain&quot;:2585,&quot;success&quot;:100&#125;</span><br><span class="line">&#123;&quot;remain&quot;:2485,&quot;success&quot;:100&#125;</span><br><span class="line">&#123;&quot;remain&quot;:2450,&quot;success&quot;:35&#125;</span><br></pre></td></tr></table></figure><p>For convenience, I also published the script as a GitHub Gist:</p><p><a href="https://gist.github.com/knktc/846950067e60a92612c1befbe4213a32">https://gist.github.com/knktc/846950067e60a92612c1befbe4213a32</a></p><p>That way, the GitHub Actions workflow can just fetch the script directly.</p><h1 id="GitHub-Actions"><a href="#GitHub-Actions" class="headerlink" title="GitHub Actions"></a>GitHub Actions</h1><p>There is a small trick when adding GitHub Actions files to a Hexo repository. See my earlier post: <a href="https://knktc.com/2021/06/26/hexo-use-github-actions-to-submit-sitemap/">Use GitHub Actions to Submit a Sitemap for a Hexo Blog</a>.</p><p>Then create a workflow file named <code>baidu_sitemap.yml</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># workflow to summit urls from sitemap to baidu</span></span><br><span class="line"></span><br><span class="line"><span class="attr">name:</span> <span class="string">Submit</span> <span class="string">baidu</span> <span class="string">Sitemap</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">schedule:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">cron:</span> <span class="string">&#x27;15 2 * * *&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">submit:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">get</span> <span class="string">gist</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">andymckay/get-gist-action@0.1</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">gistURL:</span> <span class="string">https://gist.github.com/knktc/846950067e60a92612c1befbe4213a32</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">run</span> <span class="string">script</span></span><br><span class="line">        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">BAIDU_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.BAIDU_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">python3</span> <span class="string">/tmp/baidu_submit.py</span> <span class="string">--site</span> <span class="string">https://knktc.com</span> <span class="string">--token</span> <span class="string">$BAIDU_TOKEN</span></span><br></pre></td></tr></table></figure><p>There are a few important points in this workflow:</p><ul><li>It runs every day at <code>02:15</code> UTC, which is <code>10:15</code> in Beijing time.</li><li>The script is fetched from a Gist instead of being committed into the Hexo repository.</li><li>The Baidu token is passed through GitHub Secrets, so you need to configure <code>BAIDU_TOKEN</code> in the repository settings first.</li></ul><p>After that, GitHub Actions can submit your latest sitemap URLs to Baidu automatically every day.</p>]]></content>
    
    
    <summary type="html">Use a small Python script and a scheduled GitHub Actions workflow to submit your Hexo sitemap URLs to Baidu automatically.</summary>
    
    
    
    
    <category term="github" scheme="https://knktc.com/en/tags/github/"/>
    
    <category term="hexo" scheme="https://knktc.com/en/tags/hexo/"/>
    
    <category term="sitemap" scheme="https://knktc.com/en/tags/sitemap/"/>
    
    <category term="baidu" scheme="https://knktc.com/en/tags/baidu/"/>
    
    <category term="seo" scheme="https://knktc.com/en/tags/seo/"/>
    
  </entry>
  
  <entry>
    <title>Fix Different ETags for the Same Static File Across Nginx Nodes</title>
    <link href="https://knktc.com/en/2022/02/26/nginx-etag-different-in-2-nodes/"/>
    <id>https://knktc.com/en/2022/02/26/nginx-etag-different-in-2-nodes/</id>
    <published>2022-02-25T16:06:05.000Z</published>
    <updated>2026-04-29T00:56:08.841Z</updated>
    
    <content type="html"><![CDATA[<p>I was recently using Nginx to serve some static files as configuration payloads. Since Nginx has supported ETags by default since version 1.3.3, it is convenient to let browsers fetch a new file automatically when the configuration changes.</p><p>But after deploying to a test environment, I ran into a problem. There were two Nginx nodes behind a load balancer, and the same static file returned different ETags depending on which node handled the request. As a result, the browser kept receiving <code>200</code> responses instead of <code>304</code>, so caching was effectively broken.</p><span id="more"></span><p>After looking into how Nginx calculates ETags, I found that Nginx uses the file size and the file modification time. The <code>ngx_http_set_etag</code> function in the Nginx source contains code like this:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">etag-&gt;value.len = ngx_sprintf(etag-&gt;value.data, <span class="string">&quot;\&quot;%xT-%xO\&quot;&quot;</span>,</span><br><span class="line">                                  r-&gt;headers_out.last_modified_time,</span><br><span class="line">                                  r-&gt;headers_out.content_length_n)</span><br><span class="line">                      - etag-&gt;value.data;</span><br><span class="line"></span><br><span class="line">    r-&gt;headers_out.etag = etag;</span><br></pre></td></tr></table></figure><p>Full source reference:</p><p><a href="https://github.com/nginx/nginx/blob/1f01183b9e6658749934313fd72f7f16c1918b54/src/http/ngx_http_core_module.c#L1673">https://github.com/nginx/nginx/blob/1f01183b9e6658749934313fd72f7f16c1918b54/src/http/ngx_http_core_module.c#L1673</a></p><p>That makes it pretty clear: if two Nginx nodes return different ETags, the file modification time is probably different on each node.</p><p>Fortunately, changing file modification times is easy. You can use <code>touch</code>. The <code>-t</code> option is documented like this:</p><blockquote><p><code>-t STAMP</code><br>use <code>[[CC]YY]MMDDhhmm[.ss]</code> instead of current time</p></blockquote><p>So on both nodes, the following command sets the file timestamp to <code>2022-02-25 13:13</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">touch -t 202202251313 myconf.json</span><br></pre></td></tr></table></figure><p>If you want to do the same thing in Python, you can use <code>os.utime</code>:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line">os.utime(<span class="string">&#x27;myconf.json&#x27;</span>, (<span class="number">1645806368</span>, <span class="number">1645806368</span>))</span><br></pre></td></tr></table></figure><p>After doing that, both Nginx nodes returned the same ETag, and the second request correctly came back as <code>304</code> instead of <code>200</code>.</p><p>Reference:</p><ul><li><a href="https://serverfault.com/questions/690341/algorithm-behind-nginx-etag-generation">Discussion of the Nginx ETag algorithm on Server Fault</a></li></ul>]]></content>
    
    
    <summary type="html">If identical static files return different ETags on different Nginx nodes, check and normalize their modification timestamps.</summary>
    
    
    
    
    <category term="nginx" scheme="https://knktc.com/en/tags/nginx/"/>
    
    <category term="etag" scheme="https://knktc.com/en/tags/etag/"/>
    
  </entry>
  
  <entry>
    <title>Use Jira Charts Inside Confluence</title>
    <link href="https://knktc.com/en/2022/02/17/add-jira-charts-to-confluence-macro/"/>
    <id>https://knktc.com/en/2022/02/17/add-jira-charts-to-confluence-macro/</id>
    <published>2022-02-17T14:56:53.000Z</published>
    <updated>2026-04-29T01:36:04.080Z</updated>
    
    <content type="html"><![CDATA[<p>I recently wanted to build a weekly report in Confluence and embed a Jira burndown chart directly into the page. Confluence’s built-in macros did not include the chart I needed, so I looked into it and found that Atlassian actually supports using Jira dashboard gadgets as Confluence macros.</p><span id="more"></span><p>Since Jira dashboards can already display a burndown chart, the idea is to make that same gadget available in Confluence.</p><p>First, open the Jira home page. If you already have a dashboard, there should be an “Add gadget” button somewhere near the top right:</p><p><a href="https://imgtu.com/i/HIqk0P"><img src="https://s4.ax1x.com/2022/02/17/HIqk0P.png" alt="Jira add gadget"></a></p><p>After clicking it, a gadget dialog will appear. Choose the gadget you want, click “View XML”, and copy the XML URL:</p><p><a href="https://imgtu.com/i/HIqATf"><img src="https://s4.ax1x.com/2022/02/17/HIqATf.md.png" alt="Copy gadget XML URL"></a></p><p>Next, log in to Confluence as an administrator and open the site administration page. Under the “External Gadgets” section, on the gadget specification tab, paste the XML URL you copied and click “Add”:</p><p><a href="https://imgtu.com/i/HIqZtS"><img src="https://s4.ax1x.com/2022/02/17/HIqZtS.md.png" alt="Add external gadget in Confluence"></a></p><p>After that, when you add a macro to a Confluence page, you should see the registered gadgets in the macro picker. At that point you can insert the same gadget that exists on the Jira dashboard:</p><p><a href="https://imgtu.com/i/HIqVk8"><img src="https://s4.ax1x.com/2022/02/17/HIqVk8.md.png" alt="Use Jira gadget as a macro"></a></p><p>One thing to note is that these gadgets are implemented with iframes, so in practice users usually need Confluence and Jira authentication to be connected properly, typically through OAuth or a similar integration.</p><p>Reference:</p><ul><li><a href="https://confluence.atlassian.com/doc/registering-external-gadgets-204050482.html">Atlassian official documentation</a></li></ul>]]></content>
    
    
    <summary type="html">Add Jira dashboard gadgets such as burndown charts to Confluence by registering them as external gadgets.</summary>
    
    
    
    
    <category term="jira" scheme="https://knktc.com/en/tags/jira/"/>
    
    <category term="confluence" scheme="https://knktc.com/en/tags/confluence/"/>
    
    <category term="wiki" scheme="https://knktc.com/en/tags/wiki/"/>
    
    <category term="macro" scheme="https://knktc.com/en/tags/macro/"/>
    
    <category term="gadgets" scheme="https://knktc.com/en/tags/gadgets/"/>
    
  </entry>
  
  <entry>
    <title>Shorten Poetry Virtualenv Prompt Prefix</title>
    <link href="https://knktc.com/en/2022/02/09/shorten-prompt-ps1-of-poetry-env/"/>
    <id>https://knktc.com/en/2022/02/09/shorten-prompt-ps1-of-poetry-env/</id>
    <published>2022-02-09T14:52:01.000Z</published>
    <updated>2026-04-29T00:35:33.352Z</updated>
    
    <content type="html"><![CDATA[<p>I recently started using Poetry for Python package management, and one small annoyance showed up immediately: the shell prompt inside the virtual environment is unnecessarily long.</p><p>For example, if the project is named <code>test-poetry</code>, after running <code>poetry shell</code> the prompt may look like this:</p><blockquote><p>(test-poetry-FvrREBVp-py3.6) knktc@knktc-rmbp test_poetry %</p></blockquote><p>Poetry includes both an encoded suffix and the Python version in the virtual environment name, and then uses that full directory name as the prompt prefix. On a small screen, that takes up a surprising amount of space.</p><span id="more"></span><p>Since Poetry environments are still activated through <code>bin/activate</code>, the easiest fix is to find that file and adjust the prompt manually.</p><p>From the project directory, as long as <code>pyproject.toml</code> is present, run:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">poetry env info -p</span><br></pre></td></tr></table></figure><p>This prints the path to the current virtual environment, for example:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">(test-poetry-FvrREBVp-py3.6) knktc@knktc-rmbp test_poetry % poetry env info -p</span><br><span class="line">/Users/knktc/Library/Caches/pypoetry/virtualenvs/test-poetry-FvrREBVp-py3.6</span><br></pre></td></tr></table></figure><p>Then open the <code>bin/activate</code> file in that environment:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim /Users/knktc/Library/Caches/pypoetry/virtualenvs/test-poetry-FvrREBVp-py3.6/bin/activate</span><br></pre></td></tr></table></figure><p>Around line 68, you should see the logic that sets <code>PS1</code>. It uses the environment directory name as the prompt prefix:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> [ -z <span class="string">&quot;<span class="variable">$&#123;VIRTUAL_ENV_DISABLE_PROMPT-&#125;</span>&quot;</span> ] ; <span class="keyword">then</span></span><br><span class="line">    _OLD_VIRTUAL_PS1=<span class="string">&quot;<span class="variable">$&#123;PS1-&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">if</span> [ <span class="string">&quot;x&quot;</span> != x ] ; <span class="keyword">then</span></span><br><span class="line">        PS1=<span class="string">&quot;() <span class="variable">$&#123;PS1-&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        PS1=<span class="string">&quot;(`basename \&quot;<span class="variable">$VIRTUAL_ENV</span>\&quot;`) <span class="variable">$&#123;PS1-&#125;</span>&quot;</span>    <span class="comment"># &lt;--- this line</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">    <span class="built_in">export</span> PS1</span><br><span class="line"><span class="keyword">fi</span></span><br></pre></td></tr></table></figure><p>You can replace that line with a shorter, fixed project name:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> [ -z <span class="string">&quot;<span class="variable">$&#123;VIRTUAL_ENV_DISABLE_PROMPT-&#125;</span>&quot;</span> ] ; <span class="keyword">then</span></span><br><span class="line">    _OLD_VIRTUAL_PS1=<span class="string">&quot;<span class="variable">$&#123;PS1-&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">if</span> [ <span class="string">&quot;x&quot;</span> != x ] ; <span class="keyword">then</span></span><br><span class="line">        PS1=<span class="string">&quot;() <span class="variable">$&#123;PS1-&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        PS1=<span class="string">&quot;(test-poetry) <span class="variable">$&#123;PS1-&#125;</span>&quot;</span>    <span class="comment"># &lt;--- hard-code the project name</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">    <span class="built_in">export</span> PS1</span><br><span class="line"><span class="keyword">fi</span></span><br></pre></td></tr></table></figure><p>Save the file, then reactivate the environment:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">knktc@knktc-rmbp test_poetry % poetry shell</span><br><span class="line">Spawning shell within /Users/knktc/Library/Caches/pypoetry/virtualenvs/test-poetry-FvrREBVp-py3.6</span><br><span class="line">Restored session: Wed Feb 9 23:12:53 CST 2022</span><br><span class="line">knktc@knktc-rmbp test_poetry % . /Users/knktc/Library/Caches/pypoetry/virtualenvs/test-poetry-FvrREBVp-py3.6/bin/activate</span><br><span class="line">(test-poetry) knktc@knktc-rmbp test_poetry %</span><br></pre></td></tr></table></figure><p>Much better.</p>]]></content>
    
    
    <summary type="html">How to shorten the long shell prompt prefix generated by Poetry virtual environments by editing the activate script.</summary>
    
    
    
    
    <category term="poetry" scheme="https://knktc.com/en/tags/poetry/"/>
    
    <category term="bash" scheme="https://knktc.com/en/tags/bash/"/>
    
    <category term="prompt" scheme="https://knktc.com/en/tags/prompt/"/>
    
    <category term="customize" scheme="https://knktc.com/en/tags/customize/"/>
    
  </entry>
  
  <entry>
    <title>Install Headless Chrome with Docker</title>
    <link href="https://knktc.com/en/2022/02/05/install-headless-chrome-in-docker/"/>
    <id>https://knktc.com/en/2022/02/05/install-headless-chrome-in-docker/</id>
    <published>2022-02-05T13:35:59.000Z</published>
    <updated>2026-04-29T01:04:56.833Z</updated>
    
    <content type="html"><![CDATA[<p>I recently used Headless Chrome together with Selenium to test a frontend project. The results were pretty good, and adding the tests after Jenkins deployment gave at least some confidence that the frontend still worked after each release.</p><p>This post records a Dockerfile for installing Headless Chrome on Ubuntu 20.04 so I can find it again later.</p><span id="more"></span><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> ubuntu:<span class="number">20.04</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">ARG</span> APT_MIRROR_HOST=mirrors.tuna.tsinghua.edu.cn</span><br><span class="line"><span class="keyword">ENV</span> DEBIAN_FRONTEND noninteractive</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> sed -i <span class="string">&quot;s/archive.ubuntu.com/<span class="variable">$&#123;APT_MIRROR_HOST&#125;</span>/g&quot;</span> /etc/apt/sources.list \</span></span><br><span class="line"><span class="bash">    &amp;&amp; sed -i <span class="string">&quot;s/security.ubuntu.com/<span class="variable">$&#123;APT_MIRROR_HOST&#125;</span>/g&quot;</span> /etc/apt/sources.list</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> apt update</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> apt install -y wget fonts-wqy-microhei</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> dpkg -i google-chrome-stable_current_amd64.deb;<span class="built_in">exit</span> 0</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> apt install -f -y</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime</span></span><br></pre></td></tr></table></figure><p>Notes:</p><ul><li>The APT mirror is switched to Tsinghua’s mirror to speed up dependency installation.</li><li><code>DEBIAN_FRONTEND=noninteractive</code> is set because <code>tzdata</code> may otherwise prompt during installation and cause <code>docker build</code> to hang.</li><li>Because of that non-interactive setting, the final timezone configuration is handled manually.</li><li><code>fonts-wqy-microhei</code> is installed so Chinese text can render correctly, especially when exporting PDFs.</li><li>Installing Chrome with <code>dpkg -i</code> is expected to fail at first because dependencies are missing, so the command ends with <code>exit 0</code>.</li><li><code>apt install -f -y</code> is then used to repair dependencies and complete the installation.</li></ul>]]></content>
    
    
    <summary type="html">A simple Dockerfile for installing Headless Chrome on Ubuntu 20.04, with notes about mirrors, timezone prompts, and font support.</summary>
    
    
    
    
    <category term="ubuntu" scheme="https://knktc.com/en/tags/ubuntu/"/>
    
    <category term="chrome" scheme="https://knktc.com/en/tags/chrome/"/>
    
    <category term="docker" scheme="https://knktc.com/en/tags/docker/"/>
    
    <category term="headless" scheme="https://knktc.com/en/tags/headless/"/>
    
  </entry>
  
  <entry>
    <title>Change a YApi User Role Manually</title>
    <link href="https://knktc.com/en/2021/12/22/yapi-change-user-role/"/>
    <id>https://knktc.com/en/2021/12/22/yapi-change-user-role/</id>
    <published>2021-12-22T13:49:19.000Z</published>
    <updated>2026-04-29T00:35:33.395Z</updated>
    
    <content type="html"><![CDATA[<p>YApi is not exactly a very active project anymore, but since a lot of internal projects and APIs are already stored there, it still has to stay in service.</p><p>One odd part of YApi’s design is that only the user created during installation is an administrator. Any users created later are normal members by default, and there is no obvious place in the UI to change that. If you need another admin account, the practical solution is to update the database directly.</p><span id="more"></span><p>YApi uses MongoDB on the backend, so start by entering the Mongo shell:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mongo</span><br></pre></td></tr></table></figure><p>Then switch to the <code>yapi</code> database:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">use yapi</span><br></pre></td></tr></table></figure><p>Next, find the target user’s ID by username:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">db.getCollection(&quot;user&quot;).find(&#123;&quot;username&quot;:&quot;knktc&quot;&#125;)</span><br></pre></td></tr></table></figure><p>You can also search by email:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">db.getCollection(&quot;user&quot;).find(&#123;&quot;email&quot;:&quot;hello@world.com&quot;&#125;)</span><br></pre></td></tr></table></figure><p>Look at the returned <code>_id</code> field. That is the user ID you need.</p><p>Finally, update that user’s <code>role</code> to <code>admin</code>:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">db.getCollection(&quot;user&quot;).update(&#123;&quot;_id&quot;:2&#125;, &#123;$set: &#123;&quot;role&quot;:&quot;admin&quot;&#125;&#125;)</span><br></pre></td></tr></table></figure><p>If you ever need to revoke admin privileges, change <code>role</code> back to <code>member</code>.</p>]]></content>
    
    
    <summary type="html">Promote a YApi user to admin by updating the role field directly in MongoDB.</summary>
    
    
    
    
    <category term="yapi" scheme="https://knktc.com/en/tags/yapi/"/>
    
    <category term="admin" scheme="https://knktc.com/en/tags/admin/"/>
    
    <category term="role" scheme="https://knktc.com/en/tags/role/"/>
    
  </entry>
  
</feed>
