Skip to content

Commit

Permalink
fix: Don't remove devices with linkkey from backup if they are still …
Browse files Browse the repository at this point in the history
…present in the database (#746)

* fix: Don't remove devices with linkkey from backup if they are still present in the database.

* updates
  • Loading branch information
Koenkk authored Aug 17, 2023
1 parent 160d724 commit 3226a87
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 19 deletions.
2 changes: 1 addition & 1 deletion src/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ abstract class Adapter extends events.EventEmitter {

public abstract supportsBackup(): Promise<boolean>;

public abstract backup(): Promise<Models.Backup>;
public abstract backup(ieeeAddressesInDatabase: string[]): Promise<Models.Backup>;

public abstract getNetworkParameters(): Promise<TsType.NetworkParameters>;

Expand Down
27 changes: 25 additions & 2 deletions src/adapter/z-stack/adapter/adapter-backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class AdapterBackup {
/**
* Creates a new backup from connected ZNP adapter and returns it in internal backup model format.
*/
public async createBackup(): Promise<Models.Backup> {
public async createBackup(ieeeAddressesInDatabase: string[]): Promise<Models.Backup> {
this.debug("creating backup");
const version: ZnpVersion = await this.getAdapterVersion();

Expand Down Expand Up @@ -129,7 +129,7 @@ export class AdapterBackup {

/* return backup structure */
/* istanbul ignore next */
return {
const backup: Models.Backup = {
znp: {
version: version,
trustCenterLinkKeySeed: tclkSeed?.key || undefined
Expand Down Expand Up @@ -192,6 +192,29 @@ export class AdapterBackup {
};
}).filter(e => e) || []
};

try {
/**
* Due to a bug in ZStack, some devices go missing from the backed-up device tables which makes them disappear from the backup.
* This causes the devices not to be restored when e.g. re-flashing the adapter.
* If you then try to join a new device via a Zigbee 3.0 router that went missing (those with a linkkey), joning fails as the coordinator
* does not have the linkKey anymore.
* Below we don't remove any devices from the backup which have a linkkey and are still in the database (=ieeeAddressesInDatabase)
*/
const oldBackup = await this.getStoredBackup();
const missing = oldBackup.devices.filter((d) =>
d.linkKey && ieeeAddressesInDatabase.includes(`0x${d.ieeeAddress.toString("hex")}`) &&
!backup.devices.find((dd) => d.ieeeAddress === dd.ieeeAddress));
this.debug(
`Following devices with link key are missing from new backup but present in old backup and database, ` +
`adding them back: ${missing.map((d) => d.ieeeAddress).join(', ')}`
);
backup.devices = [...backup.devices, ...missing];
} catch (error) {
this.debug(`Failed to read old backup, not checking for missing routers: ${error}`);
}

return backup;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/adapter/z-stack/adapter/zStackAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,8 +852,8 @@ class ZStackAdapter extends Adapter {
return true;
}

public async backup(): Promise<Models.Backup> {
return this.adapterManager.backup.createBackup();
public async backup(ieeeAddressesInDatabase: string[]): Promise<Models.Backup> {
return this.adapterManager.backup.createBackup(ieeeAddressesInDatabase);
}

public async setChannelInterPAN(channel: number): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions src/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ class Controller extends events.EventEmitter {
this.databaseSave();
if (this.options.backupPath && await this.adapter.supportsBackup()) {
debug.log('Creating coordinator backup');
const backup = await this.adapter.backup();
const backup = await this.adapter.backup(Device.all().map((d) => d.ieeeAddr));
const unifiedBackup = await BackupUtils.toUnifiedBackup(backup);
const tmpBackupPath = this.options.backupPath + '.tmp';
fs.writeFileSync(tmpBackupPath, JSON.stringify(unifiedBackup, null, 2));
Expand All @@ -336,7 +336,7 @@ class Controller extends events.EventEmitter {

public async coordinatorCheck(): Promise<{missingRouters: Device[]}> {
if (await this.adapter.supportsBackup()) {
const backup = await this.adapter.backup();
const backup = await this.adapter.backup(Device.all().map((d) => d.ieeeAddr));
const devicesInBackup = backup.devices.map((d) => `0x${d.ieeeAddress.toString('hex')}`);
const missingRouters = this.getDevices()
.filter((d) => d.type === 'Router' && !devicesInBackup.includes(d.ieeeAddr));
Expand Down
46 changes: 34 additions & 12 deletions test/adapter/z-stack/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import * as Zcl from '../../../src/zcl';
import * as Constants from '../../../src/adapter/z-stack/constants';
import {ZclDataPayload} from "../../../src/adapter/events";
import {UnifiedBackupStorage} from "../../../src/models";
import {ZnpAdapterManager} from "../../../src/adapter/z-stack/adapter/manager";

const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
const mockSetTimeout = () => setTimeout = jest.fn().mockImplementation((r) => r());
Expand Down Expand Up @@ -1292,7 +1291,7 @@ describe("zstack-adapter", () => {
const result = await adapter.start();
expect(result).toBe("restored");

await adapter.backup();
await adapter.backup([]);
});

it("should restore unified backup with 3.0.x adapter and create backup - no tclk seed", async () => {
Expand All @@ -1305,7 +1304,7 @@ describe("zstack-adapter", () => {
const result = await adapter.start();
expect(result).toBe("restored");

await adapter.backup();
await adapter.backup([]);
});

it("should restore unified backup with 3.x.0 adapter and create backup - empty", async () => {
Expand All @@ -1316,7 +1315,7 @@ describe("zstack-adapter", () => {
const result = await adapter.start();
expect(result).toBe("restored");

await adapter.backup();
await adapter.backup([]);
});

it("should (recommission) restore unified backup with 1.2 adapter and create backup - empty", async () => {
Expand All @@ -1327,7 +1326,7 @@ describe("zstack-adapter", () => {
const result = await adapter.start();
expect(result).toBe("restored");

const backup = await adapter.backup();
const backup = await adapter.backup([]);
expect(backup.networkKeyInfo.frameCounter).toBe(0);
});

Expand All @@ -1344,7 +1343,7 @@ describe("zstack-adapter", () => {
builder.nv(NvItemsIds.LEGACY_NWK_SEC_MATERIAL_TABLE_START + 0, secMaterialTableEntry.serialize("aligned"));
mockZnpRequestWith(builder);

const backup = await adapter.backup();
const backup = await adapter.backup([]);
expect(backup.networkKeyInfo.frameCounter).toBe(2800);
});

Expand All @@ -1356,7 +1355,7 @@ describe("zstack-adapter", () => {
for (let i = 0; i < 4; i++) { builder.nv(NvItemsIds.LEGACY_NWK_SEC_MATERIAL_TABLE_START + i, Buffer.from("000000000000000000000000", "hex")); }
mockZnpRequestWith(builder);

const backup = await adapter.backup();
const backup = await adapter.backup([]);
expect(backup.networkKeyInfo.frameCounter).toBe(1250);
});

Expand All @@ -1372,7 +1371,7 @@ describe("zstack-adapter", () => {
builder.nv(NvItemsIds.LEGACY_NWK_SEC_MATERIAL_TABLE_START + 3, genericEntry.serialize("aligned"));
mockZnpRequestWith(builder);

const backup = await adapter.backup();
const backup = await adapter.backup([]);
expect(backup.networkKeyInfo.frameCounter).toBe(8737);
});

Expand All @@ -1381,10 +1380,33 @@ describe("zstack-adapter", () => {
const result = await adapter.start();
expect(result).toBe("resumed");

const backup = await adapter.backup();
const backup = await adapter.backup([]);
expect(backup.networkKeyInfo.frameCounter).toBe(0);
});

it("should keep missing devices in backup", async () => {
const backupFile = getTempFile();
const backupWithMissingDevice = JSON.parse(JSON.stringify(backupMatchingConfig));
backupWithMissingDevice.devices.push({
"nwk_address": "20fa",
"ieee_address": "00128d11124fa80b",
"link_key": {
"key": "bff550908aa1529ee90eea3c3bdc26fc",
"rx_counter": 0,
"tx_counter": 2
}
});
fs.writeFileSync(backupFile, JSON.stringify(backupMatchingConfig), "utf8");
adapter = new ZStackAdapter(networkOptions, serialPortOptions, backupFile, {concurrent: 3});
mockZnpRequestWith(empty3AlignedRequestMock);
await adapter.start();
fs.writeFileSync(backupFile, JSON.stringify(backupWithMissingDevice), "utf8");
const backup = await adapter.backup(['0x00128d11124fa80b']);
const missingDevice = backup.devices.find((d) => d.ieeeAddress.toString('hex') == '00128d11124fa80b');
expect(missingDevice).not.toBeNull();
expect(missingDevice?.linkKey?.key.toString('hex')).toBe('bff550908aa1529ee90eea3c3bdc26fc');
});

it("should fail when backup file is corrupted - Coordinator backup is corrupted", async () => {
const backupFile = getTempFile();
fs.writeFileSync(backupFile, "{", "utf8");
Expand Down Expand Up @@ -1485,7 +1507,7 @@ describe("zstack-adapter", () => {
);
const result = await adapter.start();
expect(result).toBe("resumed");
await expect(adapter.backup()).rejects.toThrowError("Failed to read adapter IEEE address");
await expect(adapter.backup([])).rejects.toThrowError("Failed to read adapter IEEE address");
});

it("should fail to create backup with 3.0.x adapter - adapter not commissioned - missing nib", async () => {
Expand All @@ -1495,7 +1517,7 @@ describe("zstack-adapter", () => {
expect(result).toBe("reset");
builder.nv(NvItemsIds.NIB, null);
mockZnpRequestWith(builder);
await expect(adapter.backup()).rejects.toThrowError("Cannot backup - adapter not commissioned");
await expect(adapter.backup([])).rejects.toThrowError("Cannot backup - adapter not commissioned");
});

it("should fail to create backup with 3.0.x adapter - missing active key info", async () => {
Expand All @@ -1505,7 +1527,7 @@ describe("zstack-adapter", () => {
expect(result).toBe("reset");
builder.nv(NvItemsIds.NWK_ACTIVE_KEY_INFO, null);
mockZnpRequestWith(builder);
await expect(adapter.backup()).rejects.toThrowError("Cannot backup - missing active key info");
await expect(adapter.backup([])).rejects.toThrowError("Cannot backup - missing active key info");
});

it("should restore legacy backup with 3.0.x adapter - empty", async () => {
Expand Down

0 comments on commit 3226a87

Please sign in to comment.