Add script API and some related utilities (#109)

This commit is contained in:
Jeffrey Han
2022-10-24 10:57:35 -07:00
committed by GitHub
parent e1b9052ecc
commit c22ebeea31
4 changed files with 210 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ import com.facebook.openwifi.cloudsdk.models.gw.DeviceCapabilities;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceConfigureRequest;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceListWithStatus;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceWithStatus;
import com.facebook.openwifi.cloudsdk.models.gw.ScriptRequest;
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
import com.facebook.openwifi.cloudsdk.models.gw.StatisticsRecords;
import com.facebook.openwifi.cloudsdk.models.gw.SystemInfoResults;
@@ -562,6 +563,83 @@ public class UCentralClient {
}
}
/**
* Run a shell script on a device and return the result, or null upon error.
*
* @see #runScript(String, String, int)
*/
public CommandInfo runScript(String serialNumber, String script) {
return runScript(serialNumber, script, 30);
}
/**
* Run a shell script on a device and return the result, or null upon error.
*
* @see #runScript(String, String, int, String)
*/
public CommandInfo runScript(
String serialNumber,
String script,
int timeoutSec
) {
return runScript(serialNumber, script, timeoutSec, "shell");
}
/**
* Run a script on a device and return the result, or null upon error.
*
* @param serialNumber the device
* @param script the script contents
* @param timeoutSec the timeout in seconds
* @param type the script type (either "shell" or "ucode")
*
* @see UCentralUtils#getScriptOutput(CommandInfo)
*/
public CommandInfo runScript(
String serialNumber,
String script,
int timeoutSec,
String type
) {
ScriptRequest req = new ScriptRequest();
req.serialNumber = serialNumber;
req.timeout = timeoutSec;
req.type = type;
req.script = script;
req.scriptId = "1"; // ??
HttpResponse<String> response = httpPost(
String.format("device/%s/script", serialNumber),
OWGW_SERVICE,
req
);
if (!response.isSuccess()) {
logger.error("Error: {}", response.getBody());
return null;
}
try {
return gson.fromJson(response.getBody(), CommandInfo.class);
} catch (JsonSyntaxException e) {
String errMsg = String.format(
"Failed to deserialize to CommandInfo: %s",
response.getBody()
);
logger.error(errMsg, e);
return null;
}
}
/**
* Instruct a device (AP) to ping a given destination (IP/hostname),
* returning the raw ping output or null upon error.
*/
public String pingFromDevice(String serialNumber, String host) {
// TODO pass options, parse output
final int PING_COUNT = 5;
String script = String.format("ping -c %d %s", PING_COUNT, host);
CommandInfo info = runScript(serialNumber, script);
return UCentralUtils.getScriptOutput(info);
}
/** Retrieve a list of inventory from owprov. */
public InventoryTagList getProvInventory() {
HttpResponse<String> response = httpGet("inventory", OWPROV_SERVICE);

View File

@@ -8,8 +8,10 @@
package com.facebook.openwifi.cloudsdk;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -17,6 +19,8 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -27,6 +31,7 @@ import com.facebook.openwifi.cloudsdk.ies.LocalPowerConstraint;
import com.facebook.openwifi.cloudsdk.ies.QbssLoad;
import com.facebook.openwifi.cloudsdk.ies.TxPwrInfo;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.models.gw.CommandInfo;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -477,4 +482,111 @@ public class UCentralUtils {
return null;
}
}
/**
* Return a map of Wi-Fi client (STA) MAC addresses to the Client structure
* found for that interface. This does NOT support clients connected on
* multiple interfaces simultaneously.
*/
public static Map<String, State.Interface.Client> getWifiClientInfo(
State state
) {
Map<String, State.Interface.Client> ret = new HashMap<>();
// Aggregate over all interfaces
for (State.Interface iface : state.interfaces) {
if (iface.ssids == null || iface.clients == null) {
continue;
}
// Convert client array to map (for faster lookups)
Map<String, State.Interface.Client> ifaceMap = new HashMap<>();
for (State.Interface.Client client : iface.clients) {
ifaceMap.put(client.mac, client);
}
// Loop over all SSIDs and connected clients
for (State.Interface.SSID ssid : iface.ssids) {
if (ssid.associations == null) {
continue;
}
for (
State.Interface.SSID.Association association : ssid.associations
) {
State.Interface.Client client =
ifaceMap.get(association.station);
if (client != null) {
ret.put(association.station, client);
}
}
}
}
return ret;
}
/**
* Decompress (inflate) a UTF-8 string using ZLIB.
*
* @param compressed the compressed string
* @param uncompressedSize the uncompressed size (must be known)
*/
private static String inflate(String compressed, int uncompressedSize)
throws DataFormatException {
if (compressed == null) {
throw new NullPointerException("Null compressed string");
}
if (uncompressedSize < 0) {
throw new IllegalArgumentException("Invalid size");
}
byte[] input = compressed.getBytes(StandardCharsets.UTF_8);
byte[] output = new byte[uncompressedSize];
Inflater inflater = new Inflater();
inflater.setInput(input, 0, input.length);
inflater.inflate(output);
inflater.end();
return new String(output, StandardCharsets.UTF_8);
}
/**
* Given the result of the "script" API, return the actual script output
* (decoded/decompressed if needed), or null if the script returned an
* error.
*
* @see UCentralClient#runScript(String, String, int, String)
*/
public static String getScriptOutput(CommandInfo info) {
if (info == null || info.results == null) {
return null;
}
if (!info.results.has("status")) {
return null;
}
JsonObject status = info.results.get("status").getAsJsonObject();
if (!status.has("error") || status.get("error").getAsInt() != 0) {
return null;
}
if (status.has("result")) {
// Raw result
return status.get("result").getAsString();
} else if (status.has("result_64") && status.has("result_sz")) {
// Base64+compressed result
// NOTE: untested, not actually implemented on ucentral-client?
try {
String encoded = status.get("result_64").getAsString();
int uncompressedSize = status.get("result_sz").getAsInt();
String decoded = new String(
Base64.getDecoder().decode(encoded),
StandardCharsets.UTF_8
);
return inflate(decoded, uncompressedSize);
} catch (Exception e) {
logger.error("Failed to decode or inflate script result", e);
}
}
return null;
}
}

View File

@@ -54,6 +54,8 @@ public class State {
public int ack_signal;
public int ack_signal_avg;
public JsonObject[] tid_stats; // TODO: see cfg80211_tid_stats
// TODO ipaddr_v4 - either string or object (ip4leases), but duplicated in "clients"
}
public Association[] associations;

View File

@@ -0,0 +1,18 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifi.cloudsdk.models.gw;
public class ScriptRequest {
public String serialNumber;
public long timeout = 30; // in seconds
public String type; // "shell", "ucode", "uci"
public String script;
public String scriptId; // required but unused?
public long when = 0;
}