Транспорт збору конфігу бере доступ хоста, а не профіль моделі
Питання «чому всі профілі ssh» виявило справжню помилку. Список правильний: ssh — розумний типовий вибір. Але це поле ВИРІШУВАЛО транспорт, а профіль описує модель, не конкретну коробку. Хост зі старою прошивкою, де є лише telnet, і з чесно заведеним telnet-доступом усе одно набирався по SSH — і не збирався ніколи. inv.credentials.proto вже ніс потрібну відповідь і доїжджав до зонда невикористаним. Тепер транспорт бере доступ; профільне поле лишилось підказкою й діє, поки доступу немає. Логіка винесена в jobTransport() — щоб її можна було перевірити без бази й без пристрою. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
444447d86b
commit
452345da32
4 changed files with 146 additions and 11 deletions
43
HISTORY.md
43
HISTORY.md
|
|
@ -2832,3 +2832,46 @@ MikroTik RouterOS, Linux), кнопка «За зразок» на кожном
|
||||||
пошук за назвою/виробником/командою, «лише власні»,
|
пошук за назвою/виробником/командою, «лише власні»,
|
||||||
«Cisco IOS» зі зразка заповнив усі шість полів
|
«Cisco IOS» зі зразка заповнив усі шість полів
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Транспорт вирішує доступ, а не профіль
|
||||||
|
|
||||||
|
### Питання, яке виявило помилку
|
||||||
|
|
||||||
|
«Чому всі готові профілі — ssh? Хіба не має бути telnet?»
|
||||||
|
|
||||||
|
Сам список правильний: `ssh` у всіх 147 вбудованих — це не недогляд, а
|
||||||
|
розумний типовий вибір. Помилка була в тому, **що саме це поле робило**.
|
||||||
|
|
||||||
|
`buildConfigJob` брав транспорт із профілю моделі. А оскільки профіль
|
||||||
|
описує модель, а не конкретну коробку, виходило так: хост зі старою
|
||||||
|
прошивкою, де є лише telnet, і з чесно заведеним telnet-доступом усе
|
||||||
|
одно набирався по SSH — бо профіль моделі каже «ssh». І не збирався
|
||||||
|
ніколи.
|
||||||
|
|
||||||
|
Доступ при цьому вже ніс потрібне поле: `inv.credentials.proto` — саме
|
||||||
|
воно й відповідає на питання «чим до цієї коробки достукатись». Воно
|
||||||
|
доїжджало до зонда в `Credential.Transport` і не використовувалось для
|
||||||
|
набору.
|
||||||
|
|
||||||
|
### Як тепер
|
||||||
|
|
||||||
|
Транспорт бере доступ. Профільне поле лишилось підказкою «чим це залізо
|
||||||
|
зазвичай беруть» і діє, тільки поки доступу немає. Порт іде за
|
||||||
|
транспортом (22/23), а порт із доступу перекриває типовий — залізо за
|
||||||
|
NAT цілком може слухати SSH на 2222.
|
||||||
|
|
||||||
|
Логіка винесена в `jobTransport()` окремою функцією саме щоб її можна
|
||||||
|
було перевірити без бази й без пристрою: чотири випадки, чотири рядки
|
||||||
|
таблиці.
|
||||||
|
|
||||||
|
У формі профілю поле перейменоване на «Типовий транспорт» із підказкою,
|
||||||
|
що реальний задає доступ хоста. Назва колонки — «Типово».
|
||||||
|
|
||||||
|
### Перевірено
|
||||||
|
|
||||||
|
```
|
||||||
|
доступ telnet + профіль ssh → telnet, порт 23
|
||||||
|
доступ ssh + профіль telnet → ssh, порт 22
|
||||||
|
порт 2222 у доступі → 2222
|
||||||
|
без доступу → лишається підказка профілю
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -160,15 +160,7 @@ func (s *Store) buildConfigJob(ctx context.Context, tenantID, deviceID, jobID st
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
transport := npv1.Transport_TRANSPORT_SSH
|
transport, port := jobTransport(prof.Transport, cred)
|
||||||
port := uint32(22)
|
|
||||||
if prof.Transport == "telnet" {
|
|
||||||
transport = npv1.Transport_TRANSPORT_TELNET
|
|
||||||
port = 23
|
|
||||||
}
|
|
||||||
if cred != nil && cred.Port != 0 {
|
|
||||||
port = cred.Port
|
|
||||||
}
|
|
||||||
|
|
||||||
return &npv1.ConfigJob{
|
return &npv1.ConfigJob{
|
||||||
JobId: jobID,
|
JobId: jobID,
|
||||||
|
|
@ -394,3 +386,36 @@ func (s *Store) ListConfigJobs(ctx context.Context, tenantID, deviceID string, l
|
||||||
})
|
})
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jobTransport каже, чим набирати цей хост і на який порт.
|
||||||
|
//
|
||||||
|
// Транспорт вирішує ДОСТУП, а не профіль. Профіль описує модель заліза:
|
||||||
|
// які команди віддати й як упізнати запрошення. А чим до конкретної
|
||||||
|
// коробки достукатись — властивість самої коробки: та сама модель у
|
||||||
|
// клієнта може стояти з увімкненим SSH, а на сусідньому вузлі — зі
|
||||||
|
// старою прошивкою, де є лише telnet.
|
||||||
|
//
|
||||||
|
// Раніше вирішував профіль, і оскільки всі 147 вбудованих кажуть «ssh»,
|
||||||
|
// хост із telnet-доступом набирався по SSH і не збирався ніколи.
|
||||||
|
// Профільне поле лишилось підказкою «чим це залізо зазвичай беруть» і
|
||||||
|
// діє, тільки поки доступу немає.
|
||||||
|
func jobTransport(profileTransport string, cred *npv1.Credential) (npv1.Transport, uint32) {
|
||||||
|
transport := npv1.Transport_TRANSPORT_SSH
|
||||||
|
switch {
|
||||||
|
case cred != nil:
|
||||||
|
transport = cred.Transport
|
||||||
|
case profileTransport == "telnet":
|
||||||
|
transport = npv1.Transport_TRANSPORT_TELNET
|
||||||
|
}
|
||||||
|
|
||||||
|
port := uint32(22)
|
||||||
|
if transport == npv1.Transport_TRANSPORT_TELNET {
|
||||||
|
port = 23
|
||||||
|
}
|
||||||
|
// Порт із доступу перекриває типовий: залізо за NAT цілком може
|
||||||
|
// слухати SSH на 2222.
|
||||||
|
if cred != nil && cred.Port != 0 {
|
||||||
|
port = cred.Port
|
||||||
|
}
|
||||||
|
return transport, port
|
||||||
|
}
|
||||||
|
|
|
||||||
64
server/internal/store/ncm_transport_test.go
Normal file
64
server/internal/store/ncm_transport_test.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
npv1 "github.com/netpulse/netpulse/gen/go/netpulse/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Транспорт задає доступ хоста, а не профіль моделі.
|
||||||
|
//
|
||||||
|
// Усі 147 вбудованих профілів кажуть «ssh», бо так знімають з
|
||||||
|
// переважної більшості заліза. Але та сама модель у клієнта може стояти
|
||||||
|
// зі старою прошивкою, де є лише telnet, — і поки вирішував профіль,
|
||||||
|
// такий хост набирався по SSH і не збирався ніколи.
|
||||||
|
func TestTransportComesFromCredential(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
profile string
|
||||||
|
cred *npv1.Credential
|
||||||
|
want npv1.Transport
|
||||||
|
port uint32
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "доступ telnet перемагає профіль ssh",
|
||||||
|
profile: "ssh",
|
||||||
|
cred: &npv1.Credential{Transport: npv1.Transport_TRANSPORT_TELNET},
|
||||||
|
want: npv1.Transport_TRANSPORT_TELNET,
|
||||||
|
port: 23,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "доступ ssh перемагає профіль telnet",
|
||||||
|
profile: "telnet",
|
||||||
|
cred: &npv1.Credential{Transport: npv1.Transport_TRANSPORT_SSH},
|
||||||
|
want: npv1.Transport_TRANSPORT_SSH,
|
||||||
|
port: 22,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "порт з доступу перекриває типовий",
|
||||||
|
profile: "ssh",
|
||||||
|
cred: &npv1.Credential{Transport: npv1.Transport_TRANSPORT_SSH, Port: 2222},
|
||||||
|
want: npv1.Transport_TRANSPORT_SSH,
|
||||||
|
port: 2222,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "без доступу лишається підказка профілю",
|
||||||
|
profile: "telnet",
|
||||||
|
cred: nil,
|
||||||
|
want: npv1.Transport_TRANSPORT_TELNET,
|
||||||
|
port: 23,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
got, port := jobTransport(c.profile, c.cred)
|
||||||
|
if got != c.want {
|
||||||
|
t.Fatalf("транспорт: %v, очікував %v", got, c.want)
|
||||||
|
}
|
||||||
|
if port != c.port {
|
||||||
|
t.Fatalf("порт: %d, очікував %d", port, c.port)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -208,7 +208,7 @@ export function ProfilesPage() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'transport',
|
key: 'transport',
|
||||||
header: 'Транспорт',
|
header: 'Типово',
|
||||||
cell: (p) => <span className="text-slate-400">{p.transport}</span>,
|
cell: (p) => <span className="text-slate-400">{p.transport}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -413,7 +413,10 @@ function ProfileForm({
|
||||||
onChange={(e) => setVendor(e.target.value)}
|
onChange={(e) => setVendor(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Транспорт">
|
<Field
|
||||||
|
label="Типовий транспорт"
|
||||||
|
hint="Підказка, чим це залізо зазвичай беруть. Реальний транспорт задає доступ, прив'язаний до хоста"
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
value={transport}
|
value={transport}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue