Skip to content

Commit

Permalink
Merge pull request #733 from particle-iot/feature/get-network-ifaces
Browse files Browse the repository at this point in the history
Get network configuration of a device
  • Loading branch information
keeramis authored May 15, 2024
2 parents dfbe8cf + b919cf5 commit 9477747
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 20 deletions.
14 changes: 7 additions & 7 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"particle-api-js": "^10.3.0",
"particle-commands": "^1.0.1",
"particle-library-manager": "^0.1.15",
"particle-usb": "^2.8.0",
"particle-usb": "^2.9.0",
"request": "https://github.com/particle-iot/request/releases/download/v2.75.1-relativepath.1/request-2.75.1-relativepath.1.tgz",
"safe-buffer": "^5.2.0",
"semver": "^7.5.2",
Expand Down
13 changes: 13 additions & 0 deletions src/cli/usb.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ module.exports = ({ commandProcessor, root }) => {
}
});

commandProcessor.createCommand(usb, 'network-interfaces', 'Gets the network configuration of the device', {
params: '[devices...]',
options: commonOptions,
examples: {
'$0 $command': 'Gets the network configuration of the device',
'$0 $command --all': 'Gets the network configuration of all the devices connected over USB',
'$0 $command my_device': 'Gets the network configuration of the device named "my_device"'
},
handler: (args) => {
return usbCommand().getNetworkIfaces(args);
}
});

return usb;
};

48 changes: 38 additions & 10 deletions src/cli/usb.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,17 @@ describe('USB Command-Line Interface', () => {
'Help: particle help usb <command>',
'',
'Commands:',
' list List the devices connected to the host computer',
' start-listening Put a device into the listening mode',
' listen alias for start-listening',
' stop-listening Make a device exit the listening mode',
' safe-mode Put a device into the safe mode',
' dfu Put a device into the DFU mode',
' reset Reset a device',
' setup-done Set the setup done flag',
' configure Update the system USB configuration',
' cloud-status Check a device\'s cloud connection state',
' list List the devices connected to the host computer',
' start-listening Put a device into the listening mode',
' listen alias for start-listening',
' stop-listening Make a device exit the listening mode',
' safe-mode Put a device into the safe mode',
' dfu Put a device into the DFU mode',
' reset Reset a device',
' setup-done Set the setup done flag',
' configure Update the system USB configuration',
' cloud-status Check a device\'s cloud connection state',
' network-interfaces Gets the network configuration of the device',
''
].join('\n'));
});
Expand Down Expand Up @@ -378,6 +379,33 @@ describe('USB Command-Line Interface', () => {
expect(argv.timeout).to.equal(60000);
});

describe('Handles `usb network-interfaces` Command', () => {
it('Parses arguments', () => {
const argv = commandProcessor.parse(root, ['usb', 'network-interfaces']);
expect(argv.clierror).to.equal(undefined);
expect(argv.all).to.equal(false);
});

it('Includes help with examples', () => {
commandProcessor.parse(root, ['usb', 'network-interfaces', '--help'], termWidth);
commandProcessor.showHelp((helpText) => {
expect(helpText).to.equal([
'Gets the network configuration of the device',
'Usage: particle usb network-interfaces [options] [devices...]',
'',
'Options:',
' --all Send the command to all devices connected to the host computer [boolean]',
'',
'Examples:',
' particle usb network-interfaces Gets the network configuration of the device',
' particle usb network-interfaces --all Gets the network configuration of all the devices connected over USB',
' particle usb network-interfaces my_device Gets the network configuration of the device named "my_device"',
''
].join('\n'));
});
});
});

it('Includes help with examples', () => {
commandProcessor.parse(root, ['usb', 'cloud-status', '--help'], termWidth);
commandProcessor.showHelp((helpText) => {
Expand Down
94 changes: 93 additions & 1 deletion src/cmd/usb.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ const { getUsbDevices, openUsbDevice, openUsbDeviceByIdOrName, TimeoutError, Dev
const { systemSupportsUdev, udevRulesInstalled, installUdevRules } = require('./udev');
const { platformForId, isKnownPlatformId } = require('../lib/platform');
const ParticleApi = require('./api');
const spinnerMixin = require('../lib/spinner-mixin');
const CLICommandBase = require('./base');
const chalk = require('chalk');


module.exports = class UsbCommand {
module.exports = class UsbCommand extends CLICommandBase {
constructor(settings) {
super();
spinnerMixin(this);
this._auth = settings.access_token;
this._api = new ParticleApi(settings.apiUrl, { accessToken: this._auth }).api;
}
Expand Down Expand Up @@ -285,5 +290,92 @@ module.exports = class UsbCommand {
});
});
}

// Helper function to convert CIDR notation to netmask to imitate the 'ifconfig' output
_cidrToNetmask(cidr) {
let mask = [];

// Calculate number of full '1' octets in the netmask
for (let i = 0; i < Math.floor(cidr / 8); i++) {
mask.push(255);
}

// Calculate remaining bits in the next octet
if (mask.length < 4) {
mask.push((256 - Math.pow(2, 8 - cidr % 8)) & 255);
}

// Fill the remaining octets with '0' if any
while (mask.length < 4) {
mask.push(0);
}

return mask.join('.');
}

async getNetworkIfaces(args) {
// define output array with logs to prevent interleaving with the spinner
let output = [];

await this._forEachUsbDevice(args, usbDevice => {
const platform = platformForId(usbDevice.platformId);
return this.getNetworkIfaceInfo(usbDevice)
.then((nwIfaces) => {
const outputData = this._formatNetworkIfaceOutput(nwIfaces, platform.displayName, usbDevice.id);
output = output.concat(outputData);
})
.catch((error) => {
output = output.concat(`Error getting network interfaces (${platform.displayName} / ${usbDevice.id}): ${error.message}\n`);
});
});

if (output.length === 0) {
console.log('No network interfaces found.');
}
output.forEach((str) => console.log(str));
}

async getNetworkIfaceInfo(usbDevice) {
let nwIfaces = [];
const ifaceList = await usbDevice.getNetworkInterfaceList();
for (const iface of ifaceList) {
const ifaceInfo = await usbDevice.getNetworkInterface({ index: iface.index, timeout: 10000 });
nwIfaces.push(ifaceInfo);
}
return nwIfaces;
}

_formatNetworkIfaceOutput(nwIfaces, platform, deviceId) {
const output = [];
output.push(`Device ID: ${chalk.cyan(deviceId)} (${chalk.cyan(platform)})`);
for (const ifaceInfo of nwIfaces) {
const flagsStr = ifaceInfo.flagsStrings.join(',');
output.push(`\t${ifaceInfo.name}(${ifaceInfo.type}): flags=${ifaceInfo.flagsVal}<${flagsStr}> mtu ${ifaceInfo.mtu}`);

// Process IPv4 addresses
if (ifaceInfo?.ipv4Config?.addresses.length > 0) {
for (const address of ifaceInfo.ipv4Config.addresses) {
const [ipv4Address, cidrBits] = address.split('/');
const ipv4NetMask = this._cidrToNetmask(parseInt(cidrBits, 10));
output.push(`\t\tinet ${ipv4Address} netmask ${ipv4NetMask}`);
}
}

// Process IPv6 addresses
if (ifaceInfo?.ipv6Config?.addresses.length > 0) {
for (const address of ifaceInfo.ipv6Config.addresses) {
const [ipv6Address, ipv6Prefix] = address.split('/');
output.push(`\t\tinet6 ${ipv6Address} prefixlen ${ipv6Prefix}`);
}
}

// Process hardware address
if (ifaceInfo?.hwAddress) {
output.push(`\t\tether ${ifaceInfo.hwAddress}`);
}
}
return output;
}

};

112 changes: 112 additions & 0 deletions src/cmd/usb.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const { expect } = require('../../test/setup');
const UsbCommands = require('./usb');


describe('USB Commands', () => {
afterEach(() => {
});


describe('_formatNetworkIfaceOutput', () => {
it('formats the interface information to imitate linux `ifconfig` command', () => {
const nwInfo = [
{
'index': 5,
'name': 'wl4',
'type': 'WIFI',
'hwAddress': '94:94:4a:04:af:80',
'mtu': 1500,
'flagsVal': 98371,
'extFlags': 1114112,
'flagsStrings': ['UP', 'BROADCAST', 'LOWER_UP', 'LOWER_UP', 'MULTICAST', 'NOND6'],
'metric': 0,
'profile': Buffer.alloc(0),
'ipv4Config': {
'addresses': ['10.2.3.4/32'],
'gateway': null,
'peer': null,
'dns': [],
'source': 'NONE'
},
'ipv6Config': {
'addresses': [],
'gateway': null,
'dns': [],
'source': 'NONE'
}
},
{
'index': 4,
'name': 'pp3',
'type': 'PPP',
'hwAddress': '',
'mtu': 1500,
'flagsVal': 81,
'extFlags': 1048576,
'flagsStrings': ['UP', 'POINTOPOINT', 'LOWER_UP', 'LOWER_UP'],
'metric': 0,
'profile': Buffer.alloc(0),
'ipv4Config': {
'addresses': ['10.20.30.40/32'],
'gateway': null,
'peer': null,
'dns': [],
'source': 'NONE'
},
'ipv6Config': {
'addresses': [],
'gateway': null,
'dns': [],
'source': 'NONE'
}
},
{
'index': 1,
'name': 'lo0',
'type': 'LOOPBACK',
'hwAddress': '',
'mtu': 0,
'flagsVal': 73,
'extFlags': 0,
'flagsStrings': ['UP', 'LOOPBACK', 'LOWER_UP', 'LOWER_UP'],
'metric': 0,
'profile': Buffer.alloc(0),
'ipv4Config': {
'addresses': ['10.11.12.13/32'],
'gateway': null,
'peer': null,
'dns': [],
'source': 'NONE'
},
'ipv6Config': {
'addresses': ['0000:0000:0000:0000:0000:0000:0000:0001/64'],
'gateway': null,
'dns': [],
'source': 'NONE'
}
}
];

const expectedOutput = [
'Device ID: 0123456789abcdef (p2)',
'\twl4(WIFI): flags=98371<UP,BROADCAST,LOWER_UP,LOWER_UP,MULTICAST,NOND6> mtu 1500',
'\t\tinet 10.2.3.4 netmask 255.255.255.255',
'\t\tether 94:94:4a:04:af:80',
'\tpp3(PPP): flags=81<UP,POINTOPOINT,LOWER_UP,LOWER_UP> mtu 1500',
'\t\tinet 10.20.30.40 netmask 255.255.255.255',
'\tlo0(LOOPBACK): flags=73<UP,LOOPBACK,LOWER_UP,LOWER_UP> mtu 0',
'\t\tinet 10.11.12.13 netmask 255.255.255.255',
'\t\tinet6 0000:0000:0000:0000:0000:0000:0000:0001 prefixlen 64'
];

const usbCommands = new UsbCommands({
settings: {
access_token: '1234'
},
});
const res = usbCommands._formatNetworkIfaceOutput(nwInfo, 'p2', '0123456789abcdef');

expect(res).to.eql(expectedOutput);
});
});
});
2 changes: 1 addition & 1 deletion test/e2e/help.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('Help & Unknown Command / Argument Handling', () => {
'token create', 'token', 'udp send', 'udp listen', 'udp', 'update',
'update-cli', 'usb list', 'usb start-listening', 'usb listen',
'usb stop-listening', 'usb safe-mode', 'usb dfu', 'usb reset',
'usb setup-done', 'usb configure', 'usb cloud-status', 'usb',
'usb setup-done', 'usb configure', 'usb cloud-status', 'usb network-interfaces', 'usb',
'variable list', 'variable get', 'variable monitor', 'variable',
'webhook create', 'webhook list', 'webhook delete', 'webhook POST',
'webhook GET', 'webhook', 'whoami'];
Expand Down
18 changes: 18 additions & 0 deletions test/e2e/usb.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('USB Commands [@device]', function cliUSBCommands(){
' setup-done Set the setup done flag',
' configure Update the system USB configuration',
' cloud-status Check a device\'s cloud connection state',
' network-interfaces Gets the network configuration of the device',
'',
'Global Options:',
' -v, --verbose Increases how much logging to display [count]',
Expand Down Expand Up @@ -320,5 +321,22 @@ describe('USB Commands [@device]', function cliUSBCommands(){
expect(exitCode).to.equal(1);
});
});

describe('USB network-interfaces Subcommand', () => {
after(async () => {
await cli.resetDevice();
await cli.waitUntilOnline();
});

it('provides network interfaces', async () => {
const ifacePattern = /\w+\(\w+\): flags=\d+<[\w,]+> mtu \d+/;

const { stdout, stderr, exitCode } = await cli.run(['usb', 'network-interfaces']);

expect(stdout).to.match(ifacePattern);
expect(stderr).to.equal('');
expect(exitCode).to.equal(0);
});
});
});

0 comments on commit 9477747

Please sign in to comment.