diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/CheckComplianceTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/CheckComplianceTask.java index 391091e5..0bfc8087 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/CheckComplianceTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/CheckComplianceTask.java @@ -93,10 +93,15 @@ public void prepare(Session session) { */ @Override public void run() { - log.debug("Task {}. Starting check compliance task for device {}.", this.getId(), device.getId()); - this.trace(String.format("Check compliance task for device %s (%s).", + log.debug("Task {}. Starting check compliance task for device {}.", this.getId(), + device == null ? "null" : device.getId()); + if (device == null) { + this.info("The device doesn't exist, the task will be cancelled."); + this.status = Status.CANCELLED; + return; + } + this.info(String.format("Check compliance task for device %s (%s).", device.getName(), device.getMgmtAddress().getIp())); - Session session = Database.getSession(); try { session.beginTransaction(); diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/CheckGroupComplianceTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/CheckGroupComplianceTask.java index c2e2ae45..a580a90e 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/CheckGroupComplianceTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/CheckGroupComplianceTask.java @@ -115,7 +115,13 @@ public Object clone() throws CloneNotSupportedException { */ @Override public void run() { - log.debug("Task {}. Starting check compliance task for group {}.", this.getId(), deviceGroup.getId()); + log.debug("Task {}. Starting check compliance task for group {}.", + this.getId(), deviceGroup == null ? "null" : deviceGroup.getId()); + if (deviceGroup == null) { + this.info("The device group doesn't exist, the task will be cancelled."); + this.status = Status.CANCELLED; + return; + } this.trace(String.format("Check compliance task for group %s.", deviceGroup.getName())); diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/CheckGroupSoftwareTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/CheckGroupSoftwareTask.java index b4148dc2..981a4205 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/CheckGroupSoftwareTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/CheckGroupSoftwareTask.java @@ -118,7 +118,12 @@ public Object clone() throws CloneNotSupportedException { @Override public void run() { log.debug("Task {}. Starting check software compliance and hardware support status task for group {}.", - this.getId(), deviceGroup.getId()); + this.getId(), deviceGroup == null ? "null" : deviceGroup.getId()); + if (deviceGroup == null) { + this.info("The device group doesn't exist, the task will be cancelled."); + this.status = Status.CANCELLED; + return; + } this.trace(String.format("Check software compliance task for group %s.", deviceGroup.getName())); diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/DiscoverDeviceTypeTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/DiscoverDeviceTypeTask.java index 02351a9d..57f1abcd 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/DiscoverDeviceTypeTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/DiscoverDeviceTypeTask.java @@ -484,10 +484,10 @@ public JobKey getIdentity() { @XmlElement @JsonView(DefaultView.class) @Transient protected long getDeviceId() { - if (device == null) { + if (this.device == null) { return 0; } - return device.getId(); + return this.device.getId(); } } diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/RunDeviceGroupScriptTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/RunDeviceGroupScriptTask.java index 5a83e1b5..7e9befc1 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/RunDeviceGroupScriptTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/RunDeviceGroupScriptTask.java @@ -111,7 +111,9 @@ public String getTaskDescription() { @Override public void prepare(Session session) { Hibernate.initialize(this.getDeviceGroup()); - Hibernate.initialize(this.getDeviceGroup().getCachedDevices()); + if (this.getDeviceGroup() != null) { + Hibernate.initialize(this.getDeviceGroup().getCachedDevices()); + } } /* (non-Javadoc) @@ -120,7 +122,12 @@ public void prepare(Session session) { @Override public void run() { log.debug("Task {}. Starting run script task for group {}.", - this.getId(), this.getDeviceGroup().getId()); + this.getId(), deviceGroup == null ? "null" : deviceGroup.getId()); + if (deviceGroup == null) { + this.info("The device group doesn't exist, the task will be cancelled."); + this.status = Status.CANCELLED; + return; + } Set devices = this.getDeviceGroup().getCachedDevices(); log.debug("Task {}. {} devices in the group.", this.getId(), devices.size()); String comment = String.format("Started due to group %s script task", this.getDeviceGroup().getName()); diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/RunDeviceScriptTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/RunDeviceScriptTask.java index 2164c2c7..6792340d 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/RunDeviceScriptTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/RunDeviceScriptTask.java @@ -112,7 +112,13 @@ public void prepare(Session session) { @Override public void run() { - log.debug("Task {}. Starting script task for device {}.", this.getId(), device.getId()); + log.debug("Task {}. Starting script task for device {}.", this.getId(), + device == null ? "null" : device.getId()); + if (device == null) { + this.info("The device doesn't exist, the task will be cancelled."); + this.status = Status.CANCELLED; + return; + } this.info(String.format("Run script task for device %s (%s).", device.getName(), device.getMgmtAddress().getIp())); diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/RunDiagnosticsTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/RunDiagnosticsTask.java index b745bf32..1874a8e6 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/RunDiagnosticsTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/RunDiagnosticsTask.java @@ -120,7 +120,10 @@ public String getTaskDescription() { @XmlElement @JsonView(DefaultView.class) @Transient public long getDeviceId() { - return device.getId(); + if (this.device == null) { + return 0; + } + return this.device.getId(); } /* (non-Javadoc) @@ -135,8 +138,13 @@ public Object clone() throws CloneNotSupportedException { @Override public void run() { - log.debug("Task {}. Starting diagnostic task for device {}.", - this.getId(), device.getId()); + log.debug("Task {}. Starting diagnostic task for device {}.", this.getId(), + device == null ? "null" : device.getId()); + if (device == null) { + this.info("The device doesn't exist, the task will be cancelled."); + this.status = Status.CANCELLED; + return; + } this.trace(String.format("Run diagnostic task for device %s (%s).", device.getName(), device.getMgmtAddress().getIp())); boolean locked = false; diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/RunGroupDiagnosticsTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/RunGroupDiagnosticsTask.java index c85a087c..4b9f0f19 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/RunGroupDiagnosticsTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/RunGroupDiagnosticsTask.java @@ -104,7 +104,9 @@ public String getTaskDescription() { @Override public void prepare(Session session) { Hibernate.initialize(this.getDeviceGroup()); - Hibernate.initialize(this.getDeviceGroup().getCachedDevices()); + if (this.getDeviceGroup() != null) { + Hibernate.initialize(this.getDeviceGroup().getCachedDevices()); + } } /* (non-Javadoc) @@ -113,7 +115,12 @@ public void prepare(Session session) { @Override public void run() { log.debug("Task {}. Starting diagnostics task for group {}.", - this.getId(), this.getDeviceGroup().getId()); + this.getId(), deviceGroup == null ? "null" : deviceGroup.getId()); + if (deviceGroup == null) { + this.info("The device group doesn't exist, the task will be cancelled."); + this.status = Status.CANCELLED; + return; + } Set devices = this.getDeviceGroup().getCachedDevices(); log.debug("Task {}. {} devices in the group.", this.getId(), devices.size()); String comment = String.format("Started due to group %s diagnotics", this.getDeviceGroup().getName()); diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/TakeGroupSnapshotTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/TakeGroupSnapshotTask.java index c4f3504b..5fb73b73 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/TakeGroupSnapshotTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/TakeGroupSnapshotTask.java @@ -124,7 +124,9 @@ public String getTaskDescription() { @Override public void prepare(Session session) { Hibernate.initialize(this.getDeviceGroup()); - Hibernate.initialize(this.getDeviceGroup().getCachedDevices()); + if (this.getDeviceGroup() != null) { + Hibernate.initialize(this.getDeviceGroup().getCachedDevices()); + } } /* (non-Javadoc) @@ -133,7 +135,12 @@ public void prepare(Session session) { @Override public void run() { log.debug("Task {}. Starting snapshot task for group {}.", - this.getId(), this.getDeviceGroup().getId()); + this.getId(), deviceGroup == null ? "null" : deviceGroup.getId()); + if (deviceGroup == null) { + this.info("The device group doesn't exist, the task will be cancelled."); + this.status = Status.CANCELLED; + return; + } Set devices = this.getDeviceGroup().getCachedDevices(); log.debug("Task {}. {} devices in the group.", this.getId(), devices.size()); String comment = String.format("Started due to group %s snapshot", this.getDeviceGroup().getName()); diff --git a/src/main/java/onl/netfishers/netshot/work/tasks/TakeSnapshotTask.java b/src/main/java/onl/netfishers/netshot/work/tasks/TakeSnapshotTask.java index 8c9e1bd9..2d7b98b3 100644 --- a/src/main/java/onl/netfishers/netshot/work/tasks/TakeSnapshotTask.java +++ b/src/main/java/onl/netfishers/netshot/work/tasks/TakeSnapshotTask.java @@ -194,7 +194,13 @@ public void prepare(Session session) { */ @Override public void run() { - log.debug("Task {}. Starting snapshot task for device {}.", this.getId(), device.getId()); + log.debug("Task {}. Starting snapshot task for device {}.", this.getId(), + device == null ? "null" : device.getId()); + if (device == null) { + this.info("The device doesn't exist, the task will be cancelled."); + this.status = Status.CANCELLED; + return; + } this.info(String.format("Snapshot task for device %s (%s).", device.getName(), device.getMgmtAddress().getIp())); boolean locked = false; @@ -297,6 +303,9 @@ public String getTaskDescription() { @XmlElement @JsonView(DefaultView.class) @Transient protected long getDeviceId() { + if (this.device == null) { + return 0; + } return device.getId(); } diff --git a/src/main/resources/migration/netshot0.xml b/src/main/resources/migration/netshot0.xml index e0d22c07..53d72ac7 100644 --- a/src/main/resources/migration/netshot0.xml +++ b/src/main/resources/migration/netshot0.xml @@ -1324,4 +1324,49 @@ + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM task WHERE id IN ( + SELECT t1_0.id from task t1_0 + LEFT JOIN check_compliance_task t1_1 on t1_0.id = t1_1.id + LEFT JOIN check_group_compliance_task t1_2 on t1_0.id = t1_2.id + LEFT JOIN check_group_software_task t1_3 on t1_0.id = t1_3.id + LEFT JOIN discover_device_type_task t1_4 on t1_0.id = t1_4.id + LEFT JOIN purge_database_task t1_5 on t1_0.id = t1_5.id + LEFT JOIN run_device_group_script_task t1_6 on t1_0.id = t1_6.id + LEFT JOIN run_device_script_task t1_7 on t1_0.id = t1_7.id + LEFT JOIN run_diagnostics_task t1_8 on t1_0.id = t1_8.id + LEFT JOIN run_group_diagnostics_task t1_9 on t1_0.id = t1_9.id + LEFT JOIN scan_subnets_task t1_10 on t1_0.id = t1_10.id + LEFT JOIN take_group_snapshot_task t1_11 on t1_0.id = t1_11.id + LEFT JOIN take_snapshot_task t1_12 on t1_0.id = t1_12.id + WHERE + t1_1.id is null and t1_2.id is null and t1_3.id is null and + t1_4.id is null and t1_5.id is null and t1_6.id is null and + t1_7.id is null and t1_8.id is null and t1_9.id is null and + t1_10.id is null and t1_11.id is null and t1_12.id is null + ) + + + diff --git a/src/test/java/onl/netfishers/netshot/RestServiceTest.java b/src/test/java/onl/netfishers/netshot/RestServiceTest.java index 7a6651c0..23de0ca6 100644 --- a/src/test/java/onl/netfishers/netshot/RestServiceTest.java +++ b/src/test/java/onl/netfishers/netshot/RestServiceTest.java @@ -33,8 +33,12 @@ import onl.netfishers.netshot.aaa.PasswordPolicy.PasswordPolicyException; import onl.netfishers.netshot.aaa.UiUser; import onl.netfishers.netshot.database.Database; +import onl.netfishers.netshot.device.Device; +import onl.netfishers.netshot.device.DeviceDriver; import onl.netfishers.netshot.device.Domain; import onl.netfishers.netshot.device.Network4Address; +import onl.netfishers.netshot.device.DeviceDriver.DriverProtocol; +import onl.netfishers.netshot.device.attribute.AttributeDefinition; import onl.netfishers.netshot.rest.NetshotBadRequestException; import onl.netfishers.netshot.rest.RestService; @@ -85,6 +89,7 @@ protected static void initNetshot() throws Exception { Netshot.initConfig(RestServiceTest.getNetshotConfig()); Database.update(); Database.init(); + TaskManager.init(); RestService.init(); Thread.sleep(1000); } @@ -92,12 +97,24 @@ protected static void initNetshot() throws Exception { private NetshotApiClient apiClient; @BeforeEach - void createData() { + void createToken() { RestServiceTest.createApiTokens(); this.apiClient = new NetshotApiClient(RestServiceTest.apiUrl, RestServiceTest.API_TOKENS.get(UiUser.LEVEL_ADMIN)); } + @AfterEach + void flushTokens() { + try (Session session = Database.getSession()) { + session.beginTransaction(); + session + .createMutationQuery("delete from ApiToken") + .executeUpdate(); + session.getTransaction().commit(); + } + } + + @Nested @DisplayName("Authentication Tests") class ApiTokenTest { @@ -109,9 +126,6 @@ void cleanUpData() { session .createMutationQuery("delete from onl.netfishers.netshot.aaa.UiUser") .executeUpdate(); - session - .createMutationQuery("delete from ApiToken") - .executeUpdate(); session.getTransaction().commit(); } } @@ -516,9 +530,6 @@ void cleanUpData() { session .createMutationQuery("delete from Domain") .executeUpdate(); - session - .createMutationQuery("delete from ApiToken") - .executeUpdate(); session.getTransaction().commit(); } } @@ -731,9 +742,6 @@ void cleanUpData() { session .createMutationQuery("delete from onl.netfishers.netshot.aaa.UiUser") .executeUpdate(); - session - .createMutationQuery("delete from ApiToken") - .executeUpdate(); session.getTransaction().commit(); } } @@ -893,17 +901,6 @@ void updateUser() throws IOException, InterruptedException { @DisplayName("API token management API Tests") class ApiTokenTest { - @AfterEach - void cleanUpData() { - try (Session session = Database.getSession()) { - session.beginTransaction(); - session - .createMutationQuery("delete from ApiToken") - .executeUpdate(); - session.getTransaction().commit(); - } - } - @Test @DisplayName("List tokens") @ResourceLock(value = "DB") @@ -973,5 +970,237 @@ void deleteToken() throws IOException, InterruptedException { } } + + @Nested + @DisplayName("Device tests") + @ResourceLock(value = "DB") + class DeviceTest { + + private String testDomainName = "Domain 1"; + private Domain testDomain = null; + + private void createTestDomain() throws IOException { + try (Session session = Database.getSession()) { + session.beginTransaction(); + this.testDomain = new Domain( + this.testDomainName, "Test Domain for devices", + new Network4Address("10.1.1.1"), + null + ); + session.persist(this.testDomain); + session.getTransaction().commit(); + } + } + + private DeviceDriver getTestDriver() { + return DeviceDriver.getDriverByName("CiscoIOS12"); + } + + @BeforeAll + static void loadDrivers() throws Exception { + DeviceDriver.refreshDrivers(); + } + + @AfterEach + void cleanUpData() { + try (Session session = Database.getSession()) { + session.beginTransaction(); + session + .createMutationQuery("delete from Device") + .executeUpdate(); + session + .createMutationQuery("delete from Domain") + .executeUpdate(); + session.getTransaction().commit(); + } + } + + @Test + @DisplayName("List device types") + void listDeviceTypes() throws Exception { + List drivers = DeviceDriver.getAllDrivers(); + HttpResponse response = apiClient.get("/devicetypes"); + Assertions.assertEquals( + Response.Status.OK.getStatusCode(), response.statusCode(), + "Not getting 200 response for device type list"); + Assertions.assertEquals( + drivers.size(), response.body().size(), + "Not getting the right number of device drivers"); + DeviceDriver testDriver = this.getTestDriver(); + JsonNode testDriverNode = null; + Iterator driverIt = response.body().iterator(); + while (driverIt.hasNext()) { + JsonNode driverNode = driverIt.next(); + if (driverNode.get("name").asText().equals(testDriver.getName())) { + testDriverNode = driverNode; + } + } + Assertions.assertNotNull(testDriverNode, "API didn't return test driver"); + + ObjectNode targetData = JsonNodeFactory.instance.objectNode() + .put("name", testDriver.getName()) + .put("author", testDriver.getAuthor()) + .put("description", testDriver.getDescription()) + .put("version", testDriver.getVersion()) + .put("priority", Long.valueOf(testDriver.getPriority())); + for (AttributeDefinition attribute : testDriver.getAttributes()) { + targetData.withArray("attributes").add( + JsonNodeFactory.instance.objectNode() + .put("type", attribute.getType().toString()) + .put("level", attribute.getLevel().toString()) + .put("name", attribute.getName()) + .put("title", attribute.getTitle()) + .put("comparable", attribute.isComparable()) + .put("searchable", attribute.isSearchable()) + .put("checkable", attribute.isCheckable()) + ); + } + for (DriverProtocol protocol : testDriver.getProtocols()) { + targetData.withArray("protocols") + .add(protocol.toString()); + } + for (String mode : testDriver.getCliMainModes()) { + targetData.withArray("cliMainModes").add(mode); + } + targetData.put("sourceHash", testDriver.getSourceHash()) + .set("location", JsonNodeFactory.instance.objectNode() + .put("type", testDriver.getLocation().getType().toString()) + .put("fileName", testDriver.getLocation().getFileName())); + if (testDriver.getSshConfig() != null) { + targetData.set("sshConfig", JsonNodeFactory.instance.objectNode()); + } + if (testDriver.getTelnetConfig() != null) { + targetData.set("telnetConfig", + JsonNodeFactory.instance.objectNode() + .put("terminalType", testDriver.getTelnetConfig().getTerminalType())); + } + Assertions.assertEquals(targetData, testDriverNode, + "Retrieved device type doesn't match expected object"); + } + + @Test + @DisplayName("List devices") + @ResourceLock(value = "DB") + void listDevices() throws IOException, InterruptedException { + { + HttpResponse response = apiClient.get("/devices"); + Assertions.assertEquals( + Response.Status.OK.getStatusCode(), response.statusCode(), + "Not getting 200 response for initial list"); + Assertions.assertEquals(0, response.body().size(), + "Device list is not empty"); + } + this.createTestDomain(); + Device device1 = new Device(this.getTestDriver().getName(), + new Network4Address("10.1.1.1"), this.testDomain, "test"); + device1.setName("device1"); + Device device2 = new Device(this.getTestDriver().getName(), + new Network4Address("10.1.1.2"), this.testDomain, "test"); + device2.setName("device2"); + try (Session session = Database.getSession()) { + session.beginTransaction(); + session.persist(device1); + session.persist(device2); + session.getTransaction().commit(); + } + { + HttpResponse response = apiClient.get("/devices"); + Assertions.assertEquals( + Response.Status.OK.getStatusCode(), response.statusCode(), + "Not getting 200 response for device list"); + Assertions.assertEquals(2, response.body().size(), + "Device list doesn't have 2 elements"); + Iterator deviceNodeIt = response.body().iterator(); + JsonNode deviceNode1 = deviceNodeIt.next(); + Assertions.assertEquals( + JsonNodeFactory.instance.objectNode() + .put("id", device1.getId()) + .put("name", device1.getName()) + .put("family", device1.getFamily()) + .put("mgmtAddress", device1.getMgmtAddress().getIp()) + .put("status", device1.getStatus().toString()) + .put("driver", device1.getDriver()) + .put("eol", device1.isEndOfLife()) + .put("eos", device1.isEndOfSale()) + .put("configCompliant", device1.isCompliant()) + .put("softwareLevel", device1.getSoftwareLevel().toString()), + deviceNode1, + "Retrieved device doesn't match expected object"); + } + } + + @Test + @DisplayName("Create device") + @ResourceLock(value = "DB") + void createDevice() throws IOException, InterruptedException { + this.createTestDomain(); + Device device1 = new Device(this.getTestDriver().getName(), + new Network4Address("10.1.1.1"), this.testDomain, "test"); + ObjectNode data = JsonNodeFactory.instance.objectNode() + .put("autoDiscover", false) + .put("ipAddress", device1.getMgmtAddress().getIp()) + .put("domainId", this.testDomain.getId()) + .put("name", device1.getName()) + .put("deviceType", device1.getDriver()); + HttpResponse response = apiClient.post("/devices", data); + Assertions.assertEquals( + Response.Status.CREATED.getStatusCode(), response.statusCode(), + "Not getting 201 response for created device"); + + try (Session session = Database.getSession()) { + Device newDevice = session + .createQuery("from Device d", Device.class) + .uniqueResult(); + device1.setId(newDevice.getId()); + Assertions.assertEquals(device1, newDevice, "Device not created as expected"); + } + } + + @Test + @DisplayName("Delete device") + @ResourceLock(value = "DB") + void deleteDevice() throws IOException, InterruptedException { + this.createTestDomain(); + Device device1 = new Device(this.getTestDriver().getName(), + new Network4Address("10.1.1.1"), this.testDomain, "test"); + device1.setName("device1"); + Device device2 = new Device(this.getTestDriver().getName(), + new Network4Address("10.1.1.2"), this.testDomain, "test"); + device2.setName("device2"); + try (Session session = Database.getSession()) { + session.beginTransaction(); + session.persist(device1); + session.persist(device2); + session.getTransaction().commit(); + } + HttpResponse response = apiClient.delete( + String.format("/devices/%d", device1.getId())); + Assertions.assertEquals( + Response.Status.NO_CONTENT.getStatusCode(), response.statusCode(), + "Not getting 204 response for device deletion"); + HttpResponse listResponse = apiClient.get("/devices"); + Assertions.assertEquals( + Response.Status.OK.getStatusCode(), listResponse.statusCode(), + "Not getting 200 response for device listing"); + Assertions.assertEquals(1, listResponse.body().size(), + "Device list doesn't have 1 element"); + JsonNode deviceNode = listResponse.body().iterator().next(); + Assertions.assertEquals( + JsonNodeFactory.instance.objectNode() + .put("id", device2.getId()) + .put("name", device2.getName()) + .put("family", device2.getFamily()) + .put("mgmtAddress", device2.getMgmtAddress().getIp()) + .put("status", device2.getStatus().toString()) + .put("driver", device2.getDriver()) + .put("eol", device2.isEndOfLife()) + .put("eos", device2.isEndOfSale()) + .put("configCompliant", device2.isCompliant()) + .put("softwareLevel", device2.getSoftwareLevel().toString()), + deviceNode, + "Retrieved device doesn't match expected object"); + } + + } }