Skip to content

Commit f77e51c

Browse files
committed
support draining feature server manually
1 parent b0b5f1b commit f77e51c

File tree

3 files changed

+571
-0
lines changed

3 files changed

+571
-0
lines changed

lib/cli/cli.js

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
#!/usr/bin/env node
2+
3+
const net = require('net');
4+
const readline = require('readline');
5+
6+
const SOCKET_PATH = process.env.SBC_SOCKET_PATH || '/tmp/sbc-sip-sidecar.sock';
7+
8+
function sendCommand(action, key, value, item, server) {
9+
return new Promise((resolve, reject) => {
10+
const socket = net.createConnection(SOCKET_PATH);
11+
12+
socket.on('connect', () => {
13+
const cmd = { action };
14+
if (key) cmd.key = key;
15+
if (value) cmd.value = value;
16+
if (item) cmd.item = item;
17+
if (server) cmd.server = server;
18+
19+
socket.write(JSON.stringify(cmd) + '\n');
20+
});
21+
22+
socket.on('data', (data) => {
23+
try {
24+
const response = JSON.parse(data.toString().trim());
25+
socket.end();
26+
resolve(response);
27+
} catch (err) {
28+
socket.end();
29+
reject(new Error('Invalid response'));
30+
}
31+
});
32+
33+
socket.on('error', reject);
34+
socket.on('timeout', () => {
35+
socket.end();
36+
reject(new Error('Timeout'));
37+
});
38+
39+
socket.setTimeout(5000);
40+
});
41+
}
42+
43+
async function handleFeatureServerCommand(args) {
44+
const cmd = args[0];
45+
46+
switch (cmd) {
47+
case 'drain':
48+
if (!args[1]) {
49+
console.error('Error: drain requires server IP address');
50+
process.exit(1);
51+
}
52+
const drainResult = await sendCommand('fs-drain', null, null, null, args[1]);
53+
if (drainResult.error || !drainResult.success) {
54+
console.error('Error:', drainResult.error || 'Failed to drain server');
55+
process.exit(1);
56+
}
57+
console.log(`✓ Drained ${drainResult.server}`);
58+
console.log(`Drained: [${drainResult.drained.join(', ')}]`);
59+
break;
60+
61+
case 'undrain':
62+
if (!args[1]) {
63+
console.error('Error: undrain requires server IP address');
64+
process.exit(1);
65+
}
66+
const undrainResult = await sendCommand('fs-undrain', null, null, null, args[1]);
67+
if (undrainResult.error || !undrainResult.success) {
68+
console.error('Error:', undrainResult.error || 'Failed to undrain server');
69+
process.exit(1);
70+
}
71+
console.log(`✓ Undrained ${undrainResult.server}`);
72+
console.log(`Drained: [${undrainResult.drained.join(', ')}]`);
73+
break;
74+
75+
case 'drained':
76+
const drained = await sendCommand('fs-drained');
77+
if (drained.error) {
78+
console.error('Error:', drained.error);
79+
process.exit(1);
80+
}
81+
if (drained.drained.length === 0) {
82+
console.log('No servers drained');
83+
} else {
84+
console.log('Drained servers:');
85+
drained.drained.forEach(s => console.log(` - ${s}`));
86+
}
87+
break;
88+
89+
case 'list':
90+
const list = await sendCommand('fs-list');
91+
if (list.error) {
92+
console.error('Error:', list.error);
93+
process.exit(1);
94+
}
95+
if (list.servers.length === 0) {
96+
console.log('No servers configured');
97+
if (list.drained.length > 0) {
98+
console.log('Orphaned drained servers:');
99+
list.drained.forEach(s => console.log(` - ${s} (orphaned)`));
100+
}
101+
} else {
102+
console.log('Servers:');
103+
list.servers.forEach(({ server, status }) => {
104+
const icon = status === 'drained' ? '🔴' : '🟢';
105+
console.log(` ${icon} ${server} (${status})`);
106+
});
107+
}
108+
break;
109+
110+
default:
111+
console.error('Unknown command:', cmd);
112+
console.error('Use: drain, undrain, drained, list');
113+
process.exit(1);
114+
}
115+
}
116+
117+
async function main() {
118+
const args = process.argv.slice(2);
119+
120+
if (args.length === 0) {
121+
console.log('SBC CLI');
122+
console.log('Usage:');
123+
console.log(' sbc-cli fs drain <ip> Drain server by IP');
124+
console.log(' sbc-cli fs undrain <ip> Undrain server by IP');
125+
console.log(' sbc-cli fs drained List drained server IPs');
126+
console.log(' sbc-cli fs list List all servers');
127+
console.log(' sbc-cli set <key> <value> Set config');
128+
console.log(' sbc-cli get <key> Get config');
129+
console.log(' sbc-cli list List all config');
130+
console.log('');
131+
console.log('Examples:');
132+
console.log(' sbc-cli fs drain 192.168.1.10');
133+
console.log(' sbc-cli fs undrain 192.168.1.10');
134+
console.log(' sbc-cli fs drained');
135+
return;
136+
}
137+
138+
const cmd = args[0];
139+
140+
try {
141+
switch (cmd) {
142+
case 'fs':
143+
if (args.length < 2) {
144+
console.error('fs needs: drain, undrain, drained, or list');
145+
process.exit(1);
146+
}
147+
await handleFeatureServerCommand(args.slice(1));
148+
break;
149+
150+
case 'set':
151+
if (args.length < 3) {
152+
console.error('set needs key and value');
153+
process.exit(1);
154+
}
155+
const setResult = await sendCommand('set', args[1], args[2]);
156+
if (setResult.error) {
157+
console.error('Error:', setResult.error);
158+
process.exit(1);
159+
}
160+
console.log(`✓ ${setResult.key} = ${JSON.stringify(setResult.value)}`);
161+
break;
162+
163+
case 'get':
164+
if (args.length < 2) {
165+
console.error('get needs key');
166+
process.exit(1);
167+
}
168+
const getResult = await sendCommand('get', args[1]);
169+
if (getResult.error) {
170+
console.error('Error:', getResult.error);
171+
process.exit(1);
172+
}
173+
console.log(`${getResult.key} = ${JSON.stringify(getResult.value)}`);
174+
break;
175+
176+
case 'list':
177+
const listResult = await sendCommand('list');
178+
if (listResult.error) {
179+
console.error('Error:', listResult.error);
180+
process.exit(1);
181+
}
182+
console.log('Config:');
183+
for (const [key, value] of Object.entries(listResult.config)) {
184+
console.log(` ${key} = ${JSON.stringify(value)}`);
185+
}
186+
break;
187+
188+
default:
189+
console.error('Unknown command:', cmd);
190+
process.exit(1);
191+
}
192+
} catch (err) {
193+
console.error('Error:', err.message);
194+
process.exit(1);
195+
}
196+
}
197+
198+
if (require.main === module) {
199+
main().catch(err => {
200+
console.error('Error:', err.message);
201+
process.exit(1);
202+
});
203+
}

lib/cli/feature-server-config.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const runtimeConfig = require('./runtime-config');
2+
3+
function isValidIP(ip) {
4+
if (!ip || typeof ip !== 'string') return false;
5+
6+
// IPv4 check
7+
const ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
8+
if (ipv4.test(ip)) return true;
9+
10+
// IPv6 check (basic patterns)
11+
const ipv6 = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/;
12+
if (ipv6.test(ip)) return true;
13+
14+
// IPv6 compressed
15+
const ipv6Short = /^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^[0-9a-fA-F]{1,4}::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$/;
16+
if (ipv6Short.test(ip)) return true;
17+
18+
return false;
19+
}
20+
21+
async function getDrainedServers() {
22+
const drained = await runtimeConfig.get('drainedFeatureServers');
23+
24+
if (!drained) return [];
25+
26+
if (typeof drained === 'string') {
27+
return drained.split(',').map(s => s.trim()).filter(s => s.length > 0);
28+
}
29+
30+
if (Array.isArray(drained)) return drained;
31+
32+
return [];
33+
}
34+
35+
async function isDrained(serverIP) {
36+
const drained = await getDrainedServers();
37+
return drained.includes(serverIP);
38+
}
39+
40+
async function drainServer(serverIP) {
41+
if (!isValidIP(serverIP)) {
42+
return {
43+
added: false,
44+
array: await getDrainedServers(),
45+
error: `Invalid IP: ${serverIP}`
46+
};
47+
}
48+
49+
return await runtimeConfig.addToArray('drainedFeatureServers', serverIP);
50+
}
51+
52+
async function undrainServer(serverIP) {
53+
if (!isValidIP(serverIP)) {
54+
return {
55+
removed: false,
56+
array: await getDrainedServers(),
57+
error: `Invalid IP: ${serverIP}`
58+
};
59+
}
60+
61+
return await runtimeConfig.removeFromArray('drainedFeatureServers', serverIP);
62+
}
63+
64+
async function setDrainedServers(servers) {
65+
let serverList = [];
66+
67+
if (typeof servers === 'string') {
68+
serverList = servers.split(',').map(s => s.trim()).filter(s => s.length > 0);
69+
} else if (Array.isArray(servers)) {
70+
serverList = servers;
71+
} else {
72+
return {
73+
key: 'drainedFeatureServers',
74+
value: await getDrainedServers(),
75+
error: 'Invalid format'
76+
};
77+
}
78+
79+
// Check all IPs are valid
80+
const badIPs = serverList.filter(ip => !isValidIP(ip));
81+
if (badIPs.length > 0) {
82+
return {
83+
key: 'drainedFeatureServers',
84+
value: await getDrainedServers(),
85+
error: `Invalid IPs: ${badIPs.join(', ')}`
86+
};
87+
}
88+
89+
return await runtimeConfig.set('drainedFeatureServers', serverList);
90+
}
91+
92+
module.exports = {
93+
getDrainedServers,
94+
isDrained,
95+
drainServer,
96+
undrainServer,
97+
setDrainedServers,
98+
isValidIP,
99+
runtimeConfig,
100+
101+
// Legacy names for backward compatibility
102+
getDrainedFeatureServers: getDrainedServers,
103+
isFeatureServerDrained: isDrained,
104+
drainFeatureServer: drainServer,
105+
undrainFeatureServer: undrainServer,
106+
setDrainedFeatureServers: setDrainedServers
107+
};

0 commit comments

Comments
 (0)