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.
|
* `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.
|
||||||
@ -1243,4 +1244,4 @@ $ curl -X POST -H "Content-Type: application/json" --data '{"follow":true,"all":
|
|||||||
```
|
```
|
||||||
|
|
||||||
An example project using this endpoint can be found
|
An example project using this endpoint can be found
|
||||||
[in this repository](https://github.com/balena-io-playground/device-cloud-logging).
|
[in this repository](https://github.com/balena-io-playground/device-cloud-logging).
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user