From d3c6172ab4bed21f1e4f4cc3d353265f83606128 Mon Sep 17 00:00:00 2001 From: acacio90 <49293808+acacio90@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:14:48 -0300 Subject: [PATCH 1/3] feat(sessions): importa hosts e grupos do Zabbix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arquivo → Importar → Hosts do Zabbix: conecta na API (6.0+, token ou usuário/senha), lista os grupos e importa os hosts dos escolhidos; cada grupo vira pasta e cada host uma sessão, com o hostid guardado em zabbixHostId. Credencial cifrada pelo safeStorage. --- CHANGELOG.md | 6 ++ README.md | 27 +++++- desktop/app.js | 118 ++++++++++++++++++++++++ desktop/index.html | 29 ++++++ desktop/preload.cjs | 4 + desktop/styles.css | 23 +++++ src/desktop/main.ts | 5 + src/desktop/menu.ts | 3 +- src/desktop/zabbix.ts | 126 ++++++++++++++++++++++++++ src/sessions/import-zabbix.ts | 110 ++++++++++++++++++++++ src/sessions/model.ts | 7 ++ src/sessions/store.ts | 3 +- src/zabbix/client.ts | 166 ++++++++++++++++++++++++++++++++++ test-ui/zabbix.spec.ts | 73 +++++++++++++++ test/import-zabbix.test.ts | 82 +++++++++++++++++ test/zabbix-client.test.ts | 89 ++++++++++++++++++ test/zabbix-fake.ts | 96 ++++++++++++++++++++ 17 files changed, 964 insertions(+), 3 deletions(-) create mode 100644 src/desktop/zabbix.ts create mode 100644 src/sessions/import-zabbix.ts create mode 100644 src/zabbix/client.ts create mode 100644 test-ui/zabbix.spec.ts create mode 100644 test/import-zabbix.test.ts create mode 100644 test/zabbix-client.test.ts create mode 100644 test/zabbix-fake.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 50fcb7f..602345d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Todas as mudanças relevantes do MultTerm. O formato segue o ## [Não lançado] +### Adicionado + +- **Arquivo → Importar → Hosts do Zabbix…**: conecta no Zabbix (6.0 ou mais novo) por API token + ou usuário e senha, lista os grupos e importa os hosts dos escolhidos; cada grupo vira uma pasta + e cada host uma sessão (Windows vira RDP). A conexão fica lembrada, com a credencial cifrada. (#103) + ### Alterado - Importação de inventário (INI/YAML): as variáveis agora têm os nomes dos campos da sessão diff --git a/README.md b/README.md index 5384857..70461ae 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ As sessões ficam num arquivo YAML legível e versionável, **sem limite de quan - **Terminal SSH embutido em abas** (xterm.js + node-pty), usando o OpenSSH do sistema - Login por chave via agente SSH (ssh-agent do Windows, KeePassXC) - **RDP numa aba** (cliente FreeRDP embutido), ou no cliente do Windows (`mstsc`) pelo menu -- Importação de sessões de outro arquivo YAML ou de um **inventário** (INI/YAML) +- Importação de sessões de outro arquivo YAML, de um **inventário** (INI/YAML) ou dos hosts e + grupos do **Zabbix** - Exportação das sessões (todas ou uma pasta), dos sites de VPN ou de um **backup completo** para levar tudo a outra máquina - **Ferramentas de rede**: ping, traceroute, consulta DNS, consulta SNMP (v1/v2c/v3), scanner de @@ -306,6 +307,30 @@ com as sessões existentes (mesma pasta + nome não é sobrescrito): `identityFile` e `type=ssh|rdp`) preenchem a sessão; as do host valem sobre as do grupo. Faixas como `web[01:03]` são expandidas. +### Importar do Zabbix + +**Arquivo → Importar → Hosts do Zabbix…** busca os hosts de um Zabbix 6.0 ou mais novo pela API: + +1. Informe o endereço (`https://zabbix.exemplo.com`; o `/api_jsonrpc.php` é completado) e um + **API token** (Usuários → API tokens) ou usuário e senha. Para certificado autoassinado, marque + **Aceitar certificado autoassinado**. +2. **Conectar e listar grupos** mostra os grupos com a quantidade de hosts; marque os que quer. +3. **Importar**: cada grupo vira uma pasta dentro da pasta de destino (padrão `Zabbix`; um `/` no + nome do grupo cria subpastas) e cada host uma sessão. Um host em vários grupos marcados entra em + cada pasta. Sessões que já existem (mesma pasta + nome) são mantidas. + +Cada host vira uma sessão assim: + +- **Endereço**: interface principal do agente (IP ou DNS, conforme configurado no host); sem + agente, a interface SNMP. Hosts sem nenhuma das duas ficam de fora, com aviso. +- **Tipo**: a tag `multterm.type` (`ssh` ou `rdp`); sem ela, Windows no inventário (`os`) ou num + template vira RDP, o resto SSH. +- **Usuário e porta**: tags opcionais `multterm.user` e `multterm.port`. +- **MAC** (Wake-on-LAN): campo `macaddress_a` do inventário. + +O endereço, os grupos e a pasta ficam lembrados; o token e a senha ficam cifrados pelo Windows +e não precisam ser digitados de novo. A sessão guarda o `zabbixHostId` do host de origem. + ### Login sem senha (agente SSH) 1. PowerShell como administrador: diff --git a/desktop/app.js b/desktop/app.js index 27452c7..66ada40 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -280,6 +280,9 @@ async function save(event) { event.preventDefault(); clearMessages(); const session = readForm(); + // Campos que o formulário não mostra (origem no Zabbix) seguem com a sessão editada. + const edited = state.sessions.find((s) => keyOf(s) === state.editing); + if (edited?.zabbixHostId) session.zabbixHostId = edited.zabbixHostId; const others = state.sessions.filter((s) => keyOf(s) !== state.editing); try { apply(await api.save([...others, session])); @@ -1384,6 +1387,120 @@ async function importSessions() { } } +/** + * Importar hosts do Zabbix: conecta na API, lista os grupos (com a quantidade de hosts) e importa + * os escolhidos. Token e senha em branco usam os guardados; eles nunca voltam do processo principal. + */ +async function importZabbix() { + const dialog = $('zabbix-dialog'); + const zform = $('zabbix-form'); + const box = $('zabbix-groups-box'); + const list = $('zabbix-groups'); + const error = $('zabbix-error'); + const importBtn = $('zabbix-import'); + let config; + try { + config = await api.zabbixConfig(); + } catch (err) { + showEditor(); + clearMessages(); + message(cleanError(err), 'error'); + return; + } + zform.url.value = config.url; + zform.authType.value = config.authType; + zform.user.value = config.user; + zform.token.value = ''; + zform.password.value = ''; + zform.token.placeholder = config.hasToken ? 'guardado (em branco = manter)' : ''; + zform.password.placeholder = config.hasPassword ? 'guardada (em branco = manter)' : ''; + zform.insecure.checked = config.insecure; + zform.rootFolder.value = config.rootFolder; + $('zabbix-filter').value = ''; + list.replaceChildren(); + box.hidden = true; + importBtn.disabled = true; + error.textContent = ''; + + const updateAuth = () => { + const password = zform.authType.value === 'password'; + for (const el of zform.querySelectorAll('.zabbix-token')) el.hidden = password; + for (const el of zform.querySelectorAll('.zabbix-password')) el.hidden = !password; + }; + const updateImport = () => { + importBtn.disabled = !list.querySelector('input:checked'); + }; + updateAuth(); + + const connectZabbix = async () => { + error.textContent = ''; + $('zabbix-connect').disabled = true; + try { + const { version, groups } = await api.zabbixConnect({ + url: zform.url.value, + authType: zform.authType.value, + token: zform.token.value, + user: zform.user.value, + password: zform.password.value, + insecure: zform.insecure.checked, + }); + const chosen = new Set(config.groupIds); + list.replaceChildren(...groups.map((g) => { + const label = document.createElement('label'); + const check = Object.assign(document.createElement('input'), { type: 'checkbox', value: g.groupid, checked: chosen.has(g.groupid) }); + const count = Object.assign(document.createElement('span'), { className: 'count', textContent: `(${g.hosts})` }); + label.append(check, ` ${g.name} `, count); + label.dataset.name = g.name.toLowerCase(); + return label; + })); + $('zabbix-version').textContent = `Zabbix ${version}: ${groups.length} grupo(s) com hosts. Um host em vários grupos marcados entra em cada pasta.`; + box.hidden = false; + updateImport(); + } catch (err) { + error.textContent = cleanError(err); + } finally { + $('zabbix-connect').disabled = false; + } + }; + + const done = () => { + zform.onsubmit = null; + zform.authType.onchange = null; + list.onchange = null; + $('zabbix-filter').oninput = null; + $('zabbix-connect').onclick = null; + $('zabbix-cancel').onclick = null; + dialog.close(); + }; + zform.authType.onchange = updateAuth; + list.onchange = updateImport; + $('zabbix-filter').oninput = (e) => { + const q = e.target.value.trim().toLowerCase(); + for (const label of list.children) label.hidden = !!q && !label.dataset.name.includes(q); + }; + $('zabbix-connect').onclick = connectZabbix; + $('zabbix-cancel').onclick = done; + zform.onsubmit = async (e) => { + e.preventDefault(); + error.textContent = ''; + importBtn.disabled = true; + try { + const groupIds = [...list.querySelectorAll('input:checked')].map((el) => el.value); + const result = await api.zabbixImport({ groupIds, rootFolder: zform.rootFolder.value }); + done(); + showEditor(); + clearMessages(); + apply(result); + message(`${result.added} sessões importadas do Zabbix (${result.hosts} hosts), ${result.skipped} já existiam.`); + for (const w of result.warnings) message(w, 'warn'); + } catch (err) { + error.textContent = cleanError(err); + updateImport(); + } + }; + dialog.showModal(); +} + /** Troca o arquivo de sessões em uso (ex.: um sessions.yaml versionado no git). */ async function chooseSessionsFile() { showEditor(); @@ -1962,6 +2079,7 @@ const MENU_COMMANDS = { reload: () => load(), 'default-key': editDefaultKey, 'import-sessions': importSessions, + 'import-zabbix': importZabbix, 'import-vpn': importVpnSite, 'import-backup': importBackup, 'export-sessions': () => exportSessions(), diff --git a/desktop/index.html b/desktop/index.html index eeff247..31648e3 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -128,6 +128,35 @@