mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-21 14:37:49 +00:00
state: Report device MAC address to the API
When reporting device information, send the MAC address of any interfaces on the system. Also expose in the Supervisor API at the route GET /v1/device. Change-type: patch Signed-off-by: Rich Bayliss <rich@balena.io>
This commit is contained in:
parent
9d42e3518d
commit
1b91ef3405
@ -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.
|
* `api_port`: Port on which the supervisor is listening.
|
||||||
* `commit`: Hash of the current commit of the application that is running.
|
* `commit`: Hash of the current commit of the application that is running.
|
||||||
* `ip_address`: Space-separated list of IP addresses of the device.
|
* `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".
|
* `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`.
|
* `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.
|
* `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 config from '.';
|
||||||
import * as constants from '../lib/constants';
|
import * as constants from '../lib/constants';
|
||||||
import * as osRelease from '../lib/os-release';
|
import * as osRelease from '../lib/os-release';
|
||||||
|
import * as macAddress from '../lib/mac-address';
|
||||||
import log from '../lib/supervisor-console';
|
import log from '../lib/supervisor-console';
|
||||||
|
|
||||||
export const fnSchema = {
|
export const fnSchema = {
|
||||||
@ -34,6 +35,9 @@ export const fnSchema = {
|
|||||||
osVariant: () => {
|
osVariant: () => {
|
||||||
return osRelease.getOSVariant(constants.hostOSVersionPath);
|
return osRelease.getOSVariant(constants.hostOSVersionPath);
|
||||||
},
|
},
|
||||||
|
macAddress: () => {
|
||||||
|
return macAddress.getAll(constants.macAddressPath);
|
||||||
|
},
|
||||||
deviceArch: async () => {
|
deviceArch: async () => {
|
||||||
try {
|
try {
|
||||||
// FIXME: We should be mounting the following file into the supervisor from the
|
// 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]),
|
type: t.union([t.string, NullOrUndefined]),
|
||||||
default: t.never,
|
default: t.never,
|
||||||
},
|
},
|
||||||
|
macAddress: {
|
||||||
|
type: t.union([t.string, NullOrUndefined]),
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
provisioningOptions: {
|
provisioningOptions: {
|
||||||
type: t.interface({
|
type: t.interface({
|
||||||
// These types are taken from the types of the individual
|
// These types are taken from the types of the individual
|
||||||
|
@ -143,6 +143,7 @@ function createDeviceStateRouter(deviceState: DeviceState) {
|
|||||||
'api_port',
|
'api_port',
|
||||||
'ip_address',
|
'ip_address',
|
||||||
'os_version',
|
'os_version',
|
||||||
|
'mac_address',
|
||||||
'supervisor_version',
|
'supervisor_version',
|
||||||
'update_pending',
|
'update_pending',
|
||||||
'update_failed',
|
'update_failed',
|
||||||
@ -315,6 +316,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
|
|||||||
'apiSecret',
|
'apiSecret',
|
||||||
'osVersion',
|
'osVersion',
|
||||||
'osVariant',
|
'osVariant',
|
||||||
|
'macAddress',
|
||||||
'version',
|
'version',
|
||||||
'provisioned',
|
'provisioned',
|
||||||
'apiEndpoint',
|
'apiEndpoint',
|
||||||
@ -339,6 +341,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
|
|||||||
api_secret: conf.apiSecret,
|
api_secret: conf.apiSecret,
|
||||||
os_version: conf.osVersion,
|
os_version: conf.osVersion,
|
||||||
os_variant: conf.osVariant,
|
os_variant: conf.osVariant,
|
||||||
|
mac_address: conf.macAddress,
|
||||||
supervisor_version: conf.version,
|
supervisor_version: conf.version,
|
||||||
provisioning_progress: null,
|
provisioning_progress: null,
|
||||||
provisioning_state: '',
|
provisioning_state: '',
|
||||||
@ -393,8 +396,10 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
|
|||||||
log.debug('Starting periodic check for IP addresses');
|
log.debug('Starting periodic check for IP addresses');
|
||||||
|
|
||||||
await network.startIPAddressUpdate()(async (addresses) => {
|
await network.startIPAddressUpdate()(async (addresses) => {
|
||||||
|
const macAddress = await config.get('macAddress');
|
||||||
await this.reportCurrentState({
|
await this.reportCurrentState({
|
||||||
ip_address: addresses.join(' '),
|
ip_address: addresses.join(' '),
|
||||||
|
mac_address: macAddress,
|
||||||
});
|
});
|
||||||
}, constants.ipAddressUpdateInterval);
|
}, constants.ipAddressUpdateInterval);
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,9 @@ const constants = {
|
|||||||
hostOSVersionPath:
|
hostOSVersionPath:
|
||||||
checkString(process.env.HOST_OS_VERSION_PATH) ||
|
checkString(process.env.HOST_OS_VERSION_PATH) ||
|
||||||
`${rootMountPoint}/etc/os-release`,
|
`${rootMountPoint}/etc/os-release`,
|
||||||
|
macAddressPath:
|
||||||
|
checkString(process.env.MAC_ADDRESS_PATH) ||
|
||||||
|
`${rootMountPoint}/sys/class/net`,
|
||||||
privateAppEnvVars: [
|
privateAppEnvVars: [
|
||||||
'RESIN_SUPERVISOR_API_KEY',
|
'RESIN_SUPERVISOR_API_KEY',
|
||||||
'RESIN_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}$/;
|
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[] {
|
export function getIPAddresses(): string[] {
|
||||||
// We get IP addresses but ignore:
|
// We get IP addresses but ignore:
|
||||||
// - docker and balena bridges (docker0, docker1, balena0, etc)
|
// - docker and balena bridges (docker0, docker1, balena0, etc)
|
||||||
@ -126,10 +129,12 @@ export function getIPAddresses(): string[] {
|
|||||||
// - the docker network for the supervisor API (supervisor0)
|
// - the docker network for the supervisor API (supervisor0)
|
||||||
// - custom docker network bridges (br- + 12 hex characters)
|
// - custom docker network bridges (br- + 12 hex characters)
|
||||||
return _(os.networkInterfaces())
|
return _(os.networkInterfaces())
|
||||||
.omitBy((_interfaceFields, interfaceName) => IP_REGEX.test(interfaceName))
|
.filter((_interfaceFields, interfaceName) =>
|
||||||
|
shouldReportInterface(interfaceName),
|
||||||
|
)
|
||||||
.flatMap((validInterfaces) => {
|
.flatMap((validInterfaces) => {
|
||||||
return _(validInterfaces)
|
return _(validInterfaces)
|
||||||
.pickBy({ family: 'IPv4' })
|
.pickBy({ family: 'IPv4', internal: false })
|
||||||
.map('address')
|
.map('address')
|
||||||
.value();
|
.value();
|
||||||
})
|
})
|
||||||
|
@ -21,6 +21,7 @@ export type DeviceReportFields = Partial<{
|
|||||||
update_downloaded: boolean;
|
update_downloaded: boolean;
|
||||||
is_on__commit: string;
|
is_on__commit: string;
|
||||||
logs_channel: null;
|
logs_channel: null;
|
||||||
|
mac_address: string | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export interface DeviceStatus {
|
export interface DeviceStatus {
|
||||||
|
@ -7,9 +7,11 @@ import * as constants from '../src/lib/constants';
|
|||||||
import { docker } from '../src/lib/docker-utils';
|
import { docker } from '../src/lib/docker-utils';
|
||||||
import { Supervisor } from '../src/supervisor';
|
import { Supervisor } from '../src/supervisor';
|
||||||
import { expect } from './lib/chai-config';
|
import { expect } from './lib/chai-config';
|
||||||
|
import _ = require('lodash');
|
||||||
|
|
||||||
describe('Startup', () => {
|
describe('Startup', () => {
|
||||||
let initClientStub: SinonStub;
|
let initClientStub: SinonStub;
|
||||||
|
let reportCurrentStateStub: SinonStub;
|
||||||
let startStub: SinonStub;
|
let startStub: SinonStub;
|
||||||
let vpnStatusPathStub: SinonStub;
|
let vpnStatusPathStub: SinonStub;
|
||||||
let appManagerStub: SinonStub;
|
let appManagerStub: SinonStub;
|
||||||
@ -20,6 +22,10 @@ describe('Startup', () => {
|
|||||||
initClientStub = stub(APIBinder.prototype as any, 'initClient').returns(
|
initClientStub = stub(APIBinder.prototype as any, 'initClient').returns(
|
||||||
Promise.resolve(),
|
Promise.resolve(),
|
||||||
);
|
);
|
||||||
|
reportCurrentStateStub = stub(
|
||||||
|
DeviceState.prototype as any,
|
||||||
|
'reportCurrentState',
|
||||||
|
).resolves();
|
||||||
startStub = stub(APIBinder.prototype as any, 'start').returns(
|
startStub = stub(APIBinder.prototype as any, 'start').returns(
|
||||||
Promise.resolve(),
|
Promise.resolve(),
|
||||||
);
|
);
|
||||||
@ -40,6 +46,7 @@ describe('Startup', () => {
|
|||||||
vpnStatusPathStub.restore();
|
vpnStatusPathStub.restore();
|
||||||
deviceStateStub.restore();
|
deviceStateStub.restore();
|
||||||
dockerStub.restore();
|
dockerStub.restore();
|
||||||
|
reportCurrentStateStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should startup correctly', async () => {
|
it('should startup correctly', async () => {
|
||||||
@ -52,5 +59,20 @@ describe('Startup', () => {
|
|||||||
expect(anySupervisor.logger).to.not.be.null;
|
expect(anySupervisor.logger).to.not.be.null;
|
||||||
expect(anySupervisor.deviceState).to.not.be.null;
|
expect(anySupervisor.deviceState).to.not.be.null;
|
||||||
expect(anySupervisor.apiBinder).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
|
// 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', () => {
|
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
|
// Create SupervisorAPI
|
||||||
const api = new SupervisorAPI({
|
const api = new SupervisorAPI({
|
||||||
routers: [buildRoutes(appManager)],
|
routers: [deviceState.router, buildRoutes(appManager)],
|
||||||
healthchecks: [deviceState.healthcheck, apiBinder.healthcheck],
|
healthchecks: [deviceState.healthcheck, apiBinder.healthcheck],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const macAddress = await config.get('macAddress');
|
||||||
|
deviceState.reportCurrentState({
|
||||||
|
mac_address: macAddress,
|
||||||
|
});
|
||||||
|
|
||||||
// Return SupervisorAPI that is not listening yet
|
// Return SupervisorAPI that is not listening yet
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user