mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-05-05 02:22:56 +00:00
Merge pull request #1361 from balena-io/report-mac-address
state: Report device MAC address to the API
This commit is contained in:
commit
eccc353ea7
@ -327,6 +327,7 @@ The state is a JSON object that contains some or all of the following:
|
||||
* `api_port`: Port on which the supervisor is listening.
|
||||
* `commit`: Hash of the current commit of the application that is running.
|
||||
* `ip_address`: Space-separated list of IP addresses of the device.
|
||||
* `mac_address`: Space-seperated list of MAC addresses of the device.
|
||||
* `status`: Status of the device regarding the app, as a string, i.e. "Stopping", "Starting", "Downloading", "Installing", "Idle".
|
||||
* `download_progress`: Amount of the application image that has been downloaded, expressed as a percentage. If the update has already been downloaded, this will be `null`.
|
||||
* `os_version`: Version of the host OS running on the device.
|
||||
|
@ -8,6 +8,7 @@ import supervisorVersion = require('../lib/supervisor-version');
|
||||
import * as config from '.';
|
||||
import * as constants from '../lib/constants';
|
||||
import * as osRelease from '../lib/os-release';
|
||||
import * as macAddress from '../lib/mac-address';
|
||||
import log from '../lib/supervisor-console';
|
||||
|
||||
export const fnSchema = {
|
||||
@ -34,6 +35,9 @@ export const fnSchema = {
|
||||
osVariant: () => {
|
||||
return osRelease.getOSVariant(constants.hostOSVersionPath);
|
||||
},
|
||||
macAddress: () => {
|
||||
return macAddress.getAll(constants.macAddressPath);
|
||||
},
|
||||
deviceArch: async () => {
|
||||
try {
|
||||
// FIXME: We should be mounting the following file into the supervisor from the
|
||||
|
@ -192,6 +192,10 @@ export const schemaTypes = {
|
||||
type: t.union([t.string, NullOrUndefined]),
|
||||
default: t.never,
|
||||
},
|
||||
macAddress: {
|
||||
type: t.union([t.string, NullOrUndefined]),
|
||||
default: t.never,
|
||||
},
|
||||
provisioningOptions: {
|
||||
type: t.interface({
|
||||
// These types are taken from the types of the individual
|
||||
|
@ -143,6 +143,7 @@ function createDeviceStateRouter(deviceState: DeviceState) {
|
||||
'api_port',
|
||||
'ip_address',
|
||||
'os_version',
|
||||
'mac_address',
|
||||
'supervisor_version',
|
||||
'update_pending',
|
||||
'update_failed',
|
||||
@ -315,6 +316,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
|
||||
'apiSecret',
|
||||
'osVersion',
|
||||
'osVariant',
|
||||
'macAddress',
|
||||
'version',
|
||||
'provisioned',
|
||||
'apiEndpoint',
|
||||
@ -339,6 +341,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
|
||||
api_secret: conf.apiSecret,
|
||||
os_version: conf.osVersion,
|
||||
os_variant: conf.osVariant,
|
||||
mac_address: conf.macAddress,
|
||||
supervisor_version: conf.version,
|
||||
provisioning_progress: null,
|
||||
provisioning_state: '',
|
||||
@ -393,8 +396,10 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
|
||||
log.debug('Starting periodic check for IP addresses');
|
||||
|
||||
await network.startIPAddressUpdate()(async (addresses) => {
|
||||
const macAddress = await config.get('macAddress');
|
||||
await this.reportCurrentState({
|
||||
ip_address: addresses.join(' '),
|
||||
mac_address: macAddress,
|
||||
});
|
||||
}, constants.ipAddressUpdateInterval);
|
||||
}
|
||||
|
@ -21,6 +21,9 @@ const constants = {
|
||||
hostOSVersionPath:
|
||||
checkString(process.env.HOST_OS_VERSION_PATH) ||
|
||||
`${rootMountPoint}/etc/os-release`,
|
||||
macAddressPath:
|
||||
checkString(process.env.MAC_ADDRESS_PATH) ||
|
||||
`${rootMountPoint}/sys/class/net`,
|
||||
privateAppEnvVars: [
|
||||
'RESIN_SUPERVISOR_API_KEY',
|
||||
'RESIN_API_KEY',
|
||||
|
57
src/lib/mac-address.ts
Normal file
57
src/lib/mac-address.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs, exists } from 'mz/fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import log from './supervisor-console';
|
||||
import { shouldReportInterface } from '../network';
|
||||
|
||||
import TypedError = require('typed-error');
|
||||
export class MacAddressError extends TypedError {}
|
||||
|
||||
export async function getAll(sysClassNet: string): Promise<string | undefined> {
|
||||
try {
|
||||
// read the directories in the sysfs...
|
||||
const interfaces = await fs.readdir(sysClassNet);
|
||||
|
||||
return _(
|
||||
await Promise.all(
|
||||
interfaces.map(async (intf) => {
|
||||
try {
|
||||
const [addressFile, typeFile, masterFile] = [
|
||||
'address',
|
||||
'type',
|
||||
'master',
|
||||
].map((f) => path.join(sysClassNet, intf, f));
|
||||
|
||||
const [intfType, intfHasMaster] = await Promise.all([
|
||||
fs.readFile(typeFile, 'utf8'),
|
||||
exists(masterFile),
|
||||
]);
|
||||
|
||||
// check if we should report this interface at all, or if it is physical interface, or if the interface has a master interface (i.e. it's not the root interface)
|
||||
if (
|
||||
!shouldReportInterface(intf) ||
|
||||
intfType.trim() !== '1' ||
|
||||
intfHasMaster
|
||||
) {
|
||||
// we shouldn't report this MAC address
|
||||
return '';
|
||||
}
|
||||
|
||||
const addr = await fs.readFile(addressFile, 'utf8');
|
||||
return addr.split('\n')[0]?.trim()?.toUpperCase() ?? '';
|
||||
} catch (err) {
|
||||
log.warn('Error reading MAC address for interface', intf, err);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.filter((addr) => addr !== '')
|
||||
.uniq()
|
||||
.join(' ');
|
||||
} catch (err) {
|
||||
log.error(new MacAddressError(`Unable to acquire MAC address: ${err}`));
|
||||
return;
|
||||
}
|
||||
}
|
@ -115,6 +115,9 @@ export const connectivityCheckEnabled = Bluebird.method(
|
||||
);
|
||||
|
||||
const IP_REGEX = /^(?:balena|docker|rce|tun)[0-9]+|tun[0-9]+|resin-vpn|lo|resin-dns|supervisor0|balena-redsocks|resin-redsocks|br-[0-9a-f]{12}$/;
|
||||
|
||||
export const shouldReportInterface = (intf: string) => !IP_REGEX.test(intf);
|
||||
|
||||
export function getIPAddresses(): string[] {
|
||||
// We get IP addresses but ignore:
|
||||
// - docker and balena bridges (docker0, docker1, balena0, etc)
|
||||
@ -126,10 +129,12 @@ export function getIPAddresses(): string[] {
|
||||
// - the docker network for the supervisor API (supervisor0)
|
||||
// - custom docker network bridges (br- + 12 hex characters)
|
||||
return _(os.networkInterfaces())
|
||||
.omitBy((_interfaceFields, interfaceName) => IP_REGEX.test(interfaceName))
|
||||
.filter((_interfaceFields, interfaceName) =>
|
||||
shouldReportInterface(interfaceName),
|
||||
)
|
||||
.flatMap((validInterfaces) => {
|
||||
return _(validInterfaces)
|
||||
.pickBy({ family: 'IPv4' })
|
||||
.pickBy({ family: 'IPv4', internal: false })
|
||||
.map('address')
|
||||
.value();
|
||||
})
|
||||
|
@ -21,6 +21,7 @@ export type DeviceReportFields = Partial<{
|
||||
update_downloaded: boolean;
|
||||
is_on__commit: string;
|
||||
logs_channel: null;
|
||||
mac_address: string | null;
|
||||
}>;
|
||||
|
||||
export interface DeviceStatus {
|
||||
|
@ -7,9 +7,11 @@ import * as constants from '../src/lib/constants';
|
||||
import { docker } from '../src/lib/docker-utils';
|
||||
import { Supervisor } from '../src/supervisor';
|
||||
import { expect } from './lib/chai-config';
|
||||
import _ = require('lodash');
|
||||
|
||||
describe('Startup', () => {
|
||||
let initClientStub: SinonStub;
|
||||
let reportCurrentStateStub: SinonStub;
|
||||
let startStub: SinonStub;
|
||||
let vpnStatusPathStub: SinonStub;
|
||||
let appManagerStub: SinonStub;
|
||||
@ -20,6 +22,10 @@ describe('Startup', () => {
|
||||
initClientStub = stub(APIBinder.prototype as any, 'initClient').returns(
|
||||
Promise.resolve(),
|
||||
);
|
||||
reportCurrentStateStub = stub(
|
||||
DeviceState.prototype as any,
|
||||
'reportCurrentState',
|
||||
).resolves();
|
||||
startStub = stub(APIBinder.prototype as any, 'start').returns(
|
||||
Promise.resolve(),
|
||||
);
|
||||
@ -40,6 +46,7 @@ describe('Startup', () => {
|
||||
vpnStatusPathStub.restore();
|
||||
deviceStateStub.restore();
|
||||
dockerStub.restore();
|
||||
reportCurrentStateStub.restore();
|
||||
});
|
||||
|
||||
it('should startup correctly', async () => {
|
||||
@ -52,5 +59,20 @@ describe('Startup', () => {
|
||||
expect(anySupervisor.logger).to.not.be.null;
|
||||
expect(anySupervisor.deviceState).to.not.be.null;
|
||||
expect(anySupervisor.apiBinder).to.not.be.null;
|
||||
|
||||
let macAddresses: string[] = [];
|
||||
reportCurrentStateStub.getCalls().forEach((call) => {
|
||||
const m: string = call.args[0]['mac_address'];
|
||||
if (!m) {
|
||||
return;
|
||||
}
|
||||
|
||||
macAddresses = _.union(macAddresses, m.split(' '));
|
||||
});
|
||||
|
||||
const allMacAddresses = macAddresses.join(' ');
|
||||
|
||||
expect(allMacAddresses).to.have.length.greaterThan(0);
|
||||
expect(allMacAddresses).to.not.contain('NO:');
|
||||
});
|
||||
});
|
||||
|
@ -113,6 +113,17 @@ describe('SupervisorAPI', () => {
|
||||
});
|
||||
});
|
||||
// TODO: add tests for V1 endpoints
|
||||
describe('GET /v1/device', () => {
|
||||
it('returns MAC address', async () => {
|
||||
const response = await request
|
||||
.get('/v1/device')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', `Bearer ${VALID_SECRET}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).to.have.property('mac_address').that.is.not.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('V2 endpoints', () => {
|
||||
|
1
test/data/sys/class/net/balena0/address
Normal file
1
test/data/sys/class/net/balena0/address
Normal file
@ -0,0 +1 @@
|
||||
NO:NO:NO:NO:NO:NO
|
1
test/data/sys/class/net/balena0/type
Normal file
1
test/data/sys/class/net/balena0/type
Normal file
@ -0,0 +1 @@
|
||||
1
|
1
test/data/sys/class/net/enp0s3/address
Normal file
1
test/data/sys/class/net/enp0s3/address
Normal file
@ -0,0 +1 @@
|
||||
00:11:22:33:44:55
|
1
test/data/sys/class/net/enp0s3/type
Normal file
1
test/data/sys/class/net/enp0s3/type
Normal file
@ -0,0 +1 @@
|
||||
1
|
1
test/data/sys/class/net/enp0s4/address
Normal file
1
test/data/sys/class/net/enp0s4/address
Normal file
@ -0,0 +1 @@
|
||||
66:77:88:99:AA:BB
|
1
test/data/sys/class/net/enp0s4/type
Normal file
1
test/data/sys/class/net/enp0s4/type
Normal file
@ -0,0 +1 @@
|
||||
1
|
1
test/data/sys/class/net/master0/address
Normal file
1
test/data/sys/class/net/master0/address
Normal file
@ -0,0 +1 @@
|
||||
NO:NO:NO:NO:NO:NO
|
0
test/data/sys/class/net/master0/master
Normal file
0
test/data/sys/class/net/master0/master
Normal file
1
test/data/sys/class/net/master0/type
Normal file
1
test/data/sys/class/net/master0/type
Normal file
@ -0,0 +1 @@
|
||||
1
|
1
test/data/sys/class/net/sit0/address
Normal file
1
test/data/sys/class/net/sit0/address
Normal file
@ -0,0 +1 @@
|
||||
NO:NO:NO:NO:NO:NO
|
1
test/data/sys/class/net/sit0/type
Normal file
1
test/data/sys/class/net/sit0/type
Normal file
@ -0,0 +1 @@
|
||||
2
|
@ -75,9 +75,15 @@ async function create(): Promise<SupervisorAPI> {
|
||||
});
|
||||
// Create SupervisorAPI
|
||||
const api = new SupervisorAPI({
|
||||
routers: [buildRoutes(appManager)],
|
||||
routers: [deviceState.router, buildRoutes(appManager)],
|
||||
healthchecks: [deviceState.healthcheck, apiBinder.healthcheck],
|
||||
});
|
||||
|
||||
const macAddress = await config.get('macAddress');
|
||||
deviceState.reportCurrentState({
|
||||
mac_address: macAddress,
|
||||
});
|
||||
|
||||
// Return SupervisorAPI that is not listening yet
|
||||
return api;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user