@rci-tools/core
v0.4.3
Published
Interact with REST Core Interface on Keenetic/Netcraze devices
Readme
English | Русский
@rci-tools/core
Введение
@rci-tools/core — это npm-пакет для взаимодействия с RCI API.
Два основных класса, экспортируемых этим пакетом:
- SessionManager: реализует аутентификацию по паролю;
- RciManager: основной класс для работы с API в едином стиле;
Оба класса требуют экземпляр HTTP транспорта
для отправки HTTP-запросов к устройству. Пакет предоставляет два браузерных транспорта:
FetchTransport— обёртка над встроенным APIfetch;XHRTransport— транспорт на основеXMLHttpRequest.
Передавайте один и тот же экземпляр транспорта в SessionManager и RciManager, чтобы запросы
RciManager выполнялись в рамках авторизованной HTTP-сессии:
import {RciManager, SessionManager, XHRTransport} from '@rci-tools/core';
const transport = new XHRTransport();
const sessionManager = new SessionManager('http://192.168.1.1', transport);
const rciManager = new RciManager('http://192.168.1.1', transport);Установка
npm install @rci-tools/coreОписание и примеры использования
SessionManager
SessionManager используется для управления аутентификацией.
Он имеет следующий интерфейс:
interface SessionManager<ResponseType extends BaseHttpResponse = BaseHttpResponse> {
isAuthenticated(): Observable<boolean>;
login(username: string, password: string): Observable<boolean>;
logout(): Observable<unknown>;
getRealmHeader(): Observable<string>;
toggleErrorLogging(isEnabled: boolean): void;
}Используйте методы isAuthenticated/login/logout для управления сессией.
Оставшиеся два метода предназначены для вспомогательных задач:
getRealmHeader: позволяет получить имя устройства до аутентификации (например, чтобы показать его на экране входа)toggleErrorLogging: включает/отключает логирование HTTP-ошибок в консоль
RciManager
Класс RciManager используется для взаимодействия с RCI API.
Он имеет несколько преимуществ по сравнению с использованием непосредственно fetch/xhr/axios/...:
- несколько запросов через
RciManagerмогут быть объединены в один HTTP-запрос к устройству - реализована простая система приоритетов: приоритетные запросы блокируют обычные, пока не завершатся
- реализован удобный способ работы с фоновыми процессами
Класс RciManager имеет следующий интерфейс:
interface RciManager<
QueryPath extends string = string, // допустимые значения 'path' для обычных RCI-запросов
BackgroundQueryPath extends string = string // допустимые значения 'path' для фоновых процессов
> {
readonly stats$: Observable<QueryStats<QueryPath>>;
toggleStats(enabled: boolean): void;
replaceBatchScheduler(scheduler: BatchScheduler<QueryPath>, options?: {
waitIdleFor?: number
}): Observable<void>;
execute(query: RciTask<QueryPath>): Observable<any>;
queue(query: RciTask<QueryPath>, options?: QueueOptions): Observable<any>;
initBackgroundProcess(query: RciQuery<BackgroundQueryPath>, options?: RciBackgroundProcessOptions): RciBackgroundProcess;
queueBackgroundProcess(query: RciQuery<BackgroundQueryPath>, options?: RciBackgroundProcessOptions): RciBackgroundProcess;
destroy(): void;
}Дополнительные члены класса:
stats$: выдаёт объектQueryStatsдля каждого завершённого батча, если сбор статистики включён. См. Статистика запросов.toggleStats(enabled): включает или отключает сбор статистики.replaceBatchScheduler(scheduler, options?): заменяет планировщик батчей во время выполнения, дожидаясь, пока текущий батч освободится.options.waitIdleForпо умолчанию30000мс. ВыбрасываетSchedulerReplacementInProgressError, если другая замена уже выполняется. См. Планировщик.destroy(): освобождает внутренние очереди, очереди фоновых процессов и сборщик статистики.
RciManager активно использует корневой ресурс API (
/rci/).
Взаимодействие как с настройками,
так и с действиями в нем реализовано через объекты
RciQuery,
отправляемые на корневой ресурс. Интерфейс RciQuery выглядит следующим образом:
export interface RciQuery<PathType extends string = string> { // `PathType` можно сузить до подмножества допустимых значений `path`
path: PathType;
data?: Record<string, any> | string | boolean | number; // по умолчанию {}
extractData?: boolean; // по умолчанию true
}
export type RciTask<PathType extends string = string> = RciQuery<PathType> | RciQuery<PathType>[];Метод queue() принимает необязательный объект QueueOptions:
interface QueueOptions {
isPriorityTask?: boolean; // отправить через приоритетную очередь. По умолчанию: false
saveConfiguration?: boolean; // добавить запрос system/configuration/save. По умолчанию: false
}Перед отправкой на устройство объекты RciQuery преобразуются в объект,
где path становится "путём" ко вложенному свойству, а data — значением по этому пути.
Например, запрос:
const query = {
path: 'show.version'
};преобразуется в (data по умолчанию — пустой объект):
{
"show": {
"version": {}
}
}Аналогично, запрос с path и data:
const query = {
path: 'interface',
data: {
name: 'Bridge0',
description: 'My network'
}
};превращается в:
{
"interface": {
"name": "Bridge0",
"description": "My network"
}
}Отправка такого вложенного объекта на корневой ресурс RCI API вернет объект,
в котором нужные данные вложены аналогичным образом. Если флаг extractData
установлен в true (или не указан: он имеет значение true по умолчанию),
то RciManager извлечёт нужную часть ответа автоматически.
Существует определённая гибкость в том, как один и тот же объект
может быть представлен как RciQuery. Например, оба запроса
const query = {path: 'ip.telnet.session', data: {timeout: 123456}};и
const query = {path: 'ip', data: {telnet: {session: {timeout: 123456}}}};будут преобразованы в один и тот же объект:
{
"ip": {
"telnet": {
"session": {
"timeout": 123456
}
}
}
}Вы можете использовать тот вариант, который удобнее для вас.
execute vs queue
RciManager предоставляет два метода для отправки запросов к API:
execute(query): Отправляет HTTP-запрос при подписке на возвращаемый Observable. Вы полностью контролируете жизненный цикл подписки. Это может быть полезно, если нужно:- вручную управлять моментом отправки HTTP-запроса
- последовательно отправить несколько запросов с точным контролем времени
queue(query, options?): Добавляет запрос во внутреннюю очередь, которая объединяет несколько запросов в один HTTP-запрос.RciManagerсам управляет подпиской и моментом отправки HTTP-запросов. Очередь автоматически:- объединяет несколько запросов в один HTTP-запрос
- удаляет дублирующиеся запросы из батча (batch)
- ждёт определённое время перед отправкой HTTP-запроса, собирая данные из разных вызовов
queue - обрабатывает приоритетные запросы через отдельную очередь, блокируя обычную
Оба метода возвращают rxjs Observable, на который нужно подписаться, чтобы получить результат. Ниже приведено несколько примеров использования.
Пакетное планирование (Batch Scheduling)
По умолчанию queue() объединяет запросы в батчи с окном 20мс перед отправкой одного
HTTP-запроса. Это поведение управляется планировщиком (scheduler) — настройте его через
RciManagerOptions.batchScheduler или замените во время выполнения с помощью
replaceBatchScheduler().
Подробнее: см., встроенные планировщики
(TimerScheduler, RuleScheduler, raceSchedulers), интерфейс BatchSnapshot, примеры
пользовательских планировщиков и замена во время выполнения.
Примеры использования
1. Базовый пример
import {Observable, of, firstValueFrom} from 'rxjs';
import {exhaustMap} from 'rxjs/operators';
import {RciQuery, RciManager, SessionManager, FetchTransport} from '@rci-tools/core';
const transport = new FetchTransport(); // HTTP-транспорт (обёртка над встроенным `fetch`)
const host = 'http://192.168.1.1'; // IP-адрес устройства
const sessionManager = new SessionManager(host, transport);
const rciManager = new RciManager(host, transport);
// Перед отправкой запросов, возможно, потребуется аутентификация:
const auth$: Observable<boolean> = sessionManager.login('admin', 'password')
auth$
.subscribe(async (isLoggedIn) => {
if (!isLoggedIn) {
console.error('Ошибка аутентификации');
return Promise.resolve(null);
}
// Следующие запросы будут выполняться последовательно;
// Observable, возвращаемые методом `queue`,
// преобразуются в Promise для того, чтобы код примера было проще читать.
// настройка
const changeHomeDescription: RciQuery = {
path: 'interface',
data: {name: 'Bridge0', description: 'My awesome home network'},
};
const changeSettingResult = await rciManager.queue(changeHomeDescription).toPromise(); // объект со статусом выполнения команды
// соответствующее действие (настройка с префиксом 'show.rc')
const readInterfaceDescription: RciQuery = {
path: 'show.rc.interface.description', // чтение из "running-config"
data: {name: 'Bridge0'},
};
const readSettingResult = await rciManager.queue(readInterfaceDescription).toPromise(); // 'My awesome home network'
// другое действие
const showVersion: RciQuery = { // data по умолчанию {}
path: 'show.version',
};
const actionResult = await rciManager.queue(showVersion).toPromise(); // объект с информацией о версии устройства
console.log(changeSettingResult, readSettingResult, actionResult);
});2. Несколько запросов
import {forkJoin} from 'rxjs';
import {delay, exhaustMap} from 'rxjs/operators';
// другие импорты
// ... создайте экземпляр `RciManager` так же, как в предыдущем примере ...
const queries: RciQuery[] = [
{path: 'show.version'},
{path: 'show.identification'},
];
const batch1$ = rciManager.queue(queries); // Оба запроса будут отправлены одним HTTP-запросом
batch1$
.pipe(
exhaustMap((results) => {
queries.forEach((query, index) => {
console.log({
query,
result: results[index],
});
});
// Эти запросы также будут отправлены одним HTTP-запросом
return forkJoin([
rciManager.queue({path: 'show.system'}),
rciManager.queue({path: 'show.last-change'}),
rciManager.queue({path: 'whoami'}),
]);
}),
)
.subscribe((joinedResults) => {
console.log(joinedResults);
});3. Приоритетные запросы
Если вам требуется отправить запрос, не дожидаясь пока завершится сбора данных для батча, можно отправить его как приоритетный. Приоритетные запросы группируются только в рамках следующей "микрозадачи" (event loop microtask). Приоритетная очередь блокирует обычную, пока не опустеет (даже если обычная уже ждёт ответа на HTTP-запрос).
import {delay} from 'rxjs/operators';
import {forkJoin} from 'rxjs';
// другие импорты
// ... создайте экземпляр `RciManager` так же, как в предыдущем примере ...
const queries: RciQuery[] = [
{path: 'show.version'},
{path: 'show.identification'},
{path: 'show.interface'},
];
const execute$ = rciManager.queue(queries);
// Приоритетный запрос, с задержкой 20 мс (время сбора данных для "батча" в обычной очереди)
const executePriority$ = rciManager.queue({path: 'show.system'}, {isPriorityTask: true}).pipe(delay(20));
const all$ = forkJoin([
execute$,
executePriority$
]);
// Приоритетный запрос заблокирует обычный, пока не завершится.
// Если посмотреть, сколько HTTP-запросов было отправлено, то их окажется три:
//
// 1. батч-запрос из первого вызова `execute` -> отменён приоритетным запросом
// 2. приоритетный запрос из второго вызова `execute`
// 3. батч-запрос из первого вызова `execute` -> повторно выполнен после приоритетного
all$
.subscribe((allResults) => {
console.log(allResults);
});Использование RciQueue отдельно
RciManager внутри содержит два экземпляра RciQueue (батчинг-очередь и приоритетную очередь).
Если вам нужен только батчинг, вы можете использовать RciQueue напрямую — без RciManager.
Подробнее: RciQueue отдельно — полный API, опции конструктора, отличия
от RciManager и примеры, включая воспроизведение приоритетной системы с двумя очередями.
Фоновые процессы
Для фоновых процессов RciManager
предоставляет два метода, также принимающих объекты RciQuery
и дополнительные настройки:
interface RciBackgroundProcessOptions {
pollInterval?: number; // интервал между GET-запросами в мс. По умолчанию: 1000
timeout?: number; // мс до отправки DELETE для остановки процесса. По умолчанию: 0 (без таймаута)
}Разница между двумя методами заключается в том, когда фоновый процесс будет запущен:
initBackgroundProcess(query, options?): возвращает объектRciBackgroundProcess, который нужно запускать вручную. Этот метод полезен, если вам нужен полный контроль над жизненным циклом фонового процесса (вы также можете вручную прервать процесс до его завершения).queueBackgroundProcess(query, options?): Ставит фоновый процесс в очередь. Запросы с одинаковым значениемpathгруппируются в одну очередь, что гарантирует, что одна и та же команда с разными аргументами не будет выполняться параллельно. Это важно для обхода ограничений API при работе в браузере, где обычно используется одна HTTP-сессия для всех запросов.
Оба метода возвращают объект RciBackgroundProcess со следующими свойствами:
start(): boolean: Запускает процесс вручную. Возвращаетfalse, если процесс уже запущен.attachToRunning(): boolean: Подключается к уже запущенному фоновому процессу (например, запущенному черезRciManager.execute()). Пропускает начальный POST-запрос и сразу начинает опрос через GET. Возвращаетfalse, еслиstart()уже был вызван или процесс не в состоянииINIT.abort(): boolean: ОтправляетDELETEк ресурсу команды RCI и прерывает локальный процесс. Возвращаетfalse, если процесс не запущен.state$: Observable, который выдаёт изменения состояния процесса (RCI_BACKGROUND_PROCESS_STATE)data$: Observable, который выдаёт обновления данных по мере выполнения фонового процессаresult$: Observable, который выдаёт финальный результат один раз, непосредственно перед завершением процесса. Не выдаёт значения при прерывании или таймауте.done$: Observable, который выдаёт значение по завершении процесса (RCI_BACKGROUND_PROCESS_FINISH_REASON)
interface RciBackgroundProcess {
state$: Observable<RCI_BACKGROUND_PROCESS_STATE>;
data$: Observable<GenericObject | null>;
result$: Observable<GenericObject | null>;
done$: Observable<RCI_BACKGROUND_PROCESS_FINISH_REASON>;
start(): boolean;
attachToRunning(): boolean;
abort(): boolean;
getState(): RCI_BACKGROUND_PROCESS_STATE;
destroy(): void;
}Вот базовый пример использования initBackgroundProcess:
const pingQuery: RciQuery = {
path: 'tools.ping',
data: {
host: 'google.com',
packetsize: 84,
count: 5,
},
};
const ping$ = rciManager.initBackgroundProcess(pingQuery);
ping$.data$
.subscribe((data) => {
console.log('Результат ping:', data);
});
ping$.done$
.subscribe((reason) => {
console.log('Ping завершён:', reason);
});
ping$.start();Вы также можете вручную прервать фоновый процесс до его завершения:
const pingQuery: RciQuery = {
path: 'tools.ping',
data: {
host: 'google.com',
packetsize: 84,
count: 50,
},
};
const ping$ = rciManager.initBackgroundProcess(pingQuery);
ping$.data$
.subscribe((data) => {
console.log('Результат ping:', data);
});
ping$.done$
.subscribe((reason) => {
console.log('Ping завершён:', reason);
});
ping$.start();
setTimeout(() => ping$.abort(), 4000);Если фоновый процесс уже был запущен (например, через RciManager.execute()),
к нему можно подключиться с помощью attachToRunning(), чтобы получать
результаты опроса без отправки нового POST-запроса:
import {RciManager, RciQuery} from '@rci-tools/core';
const host = 'http://192.168.1.1';
const transport = new FetchTransport();
const rciManager = new RciManager(host, transport);
// Шаг 1: запускаем фоновый процесс через обычный вызов API
const startFwCheckQuery: RciQuery = {
path: 'components.list',
data: {},
};
await firstValueFrom(rciManager.execute(startFwCheckQuery));
// Устройство отвечает {continued: true} — процесс запущен
// Шаг 2: создаём экземпляр фонового процесса и подключаемся к нему
const pollProcess = rciManager.initBackgroundProcess(
{path: 'components.list'},
{pollInterval: 1000, timeout: 30000},
);
pollProcess.result$
.subscribe((result) => {
console.log('Проверка обновлений завершена:', result);
});
pollProcess.attachToRunning(); // `pollProcess` сразу начнет с GET запросаДля управления несколькими фоновыми процессами с одной и той же командой,
но разными аргументами, используйте queueBackgroundProcess,
чтобы они не выполнялись параллельно:
import {forkJoin, firstValueFrom} from 'rxjs';
// Несколько фоновых процессов с одинаковым path, но разными данными
const continuedQueries: RciQuery[] = [
{path: 'tools.ping', data: {host: 'google.com', packetsize: 84, count: 5}},
{path: 'tools.ping', data: {host: 'github.com', packetsize: 84, count: 5}},
{path: 'components.list', data: {sandbox: 'stable'}},
{path: 'components.list', data: {sandbox: 'draft'}},
];
// Поставить все процессы в очередь — запросы с одинаковым path будут объединены
const backgroundTasks = continuedQueries.map((query) => {
return rciManager.queueBackgroundProcess(query);
});
// Следить за завершением всех задач
const done$ = backgroundTasks.map((task) => task.done$);
const finalResults = await firstValueFrom(forkJoin(done$));
console.log('Все фоновые процессы завершены:', finalResults);В этом примере два запроса tools.ping будут выполнены последовательно
и два запроса components.list также будут выполнены последовательно;
Последовательное выполнение позволяет избежать неоднозначности, связанной с тем,
что, когда несколько экземпляров одного и того же фонового процесса запущены
параллельно из одной HTTP-сессии, невозможно понять, статус какого из процессов
возвращается в ответ на GET-запрос.
Статистика запросов
Когда сбор статистики включён через toggleStats(true), stats$ выдаёт объект QueryStats
для каждого завершённого батча. См. Статистика запросов
— полный интерфейс QueryStats и использование с отдельным RciQueue.
