<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>deeb.me</title><link>https://deeb.me/</link><description>Abdallah Deeb's notebook: AWS, GitLab CI, Claude Code, and the tools built along the way.</description><language>en</language><lastBuildDate>Fri, 06 Nov 2026 13:13:38 +0000</lastBuildDate><atom:link href="https://deeb.me/index.xml" rel="self" type="application/rss+xml"/><item><title>Teaching Claude Code to message my colleagues in my own voice</title><link>https://deeb.me/20261106/teaching-claude-code-to-message-my-colleagues-in-my-own-voice/</link><pubDate>Fri, 06 Nov 2026 13:13:38 +0000</pubDate><guid>https://deeb.me/20261106/teaching-claude-code-to-message-my-colleagues-in-my-own-voice/</guid><description>&lt;p&gt;I'm part of a small infrastructure team, and a surprising amount of my day is short messages: &amp;quot;can you look at this MR&amp;quot;, &amp;quot;is staging free&amp;quot;, &amp;quot;did the migration finish&amp;quot;. I already work with Claude Code most of the day, so I wanted it to send those messages for me.&lt;/p&gt;&#10;&lt;p&gt;The first drafts were the problem. I'd ask for a one-line question, and the agent would write five paragraphs: what the MR changes, why, which files it touches, the risks, a bulleted list of &amp;quot;key considerations&amp;quot;, and a closing &amp;quot;let me know if you have any questions!&amp;quot; Every word was accurate, and nobody read it. It was too long and too technical, and it looked like AI slop, so people skimmed past it or left it for later. A question my colleagues would have answered in a minute sat there for hours.&lt;/p&gt;&#10;&lt;p&gt;What I needed was the message I would have typed myself: &amp;quot;can you check the staging plan on !885 when you get a minute? no rush&amp;quot;.&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/2ief34glw9qqtq241dq7.png" alt="The agent&amp;#39;s long, technical draft next to the one-line message I would have written" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;So I wrote &lt;a href="https://github.com/abdallah/mattermost-dm"&gt;mattermost-dm&lt;/a&gt;, an agent skill and a small Python script that sends Mattermost DMs as you. Sending is the easy part. Most of the skill is about making the messages sound like the person who sends them.&lt;/p&gt;&#10;&lt;h2 id="a-style-guide-for-one-person"&gt;A style guide for one person&lt;/h2&gt;&#10;&lt;p&gt;The skill reads a file called &lt;code&gt;voice.md&lt;/code&gt; before it drafts anything. It's a short style guide, written about you, for the agent. The template covers the habits that make a message recognisably yours:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Length:&lt;/strong&gt; one line, or two or three short ones.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Capitals and punctuation:&lt;/strong&gt; sentence case or all lowercase, a full stop at the end or not.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Asks:&lt;/strong&gt; &amp;quot;can you … please?&amp;quot;, &amp;quot;when you have a minute, …&amp;quot;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Warmth:&lt;/strong&gt; &amp;quot;thanks!&amp;quot;, a smiley now and then, or none at all.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Words you use often:&lt;/strong&gt; &amp;quot;btw&amp;quot;, &amp;quot;FYI&amp;quot;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Links and tickets:&lt;/strong&gt; a bare URL, or a linked ticket number followed by its title.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Don't:&lt;/strong&gt; the things you never do, like formal sign-offs, long explanations, or bold.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The template also asks for one example of a first message of the day, because a single example shows the shape faster than any list:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;good morning&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;can you check the staging plan on !885 when you get a minute? no rush&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &amp;quot;Don't&amp;quot; list does the most work against slop. Agents add things: background nobody asked for, bullet points, bold, a summary of what they just asked, a &amp;quot;let me know if you have any questions&amp;quot;. The guide says what you never write, so the agent knows what to leave out.&lt;/p&gt;&#10;&lt;p&gt;The skill also has a few rules of its own, whatever your voice: lead with what you need, then add one or two lines of context at most. Don't invent dates or promises, and don't use review-speak like &amp;quot;the part worth your eyes&amp;quot;. Always link a ticket or MR, so the reader can click through for the detail.&lt;/p&gt;&#10;&lt;p&gt;Git ignores &lt;code&gt;voice.md&lt;/code&gt;, so it stays on your machine. Without it, the skill falls back to short, friendly, plain messages, written like a note to a teammate.&lt;/p&gt;&#10;&lt;h2 id="learning-the-voice-from-your-own-messages"&gt;Learning the voice from your own messages&lt;/h2&gt;&#10;&lt;p&gt;Writing a style guide about yourself is hard, because you don't notice your own habits. The faster route is to let Claude Code read how you actually write.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;mm-dm --history &amp;lt;person&amp;gt; --days 90&lt;/code&gt; prints your DM with one person. The README suggests this prompt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Run mm-dm --history &amp;lt;name&amp;gt; --days 90 for each of these people: &amp;lt;names&amp;gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Learn only from messages I wrote. Draft voice.md from voice.example.md. Describe my habits for&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;length, capitals, punctuation, greetings, asks, warmth, common words and links, and what I never&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;do. Copy my habits, not my typos. Don&amp;#39;t include real names, projects, clients or quoted messages.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Show me the draft before you save it.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A few lines in that prompt carry most of the weight:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Three to five people, across different kinds of work.&lt;/strong&gt; You write differently to a close teammate than to someone in another department, and the guide should cover both.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&amp;quot;Copy my habits, not my typos.&amp;quot;&lt;/strong&gt; Without this line, the agent faithfully reproduces your mistakes.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;No real names or quoted messages.&lt;/strong&gt; The guide describes patterns with invented examples, so it doesn't turn into a copy of your private conversations.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Then read the draft and fix anything that doesn't sound like you. Ask for a few test messages and adjust the file until you'd send them without editing.&lt;/p&gt;&#10;&lt;p&gt;There's a trade-off to know about: &lt;code&gt;--history&lt;/code&gt; prints both sides of each conversation, so your colleagues' messages reach your AI provider while the guide is drafted. Check your company's policy and choose conversations with nothing confidential in them. If that isn't possible, write &lt;code&gt;voice.md&lt;/code&gt; by hand from the template.&lt;/p&gt;&#10;&lt;h2 id="greetings-are-part-of-the-voice"&gt;Greetings are part of the voice&lt;/h2&gt;&#10;&lt;p&gt;Voice isn't only wording; it's also rhythm. Nobody says &amp;quot;good morning&amp;quot; to someone they messaged ten minutes ago, and nobody skips a greeting after a week of silence.&lt;/p&gt;&#10;&lt;p&gt;Messages start with a &lt;code&gt;{hi}&lt;/code&gt; placeholder, and the script fills it in from your real history. It asks Mattermost when you last posted in that DM. If you haven't spoken today, or it's been more than 4 hours, &lt;code&gt;{hi}&lt;/code&gt; becomes a greeting. Otherwise it disappears, along with the blank line after it. In a batch, this happens separately for each person.&lt;/p&gt;&#10;&lt;p&gt;The greetings themselves are yours too. Add your own lists to &lt;code&gt;~/.mattermost&lt;/code&gt;, one for the morning and one for the afternoon:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MM_DM_GREETINGS_AM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;good morning|morning :)&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MM_DM_GREETINGS_PM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;hi there|hey|good afternoon&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The script picks one per person per day and keeps it, so you don't greet the same person three different ways in one morning.&lt;/p&gt;&#10;&lt;p&gt;Batches use &lt;code&gt;{first_name}&lt;/code&gt;, so a question to five reviewers still reads as five personal messages:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{hi} {first_name}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;can you review the runbook change before Friday&amp;#39;s maintenance?&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="you-still-see-every-message"&gt;You still see every message&lt;/h2&gt;&#10;&lt;p&gt;A good voice makes approval quick, but it doesn't replace it. Every message goes through a preview first: &lt;code&gt;mm-dm --dry-run&lt;/code&gt; renders each message, greeting and name included, and Claude shows them to you. When you say yes, &lt;code&gt;mm-dm --send-pending&lt;/code&gt; sends exactly what the preview printed. It doesn't take new text, so nothing gets reworded after you've read it. Each preview sends once, and it expires after an hour.&lt;/p&gt;&#10;&lt;p&gt;The token stays out of the conversation as well. The script reads it from &lt;code&gt;~/.mattermost&lt;/code&gt; and never prints it, and the skill tells Claude not to open that file.&lt;/p&gt;&#10;&lt;h2 id="install"&gt;Install&lt;/h2&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/abdallah/mattermost-dm.git ~/.claude/skills/mattermost-dm&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;touch ~/.mattermost &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; chmod &lt;span class="m"&gt;600&lt;/span&gt; ~/.mattermost&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp ~/.claude/skills/mattermost-dm/voice.example.md ~/.claude/skills/mattermost-dm/voice.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add your server and a personal access token (&lt;strong&gt;Profile &amp;gt; Security &amp;gt; Personal Access Tokens&lt;/strong&gt;) to &lt;code&gt;~/.mattermost&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MM_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://chat.example.com&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MM_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-token&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then edit &lt;code&gt;voice.md&lt;/code&gt;, or ask Claude Code to draft it from your history. It needs Python 3.9 or later and has no other dependencies. If you use more than one machine, copy &lt;code&gt;voice.md&lt;/code&gt; to each one yourself, because Git doesn't sync it.&lt;/p&gt;&#10;&lt;h2 id="beyond-mattermost"&gt;Beyond Mattermost&lt;/h2&gt;&#10;&lt;p&gt;The script is specific to Mattermost, but the idea isn't. Any agent that writes as you, whether it's emails, ticket comments, or commit messages, benefits from a short, private file that describes how you write and, more importantly, what you never write. Draft it from your own messages, then correct it until the test messages sound right.&lt;/p&gt;&#10;&lt;p&gt;The code is MIT licensed: &lt;a href="https://github.com/abdallah/mattermost-dm"&gt;github.com/abdallah/mattermost-dm&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>I built an inbox for my Claude Code sessions. Then I never checked it.</title><link>https://deeb.me/20261025/i-built-an-inbox-for-my-claude-code-sessions-then-i-never-checked-it/</link><pubDate>Sun, 25 Oct 2026 15:27:12 +0000</pubDate><guid>https://deeb.me/20261025/i-built-an-inbox-for-my-claude-code-sessions-then-i-never-checked-it/</guid><description>&lt;p&gt;Three weeks ago I wrote about &lt;a href="https://dev.to/abdallah/one-inbox-for-every-claude-code-session-on-every-machine-2en4"&gt;sessionhub&lt;/a&gt;: one registry and one inbox for every Claude Code session on every machine I use. The hub itself took off. I have it open all day to see what's running where, and sessions register, report and move between machines without me thinking about it.&lt;/p&gt;&#10;&lt;p&gt;The inbox is a different story. I stopped looking at it.&lt;/p&gt;&#10;&lt;p&gt;Not because it broke. Because nothing in it told me what to do. A list of sessions marked &amp;quot;blocked&amp;quot; or &amp;quot;done&amp;quot; is information, not an action. If I still had to SSH into the machine to do anything about it, I'd go straight to the terminal and skip the inbox.&lt;/p&gt;&#10;&lt;p&gt;This post is about the changes that turned the inbox from something I could check into something I act from.&lt;/p&gt;&#10;&lt;h2 id="the-question-i-couldnt-answer"&gt;The question I couldn't answer&lt;/h2&gt;&#10;&lt;p&gt;I went through the database to see what the inbox had actually caught. Since October 3, Claude Code had raised 171 permission prompts across my machines. &lt;strong&gt;120 of them were not permissions at all. They were Claude asking me a question&lt;/strong&gt; (the &lt;code&gt;AskUserQuestion&lt;/code&gt; tool: &amp;quot;Which approach?&amp;quot;, &amp;quot;Should I also update the docs?&amp;quot;).&lt;/p&gt;&#10;&lt;p&gt;The hub could show those questions. It could not answer them. &amp;quot;Allow&amp;quot; from the dashboard approved the tool call and answered nothing, so the dialog stayed open in the terminal. All 120 were answered locally.&lt;/p&gt;&#10;&lt;p&gt;Claude Code hooks can answer a question on your behalf: return &lt;code&gt;allow&lt;/code&gt; together with &lt;code&gt;updatedInput&lt;/code&gt;, which is the original tool input plus an &lt;code&gt;answers&lt;/code&gt; map from question text to the chosen label. sessionhub's permission hook was already sitting in that exact spot, waiting for a decision from the server. So the change was small:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The dashboard shows each question's options as buttons, plus an &lt;strong&gt;Other&lt;/strong&gt; box.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Send answer&lt;/strong&gt; posts the choice to the server with the decision.&lt;/li&gt;&#10;&lt;li&gt;The hook hands it back to Claude Code as &lt;code&gt;updatedInput&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/6cnm6p1jr8yx70skz2z4.png" alt="A question from Claude in the inbox, with its three options as 0" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;hookSpecificOutput&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;hookEventName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;PermissionRequest&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;decision&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;behavior&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;allow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;updatedInput&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;questions&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;answers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;Which colour?&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Blue&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}}}}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first live test failed. My phone tapped the old &lt;strong&gt;Allow&lt;/strong&gt; button, because it was still running the page from before the deploy. So now every API response carries a fingerprint of the page, and an open tab shows a &lt;strong&gt;Reload&lt;/strong&gt; bar when it changes. The server also refuses a plain allow on a question, so a stale page fails loudly instead of quietly doing nothing.&lt;/p&gt;&#10;&lt;p&gt;The second test worked: I tapped &amp;quot;Blue&amp;quot; on my phone and the session carried on.&lt;/p&gt;&#10;&lt;h2 id="replying-without-opening-a-terminal"&gt;Replying without opening a terminal&lt;/h2&gt;&#10;&lt;p&gt;The other half of &amp;quot;act from the inbox&amp;quot; is answering a session that finished and asked something in its last message. Each inbox row now has a reply box. sessionhub already had a way to type a message into a session's pane when it's idle (the watcher or the Claude Code mod delivers it), so the reply box reuses that. The draft survives the page's 30-second refresh.&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/ysubxbklx45ypbaazcjk.png" alt="An inbox row waiting on two answers, with a reply box" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;Inbox rows also show the start of Claude's last reply now, Markdown stripped and line breaks kept, instead of the word &amp;quot;Finished&amp;quot;. That one change made rows readable at a glance.&lt;/p&gt;&#10;&lt;h2 id="moving-a-session-when-its-machine-is-off"&gt;Moving a session when its machine is off&lt;/h2&gt;&#10;&lt;p&gt;sessionhub can move a session between machines: pack the transcript and the uncommitted work, seal it for the target, and resume it there. Yesterday I tried to move one between machines while the source was unreachable, so nothing could pack it, and the move failed. The only sign was a line under the session's details.&lt;/p&gt;&#10;&lt;p&gt;Two fixes:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Failed moves are an inbox group&lt;/strong&gt; of their own, with a retry button, and they show up in the alerts.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Snapshots.&lt;/strong&gt; Each machine's watcher snapshots its idle sessions in the background, at most every 10 minutes and only when the transcript changed. Each snapshot is sealed with X25519 for each other machine, so the server stores blobs it cannot read. If the source machine hasn't polled for 2 minutes, a move uses the latest snapshot instead of waiting. When the source comes back, it ends the leftover pane.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Snapshots are removed when the session ends or moves, after 14 days, or oldest first past 1 GiB.&lt;/p&gt;&#10;&lt;h2 id="the-dashboard-from-a-humans-point-of-view"&gt;The dashboard, from a human's point of view&lt;/h2&gt;&#10;&lt;p&gt;I asked Claude Code for a UX review of the live dashboard. It took screenshots at phone and desktop width with headless Chrome through Playwright, read them, and listed what a person would trip over. The fixes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;One colour and one icon per state&lt;/strong&gt;: ✋ needs you, 🚚 move failed, 💬 waiting, ⚠️ machine silent, ⏳ working, ✅ finished, 💤 idle.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Sorted by state&lt;/strong&gt;, so whatever needs me is on top.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Rename&lt;/strong&gt; a session from the web, and fall back to the task's name when the title is the default &amp;quot;Claude Code&amp;quot;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Machine headers say &amp;quot;online&amp;quot; or &amp;quot;silent 8m&amp;quot;&lt;/strong&gt;, which is how I found out one of my machines had dropped off the network.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/596mhlarz1b8ovbxhjoh.png" alt="Sessions on two machines, each with a coloured state badge" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;h2 id="tasks-a-standup-and-a-place-to-park-ideas"&gt;Tasks: a standup and a place to park ideas&lt;/h2&gt;&#10;&lt;p&gt;sessionhub turns worklog notes into a daily task list. I never used that tab. When I thought about why, I found two jobs it should be doing:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;The standup I write.&lt;/strong&gt; The Tasks tab now has a &lt;strong&gt;Standup&lt;/strong&gt; button: yesterday (the last day with work, so Monday covers Friday), today, and blockers, as plain text with a &lt;strong&gt;Copy&lt;/strong&gt; button. &lt;code&gt;sessionhub task standup&lt;/code&gt; prints the same thing.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Parking ideas.&lt;/strong&gt; I switch contexts many times a day, and ideas that arrive mid-task get lost. Now there's a &amp;quot;Park an idea&amp;quot; box, a &lt;code&gt;sessionhub idea &amp;quot;…&amp;quot;&lt;/code&gt; command, and any Claude session will park one if I ask. Ideas wait in their own list until I start them or drop them, and they stay out of the standup.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/tdw0aseln4gfjjv3alrb.png" alt="The standup text, ready to copy" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;The tab itself went from a wall of cards to one line per task: open work first, then ideas, then done.&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/vgejxjxgec4hvfpm0skh.png" alt="The Tasks page: open work and ideas on the left, done on the right" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;h2 id="what-i-learned"&gt;What I learned&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;A dashboard you read is not a dashboard you use.&lt;/strong&gt; Every change above removed one trip to a terminal.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Look at the data before the design.&lt;/strong&gt; &amp;quot;Most prompts are questions&amp;quot; was not something I'd have guessed.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Test on the device you'll use.&lt;/strong&gt; The bug that mattered was a stale page on my phone, not anything a unit test would catch.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;sessionhub is open source (MIT), a single Go binary with SQLite: &lt;a href="https://github.com/abdallah/session-hub"&gt;github.com/abdallah/session-hub&lt;/a&gt;. If you run Claude Code on more than one machine, I'd like to hear what you'd want from it.&lt;/p&gt;&#10;</description></item><item><title>One inbox for every Claude Code session on every machine</title><link>https://deeb.me/20261004/one-inbox-for-every-claude-code-session-on-every-machine/</link><pubDate>Sun, 04 Oct 2026 10:40:28 +0000</pubDate><guid>https://deeb.me/20261004/one-inbox-for-every-claude-code-session-on-every-machine/</guid><description>&lt;p&gt;On a normal day I have somewhere between five and fifteen Claude Code sessions running. Some are on my laptop, some on a desktop, a few on a home server that never sleeps. They run inside &lt;a href="https://herdr.dev"&gt;herdr&lt;/a&gt;, an agent-aware terminal multiplexer, and I attach to them over SSH from whatever I'm holding at the time, which is often a phone.&lt;/p&gt;&#10;&lt;p&gt;That setup works well until you step away. Then three questions start nagging:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Which sessions exist right now, and which are still alive?&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;What has each one done, what is it doing, and is it waiting on me?&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;How do I get back into the one that needs me, from wherever I am?&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;None of the tools I already had answered all three. So I built a small thing that does. It's called &lt;strong&gt;sessionhub&lt;/strong&gt;, it's open source (MIT), and this post is about why it exists, how it works, and how you can run it.&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/aey4aay21plzw1yzn5pk.png" alt="The sessionhub dashboard on a phone: sessions grouped by machine, and the inbox with a permission prompt waiting" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;h2 id="the-problem-every-tool-knows-one-machine-or-one-session"&gt;The problem: every tool knows one machine, or one session&lt;/h2&gt;&#10;&lt;p&gt;I want to be fair to the tools I use every day, because they're good at what they do:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;herdr&lt;/strong&gt; knows every agent on &lt;em&gt;its&lt;/em&gt; machine and whether it's working, blocked, or done. It doesn't know about my other machines, and it knows &lt;em&gt;state&lt;/em&gt;, not &lt;em&gt;meaning&lt;/em&gt;: &amp;quot;blocked&amp;quot; doesn't tell me blocked on what.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude Code's own session list&lt;/strong&gt; is per machine too. Great for resuming on the box you're sitting at; no help for the session on the server in the other room.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Remote Control&lt;/strong&gt; gives a single session a link I can open in the Claude app. Excellent, but I have to turn it on per session, and I first need to know &lt;em&gt;which&lt;/em&gt; session to turn it on for.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;SSH plus tmux-style multiplexers&lt;/strong&gt; get me into a machine. Then I'm hunting through panes.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;What was missing wasn't another terminal. It was a &lt;strong&gt;registry&lt;/strong&gt;: one place that knows every session on every machine, what each one is working on in plain words, and what each one needs from me.&lt;/p&gt;&#10;&lt;h2 id="what-sessionhub-does"&gt;What sessionhub does&lt;/h2&gt;&#10;&lt;p&gt;sessionhub is one static Go binary with a SQLite database. The same binary is the server and every client. You run the server on one always-on machine and &amp;quot;join&amp;quot; each machine that runs Claude Code.&lt;/p&gt;&#10;&lt;p&gt;From there:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Registry.&lt;/strong&gt; Every session on every joined machine shows up with its machine, directory, branch, title, and status: &lt;code&gt;live&lt;/code&gt;, &lt;code&gt;blocked&lt;/code&gt;, &lt;code&gt;stale&lt;/code&gt;, or &lt;code&gt;ended&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Inbox.&lt;/strong&gt; The sessions that need you, across all machines: blocked on a permission prompt, waiting on an answer, or finished a turn you haven't looked at.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Remote approvals.&lt;/strong&gt; Allow or deny a waiting permission prompt from the dashboard or the CLI. The prompt still shows in the terminal, and whichever answer comes first wins. It never adds an &amp;quot;always allow&amp;quot; rule.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Progress reports.&lt;/strong&gt; Each session gets an MCP tool to report what it has done, what's in flight, and what it's waiting on.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Get back in.&lt;/strong&gt; &lt;code&gt;sessionhub resume &amp;lt;id&amp;gt;&lt;/code&gt; focuses the pane, restarts the session, or prints the exact &lt;code&gt;ssh&lt;/code&gt; command for the machine it's on. Or turn on Remote Control and continue in the Claude app.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Standing rules and messages.&lt;/strong&gt; Add a rule once and every session sees it with every prompt. Type a message into one session, or into all of them.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Move sessions.&lt;/strong&gt; Hand a session, with its branch and uncommitted changes, to another machine or to a Claude Code cloud session.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Start new sessions.&lt;/strong&gt; Pick a machine and a directory on the dashboard, and a new Claude session starts there with Remote Control on.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Alerts.&lt;/strong&gt; An optional Telegram message when a session has been blocked or waiting on you for 30 seconds.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Here's the inbox on a phone, with a session asking to run &lt;code&gt;npm install&lt;/code&gt;:&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/oeuueq1oocecpokwk8r2.png" alt="The inbox: a blocked session asks to use Bash for npm install, with Allow once and Deny buttons" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;h2 id="how-it-fits-together"&gt;How it fits together&lt;/h2&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/uvlolqu4u98ttyd9v5s8.png" alt="Architecture: Claude Code hooks, the MCP server, and an optional herdr watcher on each machine report to one sessionhub server; you use the dashboard, the CLI, or Telegram" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;There are three ways a session talks to the hub, and you can use any of them:&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;1. Claude Code hooks.&lt;/strong&gt; &lt;code&gt;sessionhub install-hooks&lt;/code&gt; adds entries to &lt;code&gt;~/.claude/settings.json&lt;/code&gt; (it backs the file up first, and only ever touches its own entries). Most are async, so they never slow a prompt down (paths shortened):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hooks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;SessionStart&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;matcher&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;hooks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;~/.local/bin/sessionhub hook session-start&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;async&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;~/.local/bin/sessionhub hook context&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;timeout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;UserPromptSubmit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;hooks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;~/.local/bin/sessionhub hook prompt&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;async&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;~/.local/bin/sessionhub hook context&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;timeout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]}]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;context&lt;/code&gt; hook is synchronous on purpose: its output is how the standing rules reach the prompt. It reads a local copy of the rules and never touches the network, so a prompt never waits on the hub.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;2. An MCP server.&lt;/strong&gt; &lt;code&gt;sessionhub mcp&lt;/code&gt; gives each session &lt;code&gt;report_progress&lt;/code&gt; and &lt;code&gt;set_title&lt;/code&gt;. A short snippet in &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; tells Claude when to use them:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Reporting progress to sessionhub&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Call &lt;span class="sb"&gt;`report_progress`&lt;/span&gt; when you finish a task, when you start a task that&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; will take a while, and when you are blocked waiting on me or on something&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; external. Fill &lt;span class="sb"&gt;`done`&lt;/span&gt;, &lt;span class="sb"&gt;`in_flight`&lt;/span&gt;, and &lt;span class="sb"&gt;`waiting_on`&lt;/span&gt; with short one-line items.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Call &lt;span class="sb"&gt;`set_title`&lt;/span&gt; once the session has a clear purpose, and again if the&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; purpose changes.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That's what turns &amp;quot;blocked&amp;quot; into &amp;quot;waiting on: permission to run npm install&amp;quot;.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;3. A herdr plugin (optional).&lt;/strong&gt; If you use herdr, its plugin registers the sessions in your panes, ends them when the pane closes, and runs a small watcher. The watcher is what lets the hub &lt;em&gt;do&lt;/em&gt; things on a machine: type a message, turn on Remote Control, move a session, or start a new one.&lt;/p&gt;&#10;&lt;h3 id="the-rule-i-cared-about-most-fail-open"&gt;The rule I cared about most: fail open&lt;/h3&gt;&#10;&lt;p&gt;A tool that watches your agents must never become the reason they stall. So every client fails open. Hooks always exit 0. Every call from a hook or the MCP server has a two-second limit. If the server is down, writes go to a local queue (&lt;code&gt;~/.local/state/sessionhub/queue.jsonl&lt;/code&gt;) and drain later. Claude Code and herdr never wait on sessionhub.&lt;/p&gt;&#10;&lt;h3 id="the-server-never-connects-to-your-machines"&gt;The server never connects to your machines&lt;/h3&gt;&#10;&lt;p&gt;Machines poll the server; the server never reaches into them. When you tap &lt;strong&gt;Allow once&lt;/strong&gt; on your phone, the &lt;code&gt;PermissionRequest&lt;/code&gt; hook on that machine is holding a long poll open, and it picks up your answer. When you start a new session, the machine's watcher is long-polling for work and claims the request. That keeps the firewall story simple: the only open port is the hub's.&lt;/p&gt;&#10;&lt;h2 id="using-it"&gt;Using it&lt;/h2&gt;&#10;&lt;p&gt;The CLI is where I spend most of my time:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sessionhub ls&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MACHINE ID TITLE STATUS AGE REPORT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tower e5b07a1c postgres 16 upgrade plan live 1s doing: writing the rollback steps&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bluebox c18e5f43 rewrite the quickstart live 1s waiting: your review of the wording&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bluebox a72d4b90 dark mode for settings blocked 1s doing: settings page&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tower 3f9c1e27 fix CI token rotation live 1s doing: re-running the pipeline&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sessionhub inbox&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Blocked (1)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; a72d4b90 dark mode for settings bluebox 1s asks to use Bash: npm install&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Waiting on you (1)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; c18e5f43 rewrite the quickstart bluebox 1s your review of the wording&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;sessionhub show&lt;/code&gt; gives the full picture for one session, including its report history:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ sessionhub show a72d&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;title: dark mode for settings&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;machine: bluebox&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;state: blocked&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;branch: dark-mode&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;resume: ssh -t bluebox &amp;#39;~/.local/bin/sessionhub resume a72d4b90-...&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;reports (1, newest first)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; done theme tokens&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; in flight settings page&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; waiting on permission to run npm install&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A few more I use daily:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sessionhub approve a72d &lt;span class="c1"&gt;# allow that npm install once (asks first)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sessionhub rules add &lt;span class="s2"&gt;&amp;#34;Never push to main.&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# every session sees it, on every prompt&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sessionhub send --machine tower -m &lt;span class="s2"&gt;&amp;#34;Wrap up and report progress.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sessionhub move c18e bluebox &lt;span class="c1"&gt;# conversation, branch and uncommitted changes&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sessionhub start tower --dir ~/Code/api -m &lt;span class="s2"&gt;&amp;#34;Fix the flaky test in ci.yml&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That last one is the newest feature, and it's the one I wanted most. On the dashboard it's a &lt;strong&gt;New session&lt;/strong&gt; button: pick a machine and a directory (it suggests the ones you've used), optionally type a first prompt, and you get an &lt;strong&gt;Open in Claude&lt;/strong&gt; link a few seconds later:&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/images/0jvtwuq0ut0tvnw07p2a.png" alt="The New session panel: machine tower, directory ~/Code/api, and an Open in Claude link after the start" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;Now I can start work on my home server from a train.&lt;/p&gt;&#10;&lt;h2 id="trying-it-in-five-minutes"&gt;Trying it in five minutes&lt;/h2&gt;&#10;&lt;p&gt;You can run the server and a client on one machine to see what it does:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Install the latest release (Linux or macOS, amd64 or arm64) into ~/.local/bin:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://raw.githubusercontent.com/abdallah/session-hub/main/install.sh &lt;span class="p"&gt;|&lt;/span&gt; sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# This machine holds the server:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p ~/.config/sessionhub&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;install -m &lt;span class="m"&gt;600&lt;/span&gt; /dev/null ~/.config/sessionhub/server.toml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sessionhub server &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Join it: creates a token, installs the hooks and the MCP server.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sessionhub join localhost --name &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;hostname -s&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start claude anywhere, send a prompt, then:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sessionhub ls&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sessionhub login --name my-phone &lt;span class="c1"&gt;# a one-time sign-in link for the dashboard&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For more machines, run the server on an always-on box (there's a systemd unit and a &lt;code&gt;docker-compose.yml&lt;/code&gt;), put it behind a tunnel or reverse proxy, and &lt;code&gt;sessionhub join&lt;/code&gt; each machine. The &lt;a href="https://github.com/abdallah/session-hub/blob/main/docs/self-hosting.md"&gt;self-hosting guide&lt;/a&gt; walks through it.&lt;/p&gt;&#10;&lt;h2 id="a-word-on-security"&gt;A word on security&lt;/h2&gt;&#10;&lt;p&gt;Be honest with yourself about what this server can do. A machine token or a signed-in browser can approve tool calls, add rules that every session reads, type messages into sessions, and start new ones. That's the point of the tool, and it also means &lt;strong&gt;a leaked credential can get Claude to run commands on your machines.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;So: keep the server on a private network if you can, use HTTPS if you expose it, turn on the Telegram alerts (they also fire whenever someone adds a rule), and use the per-machine opt-outs (&lt;code&gt;remote_permissions = false&lt;/code&gt;, &lt;code&gt;remote_start = false&lt;/code&gt;) on machines you want to keep hands-off. &lt;a href="https://github.com/abdallah/session-hub/blob/main/SECURITY.md"&gt;SECURITY.md&lt;/a&gt; has a table of what each credential can do.&lt;/p&gt;&#10;&lt;h2 id="how-it-was-built"&gt;How it was built&lt;/h2&gt;&#10;&lt;p&gt;I built sessionhub with Claude Code itself, which turned out to be a good test of the tool and of the process.&lt;/p&gt;&#10;&lt;p&gt;Every feature started as a short spec, then a plan broken into tasks, and each task had to meet a written definition of done before it counted. Two rules from that list did most of the work:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Don't invent APIs.&lt;/strong&gt; Claude Code hook payloads and herdr events come from captured real payloads in &lt;code&gt;testdata/&lt;/code&gt;, never from memory. The spec said it bluntly: verify against &lt;code&gt;--help&lt;/code&gt; and the docs, and quote what you found.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Tests walk the whole path.&lt;/strong&gt; Each one checks the start state, acts, asserts what must &lt;em&gt;not&lt;/em&gt; have happened as well as what must, and reads the result back the way you would see it.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The early plan was to host it on Cloud Run. An always-on instance came out at about $45–50 a month, so it moved to a systemd user service on a home server behind a Cloudflare Tunnel, which costs nothing extra.&lt;/p&gt;&#10;&lt;h2 id="whats-next"&gt;What's next&lt;/h2&gt;&#10;&lt;p&gt;sessionhub is a personal, single-user tool: one person, their machines, no accounts or teams. That's deliberate, and I don't plan to change it. Things I do want:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;a Claude Code plugin, so the hooks, MCP server and CLAUDE.md snippet install in one step;&lt;/li&gt;&#10;&lt;li&gt;cloud sessions reporting in, so the claude.ai/code sessions show up next to the local ones.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;If you run more than a couple of Claude Code sessions across more than one machine, I'd love to hear whether this matches your workflow, and where it doesn't.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/abdallah/session-hub"&gt;https://github.com/abdallah/session-hub&lt;/a&gt; (MIT, Linux and macOS binaries on the releases page)&lt;/p&gt;&#10;</description></item><item><title>Why I Built a New Memory Plugin for Hermes Agent</title><link>https://deeb.me/20260614/why-i-built-a-new-memory-plugin-for-hermes-agent/</link><pubDate>Sun, 14 Jun 2026 18:17:11 +0000</pubDate><guid>https://deeb.me/20260614/why-i-built-a-new-memory-plugin-for-hermes-agent/</guid><description>&lt;p&gt;&lt;a href="https://github.com/NousResearch/hermes-agent"&gt;Hermes Agent&lt;/a&gt; already has memory, and that matters.&lt;/p&gt;&#10;&lt;p&gt;It keeps local context, it improves over time, and it works without forcing you into a cloud service. It also supports several external memory providers.&lt;/p&gt;&#10;&lt;p&gt;I still built &lt;a href="https://github.com/abdallah/hermes-mempalace"&gt;hermes-mempalace&lt;/a&gt;, because none of the existing options fit my setup quite right.&lt;/p&gt;&#10;&lt;p&gt;I wanted something:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;local-first&lt;/li&gt;&#10;&lt;li&gt;isolated by Hermes profile&lt;/li&gt;&#10;&lt;li&gt;verbatim, not just extracted facts&lt;/li&gt;&#10;&lt;li&gt;easy to inspect on disk&lt;/li&gt;&#10;&lt;li&gt;simple enough to trust over time&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;That last part is the important one. I did not want a memory layer that turns conversations into an opaque pile of embeddings or summaries you cannot really audit. I wanted actual transcripts, mined into a readable structure, with no hidden server in the middle.&lt;/p&gt;&#10;&lt;p&gt;Why the existing options were not enough&lt;/p&gt;&#10;&lt;p&gt;Hermes already gives you a few paths:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;built-in memory and session context&lt;/li&gt;&#10;&lt;li&gt;external providers for different use cases&lt;/li&gt;&#10;&lt;li&gt;enough flexibility to adapt, if you are willing to bend your workflow around them&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;And to be clear, some of those options are good.&lt;/p&gt;&#10;&lt;p&gt;But ... I run Hermes on a headless machine at home. And I use separate profiles for different contexts. And I do not want conversation content depending on a cloud API or a separate service unless there is a very good reason.&lt;/p&gt;&#10;&lt;p&gt;So, the best fit had to check a few boxes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;input checked="" disabled="" type="checkbox"&gt; no API key&lt;/li&gt;&#10;&lt;li&gt;&lt;input checked="" disabled="" type="checkbox"&gt; no external server&lt;/li&gt;&#10;&lt;li&gt;&lt;input checked="" disabled="" type="checkbox"&gt; no extra runtime I did not already want/install&lt;/li&gt;&#10;&lt;li&gt;&lt;input checked="" disabled="" type="checkbox"&gt; storage isolated by &lt;code&gt;HERMES_HOME&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;input checked="" disabled="" type="checkbox"&gt; memory we can actually read later&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;That let to &lt;a href="https://github.com/MemPalace/mempalace"&gt;MemPalace&lt;/a&gt;, or &lt;a href="https://mempalaceofficial.com/"&gt;https://mempalaceofficial.com/&lt;/a&gt; (hopefully, that's the right one!)&lt;/p&gt;&#10;&lt;h2 id="what-hermes-mempalace-does"&gt;What hermes-mempalace does&lt;/h2&gt;&#10;&lt;p&gt;hermes-mempalace wires MemPalace into the Hermes memory provider interface.&lt;/p&gt;&#10;&lt;p&gt;It follows the same lifecycle as the rest of Hermes memory providers:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;system_prompt_block()&lt;/code&gt; adds a short memory reminder to the prompt.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;prefetch()&lt;/code&gt; can run a MemPalace search before the first model call.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;sync_turn()&lt;/code&gt; buffers completed turns without slowing the chat loop.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;on_session_end()&lt;/code&gt; writes buffered turns to markdown and mines them into the palace.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;shutdown()&lt;/code&gt; flushes anything still waiting.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;During a session, Hermes gets three tools:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;mempalace_search&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mempalace_wake_up&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mempalace_status&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;That gives the agent a simple pattern: find relevant past context, surface it early, and keep the full record available if you need to inspect it.&lt;/p&gt;&#10;&lt;h3 id="storage-is-explicit"&gt;Storage is explicit&lt;/h3&gt;&#10;&lt;p&gt;The plugin stores data under the current Hermes profile, not in some shared global bucket.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$HERMES_HOME/mempalace/palace/ # palace index&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$HERMES_HOME/mempalace/conversations/ # mined transcripts as markdown&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That was important to mw. Profile isolation should be structural, not something you hope the config handles correctly.&lt;/p&gt;&#10;&lt;h3 id="install"&gt;Install&lt;/h3&gt;&#10;&lt;p&gt;If you already use Hermes:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hermes memory setup&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Install the MemPalace CLI:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv tool install mempalace&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then verify everything is in place:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hermes memory status&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hermes plugins list&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="what-it-feels-like-in-practice"&gt;What it feels like in practice&lt;/h3&gt;&#10;&lt;p&gt;The main difference is not flashy.&lt;/p&gt;&#10;&lt;p&gt;It is that &lt;code&gt;mempalace_wake_up&lt;/code&gt; gives Hermes useful context at the start of a session without turning the whole memory layer into a black box.&lt;/p&gt;&#10;&lt;p&gt;You can inspect the transcripts. You can search them. You can open the directory and see what happened. If you want to prune or review something, you are not fighting the system.&lt;/p&gt;&#10;&lt;p&gt;That is the part I cared about most.&lt;/p&gt;&#10;&lt;p&gt;Hermes still keeps its built-in memory. hermes-mempalace does not replace that. It adds an archival layer on top of it. One that feels a lot more like a real notebook than a hidden model feature.&lt;/p&gt;&#10;&lt;p&gt;If this sounds useful... let me know :)&lt;/p&gt;&#10;&lt;p&gt;If you are already happy with Hermes memory, you probably do not need this.&lt;/p&gt;&#10;&lt;p&gt;But if you want:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;local-first memory&lt;/li&gt;&#10;&lt;li&gt;profile-scoped storage&lt;/li&gt;&#10;&lt;li&gt;verbatim transcript mining&lt;/li&gt;&#10;&lt;li&gt;a readable on-disk layout&lt;/li&gt;&#10;&lt;li&gt;no extra server to babysit&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;then hermes-mempalace may be a good fit.&lt;/p&gt;&#10;&lt;p&gt;The plugin is MIT licensed, and the repo is here:&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/abdallah/hermes-mempalace"&gt;https://github.com/abdallah/hermes-mempalace&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Quick WHM/cPanel All email passwords reset</title><link>https://deeb.me/20210507/quick-whm-cpanel-all-email-passwords-reset/</link><pubDate>Fri, 07 May 2021 08:42:14 +0000</pubDate><guid>https://deeb.me/20210507/quick-whm-cpanel-all-email-passwords-reset/</guid><description>&lt;p&gt;You generally don’t want to use this script. This is a last resort, when all mailbox passwords for the entire account need to be changed. For this script to work, you need root access to your WHM/cPanel server. This could ruin everything, be warned!&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Here be dragons!&lt;/strong&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;newpass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SomeStr0ngPasswordGoesHere!&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;accountname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;example&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;domainname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;example.com&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uapi --user&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$accountname&lt;/span&gt; Email list_pops &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; grep email &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;{print$2}&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk -F&lt;span class="se"&gt;\@&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{print$1}&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; m&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; changing password &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; uapi --user&lt;span class="o"&gt;=&lt;/span&gt;@accountname Email passwd_pop &lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$domainname&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$newpass&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here are the links to the official API docs:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://documentation.cpanel.net/display/DD/UAPI+Functions+-+Email%3A%3Apasswd_pop"&gt;https://documentation.cpanel.net/display/DD/UAPI+Functions+-+Email%3A%3Apasswd_pop&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://documentation.cpanel.net/display/DD/UAPI+Functions+-+Email%3A%3Alist_pops"&gt;https://documentation.cpanel.net/display/DD/UAPI+Functions+-+Email%3A%3Alist_pops&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;strong&gt;Is there a better way to do this?&lt;/strong&gt; probably&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Can I randomize the password?&lt;/strong&gt; sure, but how will you send the new passwords to your users?&lt;/p&gt;&#10;&lt;p&gt;Hope it helps!&lt;/p&gt;&#10;</description></item><item><title>Quick ElasticSearch Index Cleanup</title><link>https://deeb.me/20210407/quick-elasticsearch-index-cleanup/</link><pubDate>Wed, 07 Apr 2021 08:50:38 +0000</pubDate><guid>https://deeb.me/20210407/quick-elasticsearch-index-cleanup/</guid><description>&lt;p&gt;There are many solutions using &lt;code&gt;curator&lt;/code&gt; and other libraries. I’ll try to add links here later (if I remember)&lt;/p&gt;&#10;&lt;p&gt;However, most of the time, all that’s needed is a quick &lt;code&gt;curl&lt;/code&gt; call&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;curl -X DELETE &amp;#34;https://${ES-Endpoint}/${INDEX_NAME}&amp;#34;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Get the ES endpoint from the domain overview&lt;/p&gt;&#10;&lt;p&gt;List the index names using&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;curl &amp;#34;https://${ES-Endpoint}/_cat/indices&amp;#34;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So for a quick cleanup session for everything that matches a grep&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;curl &amp;#34;https://${ES-Endpoint}/_cat/indices&amp;#34; | grep &amp;#39;something&amp;#39; | awk &amp;#39;{print$3}&amp;#39; \&#10;while read INDEX_NAME; do&#10; echo &amp;#34;Deleting ${INDEX_NAME}&amp;#34;&#10; curl -X DELETE &amp;#34;https://${ES-Endpoint}/${INDEX_NAME}&amp;#34;&#10;done&#10;&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>My Ansible AWS EC2 Dynamic Inventory</title><link>https://deeb.me/20200811/my-ansible-aws-ec2-dynamic-inventory/</link><pubDate>Tue, 11 Aug 2020 11:22:35 +0000</pubDate><guid>https://deeb.me/20200811/my-ansible-aws-ec2-dynamic-inventory/</guid><description>&lt;p&gt;Start with the the Ansible configuration. This can be set in &lt;code&gt;/etc/ansible/ansible.cfg&lt;/code&gt; or &lt;code&gt;~/.ansible.cfg&lt;/code&gt; (in the home directory) or &lt;code&gt;ansible.cfg&lt;/code&gt; (in the current directory)&lt;/p&gt;&#10;&lt;p&gt;My suggestion is use one of the first 2 (ie. &lt;code&gt;/etc/&lt;/code&gt; or &lt;code&gt;~/.ansible.cfg&lt;/code&gt; if you’re going to be managing instances from your machine. Update the configuration as needed.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[defaults]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;./ansible_plugins&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;enable_plugins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;aws_ec2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;host_key_checking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;False&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;pipelining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;log_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/ansible&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You may need other plugins, this one is for aws_ec2. In the /etc/ansible/ansible_plugins directory, create the *_aws_ec2.yml configuration file for your inventory&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-YAML" data-lang="YAML"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# /etc/ansible/ansible_plugins/testing_aws_ec2.yml&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;aws_ec2&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;aws_profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;testing&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;regions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;us-east-1&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;us-east-2&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tag:Team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;testing&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;instance-state-name &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;running&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hostnames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;instance-id&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;dns-name&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;keyed_groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;team&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;tags[&amp;#39;Team&amp;#39;]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You'll notice, I’m filtering using a &lt;code&gt;tag:Team == testing&lt;/code&gt; and showing only &lt;code&gt;running&lt;/code&gt; instances.&lt;/p&gt;&#10;&lt;p&gt;I’m also using the &lt;code&gt;instance-id&lt;/code&gt; and &lt;code&gt;dns-name&lt;/code&gt; attributes as hostname&lt;/p&gt;&#10;&lt;p&gt;And I’m using the &lt;code&gt;tag['Team']&lt;/code&gt; as a grouping.&lt;/p&gt;&#10;&lt;p&gt;So now, I can do the following from any directory (since my configuration is global in &lt;code&gt;/etc/ansible&lt;/code&gt;)&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;$ ansible-inventory --list --yaml&#10;all:&#10; children:&#10; aws_ec2:&#10; hosts:&#10; i-xxxxxxxxxxxxxxx:&#10; ami_launch_index: 0&#10; architecture: x86_64&#10; block_device_mappings:&#10; - device_name: /dev/sda1&#10; ebs:&#10; attach_time: 2020-08-10 15:20:58+00:00&#10; delete_on_termination: true&#10; status: attached&#10; volume_id: vol-xxxxxxxxxxxxxx&#10;...&#10; team_testing:&#10; hosts:&#10; i-xyxyxyxyxyyxyxyy: {}&#10; i-xyxyxy2321yxyxyy: {}&#10; i-xyxyxyxyxy89yxyy: {}&#10; i-xyxy1210xyyxyxyy: {}&#10; i-xyxy999999yxyxyy: {}&#10; i-xyxyxy44xyyxyxyy: {}&#10; i-xyx2323yxyyxyxyy: {}&#10; i-xyxyxyxyxy9977yy: {}&#10; ungrouped: {}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I can also use the &lt;code&gt;team_testing&lt;/code&gt; or the individual &lt;code&gt;instance_id&lt;/code&gt; in my Ansible &lt;code&gt;hosts&lt;/code&gt; calls.&lt;/p&gt;&#10;</description></item><item><title>Get daily cost alerts on AWS</title><link>https://deeb.me/20200618/get-daily-cost-alerts-on-aws/</link><pubDate>Thu, 18 Jun 2020 15:42:00 +0000</pubDate><guid>https://deeb.me/20200618/get-daily-cost-alerts-on-aws/</guid><description>&lt;p&gt;So I wanted to have a better alarm system for when AWS hits us with unexpected costs. It’s better to know there’s something wrong rather quickly and not suffer hundreds of dollars costs for something you don’t really need or want.&lt;/p&gt;&#10;&lt;p&gt;The AWS provided alarm checks for hikes on a monthly basis. Here’s the &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html"&gt;doc they published&lt;/a&gt;. So that’s an alarm that sounds when your estimated bill is going to be higher than the budgeted amount, or what you had in mind in the first place. Not very useful honestly in our case. It will just be too late.&lt;/p&gt;&#10;&lt;p&gt;The only alternative I found was creating a daily check, that will compare yesterday’s costs against a max_amount set by default. Let’s say you want to have your daily bill not exceed 5$US.&lt;/p&gt;&#10;&lt;p&gt;For ease of use and maintainability, I’m using a lambda function triggered by a cron (EventBridge rule) for the daily checks. And I’m sending the Alarm using an SNS topic, this way I can subscribe to it by email, or send it to our Mattermost channel, etc.&lt;/p&gt;&#10;&lt;p&gt;Here’s the code:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;boto3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;yesterday&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%Y-%m-&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;twodaysago&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%Y-%m-&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cost_metric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cost_metric&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;max_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;max_amount&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ce&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sns&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_cost_and_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TimePeriod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Start&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;twodaysago&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;End&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;yesterday&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Granularity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DAILY&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cost_metric&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ResultsByTime&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Total&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost_metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Amount&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sns_topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;BillingAlert&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TopicArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sns_topic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;TopicArn&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Total cost &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt; USD&amp;#34; exceeded max_amount rate: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;statusCode&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;body&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cost check: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that you will need to add a couple of environment variables to Lambda: &lt;code&gt;cost_metric&lt;/code&gt; and &lt;code&gt;max_amount&lt;/code&gt;&lt;br&gt;&#10;And the following permissions to the role used by the lambda function: &lt;code&gt;ce:GetCostAndUsage&lt;/code&gt;, &lt;code&gt;sns:Publish&lt;/code&gt; and &lt;code&gt;sns:CreateTopic&lt;/code&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;{&#10; &amp;#34;Version&amp;#34;: &amp;#34;2012-10-17&amp;#34;,&#10; &amp;#34;Statement&amp;#34;: [&#10; {&#10; &amp;#34;Sid&amp;#34;: &amp;#34;VisualEditor0&amp;#34;,&#10; &amp;#34;Effect&amp;#34;: &amp;#34;Allow&amp;#34;,&#10; &amp;#34;Action&amp;#34;: &amp;#34;ce:GetCostAndUsage&amp;#34;,&#10; &amp;#34;Resource&amp;#34;: &amp;#34;*&amp;#34;&#10; },&#10; {&#10; &amp;#34;Sid&amp;#34;: &amp;#34;VisualEditor1&amp;#34;,&#10; &amp;#34;Effect&amp;#34;: &amp;#34;Allow&amp;#34;,&#10; &amp;#34;Action&amp;#34;: [&#10; &amp;#34;logs:CreateLogStream&amp;#34;,&#10; &amp;#34;sns:Publish&amp;#34;,&#10; &amp;#34;sns:CreateTopic&amp;#34;,&#10; &amp;#34;logs:PutLogEvents&amp;#34;&#10; ],&#10; &amp;#34;Resource&amp;#34;: [&#10; &amp;#34;arn:aws:logs:us-east-1:*:log-group:/aws/lambda/checkDailyCosts:*&amp;#34;,&#10; &amp;#34;arn:aws:sns:*:*:*&amp;#34;&#10; ]&#10; },&#10; {&#10; &amp;#34;Sid&amp;#34;: &amp;#34;VisualEditor2&amp;#34;,&#10; &amp;#34;Effect&amp;#34;: &amp;#34;Allow&amp;#34;,&#10; &amp;#34;Action&amp;#34;: &amp;#34;logs:CreateLogGroup&amp;#34;,&#10; &amp;#34;Resource&amp;#34;: &amp;#34;arn:aws:logs:us-east-1:*:*&amp;#34;&#10; }&#10; ]&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After that’s set up, go to your SNS topic (created by Lambda if it doesn’t exist) and subscribe to it. There you go, daily checks and an alarm if the bill is higher than expected.&lt;/p&gt;&#10;</description></item><item><title>Sync S3 objects across AWS account?</title><link>https://deeb.me/20200512/how-can-i-copy-s3-objects-from-another-aws-account/</link><pubDate>Tue, 12 May 2020 09:42:00 +0000</pubDate><guid>https://deeb.me/20200512/how-can-i-copy-s3-objects-from-another-aws-account/</guid><description>&lt;p&gt;How do I copy S3 objects/files/etc from another AWS account?&#10;How do I migrate an S3 bucket to another AWS account? [playbook below + extra preliminary step, create the bucket]&lt;/p&gt;&#10;&lt;p&gt;Well, you can do it manually following the notes in the AWS KB article I linked at the end of the article.&#10;Honestly though, I am always confused by those KB articles. They are concise ans usually well written. I guess it's my dislexya ;)&#10;So I have to read them a few times before I’m able to apply the steps correctly. That's not a bad thing really ... better than blindly copy/paste commands (which I NEVER DO, right).&lt;/p&gt;&#10;&lt;p&gt;Recently, I’ve been going the other way around: whenever there are steps to follow, I try to translate them into an Ansible playbook. This way I make sure I got it right, AND I get to reuse the playbook, which makes things a lot faster the second time around. Oh and as an added bonus, I can share the playbook and have someone else take care of the task!&lt;/p&gt;&#10;&lt;p&gt;Enough introductions, here’s what you came here for: an ansible playbook for syncing files between S3 buckets in different accounts:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-YAML" data-lang="YAML"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# copy-bucket.yml&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;localhost&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;my-source-bucket &lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dest_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;my-dest-bucket&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dest_user_arn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;arn:aws:iam::ACCOUNTID:user/USERNAME&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dest_user_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;USERNAME&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source_profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;src_profile&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dest_profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;dest_profile&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Attach bucket policy to source bucket&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;s3_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ source_bucket }}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ lookup(&amp;#39;template&amp;#39;,&amp;#39;bucket-policy.json.j2&amp;#39;) }}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ source_profile }}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Attach an IAM policy to a user in dest account&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;iam_policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;iam_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;iam_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ dest_user_name }}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;policy_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;s3_move_access&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;present&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;policy_json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ lookup( &amp;#39;template&amp;#39;, &amp;#39;user-policy.json.j2&amp;#39;) }}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ dest_profile }}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;register&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user_policy&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Sync the files &lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;aws s3 sync s3://{{ source_bucket }}/ s3://{{ dest_bucket }}/ --profile {{ dest_profile }}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You will also need the following json templates&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;bucket-policy.json.j2&lt;/strong&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-JSON" data-lang="JSON"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Version&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2012-10-17&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Statement&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Effect&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Allow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Action&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;s3:ListBucket&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;s3:GetObject&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Resource&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;arn:aws:s3:::{{ source_bucket }}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;arn:aws:s3:::{{ source_bucket }}/*&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Effect&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Allow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Action&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;s3:ListBucket&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;s3:PutObject&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;s3:PutObjectAcl&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Resource&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;arn:aws:s3:::{{ dest_bucket }}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;arn:aws:s3:::{{ dest_bucket }}/*&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;user-policy.json.j2&lt;/strong&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-JSON" data-lang="JSON"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Version&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2012-10-17&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Statement&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Sid&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;DelegateS3Access&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Effect&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Allow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Principal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;AWS&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{{ dest_user_arn }}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Action&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;s3:ListBucket&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;s3:GetObject&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;Resource&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;arn:aws:s3:::{{ source_bucket }}/*&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;arn:aws:s3:::{{ source_bucket }}&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;run: &lt;code&gt;ansible-playbook -v copy-bucket.yml&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;make sure the profile names are setup on your machine correctly, and the IAM user is there.&lt;/p&gt;&#10;&lt;p&gt;Ref: &lt;a href="https://aws.amazon.com/premiumsupport/knowledge-center/copy-s3-objects-account/"&gt;AWS KB: copy-s3-objects-account&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>ChatOps with Mattermost and AWS Lambda</title><link>https://deeb.me/20190614/chatops-with-mattermost-and-aws-lambda/</link><pubDate>Fri, 14 Jun 2019 18:09:07 +0000</pubDate><guid>https://deeb.me/20190614/chatops-with-mattermost-and-aws-lambda/</guid><description>&lt;p&gt;I’ve been working towards making things simpler when managing distributed resources at work. And since we spend most of our day in the chat room (was Slack, now Mattermost) I thought it’s best to get started with ChatOps&lt;/p&gt;&#10;&lt;p&gt;It’s just a fancy word for doing stuff right from the chat window. And there’s so much one can do, especially with simple Slash Commands.&lt;/p&gt;&#10;&lt;p&gt;Here’s a lambda function I setup yesterday for invalidating CloudFront distributions.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;boto3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;re&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;EXPECTED_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mmToken&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ALLOWED_USERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[, ]&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;allowedUsers&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DISTRIBUTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;site-name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;DISTRIBUTIONID&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;another.site&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;DISTRIBUTIONID05&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_command_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command_text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;(&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;)\s+(.*)&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;|&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DISTRIBUTIONS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;site&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Parse the request&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;request_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;queryStringParameters&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;statusCode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;headers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{ &amp;#34;message&amp;#34;: &amp;#34;Use GET for setting up mattermost slash command&amp;#34; }&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Check the token matches.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;token&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;EXPECTED_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Wrong Token!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;statusCode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;headers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{ &amp;#34;message&amp;#34;: &amp;#34;Mattermost token does not match&amp;#34; }&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Check the user is allowed to run the command&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;user_name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ALLOWED_USERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Wrong User! &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt; not in &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;user_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ALLOWED_USERS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;statusCode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;headers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{ &amp;#34;message&amp;#34;: &amp;#34;User not allowed to perform action&amp;#34; }&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# parse the command&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;command_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;command_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Nothing to do, bailing out&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;statusCode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;headers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{ &amp;#34;message&amp;#34;: &amp;#34;No command text sent&amp;#34; }&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parse_command_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Bad formatting - command: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command_text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;statusCode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;headers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{ &amp;#34;message&amp;#34;: &amp;#34;Wrong pattern&amp;#34; }&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Do the actual work&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cf_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cloudfront&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Invalidate&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;boto_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cf_client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_invalidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DistributionId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DISTRIBUTIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;site&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InvalidationBatch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;Paths&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;Quantity&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;Items&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;CallerReference&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Invalidation&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Build the response message text.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;##### Executing invalidation&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;| Key | Info |&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;| --- | ---- |&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;| Site | &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;| Path | &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;| ID | &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; |&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;| Status | &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; |&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;site&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;boto_response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;boto_response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Status&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Build the response object.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;response_type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;in_channel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Return the response as JSON&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;body&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;headers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;statusCode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that you need to hook that up with an API Gateway in AWS. Once that’s done, you will have a URL endpoint ready for deployment.&lt;/p&gt;&#10;&lt;p&gt;Next, I created the slash command in mattermost with the following:&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://deeb.me/wp-content/uploads/image-1.png"&#10;&#9;&#9;&#9;alt="slash command configuration"&gt;&lt;figcaption&gt;&#10;&#9;&#9;&#9;&lt;p&gt;slash command configuration&lt;/p&gt;&#10;&#9;&#9;&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;p&gt;That’s pretty much it. Rinse and repeat for a different command, different usage.&lt;/p&gt;&#10;&lt;p&gt;On my list next is to have more interaction with the user in mattermost per &lt;a href="https://docs.mattermost.com/developer/interactive-messages.html"&gt;https://docs.mattermost.com/developer/interactive-messages.html&lt;/a&gt;&lt;br&gt;&#10;Weekend Project, Yay!&lt;/p&gt;&#10;</description></item><item><title>MFA from command line</title><link>https://deeb.me/20190502/mfa-from-command-line/</link><pubDate>Thu, 02 May 2019 18:00:33 +0000</pubDate><guid>https://deeb.me/20190502/mfa-from-command-line/</guid><description>&lt;p&gt;MFA, 2FA, 2 step validation, etc. are everywhere these days. And it’s a good thing.&lt;/p&gt;&#10;&lt;p&gt;Problem with using the phone to get the authentication code is that you need to have it handy at all times (when you want to login at least) and that you have to read the code then type it in (too many steps)&lt;/p&gt;&#10;&lt;p&gt;One possible alternative is to use the command line &lt;code&gt;oathtool&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Here’s my snippet, I added the following line in my &lt;code&gt;.bashrc&lt;/code&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;function mfa () { oathtool --base32 --totp &amp;#34;$(cat ~/.mfa/$1.mfa)&amp;#34; | xclip -sel clip ;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Some preparation work:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;sudo apt install oathtool xclip&amp;lt;br&amp;gt;mkdir ~/.mfa&amp;lt;br&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you want to use the same account with both a phone based MFA generator and the shell, set them up at the same time. Simply use the generated string for setting up the account in the &lt;a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"&gt;Google Authenticator&lt;/a&gt; (as an example) and the add it to the &lt;code&gt;~/.mfa/account_name.mfa&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;The use of &lt;code&gt;xclip&lt;/code&gt; automatically copies the 6 digit authentication code to the clipboard. You can go ahead and paste it.&lt;/p&gt;&#10;&lt;p&gt;The above setup works in Ubuntu. Didn’t try it on other systems.&lt;/p&gt;&#10;</description></item><item><title>VaultPress SSH Access to Site Behind AWS Load Balancer</title><link>https://deeb.me/20190318/vaultpress-ssh-sftp-access-for-wordpress-site-behind-aws-load-balancer/</link><pubDate>Thu, 21 Mar 2019 13:32:35 +0000</pubDate><guid>https://deeb.me/20190318/vaultpress-ssh-sftp-access-for-wordpress-site-behind-aws-load-balancer/</guid><description>&lt;p&gt;I recently worked on migrating a large WordPress site from a dedicated server to AWS following the reference architecture as outlined in &lt;a href="https://github.com/aws-samples/aws-refarch-wordpress"&gt;https://github.com/aws-samples/aws-refarch-wordpress&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;One of the upsides of this architecture is that the site is entirely behind the load balancer and the instances running the web server are never accessible from the internet (not even via ssh or http). So any updates to the site need to be deployed using Systems Manager. I will post about that later … However due to this restriction, VaultPress is only able to access the site (for backups) over https, which causes extra load on the web instances.&lt;/p&gt;&#10;&lt;p&gt;To fix that, we need to allow VaultPress SSH access directly to the web instances (at least to 1 of them). And the only way in via SSH is through a bastion instance.&lt;/p&gt;&#10;&lt;p&gt;On this bastion instance, I installed Nginx with the following configuration in /etc/nginx/nginx.conf&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;stream {&#10; server {&#10; listen 1234;&#10; proxy_pass 10.0.1.12:22;&#10; }&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;where &lt;code&gt;10.0.1.12&lt;/code&gt; is the internal IP of one web instances behind the load balancer.&lt;/p&gt;&#10;&lt;p&gt;At this stage (after reloading Nginx), I added the &lt;code&gt;vaultpress&lt;/code&gt; user to the web instances and set their SSH public key in the &lt;code&gt;.ssh/authorized_keys&lt;/code&gt; for that user.&lt;/p&gt;&#10;&lt;p&gt;To test manually, I added my own public key and connected to:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;ssh -l vaultpress -p 1234 bastion.mydomain.com &#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After that works, I tested from VaultPress dashboard. No surprises: testing the connection works (SSH/SFTP), all green! Great, both the SSH and the SFTP setups should work properly.&lt;/p&gt;&#10;&lt;p&gt;&amp;lt;shrug&amp;gt;One issue I faced here was that after a few hours/the next day, VaultPress somehow forgot the setup and I had to re-add the connection information. Why? No idea! Just make sure to check on it the next day to make sure it's still there! &amp;lt;/shrug&amp;gt;&lt;/p&gt;&#10;&lt;p&gt;Since the IPs may change in case there’s an auto scaling event, I added a cron job that runs every 5 minutes on the bastion and updates the nginx configuration to point to one of the web instances internal IPs. A simplistic version looks like this:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash&#10;IP=&amp;#34;$(list-ips.sh | awk &amp;#39;{ print $1 }&amp;#39;)&amp;#34;&#10;sed -i -E &amp;#34;s/proxy_pass (.+):22/proxy_pass ${IP}:22/&amp;#34; /etc/nginx/nginx.conf&#10;nginx -t &amp;amp;&amp;amp; service nginx reload&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can also use CloudWatch Events to add a rule, that runs when AutoScaling happens. Pointing that to a Lambda function that calls Systems Manager to change the IP in Nginx. Here’s a reference for that &lt;a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/cloud-watch-events.html#create-lambda-function"&gt;https://docs.aws.amazon.com/autoscaling/ec2/userguide/cloud-watch-events.html#create-lambda-function&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Oh, and don’t forget to lock down the bastion instance. In my case, I set the Security group in the EC2 console to allow only the following:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Type: Custom TCP Rule&#10;Protocol: TCP&#10;Port Range: 1234&#10;Source: 192.0.64.0/18&#10;Description: VaultPress SSH Access&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hope it helps!&#10;If you have any thoughts on improving the setup, I'm open for discussions.&lt;/p&gt;&#10;</description></item><item><title>List IPs from CloudTrail events</title><link>https://deeb.me/20190201/list-ips-from-cloudtrail-events/</link><pubDate>Fri, 01 Feb 2019 16:17:54 +0000</pubDate><guid>https://deeb.me/20190201/list-ips-from-cloudtrail-events/</guid><description>&lt;p&gt;A quick command to list the IPs from AWS CloudTrail events.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash ACCESS\_KEY\_ID=AKIASMOETHINGHERE MAX\_ITEMS=100 aws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=${ACCESS\_KEY\_ID} --max-items ${MAX\_ITEMS} \ | jq -r &amp;#39;.Events[].CloudTrailEvent&amp;#39; \ | jq &amp;#39;.sourceIPAddress&amp;#39; \ | sort | uniq&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This of course can be extended to include more information, for example:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash ACCESS\_KEY\_ID=AKIASMOETHINGHERE MAX\_ITEMS=100 aws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=${ACCESS\_KEY\_ID} --max-items ${MAX\_ITEMS} \ | jq -r &amp;#39;.Events[].CloudTrailEvent&amp;#39; \ | jq &amp;#39;{ User: .userIdentity.userName, IP: .sourceIPAddress, Event: .eventName }&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Change profiles automatically in Tilix when connecting to SSH hosts</title><link>https://deeb.me/20190116/change-profiles-automatically-in-tilix-when-connecting-to-ssh-hosts/</link><pubDate>Wed, 16 Jan 2019 16:57:46 +0000</pubDate><guid>https://deeb.me/20190116/change-profiles-automatically-in-tilix-when-connecting-to-ssh-hosts/</guid><description>&lt;p&gt;I use &lt;a href="https://gnunn1.github.io/tilix-web/"&gt;Tilix&lt;/a&gt; as my main terminal app on Ubuntu. I like it mainly because it’s easy to use and shows multiple sessions in tiles and tabs, so it’s easy to switch between multiple servers without too much fuss&lt;/p&gt;&#10;&lt;p&gt;Tilix is also pretty customizable. I just needed to have the “profile” automatically switch according to the server/machine I switch to. This can be done according to the documentation per &lt;a href="https://gnunn1.github.io/tilix-web/manual/profileswitch/"&gt;https://gnunn1.github.io/tilix-web/manual/profileswitch/&lt;/a&gt; but I wanted something configured locally, not sent to the remote system.&lt;/p&gt;&#10;&lt;p&gt;So I just added a function that wraps my ssh command. Here’s how it looks in my .bashrc&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;ssh() { SSHAPP=`which ssh`; ARGS=$@; echo &amp;#34;switching to $ARGS&amp;#34;; printf &amp;#34;\033]7;file://%s/\007&amp;#34; &amp;#34;$ARGS&amp;#34;; $SSHAPP $ARGS; }&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This sets up the hostname of machine you’re logging into as the title. That’s your trigger. What remains is to create a profile and assign that profile to automatically switch when the trigger (hostname:directory, user can also be added) is set.&lt;/p&gt;&#10;&lt;p&gt;Go to Profiles &amp;gt; Edit Profile &amp;gt; Advanced (tab) and under Match add the hostname.&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://deeb.me/wp-content/uploads/image-1024x787.png" alt="" loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;That’s about it. I’m going to add a new profile with a RED background now for my production machines! too much?&lt;/p&gt;&#10;</description></item><item><title>packet_write_wait: Connection to xxx port 22: Broken pipe</title><link>https://deeb.me/20190115/packet_write_wait-connection-to-xxx-port-22-broken-pipe/</link><pubDate>Tue, 15 Jan 2019 20:33:56 +0000</pubDate><guid>https://deeb.me/20190115/packet_write_wait-connection-to-xxx-port-22-broken-pipe/</guid><description>&lt;p&gt;SSH connections started dropping left and right a few days ago. I thought at first it was a problem with my connection, our DSL connection in Beirut, Lebanon is getting a lot better this last year. But it still had its quirks now and then and I blamed the wires and bad weather.&lt;/p&gt;&#10;&lt;p&gt;But it happened again the second day and the third, and it was becoming really annoying with jobs killed in the middle of execution when I forget to start a screen.&lt;/p&gt;&#10;&lt;p&gt;Long story short, it seems that some settings were removed or reset after an upgrade in Ubuntu on my home machine. This is what I added in /etc/ssh/ssh_config (not sshd_config!)&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Host \* ServerAliveInterval 30 ServerAliveCountMax 5&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that’s it!&lt;/p&gt;&#10;&lt;p&gt;If you don’t want to edit the system-wide configuration, you can always edit ~/.ssh/config with the same for similar effect.&lt;/p&gt;&#10;</description></item><item><title>Handling AWS Simple Email Service notifications</title><link>https://deeb.me/20180627/handling-aws-simple-email-service-notifications/</link><pubDate>Wed, 27 Jun 2018 19:06:51 +0000</pubDate><guid>https://deeb.me/20180627/handling-aws-simple-email-service-notifications/</guid><description>&lt;p&gt;Apparently it’s not enough to &lt;a href="http://deeb.me/20180321/amazon-ses-dashboard"&gt;gather information on your email sending, bounces and complaints&lt;/a&gt;. You also need to act when there’s a problem otherwise Amazon will put your account under probation and might even shut it completely if you don’t fix the problem.&lt;/p&gt;&#10;&lt;p&gt;And it’s pretty easy to implement per their sample code and the &lt;a href="https://aws.amazon.com/blogs/messaging-and-targeting/handling-bounces-and-complaints/"&gt;explanation in the aws blog&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Here are the steps I use to set it up:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Create an SNS topic. Let’s call it SES-BOUNCES. Or create 2 SNS topics (1 for bounces, another for complaints). I will go with just one.&lt;/li&gt;&#10;&lt;li&gt;Create a service on your site to handle the bounces. I used some simple PHP code (pasted below) for that.&lt;/li&gt;&#10;&lt;li&gt;Create a subscription in your SNS topic (created first)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The service should handle 2 things:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;the subscription (that will happen once!): call the SubscribeURL and you’re done.&lt;/li&gt;&#10;&lt;li&gt;the bounces/complaints: Remove the email addresses form your mailing list, or blacklist them, or … The main idea is to stop sending emails to those who complain. And stop trying to send emails to addresses that bounce.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Here’s the code for the PHP service, it’s pretty straight forward&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;if ( &amp;#39;POST&amp;#39; !== $\_SERVER[&amp;#39;REQUEST\_METHOD&amp;#39;] ) { http\_response\_code( 405 ); die; } require &amp;#39;vendor/autoload.php&amp;#39;; use Aws\Sns\Message; use Aws\Sns\MessageValidator; function parse\_bounce( $message ) { $recipients = array(); foreach ( $message[&amp;#39;bounce&amp;#39;][&amp;#39;bouncedRecipients&amp;#39;] as $recipient ) { $recipients[] = $recipient[&amp;#39;emailAddress&amp;#39;]; } switch ( $message[&amp;#39;bounce&amp;#39;][&amp;#39;bounceType&amp;#39;] ) { case &amp;#39;Transient&amp;#39;: error\_log( &amp;#34;BouncesController - Soft bounces occurred for &amp;#34; . join( &amp;#39;, &amp;#39;, $recipients ) ); return array(); break; case &amp;#39;Permanent&amp;#39;: error\_log( &amp;#34;BouncesController - Hard bounces occurred for &amp;#34; . join( &amp;#39;, &amp;#39;, $recipients ) ); return $recipients; break; } } function parse\_complaint( $message ) { $recipients = array(); foreach ( $message[&amp;#39;complaint&amp;#39;][&amp;#39;complainedRecipients&amp;#39;] as $recipient ) { $recipients[] = $recipient[&amp;#39;emailAddress&amp;#39;]; } return $recipients; } function blacklist\_recipients( $recipients ) { foreach ( $recipients as $email\_address ) { // blacklist those emails } } try { $sns\_message = Message::fromRawPostData(); $validator = new MessageValidator(); $validator-\&amp;gt;validate( $sns\_message ); if ( $validator-\&amp;gt;isValid( $sns\_message ) ) { if ( in\_array( $sns\_message[&amp;#39;Type&amp;#39;], [&amp;#39;SubscriptionConfirmation&amp;#39;, &amp;#39;UnsubscribeConfirmation&amp;#39;] ) ) { file\_get\_contents( $sns\_message[&amp;#39;SubscribeURL&amp;#39;] ); error\_log( &amp;#39;Subscribed to &amp;#39; . $sns\_message[&amp;#39;SubscribeURL&amp;#39;] ); } if ( $sns\_message[&amp;#39;Type&amp;#39;] == &amp;#39;Notification&amp;#39; ) { $message = $sns\_message[&amp;#39;Message&amp;#39;]; $notification\_type = $message[&amp;#39;notificationType&amp;#39;]; if ( $notification\_type == &amp;#39;Bounce&amp;#39; ) { blacklist\_recipients( parse\_bounce( $message ) ); } if ( $notification\_type == &amp;#39;Complaint&amp;#39; ) { blacklist\_recipients( parse\_complaint( $message ) ); } } } } catch ( Exception $e ) { error\_log( &amp;#39;Error: &amp;#39; . $e-\&amp;gt;getMessage() ); http\_response\_code( 404 ); die; }&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Don’t forget to run composer to get those dependencies:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;{ &amp;#34;require&amp;#34;: { &amp;#34;aws/aws-sdk-php&amp;#34;: &amp;#34;3.\*&amp;#34;, &amp;#34;aws/aws-php-sns-message-validator&amp;#34;: &amp;#34;1.4.0&amp;#34; } }&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And make sure you don’t send unsolicited emails! it’s just not nice.&lt;/p&gt;&#10;</description></item><item><title>Stop un-tagged instances</title><link>https://deeb.me/20180503/stop-un-tagged-instances/</link><pubDate>Thu, 03 May 2018 15:33:17 +0000</pubDate><guid>https://deeb.me/20180503/stop-un-tagged-instances/</guid><description>&lt;p&gt;After setting up a bastion and getting GitLab runner to autoscale on EC2 spot instances, I noticed that some instances are being started but left un-tagged and probably unused. Those seem to slip through the cracks somehow .. I’m still investigating why that’s happening.&lt;br&gt;&#10;Meanwhile, to avoid paying for those instances, I set up a cron to check for non tagged instances on EC2 and terminate them.&lt;/p&gt;&#10;&lt;p&gt;Here’s my bash code using aws-cli&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;#!/bin/bash INSTANCES=$(aws ec2 describe-instances \ --filters &amp;#34;Name=key-name,Values=runner-\*&amp;#34; \ &amp;#34;Name=instance-state-name,Values=running&amp;#34; \ --query &amp;#39;Reservations[].Instances[?!not\_null(Tags[?Key == `Name`].Value)] | [].[InstanceId]&amp;#39; \ --output text) if [-n &amp;#34;$INSTANCES&amp;#34;]; then aws ec2 terminate-instances --instance-ids $INSTANCES fi&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This looks for running instances, with the &lt;em&gt;Key Name&lt;/em&gt; &lt;code&gt;runner-*&lt;/code&gt; and where the &lt;em&gt;tag&lt;/em&gt; &lt;code&gt;Name&lt;/code&gt; is not not_null (so null!)&lt;/p&gt;&#10;&lt;p&gt;It’s working so far. Will keep on looking for a more permanent solution.&lt;/p&gt;&#10;&lt;p&gt;35.5138298&lt;br&gt;&#10;24.01803670000004&lt;/p&gt;&#10;</description></item></channel></rss>