<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Александр Никитин</title><author><name>Александр Никитин</name></author><id>https://teletype.in/atom/ihumster</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/ihumster?offset=0"></link><link rel="alternate" type="text/html" href="https://blog.ihumster.ru/?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=ihumster"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/ihumster?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-10-21T22:46:09.059Z</updated><entry><id>ihumster:update-and-restart-20-plus-claude-code-sessions</id><link rel="alternate" type="text/html" href="https://blog.ihumster.ru/update-and-restart-20-plus-claude-code-sessions?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=ihumster"></link><title>Как обновлять Claude Code, не теряя 20+ живых сессий</title><published>2026-10-06T21:20:02.067Z</published><updated>2026-10-06T21:58:02.039Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img3.teletype.in/files/e5/cd/e5cd14e2-3f8b-4dc4-a08b-133f482e0395.png"></media:thumbnail><category term="llm" label="LLM"></category><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/1a/84/1a84d802-bee0-42f8-a513-3461d25bd879.png&quot;&gt;Для работы с агентом я использую прекрасный инструмент Orca, о котором узнал от Сережи Риса, читая его отличный блог.</summary><content type="html">
  &lt;figure id=&quot;AN0R&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/1a/84/1a84d802-bee0-42f8-a513-3461d25bd879.png&quot; width=&quot;1536&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;UJS2&quot;&gt;Для работы с агентом я использую прекрасный инструмент &lt;a href=&quot;https://onorca.dev/&quot; target=&quot;_blank&quot;&gt;Orca&lt;/a&gt;, о котором узнал от Сережи Риса, читая его &lt;a href=&quot;https://sereja.tech/blog/orca-menedzher-agentov/&quot; target=&quot;_blank&quot;&gt;отличный блог&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;paNY&quot;&gt;Claude Code обновляется часто, Homebrew обновляет бинарник на диске, но старые процессы продолжают жить на старой версии; закрывать 20+ сессий вручную - много лишних операций.&lt;/p&gt;
  &lt;h2 id=&quot;KcLW&quot;&gt;Автоматизация обновления бинарника&lt;/h2&gt;
  &lt;p id=&quot;ucmi&quot;&gt;Для того чтобы не обновлять бинарник руками Claude собрал мне такой шелл-скрипт:&lt;/p&gt;
  &lt;p id=&quot;51OL&quot;&gt;&lt;code&gt;&lt;a href=&quot;#Zcq1&quot;&gt;~/.local/bin/claude-code-upgrade&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
  &lt;p id=&quot;8O9Q&quot;&gt;В скрипте три решения, которые легко пропустить:&lt;/p&gt;
  &lt;ul id=&quot;JScv&quot;&gt;
    &lt;li id=&quot;fefw&quot;&gt;&lt;code&gt;launchd&lt;/code&gt; запускает задачи с минимальным &lt;code&gt;PATH&lt;/code&gt;. Без строки &lt;code&gt;export PATH=…&lt;/code&gt; brew не находит свои утилиты.&lt;/li&gt;
    &lt;li id=&quot;JXSS&quot;&gt;&lt;code&gt;brew upgrade&lt;/code&gt; обновляет метаданные не чаще одного раза за 24 часа. Ежедневная задача из-за этого может пропустить сборку. Поэтому скрипт сам вызывает &lt;code&gt;brew update&lt;/code&gt; и отключает автообновление метаданных.&lt;/li&gt;
    &lt;li id=&quot;rmwy&quot;&gt;При ошибке скрипт пишет в лог код возврата и весь вывод команды. При успехе пишет одну строку.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h3 id=&quot;SkG8&quot;&gt;LaunchAgent&lt;/h3&gt;
  &lt;p id=&quot;tM9d&quot;&gt;Для автозапуска скрипта использовал launchd:&lt;/p&gt;
  &lt;p id=&quot;UFai&quot;&gt;&lt;code&gt;&lt;a href=&quot;#gKAW&quot;&gt;~/Library/LaunchAgents/com.ihumster.claude-code-upgrade.plist&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;
  &lt;h3 id=&quot;4A68&quot;&gt;Установка и размещение&lt;/h3&gt;
  &lt;p id=&quot;ohu8&quot;&gt;Скрипт обновления кладем в &lt;code&gt;~/.local/bin/&lt;/code&gt;, LaunchAgent складываем в &lt;code&gt;~/Library/LaunchAgents/&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;o2ow&quot; data-lang=&quot;bash&quot;&gt;chmod +x ~/.local/bin/claude-code-upgrade
launchctl bootstrap &amp;quot;gui/$(id -u)&amp;quot; ~/Library/LaunchAgents/com.ihumster.claude-code-upgrade.plist
launchctl print &amp;quot;gui/$(id -u)/com.ihumster.claude-code-upgrade&amp;quot; | grep -E &amp;#x27;state|last exit&amp;#x27;&lt;/pre&gt;
  &lt;p id=&quot;4ZDa&quot;&gt;Если Mac спал в 08:00, &lt;code&gt;launchd&lt;/code&gt; запускает пропущенную задачу после пробуждения. &lt;code&gt;RunAtLoad&lt;/code&gt; добавляет запуск при входе в систему.&lt;/p&gt;
  &lt;p id=&quot;Cwx4&quot;&gt;Лог за два дня:&lt;/p&gt;
  &lt;pre id=&quot;Ac1z&quot;&gt;2026-10-05 13:39:33 OK upgraded 2.1.288 -&amp;gt; 2.1.289
2026-10-06 08:00:52 OK upgraded 2.1.289 -&amp;gt; 2.1.290
2026-10-06 16:17:03 OK upgraded 2.1.290 -&amp;gt; 2.1.291&lt;/pre&gt;
  &lt;h2 id=&quot;6AC3&quot;&gt;Часть 2. Проблема: бинарник новый, сессии старые&lt;/h2&gt;
  &lt;p id=&quot;83X2&quot;&gt;Brew меняет файл на диске. Запущенный процесс продолжает работать на том бинарнике, с которого стартовал. Каталог старой версии brew переименовывает в &lt;code&gt;&amp;lt;версия&amp;gt;.upgrading&lt;/code&gt;, и процесс держит его открытым.&lt;/p&gt;
  &lt;p id=&quot;0VuK&quot;&gt;Команда показывает, на какой версии работает каждая сессия:&lt;/p&gt;
  &lt;pre id=&quot;Qeqc&quot; data-lang=&quot;bash&quot;&gt;jq -r &amp;#x27;[.status, .version] | @tsv&amp;#x27; ~/.claude/sessions/*.json | sort | uniq -c&lt;/pre&gt;
  &lt;p id=&quot;73ZC&quot;&gt;Результат на 2026-10-05, установлена версия 2.1.289:&lt;/p&gt;
  &lt;pre id=&quot;kXqD&quot;&gt;   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&lt;/pre&gt;
  &lt;p id=&quot;D2Se&quot;&gt;Самые старые сессии работали с 2026-09-22. Закрыть вкладку и открыть новую нельзя, потому что в сессии лежит контекст задачи. Нет, Orca конечно позволяет открыть сессию из Agent Session History, для этого у нее есть отдельная вкладка на панели:&lt;/p&gt;
  &lt;figure id=&quot;8GcE&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f1/32/f132befa-a385-47bc-8518-0f6181ae7ed1.png&quot; width=&quot;314&quot; /&gt;
    &lt;figcaption&gt;Agent Session History&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;8Ck9&quot;&gt;И даже более того у Orca теперь есть поиск по всем сессиям проекта - очень удобно порой. История сессий решает задачу восстановления, но для регулярного обновления десятков открытых вкладок это слишком много ручных действий.&lt;/p&gt;
  &lt;h3 id=&quot;WcmI&quot;&gt;Почему не автоматический перезапуск в 08:00&lt;/h3&gt;
  &lt;p id=&quot;uQTy&quot;&gt;Первая идея была такой: после обновления скрипт сам перезапускает все простаивающие сессии. Я от неё отказался. Перезапуск убивает всё, что сессия держит в фоне: фоновые команды, субагентов, &lt;code&gt;Monitor&lt;/code&gt; и запланированные пробуждения &lt;code&gt;/loop&lt;/code&gt;. Статус &lt;code&gt;idle&lt;/code&gt; в реестре этого не показывает. Сессия в статусе &lt;code&gt;idle&lt;/code&gt; может ждать ответа на вопрос или окончания фоновой задачи.&lt;/p&gt;
  &lt;p id=&quot;BTeX&quot;&gt;Решение о перезапуске принимает человек, по одной вкладке. Значит, нужна кнопка.&lt;/p&gt;
  &lt;h2 id=&quot;Vnfo&quot;&gt;Часть 3. Что известно о сессии снаружи&lt;/h2&gt;
  &lt;p id=&quot;GH6x&quot;&gt;Для перезапуска нужны четыре вещи: идентификатор сессии, флаги запуска, вкладка Orca и способ ввести команду во вкладку. Все четыре можно получить без участия модели.&lt;/p&gt;
  &lt;ul id=&quot;2EcJ&quot;&gt;
    &lt;li id=&quot;Ntht&quot;&gt;&lt;strong&gt;Идентификатор сессии&lt;/strong&gt;, &lt;strong&gt;статус&lt;/strong&gt; &lt;strong&gt;и&lt;/strong&gt; &lt;strong&gt;версия&lt;/strong&gt;. Они лежат в файле &lt;code&gt;~/.claude/sessions/&amp;lt;pid&amp;gt;.json&lt;/code&gt;, в полях &lt;code&gt;sessionId&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; и &lt;code&gt;version&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;iG4b&quot;&gt;&lt;strong&gt;Флаги запуска&lt;/strong&gt;. Их показывает команда &lt;code&gt;ps -o args= -p &amp;lt;pid&amp;gt;&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;SzD6&quot;&gt;&lt;strong&gt;Вкладка Orca&lt;/strong&gt;. Её задают переменные &lt;code&gt;ORCA_PANE_KEY&lt;/code&gt; и &lt;code&gt;ORCA_TERMINAL_HANDLE&lt;/code&gt; в окружении процесса &lt;code&gt;claude&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;KLUC&quot;&gt;&lt;strong&gt;Ввод во вкладку.&lt;/strong&gt; Его делает команда &lt;code&gt;orca terminal send --terminal &amp;lt;handle&amp;gt; --text … --enter.&lt;/code&gt;&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;17zo&quot;&gt;Ещё один факт упрощает задачу. Родитель каждого процесса &lt;code&gt;claude&lt;/code&gt; во вкладке Orca это &lt;code&gt;zsh&lt;/code&gt;. После выхода из Claude Code вкладка остаётся открытой, в ней работает shell. В него можно ввести &lt;code&gt;claude --resume &amp;lt;sessionId&amp;gt;&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;4y7Y&quot;&gt;&lt;code&gt;ORCA_TERMINAL_HANDLE&lt;/code&gt; перестаёт работать после перезапуска Orca. &lt;code&gt;ORCA_PANE_KEY&lt;/code&gt; в формате &lt;code&gt;tabId:leafId&lt;/code&gt; остаётся верным, по нему скрипт ищет актуальный handle в &lt;code&gt;orca terminal list --json&lt;/code&gt;.&lt;/p&gt;
  &lt;h2 id=&quot;mhBp&quot;&gt;Часть 4. Quick Commands в Orca&lt;/h2&gt;
  &lt;p id=&quot;doe8&quot;&gt;Quick Command это сохранённая строка текста с меткой. Команды создают в &lt;strong&gt;Settings → Quick Commands&lt;/strong&gt; или кнопкой &lt;strong&gt;Add command&lt;/strong&gt; в панели вкладок. У команды есть область &lt;strong&gt;Global&lt;/strong&gt; или &lt;strong&gt;Project&lt;/strong&gt; и флаг «нажать Enter после».&lt;/p&gt;
  &lt;p id=&quot;VqOx&quot;&gt;Способ запуска меняет поведение:&lt;/p&gt;
  &lt;ul id=&quot;DIjz&quot;&gt;
    &lt;li id=&quot;BdiS&quot;&gt;Кнопка в панели вкладок открывает новую вкладку и выполняет команду в ней.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;figure id=&quot;EByD&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/2f/07/2f07b1e3-268c-4815-9d31-c9181c447a3b.png&quot; width=&quot;229&quot; /&gt;
    &lt;figcaption&gt;Неправильный способ запуска&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;ul id=&quot;7f3t&quot;&gt;
    &lt;li id=&quot;7SUX&quot;&gt;Пункт контекстного меню терминала вставляет текст в текущий терминал.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;figure id=&quot;eor4&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/7b/20/7b2080d2-8f46-44ac-819f-7709e3373d0b.png&quot; width=&quot;483&quot; /&gt;
    &lt;figcaption&gt;Правильный способ запуска&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;mhOj&quot;&gt;Для перезапуска подходит только второй способ. В новой вкладке нет агента, которого нужно перезапустить.&lt;/p&gt;
  &lt;p id=&quot;Ryo0&quot;&gt;У Quick Commands в версии Orca 1.4.220 три ограничения:&lt;/p&gt;
  &lt;ul id=&quot;fLVW&quot;&gt;
    &lt;li id=&quot;B4P2&quot;&gt;В тексте команды нет переменных, &lt;code&gt;sessionId&lt;/code&gt; подставить нельзя.&lt;/li&gt;
    &lt;li id=&quot;iE5S&quot;&gt;Одна команда это одна строка. Последовательность «выйти, подождать, возобновить» в неё не помещается.&lt;/li&gt;
    &lt;li id=&quot;vUYP&quot;&gt;Из CLI командами управлять нельзя. Группа &lt;code&gt;orca quick-command&lt;/code&gt; есть только в открытом PR &lt;a href=&quot;https://github.com/stablyai/orca/pull/20162&quot; target=&quot;_blank&quot;&gt;#20162&lt;/a&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;X4Qh&quot;&gt;Поэтому Quick Command содержит один вызов скрипта, а всю работу делает скрипт.&lt;/p&gt;
  &lt;h2 id=&quot;PjVR&quot;&gt;Часть 5. Скрипт claude-restart&lt;/h2&gt;
  &lt;h3 id=&quot;KVEs&quot;&gt;Схема&lt;/h3&gt;
  &lt;p id=&quot;2feT&quot;&gt;Текст Quick Command: &lt;code&gt;! claude-restart&lt;/code&gt;. Знак &lt;code&gt;!&lt;/code&gt; в Claude Code (да и многих других харнесах) включает режим shell-команды.&lt;/p&gt;
  &lt;ol id=&quot;gB1k&quot;&gt;
    &lt;li id=&quot;ly7n&quot;&gt;Claude Code запускает &lt;code&gt;claude-restart&lt;/code&gt; как дочерний процесс сессии.&lt;/li&gt;
    &lt;li id=&quot;yXCO&quot;&gt;Скрипт идёт вверх по дереву процессов и находит pid своего &lt;code&gt;claude&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;TEf4&quot;&gt;Скрипт читает &lt;code&gt;sessionId&lt;/code&gt; из реестра, флаги запуска из &lt;code&gt;ps&lt;/code&gt; и вкладку из окружения процесса.&lt;/li&gt;
    &lt;li id=&quot;6qIf&quot;&gt;Если у сессии есть фоновые shell-команды, скрипт отказывает с кодом 1.&lt;/li&gt;
    &lt;li id=&quot;rjtB&quot;&gt;Скрипт запускает помощника в новом сеансе процессов через &lt;code&gt;setsid&lt;/code&gt; и завершается.&lt;/li&gt;
    &lt;li id=&quot;5F1z&quot;&gt;Помощник ждёт, пока статус сессии перестанет быть &lt;code&gt;busy&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;YlIm&quot;&gt;Помощник посылает процессу &lt;code&gt;SIGTERM&lt;/code&gt; и ждёт его завершения до 30 секунд.&lt;/li&gt;
    &lt;li id=&quot;pVcb&quot;&gt;Помощник вводит во вкладку &lt;code&gt;claude --resume &amp;lt;sessionId&amp;gt;&lt;/code&gt; с исходными флагами.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;rH0B&quot;&gt;Помощник нужен потому, что скрипт работает внутри сессии и умирает вместе с ней. &lt;code&gt;setsid&lt;/code&gt; выводит помощника из группы процессов Claude Code.&lt;/p&gt;
  &lt;h3 id=&quot;d0v8&quot;&gt;Код&lt;/h3&gt;
  &lt;p id=&quot;G0no&quot;&gt;Файл &lt;code&gt;&lt;a href=&quot;#RPVL&quot;&gt;~/.local/bin/claude-restart&lt;/a&gt;&lt;/code&gt;. Зависимости: &lt;code&gt;orca&lt;/code&gt;, &lt;code&gt;jq&lt;/code&gt;, &lt;code&gt;perl&lt;/code&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;0CTM&quot;&gt;Установка&lt;/h3&gt;
  &lt;ol id=&quot;2h2B&quot;&gt;
    &lt;li id=&quot;a35h&quot;&gt;Сохраните скрипт в &lt;code&gt;~/.local/bin/claude-restart&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;Ty3A&quot;&gt;Выполните &lt;code&gt;chmod +x ~/.local/bin/claude-restart&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;jAmh&quot;&gt;Откройте в Orca &lt;strong&gt;Settings → Quick Commands&lt;/strong&gt;.&lt;/li&gt;
    &lt;li id=&quot;XQ9o&quot;&gt;Создайте команду с меткой &lt;code&gt;Restart Claude&lt;/code&gt;, областью &lt;strong&gt;Global&lt;/strong&gt; и текстом &lt;code&gt;! claude-restart&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;tT0u&quot;&gt;Включите флаг отправки Enter.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;figure id=&quot;83bl&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/8b/d9/8bd916a6-cbd0-445b-96b7-3c9851efafa4.png&quot; width=&quot;898&quot; /&gt;
    &lt;figcaption&gt;Настройки Quick Command&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;OIVU&quot;&gt;Сводка пробного запуска:&lt;/h3&gt;
  &lt;pre id=&quot;tqPy&quot;&gt;pid:        90335 (busy)
version:    2.1.289 -&amp;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&lt;/pre&gt;
  &lt;p id=&quot;lGmX&quot;&gt;Лог боевого запуска в &lt;code&gt;~/Library/Logs/claude-restart.log&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;WOyy&quot;&gt;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&lt;/pre&gt;
  &lt;h2 id=&quot;QvLl&quot;&gt;Часть 6. Три неочевидные проблемы&lt;/h2&gt;
  &lt;h3 id=&quot;QdRU&quot;&gt;Ход модели после &lt;code&gt;!&lt;/code&gt;&lt;/h3&gt;
  &lt;p id=&quot;E3Uw&quot;&gt;Вывод shell-команды попадает в контекст, и модель начинает ход: пересказывает сводку. В первой версии помощник ждал 2 секунды и посылал команду выхода. При длинном ходе команда пришла бы посреди ответа.&lt;/p&gt;
  &lt;p id=&quot;BH1j&quot;&gt;Теперь помощник читает поле &lt;code&gt;status&lt;/code&gt; в реестре и ждёт, пока оно перестанет быть &lt;code&gt;busy&lt;/code&gt;. Предел ожидания 120 секунд. После предела помощник ничего не посылает и пишет &lt;code&gt;FAIL&lt;/code&gt; в лог.&lt;/p&gt;
  &lt;h3 id=&quot;lvRQ&quot;&gt;Меню подсказок перехватывает &lt;code&gt;/exit&lt;/code&gt;&lt;/h3&gt;
  &lt;p id=&quot;kl49&quot;&gt;Первая версия вводила в TUI текст &lt;code&gt;/exit&lt;/code&gt; и Enter. Claude Code на символ &lt;code&gt;/&lt;/code&gt; открывает меню команд и скиллов. Указатель мыши над меню меняет выбранный пункт, и Enter запускает другую команду. Так одна рабочая сессия получила чужую команду вместо выхода. Скрипт подождал 30 секунд и не стал вводить команду возобновления:&lt;/p&gt;
  &lt;pre id=&quot;eqE9&quot;&gt;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&lt;/pre&gt;
  &lt;p id=&quot;BpfG&quot;&gt;Теперь скрипт ничего не вводит в TUI. Он посылает процессу &lt;code&gt;SIGTERM&lt;/code&gt;. Claude Code по этому сигналу завершается штатно и возвращает терминал в обычный режим.&lt;/p&gt;
  &lt;p id=&quot;6rVv&quot;&gt;Проверка на 30 секунд в этой истории сработала как задумано. Без неё команда &lt;code&gt;claude --resume …&lt;/code&gt; попала бы в живую сессию как запрос к модели.&lt;/p&gt;
  &lt;h3 id=&quot;GNH1&quot;&gt;Вкладка без &lt;code&gt;tabId&lt;/code&gt;&lt;/h3&gt;
  &lt;p id=&quot;lJr1&quot;&gt;Из 22 сессий две не нашлись по &lt;code&gt;ORCA_PANE_KEY&lt;/code&gt;. Их вкладки не были смонтированы в окне. Для таких вкладок &lt;code&gt;orca terminal list&lt;/code&gt; возвращает &lt;code&gt;tabId&lt;/code&gt; и &lt;code&gt;leafId&lt;/code&gt; в виде &lt;code&gt;pty:&amp;lt;worktree&amp;gt;@@&amp;lt;id&amp;gt;&lt;/code&gt;. Скрипт теперь ищет вкладку по ключу панели и по handle из окружения. После правки нашлись все 22.&lt;/p&gt;
  &lt;h2 id=&quot;QKhd&quot;&gt;Ограничения&lt;/h2&gt;
  &lt;ul id=&quot;2BAT&quot;&gt;
    &lt;li id=&quot;9ytc&quot;&gt;Не полагайтесь на предохранитель для субагентов, &lt;code&gt;Monitor&lt;/code&gt; и &lt;code&gt;/loop&lt;/code&gt;. Он видит только фоновые shell-команды. Перед нажатием проверьте их сами.&lt;/li&gt;
    &lt;li id=&quot;VKlD&quot;&gt;Скрипт берёт флаги запуска из &lt;code&gt;ps&lt;/code&gt;. Аргумент с пробелом внутри теряет кавычки.&lt;/li&gt;
    &lt;li id=&quot;mjmr&quot;&gt;Команда &lt;code&gt;! claude-restart&lt;/code&gt; стоит один ход модели. В проверке это 6 секунд.&lt;/li&gt;
    &lt;li id=&quot;exTd&quot;&gt;Команду возобновления скрипт вводит в &lt;code&gt;zsh&lt;/code&gt; как текст. Плагин автодополнения с выбором мышью может вмешаться так же, как меню Claude Code.&lt;/li&gt;
    &lt;li id=&quot;3Zsy&quot;&gt;Реестр &lt;code&gt;~/.claude/sessions&lt;/code&gt; и переменные &lt;code&gt;ORCA_*&lt;/code&gt; это внутреннее устройство двух программ. Версии указаны в начале статьи.&lt;/li&gt;
    &lt;li id=&quot;z0r9&quot;&gt;Запускайте Quick Command только из контекстного меню терминала. Кнопка в панели вкладок открывает новую вкладку, и скрипт завершается с ошибкой &lt;code&gt;not running inside a claude session&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 id=&quot;DVzK&quot;&gt;Итог&lt;/h2&gt;
  &lt;p id=&quot;c72F&quot;&gt;Вечером 2026-10-05 сессии работали на пяти версиях, от 2.1.276 до 2.1.289. На следующий день после двух обновлений осталось две версии: 12 сессий на 2.1.290 и 10 на 2.1.291. В логе &lt;code&gt;claude-restart&lt;/code&gt; за это время 8 успешных перезапусков и 2 отказа. Один отказ это ошибка с меню подсказок. Второй это намеренный тест предела ожидания.&lt;/p&gt;
  &lt;h2 id=&quot;paD6&quot;&gt;Скрипты&lt;/h2&gt;
  &lt;p id=&quot;Zcq1&quot;&gt;&lt;code&gt;claude-code-upgrade&lt;/code&gt;&lt;/p&gt;
  &lt;pre id=&quot;ggM3&quot; data-lang=&quot;bash&quot;&gt;#!/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 &amp;#x60;brew update&amp;#x60; below.
export HOMEBREW_NO_AUTO_UPDATE=1

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

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

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

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

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

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

main &amp;quot;$@&amp;quot;&lt;/pre&gt;
  &lt;p id=&quot;gKAW&quot;&gt;&lt;code&gt;com.ihumster.claude-code-upgrade.plist&lt;/code&gt;&lt;/p&gt;
  &lt;pre id=&quot;daBg&quot; data-lang=&quot;xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &amp;quot;-//Apple//DTD PLIST 1.0//EN&amp;quot; &amp;quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&amp;quot;&amp;gt;
&amp;lt;plist version=&amp;quot;1.0&amp;quot;&amp;gt;
&amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;com.ihumster.claude-code-upgrade&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;
    &amp;lt;array&amp;gt;
        &amp;lt;string&amp;gt;/Users/ihumster/.local/bin/claude-code-upgrade&amp;lt;/string&amp;gt;
    &amp;lt;/array&amp;gt;
    &amp;lt;key&amp;gt;StartCalendarInterval&amp;lt;/key&amp;gt;
    &amp;lt;dict&amp;gt;
        &amp;lt;key&amp;gt;Hour&amp;lt;/key&amp;gt;
        &amp;lt;integer&amp;gt;8&amp;lt;/integer&amp;gt;
        &amp;lt;key&amp;gt;Minute&amp;lt;/key&amp;gt;
        &amp;lt;integer&amp;gt;0&amp;lt;/integer&amp;gt;
    &amp;lt;/dict&amp;gt;
    &amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;StandardOutPath&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/Users/ihumster/Library/Logs/claude-code-upgrade.stderr.log&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/Users/ihumster/Library/Logs/claude-code-upgrade.stderr.log&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;ProcessType&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;Background&amp;lt;/string&amp;gt;
&amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;RPVL&quot;&gt;&lt;code&gt;claude-restart&lt;/code&gt;&lt;/p&gt;
  &lt;pre id=&quot;yNGd&quot; data-lang=&quot;bash&quot;&gt;#!/bin/bash
# claude-restart — restarts the Claude Code session of one Orca tab on the
# currently installed binary: SIGTERM, then &amp;#x60;claude --resume &amp;lt;id&amp;gt;&amp;#x60;.
#
# Meant to be run from inside the session it restarts, as &amp;#x60;! claude-restart&amp;#x60;
# (Orca Quick Command, inserted into the agent tab). A detached helper does
# the typing through &amp;#x60;orca terminal send&amp;#x60;, because the script itself dies
# together with the session.
#
#   claude-restart [--dry-run] [--force] [--pid &amp;lt;claude-pid&amp;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=&amp;quot;${CCR_ORCA:-orca}&amp;quot;
SESSIONS_DIR=&amp;quot;${CCR_SESSIONS_DIR:-$HOME/.claude/sessions}&amp;quot;
LOG_FILE=&amp;quot;${CCR_LOG_FILE:-$HOME/Library/Logs/claude-restart.log}&amp;quot;
EXIT_TIMEOUT=&amp;quot;${CCR_EXIT_TIMEOUT:-30}&amp;quot;
IDLE_TIMEOUT=&amp;quot;${CCR_IDLE_TIMEOUT:-120}&amp;quot;

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

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

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

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

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

# Reads one variable from the environment of another process.
env_of() {
    ps eww -o command= -p &amp;quot;$1&amp;quot; | tr &amp;#x27; &amp;#x27; &amp;#x27;\n&amp;#x27; | sed -n &amp;quot;s/^$2=//p&amp;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=&amp;quot;$1&amp;quot; launch_handle=&amp;quot;$2&amp;quot;
    &amp;quot;$ORCA&amp;quot; terminal list --json --limit 500 2&amp;gt;/dev/null |
        jq -r --arg pane &amp;quot;$pane&amp;quot; --arg handle &amp;quot;$launch_handle&amp;quot; \
            &amp;#x27;[.result.terminals[] | select(.tabId + &amp;quot;:&amp;quot; + .leafId == $pane or .handle == $handle)]
             | sort_by(.handle == $handle) | .[0].handle // empty&amp;#x27;
}

# Prints the launch flags of a claude process without the session selectors.
launch_flags() {
    ps -o args= -p &amp;quot;$1&amp;quot; | awk &amp;#x27;{
        out = &amp;quot;&amp;quot;
        for (i = 2; i &amp;lt;= NF; i++) {
            if ($i == &amp;quot;--resume&amp;quot; || $i == &amp;quot;-r&amp;quot;) { i++; continue }
            if ($i == &amp;quot;--continue&amp;quot; || $i == &amp;quot;-c&amp;quot;) continue
            out = out &amp;quot; &amp;quot; $i
        }
        print substr(out, 2)
    }&amp;#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=&amp;quot;$1&amp;quot; child mine
    mine=&amp;quot; $(ancestors | tr &amp;#x27;\n&amp;#x27; &amp;#x27; &amp;#x27;)&amp;quot;
    for child in $(pgrep -P &amp;quot;$claude_pid&amp;quot;); do
        [[ &amp;quot;$mine&amp;quot; == *&amp;quot; $child &amp;quot;* ]] &amp;amp;&amp;amp; continue
        case &amp;quot;$(comm_of &amp;quot;$child&amp;quot;)&amp;quot; in
            zsh | bash | sh | -zsh | -bash) echo &amp;quot;$child $(ps -o args= -p &amp;quot;$child&amp;quot; | cut -c1-100)&amp;quot; ;;
        esac
    done
}

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

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

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

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

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

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

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

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

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

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

main &amp;quot;$@&amp;quot;&lt;/pre&gt;

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

</content></entry><entry><id>ihumster:nK_4wEHidW8</id><link rel="alternate" type="text/html" href="https://blog.ihumster.ru/nK_4wEHidW8?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=ihumster"></link><title>Не доверяй ИИ</title><published>2025-02-25T18:50:00.354Z</published><updated>2025-02-25T18:50:00.354Z</updated><category term="mysli-vsluh" label="Мысли вслух"></category><summary type="html">Блин, товарищи коллеги. Я с чувством невероятного удовлетворения в очередной раз убеждаюсь что никакой искусственный интеллект нас не заменит. И он херовый помощник тому кто не понимает что он хочет получить в результате и как этот результат должен воспроизводиться.</summary><content type="html">
  &lt;p id=&quot;S12z&quot;&gt;Блин, товарищи коллеги. Я с чувством невероятного удовлетворения в очередной раз убеждаюсь что никакой искусственный интеллект нас не заменит. И он херовый помощник тому кто не понимает что он хочет получить в результате и как этот результат должен воспроизводиться.&lt;/p&gt;
  &lt;p id=&quot;LksA&quot;&gt;Це была фабула. А дальше мой короткий рассказ. 😅&lt;/p&gt;
  &lt;p id=&quot;qkdT&quot;&gt;Понадобилось мне стало быть в логи nginx пихнуть кусок (именно кусок, а не всю строку ибо там чувствительные данные) строки из заголовка http запроса. Пошёл читать документацию. Понял что необходимое мне можно извлечь из одной переменной в другую через директиву map. И вот я два часа &amp;quot;беседовал&amp;quot; с perplexity/openai-o3 которые отчаянно меня убеждали что результат регулярки (match) или именованную группу нельзя использовать как результат map. И действительно приведенный пример конфига nginx не принимал за валидный конфиг. После чтения документации и пары статей на хабре, в комментариях я таки нашел пример, который натолкнул меня на решение моей задачи, которая в итоге решилась и оказалась простой и элегантной (ввиду простенькой регулярочки которую я уже накидал сам).&lt;/p&gt;
  &lt;p id=&quot;FuHX&quot;&gt;В качестве эпилога хочу вам пожелать не верить всяким ИИ, даже если они утверждают что что-то не будет работать. 😁 доверяйте своему инженерному чутью!&lt;/p&gt;
  &lt;p id=&quot;1mDv&quot;&gt;У меня всё.&lt;/p&gt;

</content></entry><entry><id>ihumster:netbox-change-current-config-nbshell</id><link rel="alternate" type="text/html" href="https://blog.ihumster.ru/netbox-change-current-config-nbshell?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=ihumster"></link><title>Управление CurrentConfig через nbshell</title><published>2025-01-13T16:37:14.955Z</published><updated>2025-01-16T14:35:56.904Z</updated><category term="netbox" label="netbox"></category><summary type="html">Опытные пользователи Netbox наверняка сталкивались с таким мощным инструментом как Netbox Shell (построенный на базе Django Shell и представляющий собой интерпретатор iPython с расширенной функциональностью).</summary><content type="html">
  &lt;p id=&quot;LvKh&quot;&gt;Опытные пользователи Netbox наверняка сталкивались с таким мощным инструментом как Netbox Shell (построенный на базе Django Shell и представляющий собой интерпретатор iPython с расширенной функциональностью).&lt;/p&gt;
  &lt;h2 id=&quot;0v70&quot;&gt;Управление в интерактивном режиме&lt;/h2&gt;
  &lt;p id=&quot;xLrY&quot;&gt;Чтобы запустить nbshell необходимо открыть в командной строке папку с дистрибутивом приложения и, активировав virtual environment, выполнить команду &lt;em&gt;python netbox/manage.py nbshell&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;NZaw&quot;&gt;Что бы получить текущую ревизию конфигурации приложения (которая и содержит динамически конфигурируемые параметры) нам понадобится ID текущей (активной) конфигурации, ее можно получить из кэша приложения:&lt;/p&gt;
  &lt;pre id=&quot;EDN4&quot; data-lang=&quot;python&quot;&gt;from django.core.cache import cache

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

</content></entry><entry><id>ihumster:netbox-keycloak-custom-pipeline</id><link rel="alternate" type="text/html" href="https://blog.ihumster.ru/netbox-keycloak-custom-pipeline?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=ihumster"></link><title>Авторизация в Netbox через Keycloak (кастомный pipeline)</title><published>2025-01-04T20:53:43.008Z</published><updated>2026-04-08T13:27:15.924Z</updated><category term="netbox" label="netbox"></category><summary type="html">В сети есть хороший мануал по настройке авторизации в Netbox через Keycloak, однако он хорошо подходит для новых инсталляций, когда у вас чистая БД, ну или размер таблицы с пользователями и группами не очень большой и вам не составит труда все переделать.</summary><content type="html">
  &lt;p id=&quot;tlDW&quot;&gt;В сети есть хороший мануал по настройке &lt;a href=&quot;https://itdraft.ru/2023/05/17/sso-avtorizacija-v-netbox-cherez-keycloak/&quot; target=&quot;_blank&quot;&gt;авторизации в Netbox через Keycloak&lt;/a&gt;, однако он хорошо подходит для новых инсталляций, когда у вас чистая БД, ну или размер таблицы с пользователями и группами не очень большой и вам не составит труда все переделать.&lt;/p&gt;
  &lt;p id=&quot;1JPj&quot;&gt;Сложность, с которой я столкнулся, заключалась в том, что используемая нами инсталляция Netbox имеет множество активных пользователей импортированных из Active Directory, активную ролевую модель основанную на группах из Active Directory и потерять все эти ассоциации очень не хотелось бы.&lt;/p&gt;
  &lt;p id=&quot;s8FO&quot;&gt;Изучая документацию на модуль &lt;a href=&quot;https://python-social-auth.readthedocs.io/en/latest/&quot; target=&quot;_blank&quot;&gt;python-social-auth&lt;/a&gt; (а также код самого модуля и его бэкэнд к Keycloak) я наткнулся на раздел &lt;a href=&quot;https://python-social-auth.readthedocs.io/en/latest/pipeline.html#extending-the-pipeline&quot; target=&quot;_blank&quot;&gt;Extending the Piplene&lt;/a&gt; и понял что весь конвеер авторизации легко настраивается и изменяется, однако придётся немного по-питонить.&lt;/p&gt;
  &lt;h2 id=&quot;1xG1&quot;&gt;Проблемы с которыми я столкнулся&lt;/h2&gt;
  &lt;p id=&quot;1qCy&quot;&gt;Собственно их было две:&lt;/p&gt;
  &lt;ol id=&quot;pGKM&quot;&gt;
    &lt;li id=&quot;0xr1&quot;&gt;Дефолтный Pipeline падает при наличии в Netbox пользователей с одинаковыми email (это специфический случай, который характерен для нашей организации и с которым вы можете никогда не столкнуться. Для снятия лишних вопросов скажу что, у нас есть правило записывать в поле email соответствующий email владельца технической учётной записи)&lt;/li&gt;
    &lt;li id=&quot;ybtp&quot;&gt;Дефолтный Pipeline не обновляет членство в группах (а вот это уже критично для ролевой модели доступа основанной на группах).&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2 id=&quot;Jxpc&quot;&gt;Дефолтный Pipeline&lt;/h2&gt;
  &lt;p id=&quot;1T0x&quot;&gt;Дефолтный pipeline для Netbox определяется в модуле &amp;lt;your-netbox-folder&amp;gt;/netbox/netbox/settings.py и выглядит так:&lt;/p&gt;
  &lt;pre id=&quot;brZJ&quot; data-lang=&quot;python&quot;&gt;SOCIAL_AUTH_PIPELINE = (
    &amp;#x27;social_core.pipeline.social_auth.social_details&amp;#x27;,
    &amp;#x27;social_core.pipeline.social_auth.social_uid&amp;#x27;,
    &amp;#x27;social_core.pipeline.social_auth.social_user&amp;#x27;,
    &amp;#x27;social_core.pipeline.user.get_username&amp;#x27;,
    &amp;#x27;social_core.pipeline.social_auth.associate_by_email&amp;#x27;,
    &amp;#x27;social_core.pipeline.user.create_user&amp;#x27;,
    &amp;#x27;social_core.pipeline.social_auth.associate_user&amp;#x27;,
    &amp;#x27;netbox.authentication.user_default_groups_handler&amp;#x27;,
    &amp;#x27;social_core.pipeline.social_auth.load_extra_data&amp;#x27;,
    &amp;#x27;social_core.pipeline.user.user_details&amp;#x27;,
)&lt;/pre&gt;
  &lt;p id=&quot;02l5&quot;&gt;Он содержит функции из дефолтного, для модуля python-social-auth, конвеера, за исключением функции &lt;strong&gt;&lt;em&gt;user_default_groups_handler&lt;/em&gt;&lt;/strong&gt; из модуля &amp;lt;your-netbox-folder&amp;gt;/netbox/netbox/authentication.py (строка &amp;#x27;netbox.authentication.user_default_groups_handler&amp;#x27;).&lt;/p&gt;
  &lt;h2 id=&quot;pVXd&quot;&gt;Решение проблемы №1&lt;/h2&gt;
  &lt;p id=&quot;q9Vv&quot;&gt;В первых трех функциях Pipeline происходит получение и декодирование JWT access_token из которого извлекаются различные поля, которые передаются клиенту (в нашем случае Netbox) от Keycloak, и помещаются в переменную details (dict содержащий данные о пользователе, на базе которых пайплайн должен будет создать или обновить данные пользователя Netbox).&lt;/p&gt;
  &lt;p id=&quot;sRsK&quot;&gt;Функция get_username создает на базе совокупности различных настроек приложения и модуля python-social-auth имя пользователя, а функция associate_by_email пытается по email найти в БД Netbox соответствующего пользователя.&lt;/p&gt;
  &lt;p id=&quot;VdsC&quot;&gt;Функция associate_by_email выглядит так:&lt;/p&gt;
  &lt;pre id=&quot;jz60&quot; data-lang=&quot;python&quot;&gt;def associate_by_email(backend, details, user=None, *args, **kwargs):
    &amp;quot;&amp;quot;&amp;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.
    &amp;quot;&amp;quot;&amp;quot;
    if user:
        return None

    email = details.get(&amp;quot;email&amp;quot;)
    if email:
        # Try to associate accounts registered with the same email address,
        # only if it&amp;#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) &amp;gt; 1:
            raise AuthException(
                backend, &amp;quot;The given email address is associated with another account&amp;quot;
            )
        else:
            return {&amp;quot;user&amp;quot;: users[0], &amp;quot;is_new&amp;quot;: False}&lt;/pre&gt;
  &lt;p id=&quot;1GMd&quot;&gt;Очевидно, что при наличии в в БД нескольких пользователей в одинаковым email эта функция упадёт.&lt;/p&gt;
  &lt;p id=&quot;pvzo&quot;&gt;Чтобы решить эту проблему я создал новый модуль и переписал эту функцию так, чтобы использовать для ассоциации не только поле email но username.&lt;/p&gt;
  &lt;pre id=&quot;Cx86&quot; data-lang=&quot;python&quot;&gt;import re
from social_core.exceptions import AuthException


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

def update_user_groups(backend, response, user=None, *args, **kwargs):
    &amp;quot;&amp;quot;&amp;quot;
    Custom pipline handler with adds remote auth users to extist groups or
    create non-exist groups
    &amp;quot;&amp;quot;&amp;quot;
    logger = logging.getLogger(
        &amp;quot;netbox-social-auth-custom.auth_username.update_user_groups_handler&amp;quot;
    )
    if not user:
        return None
    else:
        user_groups = []
        auth_groups = response.get(&amp;quot;groups&amp;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&amp;quot;No any groups assignments for {user}. May be REMOTE_AUTH_DEFAULT_GROUPS incorrectly set\
                  or not returned from OAuth provider&amp;quot;
            )&lt;/pre&gt;
  &lt;h2 id=&quot;gy80&quot;&gt;Переопределение Pipeline&lt;/h2&gt;
  &lt;p id=&quot;V8qc&quot;&gt;Итак, обе функции помещены в файл, который я назвал auth_username.py, сложены в папки netbox-social-auth-custom/pipeline (вторая папка больше для красоты в названии - обозначения что это часть pipeline по аналогии с модулем social_core) и из этого собран python package (как собирать python-пакеты я описывать не буду, т.к. для этого есть куча разного инструментария и соответствующих гайдов - &lt;a href=&quot;https://packaging.python.org/en/latest/tutorials/packaging-projects/&quot; target=&quot;_blank&quot;&gt;например такой&lt;/a&gt;, разве что подскажу, что не плохо бы при сборке указать зависимость от social-auth-core&amp;gt;=4.5).&lt;/p&gt;
  &lt;p id=&quot;9IYN&quot;&gt;Пакет установлен в окружении venv нашего Netbox и остается только переопределить собственно сам Pipline. Для этого в файле configuration.py переопределяется переменная SOCIAL_AUTH_PIPELINE в виде вот такого массива:&lt;/p&gt;
  &lt;pre id=&quot;yOXy&quot; data-lang=&quot;python&quot;&gt;SOCIAL_AUTH_PIPELINE = (
    &amp;#x27;social_core.pipeline.social_auth.social_details&amp;#x27;,
    &amp;#x27;social_core.pipeline.social_auth.social_uid&amp;#x27;,
    &amp;#x27;social_core.pipeline.social_auth.social_user&amp;#x27;,
    &amp;#x27;social_core.pipeline.user.get_username&amp;#x27;,
    &amp;#x27;netbox_social_auth_custom.pipeline.auth_username.associate_by_email_and_username&amp;#x27;,
    &amp;#x27;social_core.pipeline.user.create_user&amp;#x27;,
    &amp;#x27;social_core.pipeline.social_auth.associate_user&amp;#x27;,
    &amp;#x27;social_core.pipeline.social_auth.load_extra_data&amp;#x27;,
    &amp;#x27;social_core.pipeline.user.user_details&amp;#x27;,
    &amp;#x27;netbox_social_auth_custom.pipeline.auth_username.update_user_groups&amp;#x27;,
)&lt;/pre&gt;
  &lt;p id=&quot;670e&quot;&gt;Собственно на этом всё. Спасибо что дочитали, надеюсь это кому-то пригодится.&lt;/p&gt;
  &lt;p id=&quot;HjuR&quot;&gt;UPD: Отладка Pipline&lt;/p&gt;
  &lt;p id=&quot;bBA8&quot;&gt;Забыл написать про отладку вашего (или вообще) конвеера авторизации. Чтобы в увидеть в логах или dev-консоли сервера Netbox &lt;/p&gt;
  &lt;h2 id=&quot;HjuR&quot;&gt;UPD: Отладка Pipline&lt;/h2&gt;
  &lt;p id=&quot;bBA8&quot;&gt;Забыл написать про отладку вашего (или вообще) конвеера авторизации. Чтобы в увидеть в логах или dev-консоли сервера Netbox отладочную информацию добавьте в конец (или перед падающей функцией конвеера) строку&lt;/p&gt;
  &lt;pre data-lang=&quot;python&quot; id=&quot;KfhA&quot;&gt;&amp;#x27;social_core.pipeline.debug.debug&amp;#x27;,&lt;/pre&gt;

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

</content></entry><entry><id>ihumster:interesting-cmdlet-from-the-microsoft-powershell</id><link rel="alternate" type="text/html" href="https://blog.ihumster.ru/interesting-cmdlet-from-the-microsoft-powershell?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=ihumster"></link><title>Интересный cmdlet из модуля Microsoft.PowerShell.TextUtility</title><published>2024-09-23T07:04:41.097Z</published><updated>2024-09-23T07:06:43.117Z</updated><summary type="html">Модуль Microsoft.PowerShell.TextUtility получил новый командлет ConvertFrom-TextTable. Он умеет превращать текстовую таблицу (типичный вывод большинства shell-команд Linux) в массив объектов. И даже больше - он может распарсить значения в столбцах таблицы и сконвертировать их в соответсвующие типы (например Int или Float, Boolean). Ну и наконец вместо объекта массива, командлет может вернуть массив строк в формате JSON.</summary><content type="html">
  &lt;p id=&quot;RuMN&quot;&gt;Модуль Microsoft.PowerShell.TextUtility получил новый командлет ConvertFrom-TextTable. Он умеет превращать текстовую таблицу (типичный вывод большинства shell-команд Linux) в массив объектов. И даже больше - он может распарсить значения в столбцах таблицы и сконвертировать их в соответсвующие типы (например Int или Float, Boolean). Ну и наконец вместо объекта массива, командлет может вернуть массив строк в формате JSON.&lt;/p&gt;
  &lt;p id=&quot;rEHO&quot;&gt;Установить модуль можно из PowershellGallery:&lt;/p&gt;
  &lt;pre id=&quot;ll2v&quot;&gt;Install-Module -Name Microsoft.PowerShell.TextUtility -AllowPrerelease&lt;/pre&gt;
  &lt;p id=&quot;lwli&quot;&gt;Пример использования:&lt;/p&gt;
  &lt;pre id=&quot;jz9M&quot; data-lang=&quot;powershell&quot;&gt;PS&amp;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&lt;/pre&gt;
  &lt;p id=&quot;JwjP&quot;&gt;Командлет позовляет указать где определяется строка заголовков или если ее нет просто брать табличные данные и превращать их в массив объектов.&lt;/p&gt;
  &lt;pre id=&quot;PgiV&quot; data-lang=&quot;powershell&quot;&gt;PS&amp;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&lt;/pre&gt;
  &lt;p id=&quot;lrr2&quot;&gt;Модуль еще находится в процессе разработки и если он вам понравился и пригодился, но вы нашли какую-либо ошибку, то оставить баг-репорт можно прямо в разделе &lt;a href=&quot;https://github.com/PowerShell/TextUtility/issues&quot; target=&quot;_blank&quot;&gt;Issues &lt;/a&gt;репозитория.&lt;/p&gt;

</content></entry><entry><id>ihumster:vcd-upgrade-10-4-1-nuanses</id><link rel="alternate" type="text/html" href="https://blog.ihumster.ru/vcd-upgrade-10-4-1-nuanses?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=ihumster"></link><title>Нюансы обновления до VCD 10.4.1</title><published>2024-09-23T07:03:35.187Z</published><updated>2024-09-23T07:03:35.187Z</updated><category term="cloud-director" label="Cloud Director"></category><summary type="html">После обновления до VCD 10.4.1 одна из Advisores отсылает нас к KB78885 (но да кто их читает, да?), которая опубликована в далеком уже 2021 году и которая напоминает нам о том, что после обновления VCD у нас может пропасть доверненное подключение к различным элементам нашей инфраструктуры. Ужесточение требований к сертификатам в инфраструктуре началось в VCD еще с версии 10.1 (о чём так же отдельно пишется во врезке, на странице документации посвященной обновлению VCD).</summary><content type="html">
  &lt;p id=&quot;fvYh&quot;&gt;После обновления до VCD 10.4.1 одна из Advisores отсылает нас к &lt;a href=&quot;https://kb.vmware.com/s/article/78885&quot; target=&quot;_blank&quot;&gt;KB78885&lt;/a&gt; (но да кто их читает, да?), которая опубликована в далеком уже 2021 году и которая напоминает нам о том, что после обновления VCD у нас может пропасть доверненное подключение к различным элементам нашей инфраструктуры. Ужесточение требований к сертификатам в инфраструктуре началось в VCD еще с версии 10.1 (о чём так же отдельно пишется во врезке, на странице документации &lt;a href=&quot;https://docs.vmware.com/en/VMware-Cloud-Director/10.4/VMware-Cloud-Director-Install-Configure-Upgrade-Guide/GUID-6EC093C1-4CE7-4435-9FC5-F801EA618D91.html&quot; target=&quot;_blank&quot;&gt;посвященной обновлению&lt;/a&gt; VCD).&lt;/p&gt;
  &lt;p id=&quot;hDWo&quot;&gt;В 10.4.1 ужесточили еще один аспект инфраструктуры, а именно подключение к ESXi хостам. В результате, после обновления до 10.4.1 (и автоматическом переходе на новую консоль, старую выпилили таки, и слава богу), в VCD перестанут работать операции требующие прямого доступа к хосту, а именно &lt;strong&gt;доступ к консоли ВМ, ипорту OVF/ISO, Guest Customization&lt;/strong&gt;.&lt;/p&gt;
  &lt;p id=&quot;B8Zk&quot;&gt;В логах это может отобразиться в виде следующих сообщений:&lt;/p&gt;
  &lt;pre id=&quot;yf6H&quot;&gt;[ 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&lt;/pre&gt;
  &lt;pre id=&quot;O4lr&quot;&gt; 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
&lt;/pre&gt;
  &lt;h2 id=&quot;UaNd&quot;&gt;Решение&lt;/h2&gt;
  &lt;p id=&quot;HDwa&quot;&gt;Самый простой вариант - это запустить повторно &lt;strong&gt;CMT trust-infra-certs&lt;/strong&gt;:&lt;/p&gt;
  &lt;pre id=&quot;ydsi&quot;&gt;./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.&lt;/pre&gt;
  &lt;p id=&quot;BxPL&quot;&gt;Или самостоятельно скачать с vCenters их сертификаты VMCA (&amp;lt;vcenter&amp;gt;/certs/download.zip) и вручную добавить VMCA сертификаты в Trusted Certificates вашего VCD.&lt;/p&gt;
  &lt;p id=&quot;TYoj&quot;&gt;Для VCD с большим количеством vCenters (или организаций с высокими требованиями к безопасности инфраструктурных компонентов) самым правильным путём, по моему мнению, будет вариант с &lt;a href=&quot;https://kb.vmware.com/s/article/2097936#use_of_replacing_vmca_certificate_with_custom_ca_certificate&quot; target=&quot;_blank&quot;&gt;заменой сертификата VMCA&lt;/a&gt; на vCenters на подписанный с внутреннего или внешнего CA и последующей регенерацией сертификатов ESXi.&lt;/p&gt;

</content></entry><entry><id>ihumster:vcda-initial-setup-api</id><link rel="alternate" type="text/html" href="https://blog.ihumster.ru/vcda-initial-setup-api?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=ihumster"></link><title>VMware Cloud Director Availability Initial Setup API</title><published>2024-09-23T07:02:53.641Z</published><updated>2024-09-23T07:02:53.641Z</updated><summary type="html">Встала задача по автоматизации развертывания VCDA для инсталляций VCD где VCDA еще не развернут. Начав изучение вопроса и прочтя внимательно документацию по VCDA с удивлением понял что для развертывания самих аплайнсов есть способ по автоматизации процесса, т.к. аплайнсы поставляются в формате OVF с обычными, для аплайнсов VMware, возможностями деплоя их как с помощью UI, так и с помощью OVFTool ну и прочими средствами (например, community.vmware.vmware_deploy_ovf из коллекции ansible). Но для дальнейшей первоначальной настройки нет иного официального пути, кроме как пойти в UI и потыкать кнопки, заполняя поля в Initial Setup Wizard.</summary><content type="html">
  &lt;p id=&quot;025Y&quot;&gt;Встала задача по автоматизации развертывания VCDA для инсталляций VCD где VCDA еще не развернут. Начав изучение вопроса и прочтя внимательно документацию по VCDA с удивлением понял что для развертывания самих аплайнсов есть способ по автоматизации процесса, т.к. аплайнсы поставляются в формате OVF с обычными, для аплайнсов VMware, возможностями деплоя их как с помощью UI, так и с помощью OVFTool ну и прочими средствами (например, community.vmware.vmware_deploy_ovf из коллекции ansible). Но для дальнейшей первоначальной настройки нет иного официального пути, кроме как пойти в UI и потыкать кнопки, заполняя поля в Initial Setup Wizard.&lt;/p&gt;
  &lt;p id=&quot;KBLT&quot;&gt;Вооружившись DevTools я за несколько итераций записал все необходимые API вызовы, которые происходят при выполнении этапов Initial Setup Wizard и после, проверив возможность их воспроизведения сторонними средствами (воспроизводил в Postman) решил что хочу записать их в виде документации OpenAPI 3.0 что бы любой мог импортировать ее в Postman и ознакомиться с деталями запросов.&lt;/p&gt;
  &lt;p id=&quot;lLZN&quot;&gt;По этому спешу представить Вам первую версию &lt;a href=&quot;https://github.com/ihumster/VCDAInitialSetupAPI&quot; target=&quot;_blank&quot;&gt;документации &lt;/a&gt;к VCDA Initial Setup API. В репозиторий добавлена также готовая коллекция для Postman (на случай если вы не умеете генерировать коллекцию из OpenAPI спецификации).&lt;/p&gt;
  &lt;p id=&quot;fOv6&quot;&gt;В коллекции все запросы пронумерованы в порядке выполнения их в Wizard. Отдельно не пронумерован один Get запрос - он используется для проверки доступности той или иной системы (VCD, vCenter server, ReplicatorAppliance, Tunnel Appliance) из VCDA Manager, а также возвращает SSL-сертификат используемый системой (сам сертефикат в PEM-encoded формате, его thumbprint - используются в остальных запросах).&lt;/p&gt;
  &lt;p id=&quot;qZTY&quot;&gt;Конфигурация VCDA которую я изучал - это рекомендуемая в продуктивной среде конфигурация из отдельных &amp;quot;ролевых&amp;quot; аплайнсов - manager, replicator, tunnel. Для combined и vc_combined версии, последовательность и структура запросов скорее всего иная (в vc_combined например не используется VCD).&lt;/p&gt;
  &lt;p id=&quot;3gNR&quot;&gt;Также, если вы используете несколько replicator аплайнсов то соответствующие запросы необходимо будет проделать для каждого аплайнса последовательно.&lt;/p&gt;
  &lt;p id=&quot;VRaF&quot;&gt;Для работы с этим API вам необходимы минимум 3 задеплоенных удобным вам способом аплайнса.&lt;/p&gt;

</content></entry><entry><id>ihumster:vcd-api-with-postman-2</id><link rel="alternate" type="text/html" href="https://blog.ihumster.ru/vcd-api-with-postman-2?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=ihumster"></link><title>Комфортная работа в Postman с VMware Cloud Director (2)</title><published>2024-09-23T07:02:07.305Z</published><updated>2024-09-23T07:02:07.305Z</updated><category term="cloud-director" label="Cloud Director"></category><summary type="html">В процессе работы с OpenAPI разных версий VMware Cloud Director понял что меня раздражает постоянно следить за версией API, которая указывается в Header 'Accept'. Чтобы автоматизировать актуализацию версии API в моих запросах я модифицировал свой pre-request Script из предыдущей статьи.</summary><content type="html">
  &lt;p id=&quot;ZQaK&quot;&gt;В процессе работы с OpenAPI разных версий VMware Cloud Director понял что меня раздражает постоянно следить за версией API, которая указывается в Header &amp;#x27;Accept&amp;#x27;. Чтобы автоматизировать актуализацию версии API в моих запросах я модифицировал свой pre-request Script из предыдущей статьи.&lt;/p&gt;
  &lt;p id=&quot;qTQ5&quot;&gt;Добавьте следующую строку между запросами версии и получением access_token, либо после них:&lt;/p&gt;
  &lt;pre id=&quot;CXUc&quot;&gt;pm.request.headers.get(&amp;#x27;Accept&amp;#x27;).replace(&amp;#x27;application\/json;version=(?&amp;lt;version&amp;gt;\d{2}\.\d{1})&amp;#x27;,&amp;#x27;application/json;version={{apiVersion}}&amp;#x27;)&lt;/pre&gt;

</content></entry></feed>