<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Александр Никитин</title><generator>teletype.in</generator><description><![CDATA[Александр Никитин]]></description><image><url>https://img1.teletype.in/files/01/5c/015c3538-e384-402f-a7b8-7d9314b72190.png</url><title>Александр Никитин</title><link>https://blog.ihumster.ru/</link></image><link>https://blog.ihumster.ru/?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/ihumster?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/ihumster?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Tue, 03 Nov 2026 17:48:17 GMT</pubDate><lastBuildDate>Tue, 03 Nov 2026 17:48:17 GMT</lastBuildDate><item><guid isPermaLink="true">https://blog.ihumster.ru/update-and-restart-20-plus-claude-code-sessions</guid><link>https://blog.ihumster.ru/update-and-restart-20-plus-claude-code-sessions?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/update-and-restart-20-plus-claude-code-sessions?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>Как обновлять Claude Code, не теряя 20+ живых сессий</title><pubDate>Tue, 06 Oct 2026 21:20:02 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/e5/cd/e5cd14e2-3f8b-4dc4-a08b-133f482e0395.png"></media:content><category>LLM</category><description><![CDATA[<img src="https://img2.teletype.in/files/1a/84/1a84d802-bee0-42f8-a513-3461d25bd879.png"></img>Для работы с агентом я использую прекрасный инструмент Orca, о котором узнал от Сережи Риса, читая его отличный блог.]]></description><content:encoded><![CDATA[
  <figure id="AN0R" class="m_column">
    <img src="https://img2.teletype.in/files/1a/84/1a84d802-bee0-42f8-a513-3461d25bd879.png" width="1536" />
  </figure>
  <p id="UJS2">Для работы с агентом я использую прекрасный инструмент <a href="https://onorca.dev/" target="_blank">Orca</a>, о котором узнал от Сережи Риса, читая его <a href="https://sereja.tech/blog/orca-menedzher-agentov/" target="_blank">отличный блог</a>.</p>
  <p id="paNY">Claude Code обновляется часто, Homebrew обновляет бинарник на диске, но старые процессы продолжают жить на старой версии; закрывать 20+ сессий вручную - много лишних операций.</p>
  <h2 id="KcLW">Автоматизация обновления бинарника</h2>
  <p id="ucmi">Для того чтобы не обновлять бинарник руками Claude собрал мне такой шелл-скрипт:</p>
  <p id="51OL"><code><a href="#Zcq1">~/.local/bin/claude-code-upgrade</a></code></p>
  <p id="8O9Q">В скрипте три решения, которые легко пропустить:</p>
  <ul id="JScv">
    <li id="fefw"><code>launchd</code> запускает задачи с минимальным <code>PATH</code>. Без строки <code>export PATH=…</code> brew не находит свои утилиты.</li>
    <li id="JXSS"><code>brew upgrade</code> обновляет метаданные не чаще одного раза за 24 часа. Ежедневная задача из-за этого может пропустить сборку. Поэтому скрипт сам вызывает <code>brew update</code> и отключает автообновление метаданных.</li>
    <li id="rmwy">При ошибке скрипт пишет в лог код возврата и весь вывод команды. При успехе пишет одну строку.</li>
  </ul>
  <h3 id="SkG8">LaunchAgent</h3>
  <p id="tM9d">Для автозапуска скрипта использовал launchd:</p>
  <p id="UFai"><code><a href="#gKAW">~/Library/LaunchAgents/com.ihumster.claude-code-upgrade.plist</a></code></p>
  <h3 id="4A68">Установка и размещение</h3>
  <p id="ohu8">Скрипт обновления кладем в <code>~/.local/bin/</code>, LaunchAgent складываем в <code>~/Library/LaunchAgents/</code>.</p>
  <pre id="o2ow" data-lang="bash">chmod +x ~/.local/bin/claude-code-upgrade
launchctl bootstrap &quot;gui/$(id -u)&quot; ~/Library/LaunchAgents/com.ihumster.claude-code-upgrade.plist
launchctl print &quot;gui/$(id -u)/com.ihumster.claude-code-upgrade&quot; | grep -E &#x27;state|last exit&#x27;</pre>
  <p id="4ZDa">Если Mac спал в 08:00, <code>launchd</code> запускает пропущенную задачу после пробуждения. <code>RunAtLoad</code> добавляет запуск при входе в систему.</p>
  <p id="Cwx4">Лог за два дня:</p>
  <pre id="Ac1z">2026-10-05 13:39:33 OK upgraded 2.1.288 -&gt; 2.1.289
2026-10-06 08:00:52 OK upgraded 2.1.289 -&gt; 2.1.290
2026-10-06 16:17:03 OK upgraded 2.1.290 -&gt; 2.1.291</pre>
  <h2 id="6AC3">Часть 2. Проблема: бинарник новый, сессии старые</h2>
  <p id="83X2">Brew меняет файл на диске. Запущенный процесс продолжает работать на том бинарнике, с которого стартовал. Каталог старой версии brew переименовывает в <code>&lt;версия&gt;.upgrading</code>, и процесс держит его открытым.</p>
  <p id="0VuK">Команда показывает, на какой версии работает каждая сессия:</p>
  <pre id="Qeqc" data-lang="bash">jq -r &#x27;[.status, .version] | @tsv&#x27; ~/.claude/sessions/*.json | sort | uniq -c</pre>
  <p id="73ZC">Результат на 2026-10-05, установлена версия 2.1.289:</p>
  <pre id="kXqD">   1 busy 2.1.288
   1 busy 2.1.289
   4 idle 2.1.276
   5 idle 2.1.280
   8 idle 2.1.283
   4 idle 2.1.288</pre>
  <p id="D2Se">Самые старые сессии работали с 2026-09-22. Закрыть вкладку и открыть новую нельзя, потому что в сессии лежит контекст задачи. Нет, Orca конечно позволяет открыть сессию из Agent Session History, для этого у нее есть отдельная вкладка на панели:</p>
  <figure id="8GcE" class="m_original" data-caption-align="center">
    <img src="https://img4.teletype.in/files/f1/32/f132befa-a385-47bc-8518-0f6181ae7ed1.png" width="314" />
    <figcaption>Agent Session History</figcaption>
  </figure>
  <p id="8Ck9">И даже более того у Orca теперь есть поиск по всем сессиям проекта - очень удобно порой. История сессий решает задачу восстановления, но для регулярного обновления десятков открытых вкладок это слишком много ручных действий.</p>
  <h3 id="WcmI">Почему не автоматический перезапуск в 08:00</h3>
  <p id="uQTy">Первая идея была такой: после обновления скрипт сам перезапускает все простаивающие сессии. Я от неё отказался. Перезапуск убивает всё, что сессия держит в фоне: фоновые команды, субагентов, <code>Monitor</code> и запланированные пробуждения <code>/loop</code>. Статус <code>idle</code> в реестре этого не показывает. Сессия в статусе <code>idle</code> может ждать ответа на вопрос или окончания фоновой задачи.</p>
  <p id="BTeX">Решение о перезапуске принимает человек, по одной вкладке. Значит, нужна кнопка.</p>
  <h2 id="Vnfo">Часть 3. Что известно о сессии снаружи</h2>
  <p id="GH6x">Для перезапуска нужны четыре вещи: идентификатор сессии, флаги запуска, вкладка Orca и способ ввести команду во вкладку. Все четыре можно получить без участия модели.</p>
  <ul id="2EcJ">
    <li id="Ntht"><strong>Идентификатор сессии</strong>, <strong>статус</strong> <strong>и</strong> <strong>версия</strong>. Они лежат в файле <code>~/.claude/sessions/&lt;pid&gt;.json</code>, в полях <code>sessionId</code>, <code>status</code> и <code>version</code>.</li>
    <li id="iG4b"><strong>Флаги запуска</strong>. Их показывает команда <code>ps -o args= -p &lt;pid&gt;</code>.</li>
    <li id="SzD6"><strong>Вкладка Orca</strong>. Её задают переменные <code>ORCA_PANE_KEY</code> и <code>ORCA_TERMINAL_HANDLE</code> в окружении процесса <code>claude</code>.</li>
    <li id="KLUC"><strong>Ввод во вкладку.</strong> Его делает команда <code>orca terminal send --terminal &lt;handle&gt; --text … --enter.</code></li>
  </ul>
  <p id="17zo">Ещё один факт упрощает задачу. Родитель каждого процесса <code>claude</code> во вкладке Orca это <code>zsh</code>. После выхода из Claude Code вкладка остаётся открытой, в ней работает shell. В него можно ввести <code>claude --resume &lt;sessionId&gt;</code>.</p>
  <p id="4y7Y"><code>ORCA_TERMINAL_HANDLE</code> перестаёт работать после перезапуска Orca. <code>ORCA_PANE_KEY</code> в формате <code>tabId:leafId</code> остаётся верным, по нему скрипт ищет актуальный handle в <code>orca terminal list --json</code>.</p>
  <h2 id="mhBp">Часть 4. Quick Commands в Orca</h2>
  <p id="doe8">Quick Command это сохранённая строка текста с меткой. Команды создают в <strong>Settings → Quick Commands</strong> или кнопкой <strong>Add command</strong> в панели вкладок. У команды есть область <strong>Global</strong> или <strong>Project</strong> и флаг «нажать Enter после».</p>
  <p id="VqOx">Способ запуска меняет поведение:</p>
  <ul id="DIjz">
    <li id="BdiS">Кнопка в панели вкладок открывает новую вкладку и выполняет команду в ней.</li>
  </ul>
  <figure id="EByD" class="m_original" data-caption-align="center">
    <img src="https://img3.teletype.in/files/2f/07/2f07b1e3-268c-4815-9d31-c9181c447a3b.png" width="229" />
    <figcaption>Неправильный способ запуска</figcaption>
  </figure>
  <ul id="7f3t">
    <li id="7SUX">Пункт контекстного меню терминала вставляет текст в текущий терминал.</li>
  </ul>
  <figure id="eor4" class="m_original" data-caption-align="center">
    <img src="https://img4.teletype.in/files/7b/20/7b2080d2-8f46-44ac-819f-7709e3373d0b.png" width="483" />
    <figcaption>Правильный способ запуска</figcaption>
  </figure>
  <p id="mhOj">Для перезапуска подходит только второй способ. В новой вкладке нет агента, которого нужно перезапустить.</p>
  <p id="Ryo0">У Quick Commands в версии Orca 1.4.220 три ограничения:</p>
  <ul id="fLVW">
    <li id="B4P2">В тексте команды нет переменных, <code>sessionId</code> подставить нельзя.</li>
    <li id="iE5S">Одна команда это одна строка. Последовательность «выйти, подождать, возобновить» в неё не помещается.</li>
    <li id="vUYP">Из CLI командами управлять нельзя. Группа <code>orca quick-command</code> есть только в открытом PR <a href="https://github.com/stablyai/orca/pull/20162" target="_blank">#20162</a>.</li>
  </ul>
  <p id="X4Qh">Поэтому Quick Command содержит один вызов скрипта, а всю работу делает скрипт.</p>
  <h2 id="PjVR">Часть 5. Скрипт claude-restart</h2>
  <h3 id="KVEs">Схема</h3>
  <p id="2feT">Текст Quick Command: <code>! claude-restart</code>. Знак <code>!</code> в Claude Code (да и многих других харнесах) включает режим shell-команды.</p>
  <ol id="gB1k">
    <li id="ly7n">Claude Code запускает <code>claude-restart</code> как дочерний процесс сессии.</li>
    <li id="yXCO">Скрипт идёт вверх по дереву процессов и находит pid своего <code>claude</code>.</li>
    <li id="TEf4">Скрипт читает <code>sessionId</code> из реестра, флаги запуска из <code>ps</code> и вкладку из окружения процесса.</li>
    <li id="6qIf">Если у сессии есть фоновые shell-команды, скрипт отказывает с кодом 1.</li>
    <li id="rjtB">Скрипт запускает помощника в новом сеансе процессов через <code>setsid</code> и завершается.</li>
    <li id="5F1z">Помощник ждёт, пока статус сессии перестанет быть <code>busy</code>.</li>
    <li id="YlIm">Помощник посылает процессу <code>SIGTERM</code> и ждёт его завершения до 30 секунд.</li>
    <li id="pVcb">Помощник вводит во вкладку <code>claude --resume &lt;sessionId&gt;</code> с исходными флагами.</li>
  </ol>
  <p id="rH0B">Помощник нужен потому, что скрипт работает внутри сессии и умирает вместе с ней. <code>setsid</code> выводит помощника из группы процессов Claude Code.</p>
  <h3 id="d0v8">Код</h3>
  <p id="G0no">Файл <code><a href="#RPVL">~/.local/bin/claude-restart</a></code>. Зависимости: <code>orca</code>, <code>jq</code>, <code>perl</code>.</p>
  <h3 id="0CTM">Установка</h3>
  <ol id="2h2B">
    <li id="a35h">Сохраните скрипт в <code>~/.local/bin/claude-restart</code>.</li>
    <li id="Ty3A">Выполните <code>chmod +x ~/.local/bin/claude-restart</code>.</li>
    <li id="jAmh">Откройте в Orca <strong>Settings → Quick Commands</strong>.</li>
    <li id="XQ9o">Создайте команду с меткой <code>Restart Claude</code>, областью <strong>Global</strong> и текстом <code>! claude-restart</code>.</li>
    <li id="tT0u">Включите флаг отправки Enter.</li>
  </ol>
  <figure id="83bl" class="m_original" data-caption-align="center">
    <img src="https://img1.teletype.in/files/8b/d9/8bd916a6-cbd0-445b-96b7-3c9851efafa4.png" width="898" />
    <figcaption>Настройки Quick Command</figcaption>
  </figure>
  <h3 id="OIVU">Сводка пробного запуска:</h3>
  <pre id="tqPy">pid:        90335 (busy)
version:    2.1.289 -&gt; 2.1.289
session:    2e7c11a7-ea70-4ac1-8a07-276f397aa29a
pane:       7fd32fe7-e7bf-4ff3-a972-fe2780610d5a:f2c231d4-218a-4889-b9bb-f2c47fca10d0
terminal:   term_62415b02-fb9a-4c85-ad3d-9e0879d7b063
resume:     claude --resume 2e7c11a7-ea70-4ac1-8a07-276f397aa29a
background: none
dry run: nothing sent</pre>
  <p id="lGmX">Лог боевого запуска в <code>~/Library/Logs/claude-restart.log</code>:</p>
  <pre id="WOyy">2026-10-05 18:48:00 exit pid=86624 handle=term_9cf9f3f2-0e80-477b-b3de-a41395a4fe82
2026-10-05 18:48:02 resume: claude --resume 5fd45495-3a66-421f-9a3b-df09fba7bf0b
2026-10-05 18:48:04 OK</pre>
  <h2 id="QvLl">Часть 6. Три неочевидные проблемы</h2>
  <h3 id="QdRU">Ход модели после <code>!</code></h3>
  <p id="E3Uw">Вывод shell-команды попадает в контекст, и модель начинает ход: пересказывает сводку. В первой версии помощник ждал 2 секунды и посылал команду выхода. При длинном ходе команда пришла бы посреди ответа.</p>
  <p id="BH1j">Теперь помощник читает поле <code>status</code> в реестре и ждёт, пока оно перестанет быть <code>busy</code>. Предел ожидания 120 секунд. После предела помощник ничего не посылает и пишет <code>FAIL</code> в лог.</p>
  <h3 id="lvRQ">Меню подсказок перехватывает <code>/exit</code></h3>
  <p id="kl49">Первая версия вводила в TUI текст <code>/exit</code> и Enter. Claude Code на символ <code>/</code> открывает меню команд и скиллов. Указатель мыши над меню меняет выбранный пункт, и Enter запускает другую команду. Так одна рабочая сессия получила чужую команду вместо выхода. Скрипт подождал 30 секунд и не стал вводить команду возобновления:</p>
  <pre id="eqE9">2026-10-05 17:48:04 exit pid=56293 handle=term_b861a059-2184-40b9-bb4a-17c8fb2ef4eb
2026-10-05 17:48:37 FAIL pid=56293 still alive after 30s, resume not sent</pre>
  <p id="BpfG">Теперь скрипт ничего не вводит в TUI. Он посылает процессу <code>SIGTERM</code>. Claude Code по этому сигналу завершается штатно и возвращает терминал в обычный режим.</p>
  <p id="6rVv">Проверка на 30 секунд в этой истории сработала как задумано. Без неё команда <code>claude --resume …</code> попала бы в живую сессию как запрос к модели.</p>
  <h3 id="GNH1">Вкладка без <code>tabId</code></h3>
  <p id="lJr1">Из 22 сессий две не нашлись по <code>ORCA_PANE_KEY</code>. Их вкладки не были смонтированы в окне. Для таких вкладок <code>orca terminal list</code> возвращает <code>tabId</code> и <code>leafId</code> в виде <code>pty:&lt;worktree&gt;@@&lt;id&gt;</code>. Скрипт теперь ищет вкладку по ключу панели и по handle из окружения. После правки нашлись все 22.</p>
  <h2 id="QKhd">Ограничения</h2>
  <ul id="2BAT">
    <li id="9ytc">Не полагайтесь на предохранитель для субагентов, <code>Monitor</code> и <code>/loop</code>. Он видит только фоновые shell-команды. Перед нажатием проверьте их сами.</li>
    <li id="VKlD">Скрипт берёт флаги запуска из <code>ps</code>. Аргумент с пробелом внутри теряет кавычки.</li>
    <li id="mjmr">Команда <code>! claude-restart</code> стоит один ход модели. В проверке это 6 секунд.</li>
    <li id="exTd">Команду возобновления скрипт вводит в <code>zsh</code> как текст. Плагин автодополнения с выбором мышью может вмешаться так же, как меню Claude Code.</li>
    <li id="3Zsy">Реестр <code>~/.claude/sessions</code> и переменные <code>ORCA_*</code> это внутреннее устройство двух программ. Версии указаны в начале статьи.</li>
    <li id="z0r9">Запускайте Quick Command только из контекстного меню терминала. Кнопка в панели вкладок открывает новую вкладку, и скрипт завершается с ошибкой <code>not running inside a claude session</code>.</li>
  </ul>
  <h2 id="DVzK">Итог</h2>
  <p id="c72F">Вечером 2026-10-05 сессии работали на пяти версиях, от 2.1.276 до 2.1.289. На следующий день после двух обновлений осталось две версии: 12 сессий на 2.1.290 и 10 на 2.1.291. В логе <code>claude-restart</code> за это время 8 успешных перезапусков и 2 отказа. Один отказ это ошибка с меню подсказок. Второй это намеренный тест предела ожидания.</p>
  <h2 id="paD6">Скрипты</h2>
  <p id="Zcq1"><code>claude-code-upgrade</code></p>
  <pre id="ggM3" data-lang="bash">#!/bin/bash
# claude-code-upgrade — upgrades the claude-code@latest Homebrew cask.
# Runs from a LaunchAgent daily at 08:00 and at login:
#   ~/Library/LaunchAgents/com.ihumster.claude-code-upgrade.plist
set -uo pipefail

# launchd PATH is minimal; brew and its helpers need the full one.
export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin
export HOMEBREW_NO_ENV_HINTS=1
# Metadata is refreshed by the explicit &#x60;brew update&#x60; below.
export HOMEBREW_NO_AUTO_UPDATE=1

readonly CASK=&quot;claude-code@latest&quot;

# --- Tunables (override via environment) ----------------------------------
BREW=&quot;${CCU_BREW:-/opt/homebrew/bin/brew}&quot;
LOG_FILE=&quot;${CCU_LOG_FILE:-$HOME/Library/Logs/claude-code-upgrade.log}&quot;

log() {
    printf &#x27;%s %s\n&#x27; &quot;$(/bin/date &#x27;+%Y-%m-%d %H:%M:%S&#x27;)&quot; &quot;$*&quot; &gt;&gt;&quot;$LOG_FILE&quot;
}

# Prints the installed cask version, or &quot;unknown&quot; when brew cannot tell.
cask_version() {
    local version
    version=$(&quot;$BREW&quot; list --cask --versions &quot;$CASK&quot; 2&gt;/dev/null | /usr/bin/awk &#x27;{print $2}&#x27;)
    printf &#x27;%s\n&#x27; &quot;${version:-unknown}&quot;
}

# Runs a command; on failure logs its exit code and output, returns non-zero.
run_step() {
    local name=&quot;$1&quot; output rc
    shift
    output=$(&quot;$@&quot; 2&gt;&amp;1)
    rc=$?
    if ((rc != 0)); then
        log &quot;FAIL $name rc=$rc&quot;
        while IFS= read -r line; do log &quot;  $line&quot;; done &lt;&lt;&lt;&quot;$output&quot;
    fi
    return &quot;$rc&quot;
}

main() {
    local before after
    before=$(cask_version)
    # &#x60;brew upgrade&#x60; alone refreshes metadata at most once per
    # HOMEBREW_AUTO_UPDATE_SECS (24h), so a daily job could miss a build.
    run_step update &quot;$BREW&quot; update || exit 1
    run_step upgrade &quot;$BREW&quot; upgrade --cask &quot;$CASK&quot; || exit 1
    after=$(cask_version)
    if [[ &quot;$before&quot; == &quot;$after&quot; ]]; then
        log &quot;OK up to date $after&quot;
    else
        log &quot;OK upgraded $before -&gt; $after&quot;
    fi
}

main &quot;$@&quot;</pre>
  <p id="gKAW"><code>com.ihumster.claude-code-upgrade.plist</code></p>
  <pre id="daBg" data-lang="xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
    &lt;key&gt;Label&lt;/key&gt;
    &lt;string&gt;com.ihumster.claude-code-upgrade&lt;/string&gt;
    &lt;key&gt;ProgramArguments&lt;/key&gt;
    &lt;array&gt;
        &lt;string&gt;/Users/ihumster/.local/bin/claude-code-upgrade&lt;/string&gt;
    &lt;/array&gt;
    &lt;key&gt;StartCalendarInterval&lt;/key&gt;
    &lt;dict&gt;
        &lt;key&gt;Hour&lt;/key&gt;
        &lt;integer&gt;8&lt;/integer&gt;
        &lt;key&gt;Minute&lt;/key&gt;
        &lt;integer&gt;0&lt;/integer&gt;
    &lt;/dict&gt;
    &lt;key&gt;RunAtLoad&lt;/key&gt;
    &lt;true/&gt;
    &lt;key&gt;StandardOutPath&lt;/key&gt;
    &lt;string&gt;/Users/ihumster/Library/Logs/claude-code-upgrade.stderr.log&lt;/string&gt;
    &lt;key&gt;StandardErrorPath&lt;/key&gt;
    &lt;string&gt;/Users/ihumster/Library/Logs/claude-code-upgrade.stderr.log&lt;/string&gt;
    &lt;key&gt;ProcessType&lt;/key&gt;
    &lt;string&gt;Background&lt;/string&gt;
&lt;/dict&gt;
&lt;/plist&gt;</pre>
  <p id="RPVL"><code>claude-restart</code></p>
  <pre id="yNGd" data-lang="bash">#!/bin/bash
# claude-restart — restarts the Claude Code session of one Orca tab on the
# currently installed binary: SIGTERM, then &#x60;claude --resume &lt;id&gt;&#x60;.
#
# Meant to be run from inside the session it restarts, as &#x60;! claude-restart&#x60;
# (Orca Quick Command, inserted into the agent tab). A detached helper does
# the typing through &#x60;orca terminal send&#x60;, because the script itself dies
# together with the session.
#
#   claude-restart [--dry-run] [--force] [--pid &lt;claude-pid&gt;]
#
#   --dry-run  print what would happen, change nothing
#   --force    restart even when background shell commands are running
#   --pid      target this claude process instead of the calling session
set -uo pipefail

export PATH=/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin

# --- Tunables (override via environment) ----------------------------------
ORCA=&quot;${CCR_ORCA:-orca}&quot;
SESSIONS_DIR=&quot;${CCR_SESSIONS_DIR:-$HOME/.claude/sessions}&quot;
LOG_FILE=&quot;${CCR_LOG_FILE:-$HOME/Library/Logs/claude-restart.log}&quot;
EXIT_TIMEOUT=&quot;${CCR_EXIT_TIMEOUT:-30}&quot;
IDLE_TIMEOUT=&quot;${CCR_IDLE_TIMEOUT:-120}&quot;

log() {
    printf &#x27;%s %s\n&#x27; &quot;$(/bin/date &#x27;+%Y-%m-%d %H:%M:%S&#x27;)&quot; &quot;$*&quot; &gt;&gt;&quot;$LOG_FILE&quot;
}

die() {
    echo &quot;claude-restart: $*&quot; &gt;&amp;2
    exit 1
}

comm_of() {
    basename &quot;$(ps -o comm= -p &quot;$1&quot; 2&gt;/dev/null)&quot; 2&gt;/dev/null
}

# Prints the pids from this process up to launchd, one per line.
ancestors() {
    local pid=$$
    while [[ -n &quot;$pid&quot; &amp;&amp; &quot;$pid&quot; -gt 1 ]]; do
        echo &quot;$pid&quot;
        pid=$(ps -o ppid= -p &quot;$pid&quot; | tr -d &#x27; &#x27;)
    done
}

# Prints the nearest ancestor that is a claude process.
calling_claude_pid() {
    local pid
    for pid in $(ancestors); do
        if [[ &quot;$(comm_of &quot;$pid&quot;)&quot; == claude ]]; then
            echo &quot;$pid&quot;
            return 0
        fi
    done
    return 1
}

# Reads one variable from the environment of another process.
env_of() {
    ps eww -o command= -p &quot;$1&quot; | tr &#x27; &#x27; &#x27;\n&#x27; | sed -n &quot;s/^$2=//p&quot; | head -1
}

# Resolves the live terminal handle of a session. The pane key survives an
# Orca restart, the launch-time handle does not; a tab that is not mounted
# is listed without its pane key, so the handle is the fallback.
handle_of_session() {
    local pane=&quot;$1&quot; launch_handle=&quot;$2&quot;
    &quot;$ORCA&quot; terminal list --json --limit 500 2&gt;/dev/null |
        jq -r --arg pane &quot;$pane&quot; --arg handle &quot;$launch_handle&quot; \
            &#x27;[.result.terminals[] | select(.tabId + &quot;:&quot; + .leafId == $pane or .handle == $handle)]
             | sort_by(.handle == $handle) | .[0].handle // empty&#x27;
}

# Prints the launch flags of a claude process without the session selectors.
launch_flags() {
    ps -o args= -p &quot;$1&quot; | awk &#x27;{
        out = &quot;&quot;
        for (i = 2; i &lt;= NF; i++) {
            if ($i == &quot;--resume&quot; || $i == &quot;-r&quot;) { i++; continue }
            if ($i == &quot;--continue&quot; || $i == &quot;-c&quot;) continue
            out = out &quot; &quot; $i
        }
        print substr(out, 2)
    }&#x27;
}

# Prints shell children of the session that are not part of this call:
# those are background commands the restart would kill.
background_shells() {
    local claude_pid=&quot;$1&quot; child mine
    mine=&quot; $(ancestors | tr &#x27;\n&#x27; &#x27; &#x27;)&quot;
    for child in $(pgrep -P &quot;$claude_pid&quot;); do
        [[ &quot;$mine&quot; == *&quot; $child &quot;* ]] &amp;&amp; continue
        case &quot;$(comm_of &quot;$child&quot;)&quot; in
            zsh | bash | sh | -zsh | -bash) echo &quot;$child $(ps -o args= -p &quot;$child&quot; | cut -c1-100)&quot; ;;
        esac
    done
}

send() {
    &quot;$ORCA&quot; terminal send --terminal &quot;$1&quot; --text &quot;$2&quot; --enter --json &gt;&gt;&quot;$LOG_FILE&quot; 2&gt;&amp;1
}

# Detached part: runs after the calling shell command has returned.
helper() {
    local pid=&quot;$1&quot; handle=&quot;$2&quot; resume_cmd=&quot;$3&quot; waited=0 idle_waited=0
    # The &#x60;!&#x60; output starts a model turn; the signal goes out once it is over.
    sleep 2
    while [[ &quot;$(jq -r &#x27;.status // empty&#x27; &quot;$SESSIONS_DIR/$pid.json&quot; 2&gt;/dev/null)&quot; == busy ]]; do
        if ((idle_waited &gt;= IDLE_TIMEOUT)); then
            log &quot;FAIL pid=$pid still busy after ${IDLE_TIMEOUT}s, nothing sent&quot;
            exit 1
        fi
        sleep 1
        idle_waited=$((idle_waited + 1))
    done
    # A signal, not a typed &#x60;/exit&#x60;: typed text opens the slash-command menu,
    # where a mouse hover can change which command Enter runs.
    log &quot;exit pid=$pid handle=$handle&quot;
    kill -TERM &quot;$pid&quot; 2&gt;/dev/null || { log &quot;FAIL kill -TERM $pid&quot;; exit 1; }
    while kill -0 &quot;$pid&quot; 2&gt;/dev/null; do
        if ((waited &gt;= EXIT_TIMEOUT)); then
            # Typing the resume command now would land in the TUI as a prompt.
            log &quot;FAIL pid=$pid still alive after ${EXIT_TIMEOUT}s, resume not sent&quot;
            exit 1
        fi
        sleep 1
        waited=$((waited + 1))
    done
    sleep 1
    log &quot;resume: $resume_cmd&quot;
    send &quot;$handle&quot; &quot;$resume_cmd&quot; || { log &quot;FAIL send resume&quot;; exit 1; }
    log &quot;OK&quot;
}

main() {
    local dry_run=0 force=0 pid=&quot;&quot;
    while (($#)); do
        case &quot;$1&quot; in
            --dry-run) dry_run=1 ;;
            --force) force=1 ;;
            --pid) pid=&quot;${2:-}&quot;; shift ;;
            --helper) shift; helper &quot;$@&quot;; exit 0 ;;
            *) die &quot;unknown argument: $1&quot; ;;
        esac
        shift
    done

    [[ -n &quot;$pid&quot; ]] || pid=$(calling_claude_pid) || die &quot;not running inside a claude session; pass --pid&quot;
    [[ &quot;$(comm_of &quot;$pid&quot;)&quot; == claude ]] || die &quot;pid $pid is not a claude process&quot;

    local registry=&quot;$SESSIONS_DIR/$pid.json&quot; session_id status version
    [[ -r &quot;$registry&quot; ]] || die &quot;no session registry entry: $registry&quot;
    session_id=$(jq -r &#x27;.sessionId // empty&#x27; &quot;$registry&quot;)
    status=$(jq -r &#x27;.status // &quot;unknown&quot;&#x27; &quot;$registry&quot;)
    version=$(jq -r &#x27;.version // &quot;unknown&quot;&#x27; &quot;$registry&quot;)
    [[ &quot;$session_id&quot; =~ ^[0-9a-f-]{36}$ ]] || die &quot;bad sessionId in $registry&quot;

    local pane handle
    pane=$(env_of &quot;$pid&quot; ORCA_PANE_KEY)
    [[ -n &quot;$pane&quot; ]] || die &quot;pid $pid has no ORCA_PANE_KEY: not an Orca tab&quot;
    handle=$(handle_of_session &quot;$pane&quot; &quot;$(env_of &quot;$pid&quot; ORCA_TERMINAL_HANDLE)&quot;)
    [[ -n &quot;$handle&quot; ]] || die &quot;no live Orca terminal for pane $pane&quot;

    local flags resume_cmd shells installed
    flags=$(launch_flags &quot;$pid&quot;)
    resume_cmd=&quot;claude --resume $session_id${flags:+ $flags}&quot;
    shells=$(background_shells &quot;$pid&quot;)
    installed=$(claude --version 2&gt;/dev/null | awk &#x27;{print $1}&#x27;)

    cat &lt;&lt;EOF
pid:        $pid ($status)
version:    $version -&gt; ${installed:-unknown}
session:    $session_id
pane:       $pane
terminal:   $handle
resume:     $resume_cmd
background: ${shells:-none}
EOF

    if [[ -n &quot;$shells&quot; &amp;&amp; &quot;$force&quot; -eq 0 ]]; then
        die &quot;background shell commands are running; finish them or pass --force&quot;
    fi
    if ((dry_run)); then
        echo &quot;dry run: nothing sent&quot;
        return 0
    fi

    # New session, so the helper outlives the claude process tree.
    /usr/bin/perl -MPOSIX -e &#x27;fork and exit; POSIX::setsid(); exec @ARGV&#x27; \
        &quot;$0&quot; --helper &quot;$pid&quot; &quot;$handle&quot; &quot;$resume_cmd&quot; &lt;/dev/null &gt;/dev/null 2&gt;&amp;1
    echo &quot;restarting in 2s; log: $LOG_FILE&quot;
}

main &quot;$@&quot;</pre>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.ihumster.ru/netbox-502-incident-prefix-delete</guid><link>https://blog.ihumster.ru/netbox-502-incident-prefix-delete?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/netbox-502-incident-prefix-delete?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>Как удаление префиксов оставило NetBox без свободных воркеров</title><pubDate>Wed, 30 Sep 2026 07:49:03 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a3/84/a3841d8c-2af2-4870-87cf-0fddda37cfd7.png"></media:content><category>netbox</category><description><![CDATA[<img src="https://img4.teletype.in/files/73/4f/734f6beb-577b-45aa-830a-8e5b5c51c93a.png"></img>Коллега удалял через API дубликаты префиксов в NetBox. Операция выглядела безобидно: один запрос — один объект. Но примерно с 12:30 интерфейс перестал отвечать, а балансировщик почти 40 минут возвращал 502. При этом обе ноды приложения и PostgreSQL были живы.]]></description><content:encoded><![CDATA[
  <p id="SOnn">Коллега удалял через API дубликаты префиксов в NetBox. Операция выглядела безобидно: один запрос — один объект. Но примерно с 12:30 интерфейс перестал отвечать, а балансировщик почти 40 минут возвращал 502. При этом обе ноды приложения и PostgreSQL были живы.</p>
  <p id="YkDs">Сначала я решил, что удаления идут последовательно, а тяжёлые запросы Django блокируют всю таблицу префиксов. Я выгрузил лог PostgreSQL с запросами дольше пяти секунд и попросил агента помочь разобраться. Агент поправил меня сразу в двух важных местах: запросы шли параллельно, а конкурировали они за блокировки строк. Когда мы сопоставили лог с данными NetBox и логами баласировщика, картина сложилась.</p>
  <p id="0w9B">Ниже — разбор инцидента и того, какую часть расследования удалось поручить агенту. Адреса, имена хостов и учётных записей заменены; время и количественные данные сохранены.</p>
  <hr />
  <section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <blockquote id="TqRO"><strong>О подготовке статьи.</strong> При работе над текстом я использовал LLM. Итоговые выводы и ответственность за публикацию — мои. Если заметите неточность, прошу отнестись с пониманием и написать мне — исправлю.</blockquote>
  </section>
  <h2 id="DZBF">Что происходило</h2>
  <p id="CFzf">NetBox работает на двух нодах, по 16 воркеров gunicorn на каждой. Примерно с 12:30 до 13:08 через HAProxy и nginx клиентам отдавались 502. В это время в PostgreSQL висели долгие запросы к <code>ipam_prefix</code>, а новые запросы к приложению некому было обслуживать.</p>
  <figure id="XEJo" class="m_column">
    <img src="https://img3.teletype.in/files/69/8d/698d3ba9-0369-4de6-b7f7-c3fac8754739.png" width="1600" />
  </figure>
  <p id="Skpf">Началось всё раньше. Первое удаление дубликатов попало в лог в 12:09:07, а уже в 12:09:30 PostgreSQL зафиксировал первую взаимоблокировку при обновлении <code>_children</code>. Между 12:10 и 12:19 таких случаев было 39, хотя приложение ещё отвечало. После очередной серии удалений около 12:30 число одновременных долгих запросов поднялось до 26–32 и держалось на этом уровне почти до 13:08.</p>
  <p id="57KN">Совпадение с числом воркеров выглядит убедительно, но само по себе не доказывает, что это был именно их потолок. Практический результат был виден и без них — запросы застряли, а пользователи получили 502.</p>
  <figure id="i0HB" class="m_column">
    <img src="https://img3.teletype.in/files/ea/ae/eaaeb785-7238-48e4-88fe-a301933faf73.png" width="1600" />
  </figure>
  <h2 id="2xQj">Почему удаление одной записи оказалось дорогим</h2>
  <p id="aIrn">В таблице было много точных копий префиксов без VRF.</p>
  <p id="WxLz">NetBox хранит для префикса денормализованные поля <code>_depth</code> и <code>_children</code>. После удаления обработчик <code>post_delete</code> пересчитывает иерархию в той же транзакции. Один запрос обновляет счётчики у содержащих удалённый префикс записей, другой — глубину у вложенных. В SQL используются операторы включения префиксов &gt;&gt;= и &lt;&lt;=, причём равные префиксы тоже попадают в выборку.</p>
  <p id="NRL7">Вот где дубликаты становятся проблемой. При удалении префикса внутри вышестоящего префикса пересчёт затрагивает тысячи строк, включая копии этого же и общих предков. После выборки со счётчиками NetBox обновляет строки пачками по 100. Захваченные при обновлении блокировки удерживаются до завершения транзакции.</p>
  <p id="d9oN">Одновременно началось несколько таких удалений. Транзакции пытались обновить пересекающиеся наборы строк, ждали друг друга и периодически попадали во взаимоблокировку. В логе за день набралось 84 записи о deadlock. </p>
  <p id="qPqQ">Это была не одна «тяжёлая команда», после которой всё сломалось. Множество запросов одновременно держали воркеры, ожидая возможности закончить транзакции.</p>
  <h2 id="yUiC">Откуда взялись копии</h2>
  <p id="LZLt">Такое количество копий префиксов создала одна из наших интеграций, которая занимается инвентаризацией объектов различных систем, которые не имеют возможности поддерживать наш IPAM в актуальном состоянии. Ошибка в коде интеграции породила 54 тыс. дубликатов.</p>
  <h2 id="qcle">Как избавились от дубликатов</h2>
  <p id="IC2C">Для следующей очистки мы с агентом подготовили скрипт на Django ORM: он удаляет копии без запуска обработчика пересчёта на каждом удалении, а затем один раз перестраивает иерархию через <code>manage.py rebuild_prefixes</code>. Скрипт отключил как обработчик пересчета так и сигнал ORM для отправки вебхуков в сторону шины данных об удалении префиксов - получить 54 тыс.сообщений в Kafka при отстутвии (пока еще) потребителей этих сообщений мне не улыбалось. Такие работы позволили удалить 54к префикса без какой-либо значительной нагрузки на приложение или его БД.</p>
  <section>
    <h2 id="NFY8">Где в этой истории помог агент</h2>
    <p id="hBha">У агента был доступ на чтение к локальному логу PostgreSQL и API NetBox. Права писать в прод я ему не давал. Я передал агенту окно деградации, устройство приложения, свою первоначальную гипотезу и 27,5 МБ лога с медленными запросами. От запроса до черновика разбора прошло около 20 минут.</p>
    <p id="HuZZ">Самой полезной оказалась не скорость написания текста, а то что агент связал три источника, которые мне пришлось бы сводить вручную: длительности и взаимоблокировки в PostgreSQL, повторные удаления и историю создания объектов в NetBox, обработчики <code>post_delete</code> в коде. Он построил поминутную картину запросов, посчитал дубликаты и показал, где именно моя версия не сходится с данными.</p>
    <p id="Ks8G">При этом он не стал уверенно отвечать там, где данных не хватало. По логу нельзя установить значение <code>lock_timeout</code> или подтвердить настройки gunicorn. Эти границы важны: при расследовании инцидента красивая причинно-следственная цепочка легко начинает выглядеть как доказанный факт.</p>
    <p id="Br98">За мной оставались сбор исходных данных, решения во время инцидента, проверка выводов агента и выбор способа очистки. Агент оказался полезен как второй расследователь, который быстро разбирает большой лог и спорит с первоначальной гипотезой. Его выводы всё равно приходится сверять с продовой системой.</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.ihumster.ru/nK_4wEHidW8</guid><link>https://blog.ihumster.ru/nK_4wEHidW8?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/nK_4wEHidW8?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>Не доверяй ИИ</title><pubDate>Tue, 25 Feb 2025 18:50:00 GMT</pubDate><category>Мысли вслух</category><description><![CDATA[Блин, товарищи коллеги. Я с чувством невероятного удовлетворения в очередной раз убеждаюсь что никакой искусственный интеллект нас не заменит. И он херовый помощник тому кто не понимает что он хочет получить в результате и как этот результат должен воспроизводиться.]]></description><content:encoded><![CDATA[
  <p id="S12z">Блин, товарищи коллеги. Я с чувством невероятного удовлетворения в очередной раз убеждаюсь что никакой искусственный интеллект нас не заменит. И он херовый помощник тому кто не понимает что он хочет получить в результате и как этот результат должен воспроизводиться.</p>
  <p id="LksA">Це была фабула. А дальше мой короткий рассказ. 😅</p>
  <p id="qkdT">Понадобилось мне стало быть в логи nginx пихнуть кусок (именно кусок, а не всю строку ибо там чувствительные данные) строки из заголовка http запроса. Пошёл читать документацию. Понял что необходимое мне можно извлечь из одной переменной в другую через директиву map. И вот я два часа &quot;беседовал&quot; с perplexity/openai-o3 которые отчаянно меня убеждали что результат регулярки (match) или именованную группу нельзя использовать как результат map. И действительно приведенный пример конфига nginx не принимал за валидный конфиг. После чтения документации и пары статей на хабре, в комментариях я таки нашел пример, который натолкнул меня на решение моей задачи, которая в итоге решилась и оказалась простой и элегантной (ввиду простенькой регулярочки которую я уже накидал сам).</p>
  <p id="FuHX">В качестве эпилога хочу вам пожелать не верить всяким ИИ, даже если они утверждают что что-то не будет работать. 😁 доверяйте своему инженерному чутью!</p>
  <p id="1mDv">У меня всё.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.ihumster.ru/netbox-change-current-config-nbshell</guid><link>https://blog.ihumster.ru/netbox-change-current-config-nbshell?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/netbox-change-current-config-nbshell?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>Управление CurrentConfig через nbshell</title><pubDate>Mon, 13 Jan 2025 16:37:14 GMT</pubDate><category>netbox</category><description><![CDATA[Опытные пользователи Netbox наверняка сталкивались с таким мощным инструментом как Netbox Shell (построенный на базе Django Shell и представляющий собой интерпретатор iPython с расширенной функциональностью).]]></description><content:encoded><![CDATA[
  <p id="LvKh">Опытные пользователи Netbox наверняка сталкивались с таким мощным инструментом как Netbox Shell (построенный на базе Django Shell и представляющий собой интерпретатор iPython с расширенной функциональностью).</p>
  <h2 id="0v70">Управление в интерактивном режиме</h2>
  <p id="xLrY">Чтобы запустить nbshell необходимо открыть в командной строке папку с дистрибутивом приложения и, активировав virtual environment, выполнить команду <em>python netbox/manage.py nbshell</em></p>
  <p id="NZaw">Что бы получить текущую ревизию конфигурации приложения (которая и содержит динамически конфигурируемые параметры) нам понадобится ID текущей (активной) конфигурации, ее можно получить из кэша приложения:</p>
  <pre id="EDN4" data-lang="python">from django.core.cache import cache

activeRevision = ConfigRevision.objects.get(pk=cache.get(&#x27;config_version&#x27;))</pre>
  <p id="30h2">Объект activeRevision будет содержать объект типа ConfigRevision, являющийся активной конфигурацией в данный момент (&quot;Curren configuration&quot;). Объект простой, в поле data содержит словарь с динамически конфигурируемыми параметрами (список всех параметров и их значение можно подсмотреть в <a href="https://netboxlabs.com/docs/netbox/en/stable/configuration/" target="_blank">официальной документации</a>). Однако просто изменить текущую конфигурацию нельзя, конфигурация будет применена только к новой ревизии конфигурации, а значит нам нужно ее создать.</p>
  <p id="hCHh">Однако создавать мы ее будем не с нуля, а на базе предыдущей активной конфигурации.</p>
  <p id="eXTB">Для примера мы включим параметр MAINTENANCE_MODE, переведя таким образом приложение в режим обслуживания.</p>
  <pre id="iPQn" data-lang="python"># Хорошим тоном будет записать смысл новой ревизии конфига в комментарий
# Заполняем все параметры из текущей конфигурации
newRevsion = ConfigRevision(comment=&#x27;Enable MM&#x27;, data=activeRevision.data)
# Изменяем нужный нам параметр
newRevsion.data[&#x27;MAINTENANCE_MODE&#x27;] = True
# Валидируем нашу модель конфигурации на соответствие модели данных,
# заложенных разработчиком
newRevsion.full_clean()
# Сохраняем новую конфигурацию (она автоматически станет активной)
newRevsion.save()</pre>
  <h2 id="1PWO">Управление в режиме скрипта</h2>
  <p id="zv8h">Данный код можно склеить в одну строку, разделив строки точкой с запятой <strong>; </strong>и передать одним аргументом для nbshell.</p>
  <pre id="2ju7" data-lang="shell">python netbox/manage.py nbshell -c &quot;from django.core.cache import cache; activeRevision = ConfigRevision.objects.get(pk=cache.get(&#x27;config_version&#x27;)); newRevsion = ConfigRevision(comment=&#x27;Enable MM&#x27;, data=activeRevision.data); newRevsion.data[&#x27;MAINTENANCE_MODE&#x27;] = True; newRevsion.full_clean(); newRevsion.save();&quot;</pre>
  <h2 id="gQqO">Для мультинодовых конфигураций</h2>
  <section style="background-color:hsl(hsl(170, 33%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="cDdW">не забывайте что проделывать манипуляции с конфигурациями придется для каждой ноды отдельно. </p>
  </section>
  <h2 id="pqJ0">UPD: Работа с существующей конфигурацией</h2>
  <p id="AxhH">Если же хочется изменить какую-то текущую конфигурацию или просто сделать ее активной (например, откатиться на предыдущую), то сделать это можно через встроенный метод класса ConfigRevision - activate()</p>
  <pre id="9Fkq" data-lang="python">revision = ConfigRevision.object.get(pk=revisionNumber)
revision.activate()</pre>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.ihumster.ru/netbox-keycloak-custom-pipeline</guid><link>https://blog.ihumster.ru/netbox-keycloak-custom-pipeline?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/netbox-keycloak-custom-pipeline?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>Авторизация в Netbox через Keycloak (кастомный pipeline)</title><pubDate>Sat, 04 Jan 2025 20:53:43 GMT</pubDate><category>netbox</category><description><![CDATA[В сети есть хороший мануал по настройке авторизации в Netbox через Keycloak, однако он хорошо подходит для новых инсталляций, когда у вас чистая БД, ну или размер таблицы с пользователями и группами не очень большой и вам не составит труда все переделать.]]></description><content:encoded><![CDATA[
  <p id="tlDW">В сети есть хороший мануал по настройке <a href="https://itdraft.ru/2023/05/17/sso-avtorizacija-v-netbox-cherez-keycloak/" target="_blank">авторизации в Netbox через Keycloak</a>, однако он хорошо подходит для новых инсталляций, когда у вас чистая БД, ну или размер таблицы с пользователями и группами не очень большой и вам не составит труда все переделать.</p>
  <p id="1JPj">Сложность, с которой я столкнулся, заключалась в том, что используемая нами инсталляция Netbox имеет множество активных пользователей импортированных из Active Directory, активную ролевую модель основанную на группах из Active Directory и потерять все эти ассоциации очень не хотелось бы.</p>
  <p id="s8FO">Изучая документацию на модуль <a href="https://python-social-auth.readthedocs.io/en/latest/" target="_blank">python-social-auth</a> (а также код самого модуля и его бэкэнд к Keycloak) я наткнулся на раздел <a href="https://python-social-auth.readthedocs.io/en/latest/pipeline.html#extending-the-pipeline" target="_blank">Extending the Piplene</a> и понял что весь конвеер авторизации легко настраивается и изменяется, однако придётся немного по-питонить.</p>
  <h2 id="1xG1">Проблемы с которыми я столкнулся</h2>
  <p id="1qCy">Собственно их было две:</p>
  <ol id="pGKM">
    <li id="0xr1">Дефолтный Pipeline падает при наличии в Netbox пользователей с одинаковыми email (это специфический случай, который характерен для нашей организации и с которым вы можете никогда не столкнуться. Для снятия лишних вопросов скажу что, у нас есть правило записывать в поле email соответствующий email владельца технической учётной записи)</li>
    <li id="ybtp">Дефолтный Pipeline не обновляет членство в группах (а вот это уже критично для ролевой модели доступа основанной на группах).</li>
  </ol>
  <h2 id="Jxpc">Дефолтный Pipeline</h2>
  <p id="1T0x">Дефолтный pipeline для Netbox определяется в модуле &lt;your-netbox-folder&gt;/netbox/netbox/settings.py и выглядит так:</p>
  <pre id="brZJ" data-lang="python">SOCIAL_AUTH_PIPELINE = (
    &#x27;social_core.pipeline.social_auth.social_details&#x27;,
    &#x27;social_core.pipeline.social_auth.social_uid&#x27;,
    &#x27;social_core.pipeline.social_auth.social_user&#x27;,
    &#x27;social_core.pipeline.user.get_username&#x27;,
    &#x27;social_core.pipeline.social_auth.associate_by_email&#x27;,
    &#x27;social_core.pipeline.user.create_user&#x27;,
    &#x27;social_core.pipeline.social_auth.associate_user&#x27;,
    &#x27;netbox.authentication.user_default_groups_handler&#x27;,
    &#x27;social_core.pipeline.social_auth.load_extra_data&#x27;,
    &#x27;social_core.pipeline.user.user_details&#x27;,
)</pre>
  <p id="02l5">Он содержит функции из дефолтного, для модуля python-social-auth, конвеера, за исключением функции <strong><em>user_default_groups_handler</em></strong> из модуля &lt;your-netbox-folder&gt;/netbox/netbox/authentication.py (строка &#x27;netbox.authentication.user_default_groups_handler&#x27;).</p>
  <h2 id="pVXd">Решение проблемы №1</h2>
  <p id="q9Vv">В первых трех функциях Pipeline происходит получение и декодирование JWT access_token из которого извлекаются различные поля, которые передаются клиенту (в нашем случае Netbox) от Keycloak, и помещаются в переменную details (dict содержащий данные о пользователе, на базе которых пайплайн должен будет создать или обновить данные пользователя Netbox).</p>
  <p id="sRsK">Функция get_username создает на базе совокупности различных настроек приложения и модуля python-social-auth имя пользователя, а функция associate_by_email пытается по email найти в БД Netbox соответствующего пользователя.</p>
  <p id="VdsC">Функция associate_by_email выглядит так:</p>
  <pre id="jz60" data-lang="python">def associate_by_email(backend, details, user=None, *args, **kwargs):
    &quot;&quot;&quot;
    Associate current auth with a user with the same email address in the DB.

    This pipeline entry is not 100% secure unless you know that the providers
    enabled enforce email verification on their side, otherwise a user can
    attempt to take over another user account by using the same (not validated)
    email address on some provider.  This pipeline entry is disabled by
    default.
    &quot;&quot;&quot;
    if user:
        return None

    email = details.get(&quot;email&quot;)
    if email:
        # Try to associate accounts registered with the same email address,
        # only if it&#x27;s a single object. AuthException is raised if multiple
        # objects are returned.
        users = list(backend.strategy.storage.user.get_users_by_email(email))
        if len(users) == 0:
            return None
        elif len(users) &gt; 1:
            raise AuthException(
                backend, &quot;The given email address is associated with another account&quot;
            )
        else:
            return {&quot;user&quot;: users[0], &quot;is_new&quot;: False}</pre>
  <p id="1GMd">Очевидно, что при наличии в в БД нескольких пользователей в одинаковым email эта функция упадёт.</p>
  <p id="pvzo">Чтобы решить эту проблему я создал новый модуль и переписал эту функцию так, чтобы использовать для ассоциации не только поле email но username.</p>
  <pre id="Cx86" data-lang="python">import re
from social_core.exceptions import AuthException


def associate_by_email_and_username(backend, details, user=None, *args, **kwargs):
    &quot;&quot;&quot;
    Associate current auth with a user with the same email address and username in the DB.
    &quot;&quot;&quot;
    if user:
        return None
    email = details.get(&quot;email&quot;)
    username = details.get(&quot;username&quot;)
    # If username in upn format, get username part
    if m:=re.match(r&quot;^(?:(?P&lt;username&gt;[^@]+)@(?P&lt;domain&gt;.+))quot;, username):
        username = m.group(&#x27;username&#x27;)
    if email:
        users = list(backend.strategy.storage.user.get_users_by_email(email))
        if len(users) == 0:
            return None
        elif len(users) &gt; 1:
            for u in users:
                if u.username == username:
                    return {&quot;user&quot;: u, &quot;is_new&quot;: False}
            raise AuthException(
                backend, &quot;The given email address is associated with another account&quot;
            )
        else:
            return {&quot;user&quot;: users[0], &quot;is_new&quot;: False}
    else:
        raise AuthException(backend, &quot;No email address returned&quot;)</pre>
  <p id="Kckm">Функция извлекает из словаря details username, если он в формате userPrincipalName - извлекает из него username часть и при наличии в БД Netbox пользователей с одинаковым email проводит дополнительную проверку по полю username.</p>
  <p id="TEyg">Остается только собрать пакет из модуля, импортировать его в venv окружение, в котором работает Netbox, и переопределить в настройках приложения Pipeline (об этом расскажу ниже).</p>
  <h2 id="hScU">Решение проблемы №2</h2>
  <p id="lo41">Изучив встроенную функцию netbox.authentication.user_default_groups_handler я понял что она назначает пользователю только группы определенные в переменной REMOTE_AUTH_DEFAULT_GROUPS из configuration.py. Я принял решение заменить эту функцию в конвеере совместив с функциональностью обновления групп, полученных от Keycloak.</p>
  <p id="9DKE">Код этой функции будет выглядеть так:</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="Dert"><strong>Важно!</strong> Для того чтобы эта функция корректно работала access_token, который возвращает ваш Keycloak после авторизации, должен содержать поле groups с массивом имён групп. Если этого поля нет или оно содержит пустой массив - пользователю Netbox будут сброшены все связи с группами, а новых связей создано не будет.</p>
  </section>
  <pre id="KU4p" data-lang="python">import logging
from django.conf import settings
from django.contrib.auth.models import Group

def update_user_groups(backend, response, user=None, *args, **kwargs):
    &quot;&quot;&quot;
    Custom pipline handler with adds remote auth users to extist groups or
    create non-exist groups
    &quot;&quot;&quot;
    logger = logging.getLogger(
        &quot;netbox-social-auth-custom.auth_username.update_user_groups_handler&quot;
    )
    if not user:
        return None
    else:
        user_groups = []
        auth_groups = response.get(&quot;groups&quot;, [])
        user.groups.clear()

        # Add or Create DEFAULT USER GROUPS defined in configuration.py
        for name in settings.REMOTE_AUTH_DEFAULT_GROUPS:
            group, created = Group.objects.get_or_create(name=name)
            user_groups.append(group)

        # Add or Create Social Auth user groups
        # groups should be added to access token as array (list) of names
        for name in auth_groups:
            group, created = Group.objects.get_or_create(name=name)
            user_groups.append(group)

        if user_groups:
            user.groups.add(*user_groups)
        else:
            logger.info(
                f&quot;No any groups assignments for {user}. May be REMOTE_AUTH_DEFAULT_GROUPS incorrectly set\
                  or not returned from OAuth provider&quot;
            )</pre>
  <h2 id="gy80">Переопределение Pipeline</h2>
  <p id="V8qc">Итак, обе функции помещены в файл, который я назвал auth_username.py, сложены в папки netbox-social-auth-custom/pipeline (вторая папка больше для красоты в названии - обозначения что это часть pipeline по аналогии с модулем social_core) и из этого собран python package (как собирать python-пакеты я описывать не буду, т.к. для этого есть куча разного инструментария и соответствующих гайдов - <a href="https://packaging.python.org/en/latest/tutorials/packaging-projects/" target="_blank">например такой</a>, разве что подскажу, что не плохо бы при сборке указать зависимость от social-auth-core&gt;=4.5).</p>
  <p id="9IYN">Пакет установлен в окружении venv нашего Netbox и остается только переопределить собственно сам Pipline. Для этого в файле configuration.py переопределяется переменная SOCIAL_AUTH_PIPELINE в виде вот такого массива:</p>
  <pre id="yOXy" data-lang="python">SOCIAL_AUTH_PIPELINE = (
    &#x27;social_core.pipeline.social_auth.social_details&#x27;,
    &#x27;social_core.pipeline.social_auth.social_uid&#x27;,
    &#x27;social_core.pipeline.social_auth.social_user&#x27;,
    &#x27;social_core.pipeline.user.get_username&#x27;,
    &#x27;netbox_social_auth_custom.pipeline.auth_username.associate_by_email_and_username&#x27;,
    &#x27;social_core.pipeline.user.create_user&#x27;,
    &#x27;social_core.pipeline.social_auth.associate_user&#x27;,
    &#x27;social_core.pipeline.social_auth.load_extra_data&#x27;,
    &#x27;social_core.pipeline.user.user_details&#x27;,
    &#x27;netbox_social_auth_custom.pipeline.auth_username.update_user_groups&#x27;,
)</pre>
  <p id="670e">Собственно на этом всё. Спасибо что дочитали, надеюсь это кому-то пригодится.</p>
  <p id="HjuR">UPD: Отладка Pipline</p>
  <p id="bBA8">Забыл написать про отладку вашего (или вообще) конвеера авторизации. Чтобы в увидеть в логах или dev-консоли сервера Netbox </p>
  <h2 id="HjuR">UPD: Отладка Pipline</h2>
  <p id="bBA8">Забыл написать про отладку вашего (или вообще) конвеера авторизации. Чтобы в увидеть в логах или dev-консоли сервера Netbox отладочную информацию добавьте в конец (или перед падающей функцией конвеера) строку</p>
  <pre data-lang="python" id="KfhA">&#x27;social_core.pipeline.debug.debug&#x27;,</pre>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.ihumster.ru/ansible-role-argument-validation</guid><link>https://blog.ihumster.ru/ansible-role-argument-validation?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/ansible-role-argument-validation?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>Проверка аргументов (переменных) для роли Ansible</title><pubDate>Mon, 23 Sep 2024 07:09:41 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/77/af/77af2f0a-4923-49e1-a776-2a27b8081401.png"></media:content><category>Ansible</category><description><![CDATA[<img src="https://img1.teletype.in/files/8e/0d/8e0d343a-84fc-40d4-8871-4cb5b536e99d.png"></img>При написании новой роли задумался о теме данной статьи - как проверять набор минимально необходимых параметров (аргументов, переменных, всё в Ansible сводится к переменным) для выполнения роли и как обычно пошел изучать документацию.]]></description><content:encoded><![CDATA[
  <p id="RhUn">При написании новой роли задумался о теме данной статьи - как проверять набор минимально необходимых параметров (аргументов, переменных, всё в Ansible сводится к переменным) для выполнения роли и как обычно пошел изучать <a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html#role-argument-validation" target="_blank">документацию</a>.</p>
  <p id="esCJ">С приятным удивлением обнаружил механизм, который в Ansible появился с релизом ansible-core 2.11 (апрель 2021) и называется он <strong>Role argument validation</strong>. Исходя из собственного опыта написания модулей Ansible, я предполагаю, что в данном механизме используется тот же самый код, что и при валидации аргументов модуля (по крайней мере, всё на это указывает).</p>
  <p id="yOuk">Более того, этот механизм позволяет, помимо валидации аргументов, документировать вашу роль. После добавления файла, описывающего Ваш набор переменных, Вы получаете документированную роль, документацию по которой можно вызвать при помощи ansible-doc.</p>
  <pre id="jSJS">ansible-doc --type role --roles-path &lt;you roles path&gt; &lt;role_name&gt;</pre>
  <p id="vzG2">Пример вызова такой роли:</p>
  <figure id="HjXW" class="m_original">
    <img src="https://img1.teletype.in/files/8e/0d/8e0d343a-84fc-40d4-8871-4cb5b536e99d.png" width="1339" />
  </figure>
  <h2 id="2zph">Реализация</h2>
  <p id="zEUs">Для создания подобной функциональности используется файл meta/argument_specs.yml. Его формат описан в <a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html#specification-format" target="_blank">документации </a>и довольно прост. Более того, у Вашей роли может быть не одна точка входа (по умолчанию это main.yml) но и дополнительные, возможно для вызова через <strong><em>ansible.builtin.import_role</em></strong> или <strong><em>ansible.builtin.include_role</em></strong>, набор параметров для каждой точки входа можно описать в этом файле. Также параметры можно пометить обязательными, указать тип данных (набор типов данных, которые умеет проверять AnsibleModule <a href="https://docs.ansible.com/ansible/latest/dev_guide/developing_program_flow_modules.html#argument-spec" target="_blank">тут</a>), который должен содержаться в параметре, его значение по умолчанию (если оно находится в ../defaults/&lt;entry-point&gt;.yml, например) ну и, конечно же, описание (short_description и description для точки входа и description для каждого параметра).</p>
  <p id="ND6b">В дальнейшем роль, имеющая такое описание параметров при вызове будет проверять набор необходимых параметров автоматически отдельным таском.</p>
  <figure id="03d1" class="m_original">
    <img src="https://img4.teletype.in/files/ff/f7/fff71be9-1138-49ed-bdd4-d63c1e1eef11.png" width="1314" />
  </figure>
  <p id="Dgii">При вызове роли через <strong><em>ansible.builtin.import_role</em></strong> или <strong><em>ansible.builtin.include_role</em></strong> есть возможность отключить проверку аргументов, используя параметр <strong>rolespec_validate</strong> установленный в <strong>false</strong>.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.ihumster.ru/interesting-cmdlet-from-the-microsoft-powershell</guid><link>https://blog.ihumster.ru/interesting-cmdlet-from-the-microsoft-powershell?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/interesting-cmdlet-from-the-microsoft-powershell?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>Интересный cmdlet из модуля Microsoft.PowerShell.TextUtility</title><pubDate>Mon, 23 Sep 2024 07:04:41 GMT</pubDate><description><![CDATA[Модуль Microsoft.PowerShell.TextUtility получил новый командлет ConvertFrom-TextTable. Он умеет превращать текстовую таблицу (типичный вывод большинства shell-команд Linux) в массив объектов. И даже больше - он может распарсить значения в столбцах таблицы и сконвертировать их в соответсвующие типы (например Int или Float, Boolean). Ну и наконец вместо объекта массива, командлет может вернуть массив строк в формате JSON.]]></description><content:encoded><![CDATA[
  <p id="RuMN">Модуль Microsoft.PowerShell.TextUtility получил новый командлет ConvertFrom-TextTable. Он умеет превращать текстовую таблицу (типичный вывод большинства shell-команд Linux) в массив объектов. И даже больше - он может распарсить значения в столбцах таблицы и сконвертировать их в соответсвующие типы (например Int или Float, Boolean). Ну и наконец вместо объекта массива, командлет может вернуть массив строк в формате JSON.</p>
  <p id="rEHO">Установить модуль можно из PowershellGallery:</p>
  <pre id="ll2v">Install-Module -Name Microsoft.PowerShell.TextUtility -AllowPrerelease</pre>
  <p id="lwli">Пример использования:</p>
  <pre id="jz9M" data-lang="powershell">PS&gt; df | select -first 6 | convertfrom-texttable -ConvertPropertyValue | Format-Table

Filesystem 1K-blocks      Used Available Use% Mounted_on
---------- ---------      ---- --------- ---- ----------
none         8167564         4   8167560 1%   /mnt/wsl
none       498723140 443586308  55136832 89%  /usr/lib/wsl/drivers
none         8167564         0   8167564 0%   /usr/lib/wsl/lib
/dev/sdc   263112772  27894640 221779976 12%  /
rootfs       8164316      1936   8162380 1%   /init</pre>
  <p id="JwjP">Командлет позовляет указать где определяется строка заголовков или если ее нет просто брать табличные данные и превращать их в массив объектов.</p>
  <pre id="PgiV" data-lang="powershell">PS&gt; ls -l | select -First 5 | ConvertFrom-TextTable -Skip 1 -NoHeader | ft

Property_01 Property_02 Property_03 Property_04 Property_05 Property_06 Property_07 Property_08 Property_09
----------- ----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
drwxr-xr-x  3           ihumster    ihumster    4096        Aug         21          2022        aac-base
drwxr-xr-x  3           ihumster    ihumster    4096        Jul         20          23:48       ansible_collections
drwxr-xr-x  7           ihumster    ihumster    4096        Apr         30          15:43       ansible-module-vcloud-director
drwxr-xr-x  3           ihumster    docker      4096        Jun         14          2022        ansible-netbox-dev</pre>
  <p id="lrr2">Модуль еще находится в процессе разработки и если он вам понравился и пригодился, но вы нашли какую-либо ошибку, то оставить баг-репорт можно прямо в разделе <a href="https://github.com/PowerShell/TextUtility/issues" target="_blank">Issues </a>репозитория.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.ihumster.ru/vcd-upgrade-10-4-1-nuanses</guid><link>https://blog.ihumster.ru/vcd-upgrade-10-4-1-nuanses?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/vcd-upgrade-10-4-1-nuanses?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>Нюансы обновления до VCD 10.4.1</title><pubDate>Mon, 23 Sep 2024 07:03:35 GMT</pubDate><category>Cloud Director</category><description><![CDATA[После обновления до VCD 10.4.1 одна из Advisores отсылает нас к KB78885 (но да кто их читает, да?), которая опубликована в далеком уже 2021 году и которая напоминает нам о том, что после обновления VCD у нас может пропасть доверненное подключение к различным элементам нашей инфраструктуры. Ужесточение требований к сертификатам в инфраструктуре началось в VCD еще с версии 10.1 (о чём так же отдельно пишется во врезке, на странице документации посвященной обновлению VCD).]]></description><content:encoded><![CDATA[
  <p id="fvYh">После обновления до VCD 10.4.1 одна из Advisores отсылает нас к <a href="https://kb.vmware.com/s/article/78885" target="_blank">KB78885</a> (но да кто их читает, да?), которая опубликована в далеком уже 2021 году и которая напоминает нам о том, что после обновления VCD у нас может пропасть доверненное подключение к различным элементам нашей инфраструктуры. Ужесточение требований к сертификатам в инфраструктуре началось в VCD еще с версии 10.1 (о чём так же отдельно пишется во врезке, на странице документации <a href="https://docs.vmware.com/en/VMware-Cloud-Director/10.4/VMware-Cloud-Director-Install-Configure-Upgrade-Guide/GUID-6EC093C1-4CE7-4435-9FC5-F801EA618D91.html" target="_blank">посвященной обновлению</a> VCD).</p>
  <p id="hDWo">В 10.4.1 ужесточили еще один аспект инфраструктуры, а именно подключение к ESXi хостам. В результате, после обновления до 10.4.1 (и автоматическом переходе на новую консоль, старую выпилили таки, и слава богу), в VCD перестанут работать операции требующие прямого доступа к хосту, а именно <strong>доступ к консоли ВМ, ипорту OVF/ISO, Guest Customization</strong>.</p>
  <p id="B8Zk">В логах это может отобразиться в виде следующих сообщений:</p>
  <pre id="yf6H">[ 88fc5c17-f0c7-4021-922b-5dfe1b84b7cd ] PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 - PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 - PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 - unable to find valid certification path to requested target</pre>
  <pre id="O4lr"> 2023-02-24 02:59:01,050 | ERROR    | pool-jetty-81             | ServerWebSocket                | Connecting to ESX esx01.lab.ihumster.ru [server: [L=/192.168.168.25:443 R=/192.168.168.30:60004]] [client: [id: 0x13240ef6, L:/192.168.168.25:52056 ! R:esx01.lab.ihumster.ru/192.168.168.101:443]] |
io.netty.handler.codec.DecoderException: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
</pre>
  <h2 id="UaNd">Решение</h2>
  <p id="HDwa">Самый простой вариант - это запустить повторно <strong>CMT trust-infra-certs</strong>:</p>
  <pre id="ydsi">./cell-management-tool trust-infra-certs --vsphere --unattended
Downloading certificates for 2 host(s), including 1 vCenter(s):
        vc-mng.lab.ihumster.ru                                      [Download: SUCCESS] (VMCA)
        vc-mng.lab.ihumster.ru                                      [Download: SUCCESS] (VMCA)
        vc-mng.lab.ihumster.ru                                      [Download: SUCCESS]
        nsx.lab.ihumster.ru                                         [Download: SUCCESS]
Downloaded certificates for 3/2 host(s).
Trusting certificates for 2 host(s):
        vc-mng.lab.ihumster.ru                                       [Already Trusted: SUCCESS]
        nsx.lab.ihumster.ru                                          [Already Trusted: SUCCESS]
Trusting VMCA certificates for 1 vCenter(s):
        vc-mng.lab.ihumster.ru                                       [Add Trusted: SUCCESS] (VMCA) - alias=vc-mng.lab.ihumster.ru_vmca_2023-02-24-11-05-13
Trusted 3/3 downloaded certificates.</pre>
  <p id="BxPL">Или самостоятельно скачать с vCenters их сертификаты VMCA (&lt;vcenter&gt;/certs/download.zip) и вручную добавить VMCA сертификаты в Trusted Certificates вашего VCD.</p>
  <p id="TYoj">Для VCD с большим количеством vCenters (или организаций с высокими требованиями к безопасности инфраструктурных компонентов) самым правильным путём, по моему мнению, будет вариант с <a href="https://kb.vmware.com/s/article/2097936#use_of_replacing_vmca_certificate_with_custom_ca_certificate" target="_blank">заменой сертификата VMCA</a> на vCenters на подписанный с внутреннего или внешнего CA и последующей регенерацией сертификатов ESXi.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.ihumster.ru/vcda-initial-setup-api</guid><link>https://blog.ihumster.ru/vcda-initial-setup-api?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/vcda-initial-setup-api?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>VMware Cloud Director Availability Initial Setup API</title><pubDate>Mon, 23 Sep 2024 07:02:53 GMT</pubDate><description><![CDATA[Встала задача по автоматизации развертывания VCDA для инсталляций VCD где VCDA еще не развернут. Начав изучение вопроса и прочтя внимательно документацию по VCDA с удивлением понял что для развертывания самих аплайнсов есть способ по автоматизации процесса, т.к. аплайнсы поставляются в формате OVF с обычными, для аплайнсов VMware, возможностями деплоя их как с помощью UI, так и с помощью OVFTool ну и прочими средствами (например, community.vmware.vmware_deploy_ovf из коллекции ansible). Но для дальнейшей первоначальной настройки нет иного официального пути, кроме как пойти в UI и потыкать кнопки, заполняя поля в Initial Setup Wizard.]]></description><content:encoded><![CDATA[
  <p id="025Y">Встала задача по автоматизации развертывания VCDA для инсталляций VCD где VCDA еще не развернут. Начав изучение вопроса и прочтя внимательно документацию по VCDA с удивлением понял что для развертывания самих аплайнсов есть способ по автоматизации процесса, т.к. аплайнсы поставляются в формате OVF с обычными, для аплайнсов VMware, возможностями деплоя их как с помощью UI, так и с помощью OVFTool ну и прочими средствами (например, community.vmware.vmware_deploy_ovf из коллекции ansible). Но для дальнейшей первоначальной настройки нет иного официального пути, кроме как пойти в UI и потыкать кнопки, заполняя поля в Initial Setup Wizard.</p>
  <p id="KBLT">Вооружившись DevTools я за несколько итераций записал все необходимые API вызовы, которые происходят при выполнении этапов Initial Setup Wizard и после, проверив возможность их воспроизведения сторонними средствами (воспроизводил в Postman) решил что хочу записать их в виде документации OpenAPI 3.0 что бы любой мог импортировать ее в Postman и ознакомиться с деталями запросов.</p>
  <p id="lLZN">По этому спешу представить Вам первую версию <a href="https://github.com/ihumster/VCDAInitialSetupAPI" target="_blank">документации </a>к VCDA Initial Setup API. В репозиторий добавлена также готовая коллекция для Postman (на случай если вы не умеете генерировать коллекцию из OpenAPI спецификации).</p>
  <p id="fOv6">В коллекции все запросы пронумерованы в порядке выполнения их в Wizard. Отдельно не пронумерован один Get запрос - он используется для проверки доступности той или иной системы (VCD, vCenter server, ReplicatorAppliance, Tunnel Appliance) из VCDA Manager, а также возвращает SSL-сертификат используемый системой (сам сертефикат в PEM-encoded формате, его thumbprint - используются в остальных запросах).</p>
  <p id="qZTY">Конфигурация VCDA которую я изучал - это рекомендуемая в продуктивной среде конфигурация из отдельных &quot;ролевых&quot; аплайнсов - manager, replicator, tunnel. Для combined и vc_combined версии, последовательность и структура запросов скорее всего иная (в vc_combined например не используется VCD).</p>
  <p id="3gNR">Также, если вы используете несколько replicator аплайнсов то соответствующие запросы необходимо будет проделать для каждого аплайнса последовательно.</p>
  <p id="VRaF">Для работы с этим API вам необходимы минимум 3 задеплоенных удобным вам способом аплайнса.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.ihumster.ru/vcd-api-with-postman-2</guid><link>https://blog.ihumster.ru/vcd-api-with-postman-2?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster</link><comments>https://blog.ihumster.ru/vcd-api-with-postman-2?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=ihumster#comments</comments><dc:creator>ihumster</dc:creator><title>Комфортная работа в Postman с VMware Cloud Director (2)</title><pubDate>Mon, 23 Sep 2024 07:02:07 GMT</pubDate><category>Cloud Director</category><description><![CDATA[В процессе работы с OpenAPI разных версий VMware Cloud Director понял что меня раздражает постоянно следить за версией API, которая указывается в Header 'Accept'. Чтобы автоматизировать актуализацию версии API в моих запросах я модифицировал свой pre-request Script из предыдущей статьи.]]></description><content:encoded><![CDATA[
  <p id="ZQaK">В процессе работы с OpenAPI разных версий VMware Cloud Director понял что меня раздражает постоянно следить за версией API, которая указывается в Header &#x27;Accept&#x27;. Чтобы автоматизировать актуализацию версии API в моих запросах я модифицировал свой pre-request Script из предыдущей статьи.</p>
  <p id="qTQ5">Добавьте следующую строку между запросами версии и получением access_token, либо после них:</p>
  <pre id="CXUc">pm.request.headers.get(&#x27;Accept&#x27;).replace(&#x27;application\/json;version=(?&lt;version&gt;\d{2}\.\d{1})&#x27;,&#x27;application/json;version={{apiVersion}}&#x27;)</pre>

]]></content:encoded></item></channel></rss>