30 Commits

Author SHA1 Message Date
Jun Woo Shin
9b0f99c4dd make separate uCentral schema structure
Signed-off-by: Jun Woo Shin <jwoos@meta.com>
2022-10-21 14:37:08 -04:00
Jun Woo Shin
e1b9052ecc Create separate Capabilities structure (#108) 2022-10-21 12:01:45 -04:00
Jun Woo Shin
c635af6c1d use eclipse temurin distribution of openjdk (#107)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
2022-10-20 09:34:09 -04:00
Jun Woo Shin
37a904087d [WIFI-11247] change host to 0.0.0.0 (#106) 2022-10-19 09:47:59 -04:00
RockyMandayam2
6df81b7fef Use device capabilities to identify radio band (#104) 2022-10-13 19:05:05 -07:00
RockyMandayam2
8171cc74ae Remove lower and upper channel limit maps (#102) 2022-10-13 09:51:18 -07:00
Jeffrey Han
215d73fee5 Fix vendorReferenceUrl in RRMConfig broken in #99 (#103) 2022-10-12 18:23:37 -07:00
RockyMandayam2
ecbf8fa644 Fix javadoc and add override decorator (#101) 2022-10-12 15:39:11 -07:00
RockyMandayam2
19928e0286 Move AVAILABLE_CHANNELS_BAND to UCentralUtils before adding 6G support (#100) 2022-10-12 15:27:11 -07:00
Jun Woo Shin
da978611d0 [WIFI-10736] Spin up separate ports for internal and external (#64)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
2022-10-12 15:45:44 -04:00
Jeffrey Han
e42eaa747b [WIFI-11196] Restructure repository into multimodule project (#99) 2022-10-12 11:57:32 -07:00
Dmitry Dunaev
264f114be2 [WIFI-10910] Chg: internal endpoint port (#98)
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>

Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-10-11 19:44:46 +03:00
zhiqiand
dd2b485b00 Follow-up on stats aggregation (#96)
* fix some comments from Jerrey

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

Signed-off-by: zhiqiand <zhiqian@fb.com>
2022-10-10 15:59:04 -07:00
Jun Woo Shin
033d93beff Fix IE parsing (#97)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
2022-10-07 14:59:50 -04:00
zhiqiand
e5d5f7d5c0 States aggregation (#82)
* initial

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix comments on AggregatedState and ModelerUtils

Signed-off-by: zhiqiand <zhiqian@fb.com>

* reformat and thread-safe

Signed-off-by: zhiqiand <zhiqian@fb.com>

* add buffer size for state

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

* add javadoc

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix comments in TestUtils

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix some comments

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix tx_power

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix channel number

Signed-off-by: zhiqiand <zhiqian@fb.com>

* fix long type

Signed-off-by: zhiqiand <zhiqian@fb.com>

Signed-off-by: zhiqiand <zhiqian@fb.com>
2022-10-06 10:10:20 -07:00
RockyMandayam2
0b4fd49627 Handle invalid IEs (#94) 2022-10-03 12:41:22 -07:00
Jun Woo Shin
d81df03637 [WIFI-10943] Deal with "auto" value for channel and fix 80p80 representation (#92) 2022-09-30 17:53:32 -04:00
RockyMandayam2
594fd9fa91 Refactor IE parsing (#90) 2022-09-30 09:42:59 -07:00
RockyMandayam2
8c48a8901b Rename HTOperationElement, HTOperationElementTest, VHTOperationElement, and VHTOperationElementTest (#91) 2022-09-29 20:52:19 -07:00
Jun Woo Shin
0ac189f493 [WIFI-10819] parse cron into valid quartz cron (#89)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
2022-09-29 14:37:18 -04:00
RockyMandayam2
df21d07ec9 Discard unneeded IEs (#78) 2022-09-29 11:28:48 -07:00
RockyMandayam2
01a070c9b7 Only push updates for desired zones/venues (#80) 2022-09-28 12:37:19 -07:00
Jun Woo Shin
5211eae7c6 update comment around token validation to clarify behavior (#87)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
2022-09-26 17:01:58 -04:00
Jun Woo Shin
fafbda0bd8 update comments about validating tokens (#86)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
2022-09-26 15:51:10 -04:00
Jun Woo Shin
43c9aaafb2 Make inner classes static as necessary (#84) 2022-09-21 15:13:19 -04:00
RockyMandayam2
89e637cfeb Use short instead of byte to store unsigned byte values in VHTOperationElement (#81) 2022-09-21 08:19:54 -07:00
RockyMandayam2
0a64fb4963 Minor cleanup in TPC classes (#71) 2022-09-20 14:57:56 -07:00
RockyMandayam2
4191bc1a70 Separate createModel into two methods, one for single band and one for multi-band; sync the state and device status in multi-band test (#73) 2022-09-19 17:04:34 -07:00
Jeffrey Han
3b6e83d103 Bump default event loop timers (#77)
Signed-off-by: Jeffrey Han <39203126+elludraon@users.noreply.github.com>
2022-09-19 11:03:41 -07:00
Jun Woo Shin
27c36ff444 Make AP-AP TPC algorithm use tx power from statistics and fix TPC application to correct band (#76) 2022-09-16 17:45:40 -04:00
144 changed files with 5488 additions and 2218 deletions

View File

@@ -18,7 +18,7 @@ jobs:
distribution: 'adopt'
cache: maven
- name: Build with Maven
run: mvn javadoc:javadoc
run: mvn javadoc:aggregate
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:

14
.gitignore vendored
View File

@@ -1,12 +1,9 @@
/target
/*.log*
/device_config.json
/settings.json
/topology.json
/target/
*/target/
# Eclipse
/.settings/
/bin/
.settings/
bin/
.metadata
.classpath
.project
@@ -17,3 +14,6 @@
*.iml
*.iws
*.ipr
# Miscellaneous files thzt should not be checked in
temp/

View File

@@ -1,7 +1,7 @@
FROM maven:3-jdk-11 as build
FROM maven:3-eclipse-temurin-11 as build
WORKDIR /usr/src/java
COPY . .
RUN mvn clean package -DappendVersionString="$(./scripts/get_build_version.sh)"
RUN mvn clean package -pl owrrm -am -DappendVersionString="$(./scripts/get_build_version.sh)"
FROM adoptopenjdk/openjdk11-openj9:latest
RUN apt-get update && apt-get install -y gettext-base wget
@@ -10,8 +10,8 @@ RUN wget https://raw.githubusercontent.com/Telecominfraproject/wlan-cloud-ucentr
RUN mkdir /owrrm-data
WORKDIR /usr/src/java
COPY docker-entrypoint.sh /
COPY --from=build /usr/src/java/target/openwifi-rrm.jar /usr/local/bin/
EXPOSE 16789
COPY --from=build /usr/src/java/owrrm/target/openwifi-rrm.jar /usr/local/bin/
EXPOSE 16789 16790
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["java", "-XX:+IdleTuningGcOnIdle", "-Xtune:virtualized", \
"-jar", "/usr/local/bin/openwifi-rrm.jar", \

View File

@@ -1,12 +1,10 @@
# OpenWiFi RRM Service
OpenWiFi uCentral-based radio resource management (RRM) service, providing a
cloud-based Wi-Fi RRM layer for APs running the OpenWiFi SDK.
[See here](owrrm/README.md) for details.
This service collects data from OpenWiFi APs (e.g. Wi-Fi scans, stats,
capabilities) via the uCentral Gateway and Kafka, and integrates with the
OpenWiFi Provisioning service to perform optimization across configured
"venues". It pushes new device configuration parameters to APs after RRM
algorithms are run (manually or periodically).
## Project Structure
This is an [Apache Maven] project with the following modules:
* `lib-cloudsdk` - OpenWiFi CloudSDK Java Library
* `owrrm` - OpenWiFi RRM Service
## Requirements
* **Running:** JRE 11.
@@ -16,7 +14,7 @@ algorithms are run (manually or periodically).
```
$ mvn package [-DskipTests]
```
This will build a runnable JAR located at `target/openwifi-rrm.jar`.
This will build a runnable JAR located at `owrrm/target/openwifi-rrm.jar`.
Alternatively, Docker builds can be launched using the provided
[Dockerfile](Dockerfile).
@@ -27,34 +25,7 @@ $ mvn test
```
Unit tests are written using [JUnit 5].
## Usage
```
$ java -jar openwifi-rrm.jar [-h]
```
To start the service, use the `run` command while providing configuration via
either environment variables (`--config-env`) or a static JSON file
(`--config-file`, default `settings.json`). The following data is *required*:
* Service configuration
* Env: `SERVICECONFIG_PRIVATEENDPOINT`, `SERVICECONFIG_PUBLICENDPOINT`
* JSON: `serviceConfig` structure
* Kafka broker URL
* Env: `KAFKACONFIG_BOOTSTRAPSERVER`
* JSON: `kafkaConfig` structure
* MySQL database credentials
* Env: `DATABASECONFIG_SERVER`, `DATABASECONFIG_USER`, `DATABASECONFIG_PASSWORD`
* JSON: `databaseConfig` structure
## OpenAPI
This service provides an OpenAPI HTTP interface on the port specified in the
service configuration (`moduleConfig.apiServerParams`). An auto-generated
OpenAPI 3.0 document is hosted at the endpoints `/openapi.{yaml,json}` and is
written to [openapi.yaml](openapi.yaml) during the Maven "compile" phase.
## For Developers
See [IMPLEMENTATION.md](IMPLEMENTATION.md) for service architecture details and
[ALGORITHMS.md](ALGORITHMS.md) for descriptions of the RRM algorithms.
## Code Style
Code is auto-formatted using [Spotless] with a custom Eclipse style config (see
[spotless/eclipse-java-formatter.xml](spotless/eclipse-java-formatter.xml)).
This can be applied via Maven (but is *not* enforced at build time):

View File

@@ -29,8 +29,8 @@ services:
targetPort: 16789
protocol: TCP
restapiinternal:
servicePort: 17007
targetPort: 17007
servicePort: 16790
targetPort: 16790
protocol: TCP
checks:

3
lib-cloudsdk/README.md Normal file
View File

@@ -0,0 +1,3 @@
# OpenWiFi CloudSDK Java Library
A Java library providing clients and models for the OpenWiFi uCentral-based
CloudSDK.

80
lib-cloudsdk/pom.xml Normal file
View File

@@ -0,0 +1,80 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>openwifi-cloudsdk</artifactId>
<packaging>jar</packaging>
<parent>
<groupId>com.facebook</groupId>
<artifactId>openwifi-base</artifactId>
<version>2.7.0</version>
</parent>
<properties>
<!-- Hack for static files located in root project -->
<myproject.root>${project.basedir}/..</myproject.root>
</properties>
<build>
<finalName>openwifi-cloudsdk</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,201 @@
/*
* 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;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association;
import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association.Rate;
/**
* Aggregation model for State aggregation. Only contains info useful for
* analysis.
*/
public class AggregatedState {
/** Rate information with aggregated fields. */
public static class AggregatedRate {
/**
* This is the common bitRate for all the aggregated fields.
*/
public long bitRate;
/**
* This is the common channel width for all the aggregated fields.
*/
public int chWidth;
/**
* Aggregated fields mcs
*/
public List<Integer> mcs = new ArrayList<>();
/** Constructor with no args */
private AggregatedRate() {}
/** Add a Rate to the AggregatedRate */
private void add(Rate rate) {
if (rate == null) {
return;
}
if (mcs.isEmpty()) {
bitRate = rate.bitrate;
chWidth = rate.chwidth;
}
mcs.add(rate.mcs);
}
/**
* Add an AggregatedRate with the same channel_width to the
* AggregatedRate
*/
private void add(AggregatedRate rate) {
if (rate == null || rate.chWidth != chWidth) {
return;
}
if (mcs.isEmpty()) {
bitRate = rate.bitRate;
chWidth = rate.chWidth;
}
mcs.addAll(rate.mcs);
}
}
/**
* Radio information with channel, channel_width and tx_power.
*/
public static class Radio {
public int channel;
public int channelWidth;
public int txPower;
private Radio() {}
public Radio(int channel, int channelWidth, int txPower) {
this.channel = channel;
this.channelWidth = channelWidth;
this.txPower = txPower;
}
private Radio(Map<String, Integer> radioInfo) {
channel = radioInfo.getOrDefault("channel", -1);
channelWidth = radioInfo.getOrDefault("channel_width", -1);
txPower = radioInfo.getOrDefault("tx_power", -1);
}
@Override
public int hashCode() {
return Objects.hash(channel, channelWidth, txPower);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Radio other = (Radio) obj;
return channel == other.channel &&
channelWidth == other.channelWidth && txPower == other.txPower;
}
}
public String bssid;
public String station;
public long connected;
public long inactive;
public List<Integer> rssi;
public long rxBytes;
public long rxPackets;
public AggregatedRate rxRate;
public long txBytes;
public long txDuration;
public long txFailed;
public long txPackets;
public AggregatedRate txRate;
public long txRetries;
public int ackSignal;
public int ackSignalAvg;
public Radio radio;
/** Constructor with no args */
public AggregatedState() {
this.rxRate = new AggregatedRate();
this.txRate = new AggregatedRate();
this.rssi = new ArrayList<>();
this.radio = new Radio();
}
/** Construct from Association and radio */
public AggregatedState(
Association association,
Map<String, Integer> radioInfo
) {
this.rxRate = new AggregatedRate();
this.txRate = new AggregatedRate();
this.rssi = new ArrayList<>();
this.bssid = association.bssid;
this.station = association.station;
this.connected = association.connected;
this.inactive = association.inactive;
this.rssi.add(association.rssi);
this.rxBytes = association.rx_bytes;
this.rxPackets = association.rx_packets;
this.rxRate.add(association.rx_rate);
this.txBytes = association.tx_bytes;
this.txDuration = association.tx_duration;
this.txFailed = association.tx_failed;
this.txPackets = association.tx_packets;
this.txRate.add(association.tx_rate);
this.txRetries = association.tx_retries;
this.ackSignal = association.ack_signal;
this.ackSignalAvg = association.ack_signal_avg;
this.radio = new Radio(radioInfo);
}
/**
* Check whether the passed-in AggregatedState and this one match for aggregation.
* If the two match in bssid, station and radio. Then they could be aggregated.
*
* @param state the reference AggregatedState with which to check with.
* @return boolean return true if the two matches for aggregation.
*/
public boolean matchesForAggregation(AggregatedState state) {
return bssid == state.bssid && station == state.station &&
Objects.equals(radio, state.radio);
}
/**
* Add an AggregatedState to this AggregatedState. Succeed only when the two
* match for aggregation.
*
* @param state input AggregatedState
* @return boolean true if the two match in bssid, station, channel,
* channel_width and tx_power
*/
public boolean add(AggregatedState state) {
if (matchesForAggregation(state)) {
this.rssi.addAll(state.rssi);
this.rxRate.add(state.rxRate);
this.txRate.add(state.txRate);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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;
import java.util.Objects;
import com.facebook.openwifi.cloudsdk.ies.Country;
import com.facebook.openwifi.cloudsdk.ies.LocalPowerConstraint;
import com.facebook.openwifi.cloudsdk.ies.QbssLoad;
import com.facebook.openwifi.cloudsdk.ies.TxPwrInfo;
/** Wrapper class containing information elements */
public final class InformationElements {
public Country country;
public QbssLoad qbssLoad;
public LocalPowerConstraint localPowerConstraint;
public TxPwrInfo txPwrInfo;
@Override
public int hashCode() {
return Objects.hash(country, localPowerConstraint, qbssLoad, txPwrInfo);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
InformationElements other = (InformationElements) obj;
return Objects.equals(country, other.country) && Objects.equals(
localPowerConstraint,
other.localPowerConstraint
) && Objects.equals(qbssLoad, other.qbssLoad) &&
Objects.equals(txPwrInfo, other.txPwrInfo);
}
}

View File

@@ -6,14 +6,16 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral;
package com.facebook.openwifi.cloudsdk;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
@@ -21,16 +23,16 @@ import com.google.gson.JsonObject;
*/
public class UCentralApConfiguration {
/** The raw configuration. */
private final JsonObject config;
private final UCentralSchema config;
/** Constructor from JSON string. */
public UCentralApConfiguration(String configJson) {
this.config = new Gson().fromJson(configJson, JsonObject.class);
this.config = new Gson().fromJson(configJson, UCentralSchema.class);
}
/** Constructor from JsonObject (makes deep copy). */
/** Constructor from JsonObject */
public UCentralApConfiguration(JsonObject config) {
this.config = config.deepCopy();
this.config = new Gson().fromJson(config, UCentralSchema.class);
}
@Override
@@ -45,22 +47,21 @@ public class UCentralApConfiguration {
/** Return the number of radios, or -1 if the field is missing/malformed. */
public int getRadioCount() {
if (!config.has("radios") || !config.get("radios").isJsonArray()) {
if (config.radios == null) {
return -1;
}
return config.getAsJsonArray("radios").size();
return config.radios.size();
}
/** Return all info in the radio config (or an empty array if none). */
public JsonArray getRadioConfigList() {
if (!config.has("radios") || !config.get("radios").isJsonArray()) {
return new JsonArray();
}
return config.getAsJsonArray("radios");
public List<UCentralSchema.Radio> getRadioConfigList() {
return config.radios;
}
/** Return all the operational bands of an AP (from the radio config) */
public Set<String> getRadioBandsSet(JsonArray radioConfigList) {
public Set<String> getRadioBandsSet(
List<UCentralSchema.Radio> radioConfigList
) {
Set<String> radioBandsSet = new HashSet<>();
if (radioConfigList == null) {
return radioBandsSet;
@@ -69,46 +70,39 @@ public class UCentralApConfiguration {
int radioIndex = 0; radioIndex < radioConfigList.size();
radioIndex++
) {
JsonElement e = radioConfigList.get(radioIndex);
if (!e.isJsonObject()) {
UCentralSchema.Radio radio = radioConfigList.get(radioIndex);
if (radio == null || radio.band == null) {
continue;
}
JsonObject radioObject = e.getAsJsonObject();
if (!radioObject.has("band")) {
continue;
}
radioBandsSet.add(radioObject.get("band").getAsString());
radioBandsSet.add(radio.band);
}
return radioBandsSet;
}
/** Return the radio config at the given index, or null if invalid. */
public JsonObject getRadioConfig(int index) {
public UCentralSchema.Radio getRadioConfig(int index) {
if (getRadioCount() < index) {
return null;
}
JsonArray radios = config.getAsJsonArray("radios");
List<UCentralSchema.Radio> radios = config.radios;
if (radios == null) {
return null;
}
JsonElement e = radios.get(index);
if (!e.isJsonObject()) {
return null;
}
return e.getAsJsonObject();
UCentralSchema.Radio radio = radios.get(index);
return radio;
}
/** Set radio config at the given index. Adds empty objects as needed. */
public void setRadioConfig(int index, JsonObject radioConfig) {
public void setRadioConfig(int index, UCentralSchema.Radio radioConfig) {
int radioCount = getRadioCount();
if (radioCount == -1) {
config.add("radios", new JsonArray());
config.radios = new ArrayList<UCentralSchema.Radio>();
radioCount = 0;
}
JsonArray radios = config.getAsJsonArray("radios");
List<UCentralSchema.Radio> radios = config.radios;
for (int i = radioCount; i <= index; i++) {
// insert empty objects as needed
radios.add(new JsonObject());
radios.add(new UCentralSchema.Radio());
}
radios.set(index, radioConfig);
}
@@ -119,11 +113,7 @@ public class UCentralApConfiguration {
*/
public int getStatisticsInterval() {
try {
return config
.getAsJsonObject("metrics")
.getAsJsonObject("statistics")
.get("interval")
.getAsInt();
return config.metrics.statistics.interval;
} catch (Exception e) {
return -1;
}
@@ -131,17 +121,14 @@ public class UCentralApConfiguration {
/** Set the statistics interval to the given value (in seconds). */
public void setStatisticsInterval(int intervalSec) {
if (!config.has("metrics") || !config.get("metrics").isJsonObject()) {
config.add("metrics", new JsonObject());
if (config.metrics == null) {
config.metrics = new UCentralSchema.Metrics();
}
JsonObject metrics = config.getAsJsonObject("metrics");
if (
!metrics.has("statistics") ||
!metrics.get("statistics").isJsonObject()
config.metrics.statistics == null
) {
metrics.add("statistics", new JsonObject());
config.metrics.statistics = new UCentralSchema.Metrics.Statistics();
}
JsonObject statistics = metrics.getAsJsonObject("statistics");
statistics.addProperty("interval", intervalSec);
config.metrics.statistics.interval = intervalSec;
}
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral;
package com.facebook.openwifi.cloudsdk;
import java.util.Collections;
import java.util.HashMap;
@@ -20,22 +20,21 @@ import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.RRMConfig.UCentralConfig.UCentralSocketParams;
import com.facebook.openwifirrm.ucentral.gw.models.CommandInfo;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceCapabilities;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceConfigureRequest;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceListWithStatus;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceWithStatus;
import com.facebook.openwifirrm.ucentral.gw.models.ServiceEvent;
import com.facebook.openwifirrm.ucentral.gw.models.StatisticsRecords;
import com.facebook.openwifirrm.ucentral.gw.models.SystemInfoResults;
import com.facebook.openwifirrm.ucentral.gw.models.TokenValidationResult;
import com.facebook.openwifirrm.ucentral.gw.models.WifiScanRequest;
import com.facebook.openwifirrm.ucentral.prov.models.EntityList;
import com.facebook.openwifirrm.ucentral.prov.models.InventoryTagList;
import com.facebook.openwifirrm.ucentral.prov.models.RRMDetails;
import com.facebook.openwifirrm.ucentral.prov.models.SerialNumberList;
import com.facebook.openwifirrm.ucentral.prov.models.VenueList;
import com.facebook.openwifi.cloudsdk.models.gw.CommandInfo;
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.ServiceEvent;
import com.facebook.openwifi.cloudsdk.models.gw.StatisticsRecords;
import com.facebook.openwifi.cloudsdk.models.gw.SystemInfoResults;
import com.facebook.openwifi.cloudsdk.models.gw.TokenValidationResult;
import com.facebook.openwifi.cloudsdk.models.gw.WifiScanRequest;
import com.facebook.openwifi.cloudsdk.models.prov.EntityList;
import com.facebook.openwifi.cloudsdk.models.prov.InventoryTagList;
import com.facebook.openwifi.cloudsdk.models.prov.RRMDetails;
import com.facebook.openwifi.cloudsdk.models.prov.SerialNumberList;
import com.facebook.openwifi.cloudsdk.models.prov.VenueList;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
@@ -127,8 +126,14 @@ public class UCentralClient {
/** uCentral password */
private final String password;
/** Socket parameters */
private final UCentralSocketParams socketParams;
/** Connection timeout for all requests, in ms */
private final int connectTimeoutMs;
/** Socket timeout for all requests, in ms */
private final int socketTimeoutMs;
/** Socket timeout for wifi scan requests, in ms */
private final int wifiScanTimeoutMs;
/** The learned service endpoints. */
private final Map<String, ServiceEvent> serviceEndpoints = new HashMap<>();
@@ -147,7 +152,9 @@ public class UCentralClient {
* (if needed)
* @param username uCentral username (for public endpoints only)
* @param password uCentral password (for public endpoints only)
* @param socketParams Socket parameters
* @param connectTimeoutMs connection timeout for all requests, in ms
* @param socketTimeoutMs socket timeout for all requests, in ms
* @param wifiScanTimeoutMs socket timeout for wifi scan requests, in ms
*/
public UCentralClient(
String rrmEndpoint,
@@ -155,13 +162,17 @@ public class UCentralClient {
String uCentralSecPublicEndpoint,
String username,
String password,
UCentralSocketParams socketParams
int connectTimeoutMs,
int socketTimeoutMs,
int wifiScanTimeoutMs
) {
this.rrmEndpoint = rrmEndpoint;
this.usePublicEndpoints = usePublicEndpoints;
this.username = username;
this.password = password;
this.socketParams = socketParams;
this.connectTimeoutMs = connectTimeoutMs;
this.socketTimeoutMs = socketTimeoutMs;
this.wifiScanTimeoutMs = wifiScanTimeoutMs;
if (usePublicEndpoints) {
setServicePublicEndpoint(OWSEC_SERVICE, uCentralSecPublicEndpoint);
@@ -305,8 +316,8 @@ public class UCentralClient {
endpoint,
service,
parameters,
socketParams.connectTimeoutMs,
socketParams.socketTimeoutMs
connectTimeoutMs,
socketTimeoutMs
);
}
@@ -349,8 +360,8 @@ public class UCentralClient {
endpoint,
service,
body,
socketParams.connectTimeoutMs,
socketParams.socketTimeoutMs
connectTimeoutMs,
socketTimeoutMs
);
}
@@ -454,8 +465,8 @@ public class UCentralClient {
String.format("device/%s/wifiscan", serialNumber),
OWGW_SERVICE,
req,
socketParams.connectTimeoutMs,
socketParams.wifiScanTimeoutMs
connectTimeoutMs,
wifiScanTimeoutMs
);
if (!response.isSuccess()) {
logger.error("Error: {}", response.getBody());

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral;
package com.facebook.openwifi.cloudsdk;
import java.util.Arrays;
import java.util.Collections;
@@ -22,7 +22,7 @@ public final class UCentralConstants {
public static final String BAND_2G = "2G";
/** String of the 5 GHz band */
public static final String BAND_5G = "5G";
/** List of all bands */
/** List of all bands ordered from lowest to highest */
public static final List<String> BANDS = Collections
.unmodifiableList(Arrays.asList(BAND_2G, BAND_5G));

View File

@@ -0,0 +1,509 @@
/*
* 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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.ies.Country;
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.ap.UCentralSchema;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
/**
* uCentral utility methods/structures.
*/
public class UCentralUtils {
private static final Logger logger =
LoggerFactory.getLogger(UCentralUtils.class);
/** Information Element (IE) content field key */
private static final String IE_CONTENT_FIELD_KEY = "content";
/** The Gson instance. */
private static final Gson gson = new Gson();
/** Map from band to ordered (increasing) list of available channels */
public static final Map<String, List<Integer>> AVAILABLE_CHANNELS_BAND =
Collections
.unmodifiableMap(buildBandToChannelsMap());
// This class should not be instantiated.
private UCentralUtils() {}
/**
* Builds map from band to ordered (increasing) list of available channels.
*/
private static Map<String, List<Integer>> buildBandToChannelsMap() {
Map<String, List<Integer>> bandToChannelsMap = new HashMap<>();
bandToChannelsMap.put(
UCentralConstants.BAND_5G,
Collections.unmodifiableList(
Arrays.asList(36, 40, 44, 48, 149, 153, 157, 161, 165)
)
);
// NOTE: later, we may want to support channels 12, 13, and/or 14, if
// the AP supports it and OWF vendors will use them
bandToChannelsMap.put(
UCentralConstants.BAND_2G,
Collections.unmodifiableList(
Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
)
);
return bandToChannelsMap;
}
/** Return the lowest available channel for the given band */
public static int getLowerChannelLimit(String band) {
return AVAILABLE_CHANNELS_BAND.get(band).get(0);
}
/** Return the lowest available channel for the given band */
public static int getUpperChannelLimit(String band) {
List<Integer> channels = AVAILABLE_CHANNELS_BAND.get(band);
return channels.get(channels.size() - 1);
}
/**
* Parse a JSON wifi scan result into a list of WifiScanEntry objects.
*
* @param result result of the wifiscan
* @param timestampMs Unix time in ms
* @return list of wifiscan entries, or null if any parsing/deserialization
* error occurred.
*/
public static List<WifiScanEntry> parseWifiScanEntries(
JsonObject result,
long timestampMs
) {
List<WifiScanEntry> entries = new ArrayList<>();
try {
JsonArray scanInfo = result
.getAsJsonObject("status")
.getAsJsonArray("scan");
for (JsonElement e : scanInfo) {
WifiScanEntry entry = gson.fromJson(e, WifiScanEntry.class);
entry.unixTimeMs = timestampMs;
extractIEs(e, entry);
entries.add(entry);
}
} catch (Exception e) {
logger.debug("Exception when parsing wifiscan entries", e);
return null;
}
return entries;
}
/**
* Extract desired information elements (IEs) from the wifiscan entry.
* Modifies {@code entry} argument. Skips invalid IEs (IEs with missing
* fields).
*/
private static void extractIEs(
JsonElement entryJsonElement,
WifiScanEntry entry
) {
JsonElement iesJsonElement =
entryJsonElement.getAsJsonObject().get("ies");
if (iesJsonElement == null) {
logger.debug("Wifiscan entry does not contain 'ies' field.");
return;
}
JsonArray iesJsonArray = iesJsonElement.getAsJsonArray();
InformationElements ieContainer = new InformationElements();
for (JsonElement ieJsonElement : iesJsonArray) {
JsonElement typeElement =
ieJsonElement.getAsJsonObject().get("type");
if (typeElement == null) { // shouldn't happen
continue;
}
if (!ieJsonElement.isJsonObject()) {
// the IEs we are interested in are Json objects
continue;
}
JsonObject ie = ieJsonElement.getAsJsonObject();
JsonElement contentsJsonElement = ie.get(IE_CONTENT_FIELD_KEY);
if (
contentsJsonElement == null ||
!contentsJsonElement.isJsonObject()
) {
continue;
}
JsonObject contents = contentsJsonElement.getAsJsonObject();
try {
switch (typeElement.getAsInt()) {
case Country.TYPE:
ieContainer.country = Country.parse(contents);
break;
case QbssLoad.TYPE:
ieContainer.qbssLoad = QbssLoad.parse(contents);
break;
case LocalPowerConstraint.TYPE:
ieContainer.localPowerConstraint =
LocalPowerConstraint.parse(contents);
break;
case TxPwrInfo.TYPE:
ieContainer.txPwrInfo = TxPwrInfo.parse(contents);
break;
}
} catch (Exception e) {
logger.error(String.format("Skipping invalid IE %s", ie), e);
continue;
}
}
entry.ieContainer = ieContainer;
}
/**
* Set all radios config channel of an AP to a given value.
*
* Returns true if changed, or false if unchanged for any reason.
*/
public static boolean setRadioConfigChannel(
String serialNumber,
UCentralApConfiguration config,
Map<String, Integer> newValueList
) {
return setRadioConfigField(
serialNumber,
config,
"channel",
newValueList
);
}
/**
* Set all radios config tx power of an AP to a given value.
*
* Returns true if changed, or false if unchanged for any reason.
*/
public static boolean setRadioConfigTxPower(
String serialNumber,
UCentralApConfiguration config,
Map<String, Integer> newValueList
) {
return setRadioConfigField(
serialNumber,
config,
"tx-power",
newValueList
);
}
/**
* Set all radios config of an AP to a given value.
*
* Returns true if changed, or false if unchanged for any reason.
*/
private static boolean setRadioConfigField(
String serialNumber,
UCentralApConfiguration config,
String fieldName,
Map<String, Integer> newValueList
) {
boolean wasModified = false;
// Iterate all the radios of an AP to find the corresponding band
for (
int radioIndex = 0; radioIndex < config.getRadioCount();
radioIndex++
) {
UCentralSchema.Radio radioConfig =
config.getRadioConfig(radioIndex);
if (radioConfig == null) {
continue;
}
String operationalBand = radioConfig.band;
if (!newValueList.containsKey(operationalBand)) {
continue;
}
// If the fieldName doesn't exist in config, we generate the fieldName and
// assign the new value to it.
int newValue = newValueList.get(operationalBand);
Integer currentValue = null;
switch (fieldName) {
case "channel":
if (
radioConfig.channel == null ||
radioConfig.channel.isString()
) {
wasModified = true;
} else {
currentValue = radioConfig.channel.getAsInt();
if (currentValue != newValue) {
wasModified = true;
}
}
radioConfig.channel = new JsonPrimitive(newValue);
break;
case "tx-power":
currentValue = radioConfig.txPower;
if (currentValue != newValue) {
radioConfig.txPower = newValue;
wasModified = true;
}
break;
}
if (wasModified) {
logger.info(
"Device {}: setting {} {} to {} (was {})",
serialNumber,
operationalBand,
fieldName,
newValue,
currentValue
);
}
if (currentValue != null && currentValue == newValue) {
logger.info(
"Device {}: {} {} is already {}",
serialNumber,
operationalBand,
fieldName,
newValue
);
}
}
return wasModified;
}
/**
* Get the APs on a band who participate in an optimization algorithm.
* Get the info from the configuration field in deviceStatus
* (Since the State doesn't explicitly show the "band" info)
*
* Returns the results map
*/
public static Map<String, List<String>> getBandsMap(
Map<String, List<UCentralSchema.Radio>> deviceStatus
) {
Map<String, List<String>> bandsMap = new HashMap<>();
for (String serialNumber : deviceStatus.keySet()) {
List<UCentralSchema.Radio> radioList =
deviceStatus.get(serialNumber);
for (
int radioIndex = 0; radioIndex < radioList.size(); radioIndex++
) {
UCentralSchema.Radio radio = radioList.get(radioIndex);
String band = radio.band;
bandsMap
.computeIfAbsent(band, k -> new ArrayList<>())
.add(serialNumber);
}
}
return bandsMap;
}
/**
* Get the capabilities of the APs who participate in an optimization algorithm.
*
* @param deviceStatus map of {device, status}
* @param deviceCapabilities map of {device, capabilities info}
* @param defaultAvailableChannels map of {band, list of available channels}
*
* @return the results map of {band, {device, list of available channels}}
*/
public static Map<String, Map<String, List<Integer>>> getDeviceAvailableChannels(
Map<String, List<UCentralSchema.Radio>> deviceStatus,
Map<String, Map<String, Capabilities.Phy>> deviceCapabilities,
Map<String, List<Integer>> defaultAvailableChannels
) {
Map<String, Map<String, List<Integer>>> deviceAvailableChannels =
new HashMap<>();
for (String serialNumber : deviceStatus.keySet()) {
List<UCentralSchema.Radio> radioList =
deviceStatus.get(serialNumber);
for (
int radioIndex = 0; radioIndex < radioList.size(); radioIndex++
) {
UCentralSchema.Radio radio = radioList.get(radioIndex);
String band = radio.band;
Map<String, Capabilities.Phy> capabilitiesPhyMap =
deviceCapabilities.get(serialNumber);
List<Integer> availableChannels = new ArrayList<>();
if (capabilitiesPhyMap == null) {
availableChannels
.addAll(defaultAvailableChannels.get(band));
} else {
Set<Entry<String, Capabilities.Phy>> entrySet =
capabilitiesPhyMap
.entrySet();
for (Map.Entry<String, Capabilities.Phy> f : entrySet) {
Capabilities.Phy phy = f.getValue();
String bandInsideObject = phy.band.toString();
if (bandInsideObject.equals(band)) {
// (TODO) Remove the following dfsChannels code block
// when the DFS channels are available
Set<Integer> dfsChannels = new HashSet<>();
try {
int[] channelInfo = phy.dfs_channels;
for (int d : channelInfo) {
dfsChannels.add(d);
}
} catch (Exception d) {}
try {
int[] channelInfo = phy.channels;
for (int channel : channelInfo) {
if (!dfsChannels.contains(channel)) {
availableChannels.add(channel);
}
}
} catch (Exception c) {
availableChannels
.addAll(defaultAvailableChannels.get(band));
}
}
}
}
deviceAvailableChannels.computeIfAbsent(
band,
k -> new HashMap<>()
)
.put(
serialNumber,
availableChannels
);
}
}
return deviceAvailableChannels;
}
/**
* Get the mapping between bssids and APs.
* Get the info from the State data
*
* Returns the results map
*/
public static Map<String, String> getBssidsMap(
Map<String, State> latestState
) {
Map<String, String> bssidMap = new HashMap<>();
for (Map.Entry<String, State> e : latestState.entrySet()) {
State state = e.getValue();
for (
int interfaceIndex = 0;
interfaceIndex < state.interfaces.length;
interfaceIndex++
) {
if (state.interfaces[interfaceIndex].ssids == null) {
continue;
}
for (
int ssidIndex = 0;
ssidIndex < state.interfaces[interfaceIndex].ssids.length;
ssidIndex++
) {
bssidMap.put(
state.interfaces[interfaceIndex].ssids[ssidIndex].bssid,
e.getKey()
);
}
}
}
return bssidMap;
}
/**
* Determines if the given channel is in the given band.
*
* @param channel channel number
* @param band "2G" or "5G"
* @return true if the given channel is in the given band; false otherwise
*/
public static boolean isChannelInBand(int channel, String band) {
return AVAILABLE_CHANNELS_BAND.get(band).contains(channel);
}
/** Return which band contains the given frequency (MHz). */
public static String freqToBand(int freqMHz) {
if (2412 <= freqMHz && freqMHz <= 2484) {
return "2G";
} else {
return "5G";
}
}
/**
* Tries to parse channel width, if it encounters an error it will return null.
* It can handle 80p80 in two ways. First it can just treat it as 160. Second,
* it can just apply to the first 80 channel and ignore the second. This is
* controlled by treatSeparate.
*
* @param channelWidthStr the channel width
* @param treatSeparate treats each band separately
* @return channel width in MHz
*/
public static Integer parseChannelWidth(
String channelWidthStr,
boolean treatSeparate
) {
// 80p80 is the only case where it can't be parsed into an integer
if (channelWidthStr.equals("80p80")) {
return treatSeparate ? 80 : 160;
}
try {
return Integer.parseInt(channelWidthStr);
} catch (NumberFormatException e) {
return null;
}
}
/**
* Tries to parse the index from the reference string in the JSON returned from
* other services. Note that this only returns the index, the caller is
* responsible for making sure that the correct field is passed in and the
* index is used in the correct fields. If there's an error parsing, it will
* return null.
*
* @param reference The reference string, keyed under "$ref"
* @return the index of the reference or null if an error occurred.
*/
public static Integer parseReferenceIndex(String reference) {
try {
return Integer.parseInt(
reference,
reference.lastIndexOf("/") + 1,
reference.length(),
10
);
} catch (NumberFormatException e) {
return null;
}
}
}

View File

@@ -6,11 +6,11 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral;
package com.facebook.openwifi.cloudsdk;
import java.util.Objects;
import com.facebook.openwifirrm.ucentral.models.WifiScanEntryResult;
import com.facebook.openwifi.cloudsdk.models.ap.WifiScanEntryResult;
/**
* Extends {@link WifiScanEntryResult} to track the response time of the entry.
@@ -22,6 +22,8 @@ public class WifiScanEntry extends WifiScanEntryResult {
* time reference.
*/
public long unixTimeMs;
/** Stores Information Elements (IEs) from the wifiscan entry. */
public InformationElements ieContainer;
/** Default Constructor. */
public WifiScanEntry() {}
@@ -30,13 +32,14 @@ public class WifiScanEntry extends WifiScanEntryResult {
public WifiScanEntry(WifiScanEntry o) {
super(o);
this.unixTimeMs = o.unixTimeMs;
this.ieContainer = o.ieContainer;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Objects.hash(unixTimeMs);
result = prime * result + Objects.hash(ieContainer, unixTimeMs);
return result;
}
@@ -52,7 +55,8 @@ public class WifiScanEntry extends WifiScanEntryResult {
return false;
}
WifiScanEntry other = (WifiScanEntry) obj;
return unixTimeMs == other.unixTimeMs;
return Objects.equals(ieContainer, other.ieContainer) &&
unixTimeMs == other.unixTimeMs;
}
@Override

View File

@@ -0,0 +1,156 @@
/*
* 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.ies;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* This information element (IE) appears in wifiscan entries.
* Refer to the 802.11 specification for more details. Language in
* javadocs is taken from the specification.
*/
public class Country {
/** Defined in 802.11 */
public static final int TYPE = 7;
/** Constraints for a subset of channels in the AP's country */
public static class CountryInfo {
/**
* The lowest channel number in the CountryInfo.
*/
public final int firstChannelNumber;
/**
* The maximum power, in dBm, allowed to be transmitted.
*/
public final int maximumTransmitPowerLevel;
/**
* Number of channels this CountryInfo applies to. E.g., if First
* Channel Number is 2 and Number of Channels is 4, this CountryInfo
* describes channels 2, 3, 4, and 5.
*/
public final int numberOfChannels;
/** Constructor. */
public CountryInfo(
int firstChannelNumber,
int maximumTransmitPowerLevel,
int numberOfChannels
) {
this.firstChannelNumber = firstChannelNumber;
this.maximumTransmitPowerLevel = maximumTransmitPowerLevel;
this.numberOfChannels = numberOfChannels;
}
/** Parse CountryInfo from the appropriate Json object. */
public static CountryInfo parse(JsonObject contents) {
final int firstChannelNumber =
contents.get("First Channel Number").getAsInt();
final int maximumTransmitPowerLevel = contents
.get("Maximum Transmit Power Level (in dBm)")
.getAsInt();
final int numberOfChannels =
contents.get("Number of Channels").getAsInt();
return new CountryInfo(
firstChannelNumber,
maximumTransmitPowerLevel,
numberOfChannels
);
}
@Override
public int hashCode() {
return Objects.hash(
firstChannelNumber,
maximumTransmitPowerLevel,
numberOfChannels
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CountryInfo other = (CountryInfo) obj;
return firstChannelNumber == other.firstChannelNumber &&
maximumTransmitPowerLevel == other.maximumTransmitPowerLevel &&
numberOfChannels == other.numberOfChannels;
}
@Override
public String toString() {
return "CountryInfo [firstChannelNumber=" + firstChannelNumber +
", maximumTransmitPowerLevel=" + maximumTransmitPowerLevel +
", numberOfChannels=" + numberOfChannels + "]";
}
}
/**
* Each constraint is a CountryInfo describing tx power constraints on
* one or more channels, for the current country.
*/
public final List<CountryInfo> constraints;
/** Constructor */
public Country(List<CountryInfo> countryInfos) {
this.constraints = Collections.unmodifiableList(countryInfos);
}
/** Parse Country IE from the appropriate Json object. */
public static Country parse(JsonObject contents) {
List<CountryInfo> constraints = new ArrayList<>();
JsonElement constraintsObject = contents.get("constraints");
if (constraintsObject != null) {
for (JsonElement jsonElement : constraintsObject.getAsJsonArray()) {
JsonObject innerElem = jsonElement.getAsJsonObject();
CountryInfo countryInfo =
CountryInfo.parse(innerElem.get("Country Info").getAsJsonObject());
constraints.add(countryInfo);
}
}
return new Country(constraints);
}
@Override
public int hashCode() {
return Objects.hash(constraints);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Country other = (Country) obj;
return Objects.equals(constraints, other.constraints);
}
@Override
public String toString() {
return "Country [constraints=" + constraints + "]";
}
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.operationelement;
package com.facebook.openwifi.cloudsdk.ies;
import java.util.Arrays;
import java.util.Objects;
@@ -17,7 +17,7 @@ import org.apache.commons.codec.binary.Base64;
* High Throughput (HT) Operation Element, which is potentially present in
* wifiscan entries. Introduced in 802.11n (2009).
*/
public class HTOperationElement {
public class HTOperation {
/** Channel number of the primary channel. */
public final byte primaryChannel;
@@ -78,7 +78,7 @@ public class HTOperationElement {
* For details about the parameters, see the javadocs for the corresponding
* member variables.
*/
public HTOperationElement(
public HTOperation(
byte primaryChannel,
byte secondaryChannelOffset,
boolean staChannelWidth,
@@ -114,7 +114,7 @@ public class HTOperationElement {
}
/** Constructor with the most used parameters. */
public HTOperationElement(
public HTOperation(
byte primaryChannel,
byte secondaryChannelOffset,
boolean staChannelWidth,
@@ -141,7 +141,7 @@ public class HTOperationElement {
* @param htOper a base64 encoded properly formatted HT operation element (see
* 802.11)
*/
public HTOperationElement(String htOper) {
public HTOperation(String htOper) {
byte[] bytes = Base64.decodeBase64(htOper);
/*
* Note that the code here may seem to read "reversed" compared to 802.11. This
@@ -182,7 +182,7 @@ public class HTOperationElement {
* @return true if the the operation elements "match" for the purpose of
* aggregating statistics; false otherwise.
*/
public boolean matchesForAggregation(HTOperationElement other) {
public boolean matchesForAggregation(HTOperation other) {
return other != null && primaryChannel == other.primaryChannel &&
secondaryChannelOffset == other.secondaryChannelOffset &&
staChannelWidth == other.staChannelWidth &&
@@ -211,8 +211,8 @@ public class HTOperationElement {
if (htOper1 == null || htOper2 == null) {
return false; // false if exactly one is null
}
HTOperationElement htOperObj1 = new HTOperationElement(htOper1);
HTOperationElement htOperObj2 = new HTOperationElement(htOper2);
HTOperation htOperObj1 = new HTOperation(htOper1);
HTOperation htOperObj2 = new HTOperation(htOper2);
return htOperObj1.matchesForAggregation(htOperObj2);
}
@@ -248,7 +248,7 @@ public class HTOperationElement {
if (getClass() != obj.getClass()) {
return false;
}
HTOperationElement other = (HTOperationElement) obj;
HTOperation other = (HTOperation) obj;
return Arrays.equals(basicHtMcsSet, other.basicHtMcsSet) &&
channelCenterFrequencySegment2 ==
other.channelCenterFrequencySegment2 &&

View File

@@ -0,0 +1,72 @@
/*
* 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.ies;
import java.util.Objects;
import com.google.gson.JsonObject;
/**
* This information element (IE) appears in wifiscan entries. It is called
* "Local Power Constraint" in these entries, and just "Power Constraint" in
* the 802.11 specification. Refer to the specification for more details.
* Language in javadocs is taken from the specification.
*/
public class LocalPowerConstraint {
/** Defined in 802.11 */
public static final int TYPE = 32;
/**
* Units are dB.
* <p>
* The local maximum transmit power for a channel is defined as the maximum
* transmit power level specified for the channel in the Country IE minus
* this variable for the given channel.
*/
public final int localPowerConstraint;
/** Constructor */
public LocalPowerConstraint(int localPowerConstraint) {
this.localPowerConstraint = localPowerConstraint;
}
/** Parse LocalPowerConstraint IE from appropriate Json object. */
public static LocalPowerConstraint parse(JsonObject contents) {
final int localPowerConstraint =
contents.get("Local Power Constraint").getAsInt();
return new LocalPowerConstraint(localPowerConstraint);
}
@Override
public int hashCode() {
return Objects.hash(localPowerConstraint);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
LocalPowerConstraint other = (LocalPowerConstraint) obj;
return localPowerConstraint == other.localPowerConstraint;
}
@Override
public String toString() {
return "LocalPowerConstraint [localPowerConstraint=" +
localPowerConstraint + "]";
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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.ies;
import java.util.Objects;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* This information element (IE) appears in wifiscan entries. It is called
* "QBSS Load" in these entries, and just "BSS Load" in the 802.11
* specification. Refer to the specification for more details. Language in
* javadocs is taken from the specification.
*/
public class QbssLoad {
/** Defined in 802.11 */
public static final int TYPE = 11;
/**
* The total number of STAs currently associated with the BSS.
*/
public final int stationCount;
/**
* The Channel Utilization field is defined as the percentage of time,
* linearly scaled with 255 representing 100%, that the AP sensed the
* medium was busy, as indicated by either the physical or virtual carrier
* sense (CS) mechanism. When more than one channel is in use for the BSS,
* the Channel Utilization field value is calculated only for the primary
* channel. This percentage is computed using the following formula:
* <p>
* floor(255 * channelBusyTime /
* (dot11ChannelUtilizationBeaconIntervals * dot11BeaconPeriod * 1024)
* )
*/
public final int channelUtilization;
/**
* The Available Admission Capacity field contains an unsigned integer that
* specifies the remaining amount of medium time available via explicit
* admission control, in units of 32 miscrosecond/second. The field is
* helpful for roaming STAs to select an AP that is likely to accept future
* admission control requests, but it does not represent an assurance that
* the HC admits these requests.
*/
public final int availableAdmissionCapacity;
/** Constructor */
public QbssLoad(
int stationCount,
int channelUtilization,
int availableAdmissionCapacity
) {
this.stationCount = stationCount;
this.channelUtilization = channelUtilization;
this.availableAdmissionCapacity = availableAdmissionCapacity;
}
/** Parse QbssLoad IE from appropriate Json object; return null if invalid. */
public static QbssLoad parse(JsonObject contents) {
// unclear why there is this additional nested layer
JsonElement ccaContentJsonElement = contents.get("802.11e CCA Version");
if (ccaContentJsonElement == null) {
return null;
}
contents = ccaContentJsonElement.getAsJsonObject();
final int stationCount = contents.get("Station Count").getAsInt();
final int channelUtilization =
contents.get("Channel Utilization").getAsInt();
final int availableAdmissionCapacity =
contents.get("Available Admission Capabilities").getAsInt();
return new QbssLoad(
stationCount,
channelUtilization,
availableAdmissionCapacity
);
}
@Override
public int hashCode() {
return Objects.hash(
availableAdmissionCapacity,
channelUtilization,
stationCount
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
QbssLoad other = (QbssLoad) obj;
return availableAdmissionCapacity == other.availableAdmissionCapacity &&
channelUtilization == other.channelUtilization &&
stationCount == other.stationCount;
}
@Override
public String toString() {
return "QbssLoad [stationCount=" + stationCount +
", channelUtilization=" + channelUtilization +
", availableAdmissionCapacity=" + availableAdmissionCapacity + "]";
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.ies;
import java.util.Objects;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* This information element (IE) appears in wifiscan entries. It is called
* "Tx Pwr Info" in these entries, and "Transmit Power Envelope" in the 802.11
* specification. Refer to the specification for more details. Language in
* javadocs is taken from the specification.
*/
public class TxPwrInfo {
/** Defined in 802.11 */
public static final int TYPE = 195;
/** Local maximum transmit power for 20 MHz. Required field. */
public final int localMaxTxPwrConstraint20MHz;
/** Local maximum transmit power for 40 MHz. Optional field. */
public final Integer localMaxTxPwrConstraint40MHz;
/** Local maximum transmit power for 80 MHz. Optional field. */
public final Integer localMaxTxPwrConstraint80MHz;
/** Local maximum transmit power for both 160 MHz and 80+80 MHz. Optional field. */
public final Integer localMaxTxPwrConstraint160MHz;
/** Constructor */
public TxPwrInfo(
int localMaxTxPwrConstraint20MHz,
Integer localMaxTxPwrConstraint40MHz,
Integer localMaxTxPwrConstraint80MHz,
Integer localMaxTxPwrConstraint160MHz
) {
this.localMaxTxPwrConstraint20MHz = localMaxTxPwrConstraint20MHz;
this.localMaxTxPwrConstraint40MHz = localMaxTxPwrConstraint40MHz;
this.localMaxTxPwrConstraint80MHz = localMaxTxPwrConstraint80MHz;
this.localMaxTxPwrConstraint160MHz = localMaxTxPwrConstraint160MHz;
}
/** Parse TxPwrInfo IE from appropriate Json object. */
public static TxPwrInfo parse(JsonObject contents) {
JsonObject innerObj = contents.get("Tx Pwr Info").getAsJsonObject();
// required field
int localMaxTxPwrConstraint20MHz =
innerObj.get("Local Max Tx Pwr Constraint 20MHz").getAsInt();
// optional field
Integer localMaxTxPwrConstraint40MHz =
parseOptionalField(innerObj, "Local Max Tx Pwr Constraint 40MHz");
Integer localMaxTxPwrConstraint80MHz =
parseOptionalField(innerObj, "Local Max Tx Pwr Constraint 80MHz");
Integer localMaxTxPwrConstraint160MHz =
parseOptionalField(innerObj, "Local Max Tx Pwr Constraint 160MHz");
return new TxPwrInfo(
localMaxTxPwrConstraint20MHz,
localMaxTxPwrConstraint40MHz,
localMaxTxPwrConstraint80MHz,
localMaxTxPwrConstraint160MHz
);
}
private static Integer parseOptionalField(
JsonObject contents,
String fieldName
) {
JsonElement element = contents.get(fieldName);
if (element == null) {
return null;
}
return element.getAsInt();
}
@Override
public int hashCode() {
return Objects.hash(
localMaxTxPwrConstraint160MHz,
localMaxTxPwrConstraint20MHz,
localMaxTxPwrConstraint40MHz,
localMaxTxPwrConstraint80MHz
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TxPwrInfo other = (TxPwrInfo) obj;
return localMaxTxPwrConstraint160MHz ==
other.localMaxTxPwrConstraint160MHz &&
localMaxTxPwrConstraint20MHz ==
other.localMaxTxPwrConstraint20MHz &&
localMaxTxPwrConstraint40MHz ==
other.localMaxTxPwrConstraint40MHz &&
localMaxTxPwrConstraint80MHz == other.localMaxTxPwrConstraint80MHz;
}
@Override
public String toString() {
return "TxPwrInfo [localMaxTxPwrConstraint20MHz=" +
localMaxTxPwrConstraint20MHz + ", localMaxTxPwrConstraint40MHz=" +
localMaxTxPwrConstraint40MHz + ", localMaxTxPwrConstraint80MHz=" +
localMaxTxPwrConstraint80MHz + ", localMaxTxPwrConstraint160MHz=" +
localMaxTxPwrConstraint160MHz + "]";
}
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.operationelement;
package com.facebook.openwifi.cloudsdk.ies;
import java.util.Arrays;
import java.util.Objects;
@@ -17,7 +17,7 @@ import org.apache.commons.codec.binary.Base64;
* Very High Throughput (VHT) Operation Element, which is potentially present in
* wifiscan entries. Introduced in 802.11ac (2013).
*/
public class VHTOperationElement {
public class VHTOperation {
/**
* This field is 0 if the channel width is 20 MHz or 40 MHz, and 1 otherwise.
@@ -30,15 +30,23 @@ public class VHTOperationElement {
* 160 MHz wide channel, this parameter is the channel number of the 80MHz
* channel that contains the primary channel. For a 80+80 MHz wide channel, this
* parameter is the channel number of the primary channel.
* <p>
* This field is an unsigned byte in the specification (i.e., with values
* between 0 and 255). But because Java only supports signed bytes, a short
* data type is used to store the value.
*/
public final byte channel1;
public final short channel1;
/**
* This should be zero unless the channel is 160MHz or 80+80 MHz wide. If the
* channel is 160 MHz wide, this parameter is the channel number of the 160 MHz
* wide channel. If the channel is 80+80 MHz wide, this parameter is the channel
* index of the secondary 80 MHz wide channel.
* <p>
* This field is an unsigned byte in the specification (i.e., with values
* between 0 and 255). But because Java only supports signed bytes, a short
* data type is used to store the value.
*/
public final byte channel2;
public final short channel2;
/**
* An 8-element array where each element is between 0 and 4 inclusive. MCS means
* Modulation and Coding Scheme. NSS means Number of Spatial Streams. There can
@@ -57,11 +65,11 @@ public class VHTOperationElement {
* @param vhtOper a base64 encoded properly formatted VHT operation element (see
* 802.11 standard)
*/
public VHTOperationElement(String vhtOper) {
public VHTOperation(String vhtOper) {
byte[] bytes = Base64.decodeBase64(vhtOper);
this.channelWidth = bytes[0];
this.channel1 = bytes[1];
this.channel2 = bytes[2];
this.channel1 = (short) (bytes[1] & 0xff); // read as unsigned value
this.channel2 = (short) (bytes[2] & 0xff); // read as unsigned value
byte[] vhtMcsForNss = new byte[8];
vhtMcsForNss[0] = (byte) (bytes[3] >>> 6);
vhtMcsForNss[1] = (byte) ((bytes[3] & 0b00110000) >>> 4);
@@ -81,10 +89,10 @@ public class VHTOperationElement {
* For details about the parameters, see the javadocs for the corresponding
* member variables.
*/
public VHTOperationElement(
public VHTOperation(
byte channelWidth,
byte channel1,
byte channel2,
short channel1,
short channel2,
byte[] vhtMcsForNss
) {
/*
@@ -106,7 +114,7 @@ public class VHTOperationElement {
* @return true if the the operation elements "match" for the purpose of
* aggregating statistics; false otherwise.
*/
public boolean matchesForAggregation(VHTOperationElement other) {
public boolean matchesForAggregation(VHTOperation other) {
// check everything except vhtMcsForNss
return other != null && channel1 == other.channel1 &&
channel2 == other.channel2 && channelWidth == other.channelWidth;
@@ -134,8 +142,8 @@ public class VHTOperationElement {
if (vhtOper1 == null || vhtOper2 == null) {
return false; // false if exactly one is null
}
VHTOperationElement vhtOperObj1 = new VHTOperationElement(vhtOper1);
VHTOperationElement vhtOperObj2 = new VHTOperationElement(vhtOper2);
VHTOperation vhtOperObj1 = new VHTOperation(vhtOper1);
VHTOperation vhtOperObj2 = new VHTOperation(vhtOper2);
return vhtOperObj1.matchesForAggregation(vhtOperObj2);
}
@@ -160,7 +168,7 @@ public class VHTOperationElement {
if (getClass() != obj.getClass()) {
return false;
}
VHTOperationElement other = (VHTOperationElement) obj;
VHTOperation other = (VHTOperation) obj;
return channel1 == other.channel1 && channel2 == other.channel2 &&
channelWidth == other.channelWidth &&
Arrays.equals(vhtMcsForNss, other.vhtMcsForNss);

View File

@@ -6,13 +6,13 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral;
package com.facebook.openwifi.cloudsdk.kafka;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.kafka.common.errors.WakeupException;
import com.facebook.openwifirrm.ucentral.gw.models.ServiceEvent;
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
/**
* Kafka runner.

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral;
package com.facebook.openwifi.cloudsdk.kafka;
import java.time.Duration;
import java.util.ArrayList;
@@ -29,7 +29,8 @@ import org.apache.kafka.common.serialization.StringDeserializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.ucentral.gw.models.ServiceEvent;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
import com.google.gson.Gson;
import com.google.gson.JsonObject;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral;
package com.facebook.openwifi.cloudsdk.kafka;
import java.util.Properties;
@@ -18,7 +18,7 @@ import org.apache.kafka.common.serialization.StringSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.ucentral.gw.models.ServiceEvent;
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
import com.google.gson.Gson;
/**

View File

@@ -0,0 +1,51 @@
/*
* 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.ap;
import java.util.Map;
import java.util.List;
import com.google.gson.annotations.SerializedName;
public class Capabilities {
public String compatible;
public String model;
public String platform;
public Map<String, List<String>> network;
public static class Switch {
public boolean enable;
public boolean reset;
}
@SerializedName("switch") public Map<String, Switch> switch_;
public static class Phy {
public int tx_ant;
public int rx_ant;
public int[] frequencies;
public int[] channels;
public int[] dfs_channels;
public String[] htmode;
public String[] band;
public int ht_capa;
public int vht_capa;
public int[] he_phy_capa;
public int[] he_mac_capa;
public String country;
public String dfs_region;
public int temperature;
}
public Map<String, Phy> wifi;
// TODO The fields below were omitted
// macaddr;
// country_code;
// label_macaddr;
}

View File

@@ -6,14 +6,14 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.models;
package com.facebook.openwifi.cloudsdk.models.ap;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
public class State {
public class Interface {
public class Client {
public static class Interface {
public static class Client {
public String mac;
public String[] ipv4_addresses;
public String[] ipv6_addresses;
@@ -21,9 +21,9 @@ public class State {
// TODO last_seen
}
public class SSID {
public class Association {
public class Rate {
public static class SSID {
public static class Association {
public static class Rate {
public long bitrate;
public int chwidth;
public boolean sgi;
@@ -66,7 +66,7 @@ public class State {
public JsonObject radio;
}
public class Counters {
public static class Counters {
public long collisions;
public long multicast;
public long rx_bytes;
@@ -96,8 +96,8 @@ public class State {
public Interface[] interfaces;
public class Unit {
public class Memory {
public static class Unit {
public static class Memory {
public long buffered;
public long cached;
public long free;
@@ -112,8 +112,21 @@ public class State {
public Unit unit;
public static class Radio {
public long active_ms;
public long busy_ms;
public int channel;
public String channel_width;
public long noise;
public String phy;
public long receive_ms;
public long transmit_ms;
public int tx_power;
}
public Radio[] radios;
// TODO
public JsonObject[] radios;
@SerializedName("link-state") public JsonObject linkState;
public JsonObject gps;
public JsonObject poe;

View File

@@ -0,0 +1,95 @@
/*
* 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.ap;
import java.util.List;
import com.google.gson.JsonPrimitive;
import com.google.gson.annotations.SerializedName;
public class UCentralSchema {
public static class Radio {
public String band;
public int bandwidth;
// either "auto" or int
public JsonPrimitive channel;
@SerializedName("valid-channels") public int[] validChannels;
public String country;
@SerializedName("allow-dfs") public boolean allowDfs = true;
@SerializedName("channel-mode") public String channelMode = "HE";
@SerializedName("channel-wdith") public int channelWidth = 80;
@SerializedName("require-mode") public String requireMode;
public String mimo;
@SerializedName("tx-power") public int txPower;
@SerializedName("legacy-rates") public boolean legacyRates = false;
@SerializedName("beacon-interval") public int beaconInterval = 100;
@SerializedName("dtim-period") public int dtimPeriod = 2;
@SerializedName("maximum-clients") public int maximumClients;
public static class Rates {
public int beacon = 6000;
public int multicast = 24000;
}
public Rates rates;
public static class HESettings {
@SerializedName(
"multiple-bssid"
) public boolean multipleBssid = false;
public boolean ema = false;
@SerializedName("bss-color") public int bssColor = 64;
}
@SerializedName("he-settings") public HESettings heSettings;
@SerializedName("hostapd-iface-raw") public String[] hostapdIfaceRaw;
}
public List<Radio> radios;
public static class Metrics {
public static class Statistics {
public int interval;
public List<String> types;
}
public Statistics statistics;
public static class Health {
public int interval;
}
public Health health;
public static class WifiFrames {
public List<String> filters;
}
@SerializedName("wifi-frames") public WifiFrames wifiFrames;
public static class DhcpSnooping {
public List<String> filters;
}
@SerializedName("dhcp-snooping") public DhcpSnooping dhcpSnooping;
}
public Metrics metrics;
// metrics
// TODO the below fields are unused right now - include them as necessary
// unit
// globals
// definitions
// ethernet
// switch
// interfaces
// services
}

View File

@@ -6,13 +6,15 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.models;
package com.facebook.openwifi.cloudsdk.models.ap;
import java.util.Objects;
import com.google.gson.JsonArray;
/** Represents a single entry in wifi scan results. */
/**
* Represents a single entry in wifi scan results.
* ies[] array is not stored directly, but parsed into WifiScanEntry fields
*
*/
public class WifiScanEntryResult {
public int channel;
public long last_seen;
@@ -50,8 +52,6 @@ public class WifiScanEntryResult {
public String vht_oper;
public int capability;
public int frequency;
/** IE = information element */
public JsonArray ies;
/** Default Constructor. */
public WifiScanEntryResult() {}
@@ -68,7 +68,6 @@ public class WifiScanEntryResult {
this.vht_oper = o.vht_oper;
this.capability = o.capability;
this.frequency = o.frequency;
this.ies = o.ies;
}
@Override
@@ -79,7 +78,6 @@ public class WifiScanEntryResult {
channel,
frequency,
ht_oper,
ies,
last_seen,
signal,
ssid,
@@ -104,7 +102,9 @@ public class WifiScanEntryResult {
capability == other.capability && channel == other.channel &&
frequency == other.frequency && Objects
.equals(ht_oper, other.ht_oper) &&
Objects.equals(ies, other.ies) && last_seen == other.last_seen && signal == other.signal && Objects.equals(ssid, other.ssid) && tsf == other.tsf && Objects.equals(vht_oper, other.vht_oper);
last_seen == other.last_seen &&
Objects.equals(ssid, other.ssid) && tsf == other.tsf &&
Objects.equals(vht_oper, other.vht_oper);
}
@Override

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public class AclTemplate {
public boolean Read;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import com.google.gson.JsonObject;

View File

@@ -6,12 +6,12 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import com.google.gson.JsonObject;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
public class DeviceCapabilities {
public JsonObject capabilities;
public Capabilities capabilities;
public long firstUpdate;
public long lastUpdate;
public String serialNumber;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public class DeviceConfigureRequest {
public String serialNumber;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import java.util.List;

View File

@@ -6,6 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public enum DeviceType { AP, SWITCH, IOT, MESH }

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public class MfaAuthInfo {
public boolean enabled;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public class MobilePhoneNumber {
public String number;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public class NoteInfo {
public long created;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public class ServiceEvent {
public static final String EVENT_JOIN = "join";

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import com.google.gson.JsonObject;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public class TokenValidationResult {
public UserInfo userInfo;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public enum VerifiedCertificate {
NO_CERTIFICATE, VALID_CERTIFICATE, MISMATCH_SERIAL, VERIFIED

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public class WebTokenAclTemplate {
public AclTemplate aclTemplate;

View File

@@ -6,13 +6,13 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
public class WebTokenResult {
public String access_token;
public String refresh_token;
public String token_type;
public int expires_in;
public long expires_in;
public int idle_timeout;
public String username;
public long created;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.gw.models;
package com.facebook.openwifi.cloudsdk.models.gw;
import java.util.List;

View File

@@ -6,14 +6,14 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;
import com.facebook.openwifirrm.ucentral.gw.models.NoteInfo;
import com.facebook.openwifi.cloudsdk.models.gw.NoteInfo;
public class DeviceConfiguration {
public class DeviceConfigurationElement {
public static class DeviceConfigurationElement {
public String name;
public String description;
public Integer weight;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
public class DeviceRules {
public String rcOnly;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.ArrayList;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
public class DiGraphEntry {
public String parent;

View File

@@ -6,11 +6,11 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;
import com.facebook.openwifirrm.ucentral.gw.models.NoteInfo;
import com.facebook.openwifi.cloudsdk.models.gw.NoteInfo;
public class Entity {
// from ObjectInfo

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;

View File

@@ -6,11 +6,11 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;
import com.facebook.openwifirrm.ucentral.gw.models.NoteInfo;
import com.facebook.openwifi.cloudsdk.models.gw.NoteInfo;
public class InventoryTag {
// from ObjectInfo

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
public class RRMAlgorithmDetails {
public String name;

View File

@@ -6,12 +6,12 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;
public class RRMDetails {
public class RRMDetailsImpl {
public static class RRMDetailsImpl {
public String vendor;
public String schedule;
public List<RRMAlgorithmDetails> algorithms;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;

View File

@@ -6,11 +6,11 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;
import com.facebook.openwifirrm.ucentral.gw.models.NoteInfo;
import com.facebook.openwifi.cloudsdk.models.gw.NoteInfo;
public class Venue {
// from ObjectInfo

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.models;
package com.facebook.openwifi.cloudsdk.models.prov;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.rrm.models;
package com.facebook.openwifi.cloudsdk.models.prov.rrm;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.prov.rrm.models;
package com.facebook.openwifi.cloudsdk.models.prov.rrm;
public class Provider {
public String vendor;

View File

@@ -0,0 +1,6 @@
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5p [%c{1}:%L] - %m%n

View File

@@ -0,0 +1,81 @@
/*
* 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;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class UCentralUtilsTest {
@Test
void test_placeholder() throws Exception {
assertEquals(3, 1 + 2);
}
@Test
void test_setRadioConfigFieldChannel() throws Exception {
final String serialNumber = "aaaaaaaaaaaa";
final int expectedChannel = 1;
final Map<String, Integer> newValueList = Collections
.singletonMap(UCentralConstants.BAND_5G, expectedChannel);
// test case where channel value is a string and not an integer
UCentralApConfiguration config = new UCentralApConfiguration(
"{\"interfaces\": [], \"radios\": [{\"band\": \"5G\", \"channel\": \"auto\"}]}"
);
boolean modified = UCentralUtils
.setRadioConfigChannel(serialNumber, config, newValueList);
assertTrue(modified);
assertEquals(
config.getRadioConfig(0).channel.getAsInt(),
expectedChannel
);
// field doesn't exist
config = new UCentralApConfiguration(
"{\"interfaces\": [], \"radios\": [{\"band\": \"5G\"}]}"
);
modified = UCentralUtils
.setRadioConfigChannel(serialNumber, config, newValueList);
assertTrue(modified);
assertEquals(
config.getRadioConfig(0).channel.getAsInt(),
expectedChannel
);
// normal field, not modified
config = new UCentralApConfiguration(
"{\"interfaces\": [], \"radios\": [{\"band\": \"5G\", \"channel\": 1}]}"
);
modified = UCentralUtils
.setRadioConfigChannel(serialNumber, config, newValueList);
assertFalse(modified);
assertEquals(
config.getRadioConfig(0).channel.getAsInt(),
expectedChannel
);
// normal field, modified
config = new UCentralApConfiguration(
"{\"interfaces\": [], \"radios\": [{\"band\": \"5G\", \"channel\": 15}]}"
);
modified = UCentralUtils
.setRadioConfigChannel(serialNumber, config, newValueList);
assertTrue(modified);
assertEquals(
config.getRadioConfig(0).channel.getAsInt(),
expectedChannel
);
}
}

View File

@@ -6,17 +6,17 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.operationelement;
package com.facebook.openwifi.cloudsdk.ies;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class HTOperationElementTest {
public class HTOperationTest {
@Test
void testGetHtOper() {
String htOper = "AQAEAAAAAAAAAAAAAAAAAAAAAAAAAA==";
HTOperationElement htOperObj = new HTOperationElement(htOper);
HTOperation htOperObj = new HTOperation(htOper);
byte expectedPrimaryChannel = 1;
byte expectedSecondaryChannelOffset = 0;
boolean expectedStaChannelWidth = false;
@@ -28,7 +28,7 @@ public class HTOperationElementTest {
boolean expectedDualBeacon = false;
boolean expectedDualCtsProtection = false;
boolean expectedStbcBeacon = false;
HTOperationElement expectedHtOperObj = new HTOperationElement(
HTOperation expectedHtOperObj = new HTOperation(
expectedPrimaryChannel,
expectedSecondaryChannelOffset,
expectedStaChannelWidth,
@@ -44,11 +44,11 @@ public class HTOperationElementTest {
assertEquals(expectedHtOperObj, htOperObj);
htOper = "JAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
htOperObj = new HTOperationElement(htOper);
htOperObj = new HTOperation(htOper);
// all fields except the primary channel and nongreenfield field are the same
expectedPrimaryChannel = 36;
expectedNongreenfieldHtStasPresent = false;
expectedHtOperObj = new HTOperationElement(
expectedHtOperObj = new HTOperation(
expectedPrimaryChannel,
expectedSecondaryChannelOffset,
expectedStaChannelWidth,

View File

@@ -6,23 +6,23 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.ucentral.operationelement;
package com.facebook.openwifi.cloudsdk.ies;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class VHTOperationElementTest {
public class VHTOperationTest {
@Test
void testGetVhtOper() {
String vhtOper = "ACQAAAA=";
VHTOperationElement vhtOperObj = new VHTOperationElement(vhtOper);
VHTOperation vhtOperObj = new VHTOperation(vhtOper);
byte expectedChannelWidthIndicator = 0; // 20 MHz channel width
byte expectedChannel1 = 36;
byte expectedChannel2 = 0;
short expectedChannel1 = 36;
short expectedChannel2 = 0;
byte[] expectedVhtMcsForNss = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
VHTOperationElement expectedVhtOperObj = new VHTOperationElement(
VHTOperation expectedVhtOperObj = new VHTOperation(
expectedChannelWidthIndicator,
expectedChannel1,
expectedChannel2,
@@ -31,12 +31,12 @@ public class VHTOperationElementTest {
assertEquals(expectedVhtOperObj, vhtOperObj);
vhtOper = "AToAUAE=";
vhtOperObj = new VHTOperationElement(vhtOper);
vhtOperObj = new VHTOperation(vhtOper);
expectedChannelWidthIndicator = 1; // 80 MHz channel width
expectedChannel1 = 58;
// same channel2
expectedVhtMcsForNss = new byte[] { 1, 1, 0, 0, 0, 0, 0, 1 };
expectedVhtOperObj = new VHTOperationElement(
expectedVhtOperObj = new VHTOperation(
expectedChannelWidthIndicator,
expectedChannel1,
expectedChannel2,
@@ -45,12 +45,27 @@ public class VHTOperationElementTest {
assertEquals(expectedVhtOperObj, vhtOperObj);
vhtOper = "ASoyUAE=";
vhtOperObj = new VHTOperationElement(vhtOper);
vhtOperObj = new VHTOperation(vhtOper);
// same channel width indicator (160 MHz channel width)
expectedChannel1 = 42;
expectedChannel2 = 50;
// same vhtMcsForNss
expectedVhtOperObj = new VHTOperationElement(
expectedVhtOperObj = new VHTOperation(
expectedChannelWidthIndicator,
expectedChannel1,
expectedChannel2,
expectedVhtMcsForNss
);
assertEquals(expectedVhtOperObj, vhtOperObj);
// test with channel number >= 128 (channel fields should be unsigned)
vhtOper = "AJUAAAA=";
vhtOperObj = new VHTOperation(vhtOper);
expectedChannelWidthIndicator = 0;
expectedChannel1 = 149;
expectedChannel2 = 0;
expectedVhtMcsForNss = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
expectedVhtOperObj = new VHTOperation(
expectedChannelWidthIndicator,
expectedChannel1,
expectedChannel2,

4
owrrm/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/*.log*
/device_config.json
/settings.json
/topology.json

39
owrrm/README.md Normal file
View File

@@ -0,0 +1,39 @@
# OpenWiFi RRM Service
OpenWiFi uCentral-based radio resource management (RRM) service, providing a
cloud-based Wi-Fi RRM layer for APs running the OpenWiFi SDK.
This service collects data from OpenWiFi APs (e.g. Wi-Fi scans, stats,
capabilities) via the uCentral Gateway and Kafka, and integrates with the
OpenWiFi Provisioning service to perform optimization across configured
"venues". It pushes new device configuration parameters to APs after RRM
algorithms are run (manually or periodically).
See [IMPLEMENTATION.md](IMPLEMENTATION.md) for service architecture details and
[ALGORITHMS.md](ALGORITHMS.md) for descriptions of the RRM algorithms.
## Usage
```
$ java -jar openwifi-rrm.jar [-h]
```
To start the service, use the `run` command while providing configuration via
either environment variables (`--config-env`) or a static JSON file
(`--config-file`, default `settings.json`). The following data is *required*:
* Service configuration
* Env: `SERVICECONFIG_PRIVATEENDPOINT`, `SERVICECONFIG_PUBLICENDPOINT`
* JSON: `serviceConfig` structure
* Kafka broker URL
* Env: `KAFKACONFIG_BOOTSTRAPSERVER`
* JSON: `kafkaConfig` structure
* MySQL database credentials
* Env: `DATABASECONFIG_SERVER`, `DATABASECONFIG_USER`, `DATABASECONFIG_PASSWORD`
* JSON: `databaseConfig` structure
## OpenAPI
This service provides an OpenAPI HTTP interface on the port specified in the
service configuration (`moduleConfig.apiServerParams`). An auto-generated
OpenAPI 3.0 document is hosted at the endpoints `/openapi.{yaml,json}` and is
written to [openapi.yaml](openapi.yaml) during the Maven "compile" phase.
[JUnit 5]: https://junit.org/junit5/

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -478,8 +478,10 @@ components:
RRMSchedule:
type: object
properties:
cron:
type: string
crons:
type: array
items:
type: string
algorithms:
type: array
items:

139
owrrm/pom.xml Normal file
View File

@@ -0,0 +1,139 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>openwifi-rrm</artifactId>
<packaging>jar</packaging>
<parent>
<groupId>com.facebook</groupId>
<artifactId>openwifi-base</artifactId>
<version>2.7.0</version>
</parent>
<properties>
<!-- Hack for static files located in root project -->
<myproject.root>${project.basedir}/..</myproject.root>
<mainClassName>com.facebook.openwifi.rrm.Launcher</mainClassName>
<appendVersionString></appendVersionString>
</properties>
<build>
<finalName>openwifi-rrm</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/resources-filtered</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>${mainClassName}</Main-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.facebook</groupId>
<artifactId>openwifi-cloudsdk</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,160 @@
/*
* 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.rrm;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import spark.embeddedserver.jetty.JettyServerFactory;
import spark.utils.Assert;
/**
* Creates Jetty Server instances. Majority of the logic is taken from
* JettyServerFactory. The additional feature is that this class will actually
* set two connectors (original class doesn't set any connectors at all and
* leaves it up to the serivce start logic). Since we set two connectors here on
* the server, Spark uses the existing conectors instead of trying to spin up
* its own connectors. The other difference is that it uses a different
* ServerConnector constructor to avoid allocating additional threads that
* aren't necessary ({@link #makeConnector})
*
* @see spark.embeddedserver.jetty.EmbeddedJettyFactory
*/
public class CustomJettyServerFactory implements JettyServerFactory {
// normally this is set in EmbeddedJettyServer but since we create our own connectors here,
// we need the value here
private boolean trustForwardHeaders = true; // true by default
private final int internalPort;
private final int externalPort;
public CustomJettyServerFactory(int internalPort, int externalPort) {
this.internalPort = internalPort;
this.externalPort = externalPort;
}
public void setTrustForwardHeaders(boolean trustForwardHeaders) {
this.trustForwardHeaders = trustForwardHeaders;
}
/**
* This is basically
* spark.embeddedserver.jetty.SocketConnectorFactory.createSocketConnector,
* the only difference being that we use a different constructor for the
* Connector and that the private methods called are just inlined.
*/
public Connector makeConnector(
Server server,
String host,
int port,
boolean trustForwardHeaders
) {
Assert.notNull(server, "'server' must not be null");
Assert.notNull(host, "'host' must not be null");
// spark.embeddedserver.jetty.SocketConnectorFactory.createHttpConnectionFactory
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSecureScheme("https");
if (trustForwardHeaders) {
httpConfig.addCustomizer(new ForwardedRequestCustomizer());
}
HttpConnectionFactory httpConnectionFactory =
new HttpConnectionFactory(httpConfig);
ServerConnector connector = new ServerConnector(
server,
0, // acceptors, don't allocate separate threads for acceptor
0, // selectors, use default number
httpConnectionFactory
);
// spark.embeddedserver.jetty.SocketConnectorFactory.initializeConnector
connector.setIdleTimeout(TimeUnit.HOURS.toMillis(1));
connector.setHost(host);
connector.setPort(port);
return connector;
}
/**
* Creates a Jetty server.
*
* @param maxThreads maxThreads
* @param minThreads minThreads
* @param threadTimeoutMillis threadTimeoutMillis
* @return a new jetty server instance
*/
@Override
public Server create(
int maxThreads,
int minThreads,
int threadTimeoutMillis
) {
Server server;
if (maxThreads > 0) {
int max = maxThreads;
int min = (minThreads > 0) ? minThreads : 8;
int idleTimeout =
(threadTimeoutMillis > 0) ? threadTimeoutMillis : 60000;
server = new Server(new QueuedThreadPool(max, min, idleTimeout));
} else {
server = new Server();
}
Connector internalConnector = null;
if (internalPort != -1) {
internalConnector = makeConnector(
server,
"0.0.0.0",
internalPort,
trustForwardHeaders
);
}
Connector externalConnector = null;
if (externalPort != -1) {
externalConnector = makeConnector(
server,
"0.0.0.0",
externalPort,
trustForwardHeaders
);
}
if (internalConnector == null) {
server.setConnectors(new Connector[] { externalConnector });
} else if (externalConnector == null) {
server.setConnectors(new Connector[] { internalConnector });
} else {
server.setConnectors(
new Connector[] { internalConnector, externalConnector }
);
}
return server;
}
/**
* Creates a Jetty server with supplied thread pool
* @param threadPool thread pool
* @return a new jetty server instance
*/
@Override
public Server create(ThreadPool threadPool) {
return threadPool != null ? new Server(threadPool) : new Server();
}
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.lang.reflect.Field;
import java.util.List;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.io.File;
import java.io.FileNotFoundException;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.util.Map;
import java.util.TreeMap;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.util.Set;
import java.util.TreeMap;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.io.File;
import java.io.FileWriter;
@@ -18,11 +18,10 @@ import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.mysql.DatabaseManager;
import com.facebook.openwifirrm.ucentral.UCentralClient;
import com.facebook.openwifirrm.ucentral.UCentralKafkaConsumer;
import com.facebook.openwifirrm.ucentral.UCentralKafkaProducer;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaProducer;
import com.facebook.openwifi.rrm.mysql.DatabaseManager;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -155,8 +154,7 @@ public class Launcher implements Callable<Integer> {
: DEFAULT_DEVICE_LAYERED_CONFIG_FILE
);
String serviceKey =
UCentralUtils.generateServiceKey(config.serviceConfig);
String serviceKey = Utils.generateServiceKey(config.serviceConfig);
// Instantiate clients
UCentralClient.verifySsl(config.uCentralConfig.verifySsl);
@@ -166,7 +164,9 @@ public class Launcher implements Callable<Integer> {
config.uCentralConfig.uCentralSecPublicEndpoint,
config.uCentralConfig.username,
config.uCentralConfig.password,
config.uCentralConfig.uCentralSocketParams
config.uCentralConfig.uCentralSocketParams.connectTimeoutMs,
config.uCentralConfig.uCentralSocketParams.socketTimeoutMs,
config.uCentralConfig.uCentralSocketParams.wifiScanTimeoutMs
);
UCentralKafkaConsumer consumer;
UCentralKafkaProducer producer;
@@ -265,7 +265,7 @@ public class Launcher implements Callable<Integer> {
.setPrettyPrinting()
.serializeNulls() // for here only!!
.create();
logger.info(gson.toJson(DeviceConfig.createDefault()));
System.out.println(gson.toJson(DeviceConfig.createDefault()));
return 0;
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.util.Arrays;
import java.util.List;
@@ -18,18 +18,18 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.modules.ApiServer;
import com.facebook.openwifirrm.modules.ConfigManager;
import com.facebook.openwifirrm.modules.DataCollector;
import com.facebook.openwifirrm.modules.Modeler;
import com.facebook.openwifirrm.modules.ProvMonitor;
import com.facebook.openwifirrm.modules.RRMScheduler;
import com.facebook.openwifirrm.mysql.DatabaseManager;
import com.facebook.openwifirrm.ucentral.KafkaRunner;
import com.facebook.openwifirrm.ucentral.UCentralClient;
import com.facebook.openwifirrm.ucentral.UCentralKafkaConsumer;
import com.facebook.openwifirrm.ucentral.UCentralKafkaProducer;
import com.facebook.openwifirrm.ucentral.gw.models.SystemInfoResults;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.kafka.KafkaRunner;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaProducer;
import com.facebook.openwifi.cloudsdk.models.gw.SystemInfoResults;
import com.facebook.openwifi.rrm.modules.ApiServer;
import com.facebook.openwifi.rrm.modules.ConfigManager;
import com.facebook.openwifi.rrm.modules.DataCollector;
import com.facebook.openwifi.rrm.modules.Modeler;
import com.facebook.openwifi.rrm.modules.ProvMonitor;
import com.facebook.openwifi.rrm.modules.RRMScheduler;
import com.facebook.openwifi.rrm.mysql.DatabaseManager;
/**
* RRM service runner.

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.util.HashMap;
import java.util.Map;
@@ -14,17 +14,17 @@ import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.modules.ConfigManager;
import com.facebook.openwifirrm.modules.Modeler;
import com.facebook.openwifirrm.optimizers.channel.ChannelOptimizer;
import com.facebook.openwifirrm.optimizers.channel.LeastUsedChannelOptimizer;
import com.facebook.openwifirrm.optimizers.channel.RandomChannelInitializer;
import com.facebook.openwifirrm.optimizers.channel.UnmanagedApAwareChannelOptimizer;
import com.facebook.openwifirrm.optimizers.tpc.LocationBasedOptimalTPC;
import com.facebook.openwifirrm.optimizers.tpc.MeasurementBasedApApTPC;
import com.facebook.openwifirrm.optimizers.tpc.MeasurementBasedApClientTPC;
import com.facebook.openwifirrm.optimizers.tpc.RandomTxPowerInitializer;
import com.facebook.openwifirrm.optimizers.tpc.TPC;
import com.facebook.openwifi.rrm.modules.ConfigManager;
import com.facebook.openwifi.rrm.modules.Modeler;
import com.facebook.openwifi.rrm.optimizers.channel.ChannelOptimizer;
import com.facebook.openwifi.rrm.optimizers.channel.LeastUsedChannelOptimizer;
import com.facebook.openwifi.rrm.optimizers.channel.RandomChannelInitializer;
import com.facebook.openwifi.rrm.optimizers.channel.UnmanagedApAwareChannelOptimizer;
import com.facebook.openwifi.rrm.optimizers.tpc.LocationBasedOptimalTPC;
import com.facebook.openwifi.rrm.optimizers.tpc.MeasurementBasedApApTPC;
import com.facebook.openwifi.rrm.optimizers.tpc.MeasurementBasedApClientTPC;
import com.facebook.openwifi.rrm.optimizers.tpc.RandomTxPowerInitializer;
import com.facebook.openwifi.rrm.optimizers.tpc.TPC;
/**
* RRM algorithm model and utility methods.
@@ -143,6 +143,9 @@ public class RRMAlgorithm {
* @param dryRun if set, do not apply changes
* @param allowDefaultMode if false, "mode" argument must be present and
* valid (returns error if invalid)
* @param updateImmediately true if the method should queue the zone for
* update and interrupt the config manager thread
* to trigger immediate update
*
* @return the algorithm result, with exactly one field set ("error" upon
* failure, any others upon success)
@@ -153,7 +156,8 @@ public class RRMAlgorithm {
Modeler modeler,
String zone,
boolean dryRun,
boolean allowDefaultMode
boolean allowDefaultMode,
boolean updateImmediately
) {
AlgorithmResult result = new AlgorithmResult();
if (name == null || args == null) {
@@ -212,11 +216,14 @@ public class RRMAlgorithm {
}
result.channelMap = optimizer.computeChannelMap();
if (!dryRun) {
optimizer.applyConfig(
optimizer.updateDeviceApConfig(
deviceDataManager,
configManager,
result.channelMap
);
if (updateImmediately) {
configManager.queueZoneAndWakeUp(zone);
}
}
} else if (
name.equals(RRMAlgorithm.AlgorithmType.OptimizeTxPower.name())
@@ -270,11 +277,14 @@ public class RRMAlgorithm {
}
result.txPowerMap = optimizer.computeTxPowerMap();
if (!dryRun) {
optimizer.applyConfig(
optimizer.updateDeviceApConfig(
deviceDataManager,
configManager,
result.txPowerMap
);
if (updateImmediately) {
configManager.queueZoneAndWakeUp(zone);
}
}
} else {
result.error = String.format("Unknown algorithm: '%s'", name);

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.util.Map;
@@ -34,7 +34,7 @@ public class RRMConfig {
* Private endpoint for the RRM service
* ({@code SERVICECONFIG_PRIVATEENDPOINT})
*/
public String privateEndpoint = "http://owrrm.wlan.local:16789"; // see ApiServerParams.httpPort
public String privateEndpoint = "http://owrrm.wlan.local:16790"; // see ApiServerParams.internalHttpPort
/**
* Public endpoint for the RRM service
@@ -60,7 +60,7 @@ public class RRMConfig {
* ({@code SERVICECONFIG_VENDORREFERENCEURL})
*/
public String vendorReferenceUrl =
"https://github.com/Telecominfraproject/wlan-cloud-rrm/blob/main/ALGORITHMS.md";
"https://github.com/Telecominfraproject/wlan-cloud-rrm/blob/main/owrrm/ALGORITHMS.md";
}
/** Service configuration. */
@@ -232,7 +232,7 @@ public class RRMConfig {
* The main logic loop interval (i.e. sleep time), in ms
* ({@code DATACOLLECTORPARAMS_UPDATEINTERVALMS})
*/
public int updateIntervalMs = 5000;
public int updateIntervalMs = 30000; // 30sec
/**
* The expected device statistics interval, in seconds (or -1 to
@@ -246,13 +246,13 @@ public class RRMConfig {
* automatic scans)
* ({@code DATACOLLECTORPARAMS_WIFISCANINTERVALSEC})
*/
public int wifiScanIntervalSec = 900;
public int wifiScanIntervalSec = 900; // 15min
/**
* The capabilities request interval (per device), in seconds
* ({@code DATACOLLECTORPARAMS_CAPABILITIESINTERVALSEC})
*/
public int capabilitiesIntervalSec = 3600;
public int capabilitiesIntervalSec = 3600; // 1hr
/**
* Number of executor threads for async tasks (ex. wifi scans)
@@ -273,7 +273,7 @@ public class RRMConfig {
* The main logic loop interval (i.e. sleep time), in ms
* ({@code CONFIGMANAGERPARAMS_UPDATEINTERVALMS})
*/
public int updateIntervalMs = 60000;
public int updateIntervalMs = 120000; // 2min
/**
* Enable pushing device config changes?
@@ -309,6 +309,12 @@ public class RRMConfig {
* ({@code MODELERPARAMS_WIFISCANBUFFERSIZE})
*/
public int wifiScanBufferSize = 10;
/**
* Maximum rounds of States to store per device
* ({@code MODELERPARAMS_STATEBUFFERSIZE})
*/
public int stateBufferSize = 10;
}
/** Modeler parameters. */
@@ -319,10 +325,16 @@ public class RRMConfig {
*/
public class ApiServerParams {
/**
* The HTTP port to listen on, or -1 to disable
* ({@code APISERVERPARAMS_HTTPPORT})
* The HTTP port to listen on for internal traffic, or -1 to disable
* ({@code APISERVERPARAMS_INTERNALHTTPPORT})
*/
public int httpPort = 16789;
public int internalHttpPort = 16790;
/**
* The HTTP port to listen on for external traffic, or -1 to disable
* ({@code APISERVERPARAMS_EXTERNALHTTPPORT})
*/
public int externalHttpPort = 16789;
/**
* Comma-separated list of all allowed CORS domains (exact match
@@ -363,7 +375,7 @@ public class RRMConfig {
* Sync interval, in ms, for owprov venue information etc.
* ({@code PROVMONITORPARAMS_SYNCINTERVALMS})
*/
public int syncIntervalMs = 300000;
public int syncIntervalMs = 300000; // 5min
}
/** ProvMonitor parameters. */
@@ -532,10 +544,16 @@ public class RRMConfig {
if ((v = env.get("MODELERPARAMS_WIFISCANBUFFERSIZE")) != null) {
modelerParams.wifiScanBufferSize = Integer.parseInt(v);
}
if ((v = env.get("MODELERPARAMS_STATEBUFFERSIZE")) != null) {
modelerParams.stateBufferSize = Integer.parseInt(v);
}
ModuleConfig.ApiServerParams apiServerParams =
config.moduleConfig.apiServerParams;
if ((v = env.get("APISERVERPARAMS_HTTPPORT")) != null) {
apiServerParams.httpPort = Integer.parseInt(v);
if ((v = env.get("APISERVERPARAMS_INTERNALHTTPPORT")) != null) {
apiServerParams.internalHttpPort = Integer.parseInt(v);
}
if ((v = env.get("APISERVERPARAMS_EXTERNALHTTPPORT")) != null) {
apiServerParams.externalHttpPort = Integer.parseInt(v);
}
if ((v = env.get("APISERVERPARAMS_CORSDOMAINLIST")) != null) {
apiServerParams.corsDomainList = v;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.util.List;
@@ -19,9 +19,9 @@ public class RRMSchedule {
*
* This field expects a cron-like format as defined by the Quartz Job
* Scheduler (CronTrigger):
* https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
* https://www.quartz-scheduler.org/documentation/quartz-2.4.0/tutorials/crontrigger.html
*/
public String cron;
public List<String> crons;
/**
* The list of RRM algorithms to run.

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import java.io.File;
import java.io.FileNotFoundException;
@@ -16,6 +16,8 @@ import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Scanner;
@@ -23,6 +25,8 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -31,6 +35,8 @@ import com.google.gson.GsonBuilder;
* Generic utility methods.
*/
public class Utils {
private static final Logger logger = LoggerFactory.getLogger(Utils.class);
/** Hex value array for use in {@link #longToMac(long)}. */
private static final char[] HEX_VALUES = "0123456789abcdef".toCharArray();
@@ -193,4 +199,19 @@ public class Utils {
public static <T> T deepCopy(T obj, Class<T> classOfT) {
return gson.fromJson(gson.toJson(obj), classOfT);
}
/** Generate the RRM service key. */
public static String generateServiceKey(
RRMConfig.ServiceConfig serviceConfig
) {
try {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
sha256.update(serviceConfig.publicEndpoint.getBytes());
sha256.update(serviceConfig.privateEndpoint.getBytes());
return Utils.bytesToHex(sha256.digest());
} catch (NoSuchAlgorithmException e) {
logger.error("Unable to generate service key", e);
return "";
}
}
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm;
package com.facebook.openwifi.rrm;
import picocli.CommandLine.IVersionProvider;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.aggregators;
package com.facebook.openwifi.rrm.aggregators;
/**
* Aggregates added values into one "aggregate" measure.

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.aggregators;
package com.facebook.openwifi.rrm.aggregators;
/**
* Tracks the mean of all added values. If no values are added, the mean is 0.

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.modules;
package com.facebook.openwifi.rrm.modules;
import java.net.InetAddress;
import java.net.URI;
@@ -35,28 +35,29 @@ import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.DeviceLayeredConfig;
import com.facebook.openwifirrm.DeviceTopology;
import com.facebook.openwifirrm.RRMAlgorithm;
import com.facebook.openwifirrm.RRMConfig.ModuleConfig.ApiServerParams;
import com.facebook.openwifirrm.RRMConfig.ServiceConfig;
import com.facebook.openwifirrm.Utils.LruCache;
import com.facebook.openwifirrm.VersionProvider;
import com.facebook.openwifirrm.optimizers.channel.LeastUsedChannelOptimizer;
import com.facebook.openwifirrm.optimizers.channel.RandomChannelInitializer;
import com.facebook.openwifirrm.optimizers.channel.UnmanagedApAwareChannelOptimizer;
import com.facebook.openwifirrm.optimizers.tpc.LocationBasedOptimalTPC;
import com.facebook.openwifirrm.optimizers.tpc.MeasurementBasedApApTPC;
import com.facebook.openwifirrm.optimizers.tpc.MeasurementBasedApClientTPC;
import com.facebook.openwifirrm.optimizers.tpc.RandomTxPowerInitializer;
import com.facebook.openwifirrm.ucentral.UCentralClient;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.gw.models.SystemInfoResults;
import com.facebook.openwifirrm.ucentral.gw.models.TokenValidationResult;
import com.facebook.openwifirrm.ucentral.prov.rrm.models.Algorithm;
import com.facebook.openwifirrm.ucentral.prov.rrm.models.Provider;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.models.gw.SystemInfoResults;
import com.facebook.openwifi.cloudsdk.models.gw.TokenValidationResult;
import com.facebook.openwifi.cloudsdk.models.prov.rrm.Algorithm;
import com.facebook.openwifi.cloudsdk.models.prov.rrm.Provider;
import com.facebook.openwifi.rrm.CustomJettyServerFactory;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.DeviceLayeredConfig;
import com.facebook.openwifi.rrm.DeviceTopology;
import com.facebook.openwifi.rrm.RRMAlgorithm;
import com.facebook.openwifi.rrm.Utils;
import com.facebook.openwifi.rrm.VersionProvider;
import com.facebook.openwifi.rrm.RRMConfig.ServiceConfig;
import com.facebook.openwifi.rrm.RRMConfig.ModuleConfig.ApiServerParams;
import com.facebook.openwifi.rrm.Utils.LruCache;
import com.facebook.openwifi.rrm.optimizers.channel.LeastUsedChannelOptimizer;
import com.facebook.openwifi.rrm.optimizers.channel.RandomChannelInitializer;
import com.facebook.openwifi.rrm.optimizers.channel.UnmanagedApAwareChannelOptimizer;
import com.facebook.openwifi.rrm.optimizers.tpc.LocationBasedOptimalTPC;
import com.facebook.openwifi.rrm.optimizers.tpc.MeasurementBasedApApTPC;
import com.facebook.openwifi.rrm.optimizers.tpc.MeasurementBasedApClientTPC;
import com.facebook.openwifi.rrm.optimizers.tpc.RandomTxPowerInitializer;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -81,7 +82,9 @@ import io.swagger.v3.oas.models.OpenAPI;
import spark.Request;
import spark.Response;
import spark.Route;
import spark.Spark;
import spark.Service;
import spark.embeddedserver.EmbeddedServers;
import spark.embeddedserver.jetty.EmbeddedJettyFactory;
/**
* HTTP API server.
@@ -110,6 +113,27 @@ public class ApiServer implements Runnable {
private static final Logger logger =
LoggerFactory.getLogger(ApiServer.class);
/**
* This is the identifier for the server factory that Spark should use. This
* particular identifier points to the custom factory that we register to
* enable running multiple ports on one service.
*
* @see #run()
*/
private static final String SPARK_EMBEDDED_SERVER_IDENTIFIER =
ApiServer.class.getName();
/**
* The Spark service instance. Normally, you would use the static methods on
* Spark, but since we need to spin up multiple instances of Spark for testing,
* we choose to go with instantiating the service ourselves. There is really no
* difference except with the static method, Spark calls ignite and holds a
* singleton instance for us.
*
* @see Spark
*/
private final Service service;
/** The module parameters. */
private final ApiServerParams params;
@@ -164,9 +188,10 @@ public class ApiServer implements Runnable {
UCentralClient client,
RRMScheduler scheduler
) {
this.service = Service.ignite();
this.params = params;
this.serviceConfig = serviceConfig;
this.serviceKey = UCentralUtils.generateServiceKey(serviceConfig);
this.serviceKey = Utils.generateServiceKey(serviceConfig);
this.deviceDataManager = deviceDataManager;
this.configManager = configManager;
this.modeler = modeler;
@@ -194,64 +219,116 @@ public class ApiServer implements Runnable {
return ret;
}
/**
* Block until initialization finishes. Just calls the method on the
* underlying service.
*/
public void awaitInitialization() {
service.awaitInitialization();
}
@Override
public void run() {
this.startTimeMs = System.currentTimeMillis();
if (params.httpPort == -1) {
if (params.internalHttpPort == -1 && params.externalHttpPort == -1) {
logger.info("API server is disabled.");
return;
} else if (params.internalHttpPort == -1) {
logger.info("Internal API server is disabled");
} else if (params.externalHttpPort == -1) {
logger.info("External API server is disabled");
}
if (params.internalHttpPort == params.externalHttpPort) {
logger.error(
"Internal and external port cannot be the same - not starting API server"
);
return;
}
Spark.port(params.httpPort);
EmbeddedServers.add(
SPARK_EMBEDDED_SERVER_IDENTIFIER,
new EmbeddedJettyFactory(
new CustomJettyServerFactory(
params.internalHttpPort,
params.externalHttpPort
)
)
);
// use the embedded server factory added above, this is required so that we
// don't mess up the default factory which can and will be used for
// additional Spark services in testing
service.embeddedServerIdentifier(SPARK_EMBEDDED_SERVER_IDENTIFIER);
// Usually you would call this with an actual port and Spark would spin up a
// port on it. However, since we're putting our own connectors in so that we
// can use two ports and Spark has logic to use connectors that already exist
// so it doesn't matter what port we pass in here as long as it's not one of
// the actual ports we're using (Spark has some weird logic where it still
// tries to bind to the port).
// @see EmbeddedJettyServer
service.port(0);
// Configure API docs hosting
Spark.staticFiles.location("/public");
Spark.get("/openapi.yaml", this::getOpenApiYaml);
Spark.get("/openapi.json", this::getOpenApiJson);
service.staticFiles.location("/public");
service.get("/openapi.yaml", this::getOpenApiYaml);
service.get("/openapi.json", this::getOpenApiJson);
// Install routes
Spark.before(this::beforeFilter);
Spark.after(this::afterFilter);
Spark.options("/*", this::options);
Spark.get("/api/v1/system", new SystemEndpoint());
Spark.post("/api/v1/system", new SetSystemEndpoint());
Spark.get("/api/v1/provider", new ProviderEndpoint());
Spark.get("/api/v1/algorithms", new AlgorithmsEndpoint());
Spark.put("/api/v1/runRRM", new RunRRMEndpoint());
Spark.get("/api/v1/getTopology", new GetTopologyEndpoint());
Spark.post("/api/v1/setTopology", new SetTopologyEndpoint());
Spark.get(
service.before(this::beforeFilter);
service.after(this::afterFilter);
service.options("/*", this::options);
service.get("/api/v1/system", new SystemEndpoint());
service.post("/api/v1/system", new SetSystemEndpoint());
service.get("/api/v1/provider", new ProviderEndpoint());
service.get("/api/v1/algorithms", new AlgorithmsEndpoint());
service.put("/api/v1/runRRM", new RunRRMEndpoint());
service.get("/api/v1/getTopology", new GetTopologyEndpoint());
service.post("/api/v1/setTopology", new SetTopologyEndpoint());
service.get(
"/api/v1/getDeviceLayeredConfig",
new GetDeviceLayeredConfigEndpoint()
);
Spark.get("/api/v1/getDeviceConfig", new GetDeviceConfigEndpoint());
Spark.post(
service.get("/api/v1/getDeviceConfig", new GetDeviceConfigEndpoint());
service.post(
"/api/v1/setDeviceNetworkConfig",
new SetDeviceNetworkConfigEndpoint()
);
Spark.post(
service.post(
"/api/v1/setDeviceZoneConfig",
new SetDeviceZoneConfigEndpoint()
);
Spark.post(
service.post(
"/api/v1/setDeviceApConfig",
new SetDeviceApConfigEndpoint()
);
Spark.post(
service.post(
"/api/v1/modifyDeviceApConfig",
new ModifyDeviceApConfigEndpoint()
);
Spark.get("/api/v1/currentModel", new GetCurrentModelEndpoint());
Spark.get("/api/v1/optimizeChannel", new OptimizeChannelEndpoint());
Spark.get("/api/v1/optimizeTxPower", new OptimizeTxPowerEndpoint());
service.get("/api/v1/currentModel", new GetCurrentModelEndpoint());
service.get("/api/v1/optimizeChannel", new OptimizeChannelEndpoint());
service.get("/api/v1/optimizeTxPower", new OptimizeTxPowerEndpoint());
logger.info("API server listening on HTTP port {}", params.httpPort);
logger.info(
"API server listening for HTTP internal on port {} and external on port {}",
params.internalHttpPort,
params.externalHttpPort
);
}
/** Stop the server. */
public void shutdown() {
Spark.stop();
service.stop();
}
/**
* Block until stop finishes. Just calls the method on the underlying service.
*/
public void awaitStop() {
service.awaitStop();
}
/** Reconstructs a URL. */
@@ -269,15 +346,17 @@ public class ApiServer implements Runnable {
* HTTP 403 response and return false.
*/
private boolean performOpenWifiAuth(Request request, Response response) {
// TODO check if request came from internal endpoint
boolean internal = true;
String internalName = request.headers("X-INTERNAL-NAME");
if (internal && internalName != null) {
// Internal request, validate "X-API-KEY"
String apiKey = request.headers("X-API-KEY");
if (apiKey != null && apiKey.equals(serviceKey)) {
// auth success
return true;
int port = request.port();
boolean internal = port > 0 && port == params.internalHttpPort;
if (internal) {
String internalName = request.headers("X-INTERNAL-NAME");
if (internalName != null) {
// Internal request, validate "X-API-KEY"
String apiKey = request.headers("X-API-KEY");
if (apiKey != null && apiKey.equals(serviceKey)) {
// auth success
return true;
}
}
} else {
// External request, validate token:
@@ -297,17 +376,18 @@ public class ApiServer implements Runnable {
}
// auth failure
Spark.halt(403, "Forbidden");
service.halt(403, "Forbidden");
return false;
}
/**
* Validate an OpenWiFi token (external), caching successful lookups.
* Validate an OpenWiFi token (external), caching successful lookups. This will
* validate a USER token - subscriber token won't work and will fail (plus only
* users should be dealing with RRM).
* @return true if token is valid
*/
private boolean validateOpenWifiToken(String token) {
// The below only checks /api/v1/validateToken and caches it as necessary.
// TODO - /api/v1/validateSubToken still has to be implemented.
Long expiry = tokenCache.get(token);
if (expiry == null) {
TokenValidationResult result = client.validateToken(token);
@@ -324,10 +404,11 @@ public class ApiServer implements Runnable {
private void beforeFilter(Request request, Response response) {
// Log requests
logger.debug(
"[{}] {} {}",
"[{}] {} {} on port {}",
request.ip(),
request.requestMethod(),
getFullUrl(request.pathInfo(), request.queryString())
getFullUrl(request.pathInfo(), request.queryString()),
request.port()
);
// Remove "Server: Jetty" header
@@ -711,7 +792,8 @@ public class ApiServer implements Runnable {
modeler,
venue,
mock,
true /* allowDefaultMode */
true, /* allowDefaultMode */
true /* updateImmediately */
);
if (result.error != null) {
response.status(400);
@@ -917,7 +999,7 @@ public class ApiServer implements Runnable {
DeviceConfig networkConfig =
gson.fromJson(request.body(), DeviceConfig.class);
deviceDataManager.setDeviceNetworkConfig(networkConfig);
configManager.wakeUp();
configManager.queueAllZonesAndWakeUp();
// Revalidate data model
modeler.revalidate();
@@ -981,7 +1063,7 @@ public class ApiServer implements Runnable {
DeviceConfig zoneConfig =
gson.fromJson(request.body(), DeviceConfig.class);
deviceDataManager.setDeviceZoneConfig(zone, zoneConfig);
configManager.wakeUp();
configManager.queueZoneAndWakeUp(zone);
// Revalidate data model
modeler.revalidate();
@@ -1044,7 +1126,10 @@ public class ApiServer implements Runnable {
DeviceConfig apConfig =
gson.fromJson(request.body(), DeviceConfig.class);
deviceDataManager.setDeviceApConfig(serialNumber, apConfig);
configManager.wakeUp();
// TODO enable updates to device(s), not just the entire zone
final String zone =
deviceDataManager.getDeviceZone(serialNumber);
configManager.queueZoneAndWakeUp(zone);
// Revalidate data model
modeler.revalidate();
@@ -1117,7 +1202,10 @@ public class ApiServer implements Runnable {
.computeIfAbsent(serialNumber, k -> new DeviceConfig())
.apply(apConfig);
});
configManager.wakeUp();
final String zone =
deviceDataManager.getDeviceZone(serialNumber);
// TODO enable updates to device(s), not just the entire zone
configManager.queueZoneAndWakeUp(zone);
// Revalidate data model
modeler.revalidate();
@@ -1260,7 +1348,8 @@ public class ApiServer implements Runnable {
modeler,
zone,
dryRun,
false /* allowDefaultMode */
false, /* allowDefaultMode */
true /* updateImmediately */
);
if (result.error != null) {
response.status(400);
@@ -1371,7 +1460,8 @@ public class ApiServer implements Runnable {
modeler,
zone,
dryRun,
false /* allowDefaultMode */
false, /* allowDefaultMode */
true /* updateImmediately */
);
if (result.error != null) {
response.status(400);

View File

@@ -6,25 +6,28 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.modules;
package com.facebook.openwifi.rrm.modules;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.RRMConfig.ModuleConfig.ConfigManagerParams;
import com.facebook.openwifirrm.ucentral.UCentralApConfiguration;
import com.facebook.openwifirrm.ucentral.UCentralClient;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceWithStatus;
import com.facebook.openwifi.cloudsdk.UCentralApConfiguration;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceWithStatus;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.RRMConfig.ModuleConfig.ConfigManagerParams;
/**
* Device configuration manager module.
@@ -63,8 +66,11 @@ public class ConfigManager implements Runnable {
/** Is the main thread sleeping? */
private final AtomicBoolean sleepingFlag = new AtomicBoolean(false);
/** Was a manual config update requested? */
private final AtomicBoolean eventFlag = new AtomicBoolean(false);
/**
* Thread-safe set of zones for which manual config updates have been
* requested.
*/
private Set<String> zonesToUpdate = ConcurrentHashMap.newKeySet();
/** Config listener interface. */
public interface ConfigListener {
@@ -180,7 +186,10 @@ public class ConfigManager implements Runnable {
List<String> devicesNeedingUpdate = new ArrayList<>();
final long CONFIG_DEBOUNCE_INTERVAL_NS =
params.configDebounceIntervalSec * 1_000_000_000L;
final boolean isEvent = eventFlag.getAndSet(false);
Set<String> zonesToUpdateCopy = new HashSet<>(zonesToUpdate);
// use removeAll() instead of clear() in case items are added between
// the previous line and the following line
zonesToUpdate.removeAll(zonesToUpdateCopy);
for (DeviceWithStatus device : devices) {
// Update config structure
DeviceData data = deviceDataMap.computeIfAbsent(
@@ -201,11 +210,13 @@ public class ConfigManager implements Runnable {
for (ConfigListener listener : configListeners.values()) {
listener.receiveDeviceConfig(device.serialNumber, data.config);
}
// Check event flag
// Check if there are requested updates for this zone
String deviceZone =
deviceDataManager.getDeviceZone(device.serialNumber);
boolean isEvent = zonesToUpdateCopy.contains(deviceZone);
if (params.configOnEventOnly && !isEvent) {
logger.debug(
"Skipping config for {} (event flag not set)",
"Skipping config for {} (zone not marked for updates)",
device.serialNumber
);
continue;
@@ -251,15 +262,16 @@ public class ConfigManager implements Runnable {
}
}
final boolean shouldUpdate = !zonesToUpdateCopy.isEmpty();
// Send config changes to devices
if (!params.configEnabled) {
logger.trace("Config changes are disabled.");
} else if (devicesNeedingUpdate.isEmpty()) {
logger.debug("No device configs to send.");
} else if (params.configOnEventOnly && !isEvent) {
} else if (params.configOnEventOnly && !shouldUpdate) {
// shouldn't happen
logger.error(
"ERROR!! {} device(s) queued for config update, but event flag not set",
"ERROR!! {} device(s) queued for config update, but no zones queued for update.",
devicesNeedingUpdate.size()
);
} else {
@@ -317,10 +329,9 @@ public class ConfigManager implements Runnable {
channelList.putAll(deviceConfig.userChannels);
}
if (!channelList.isEmpty()) {
modified |= UCentralUtils.setRadioConfigField(
modified |= UCentralUtils.setRadioConfigChannel(
serialNumber,
config,
"channel",
channelList
);
}
@@ -334,10 +345,9 @@ public class ConfigManager implements Runnable {
txPowerList.putAll(deviceConfig.userTxPowers);
}
if (!txPowerList.isEmpty()) {
modified |= UCentralUtils.setRadioConfigField(
modified |= UCentralUtils.setRadioConfigTxPower(
serialNumber,
config,
"tx-power",
txPowerList
);
}
@@ -364,9 +374,38 @@ public class ConfigManager implements Runnable {
return (configListeners.remove(id) != null);
}
/** Interrupt the main thread, possibly triggering an update immediately. */
public void wakeUp() {
eventFlag.set(true);
/**
* Mark the zone to be updated, then interrupt the main thread to possibly
* trigger an update immediately.
*
* @param zone non-null zone (i.e., venue)
*/
public void queueZoneAndWakeUp(String zone) {
if (zone == null) {
logger.debug("Zone to queue must be a non-null String.");
return;
}
zonesToUpdate.add(zone);
wakeUp();
}
/**
* Track all zones to be updated, then interrupt the main thread to possibly
* trigger an update immediately.
*/
public void queueAllZonesAndWakeUp() {
/*
* Note, addAll is not atomic, but that is ok. This just means that it
* is possible that some zones may get updated now by the main thread
* while others get updated either when the main thread is woken up or
* the next time the main thread does its periodic update.
*/
zonesToUpdate.addAll(deviceDataManager.getZones());
wakeUp();
}
/** Interrupt the main thread to possibly trigger an update immediately. */
private void wakeUp() {
if (mainThread != null && mainThread.isAlive() && sleepingFlag.get()) {
wakeupFlag.set(true);
mainThread.interrupt();

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.modules;
package com.facebook.openwifi.rrm.modules;
import java.sql.SQLException;
import java.util.ArrayList;
@@ -23,22 +23,22 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.RRMConfig.ModuleConfig.DataCollectorParams;
import com.facebook.openwifirrm.Utils;
import com.facebook.openwifirrm.mysql.DatabaseManager;
import com.facebook.openwifirrm.mysql.StateRecord;
import com.facebook.openwifirrm.ucentral.UCentralApConfiguration;
import com.facebook.openwifirrm.ucentral.UCentralClient;
import com.facebook.openwifirrm.ucentral.UCentralKafkaConsumer;
import com.facebook.openwifirrm.ucentral.UCentralKafkaConsumer.KafkaRecord;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.gw.models.CommandInfo;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceCapabilities;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceWithStatus;
import com.facebook.openwifirrm.ucentral.gw.models.ServiceEvent;
import com.facebook.openwifi.cloudsdk.UCentralApConfiguration;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer.KafkaRecord;
import com.facebook.openwifi.cloudsdk.models.gw.CommandInfo;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceCapabilities;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceWithStatus;
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.Utils;
import com.facebook.openwifi.rrm.RRMConfig.ModuleConfig.DataCollectorParams;
import com.facebook.openwifi.rrm.mysql.DatabaseManager;
import com.facebook.openwifi.rrm.mysql.StateRecord;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.modules;
package com.facebook.openwifi.rrm.modules;
import java.util.LinkedList;
import java.util.List;
@@ -20,23 +20,24 @@ import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.RRMConfig.ModuleConfig.ModelerParams;
import com.facebook.openwifirrm.Utils;
import com.facebook.openwifirrm.ucentral.UCentralApConfiguration;
import com.facebook.openwifirrm.ucentral.UCentralClient;
import com.facebook.openwifirrm.ucentral.UCentralKafkaConsumer;
import com.facebook.openwifirrm.ucentral.UCentralKafkaConsumer.KafkaRecord;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceCapabilities;
import com.facebook.openwifirrm.ucentral.gw.models.DeviceWithStatus;
import com.facebook.openwifirrm.ucentral.gw.models.ServiceEvent;
import com.facebook.openwifirrm.ucentral.gw.models.StatisticsRecords;
import com.facebook.openwifirrm.ucentral.models.State;
import com.facebook.openwifi.cloudsdk.UCentralApConfiguration;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer;
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer.KafkaRecord;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.models.ap.UCentralSchema;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceCapabilities;
import com.facebook.openwifi.cloudsdk.models.gw.DeviceWithStatus;
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
import com.facebook.openwifi.cloudsdk.models.gw.StatisticsRecords;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.RRMConfig.ModuleConfig.ModelerParams;
import com.facebook.openwifi.rrm.Utils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
@@ -93,15 +94,16 @@ public class Modeler implements Runnable {
public Map<String, List<List<WifiScanEntry>>> latestWifiScans =
new ConcurrentHashMap<>();
/** List of latest state per device. */
public Map<String, State> latestState = new ConcurrentHashMap<>();
/** List of latest states per device. */
public Map<String, List<State>> latestStates =
new ConcurrentHashMap<>();
/** List of radio info per device. */
public Map<String, JsonArray> latestDeviceStatus =
public Map<String, List<UCentralSchema.Radio>> latestDeviceStatusRadios =
new ConcurrentHashMap<>();
/** List of capabilities per device. */
public Map<String, JsonObject> latestDeviceCapabilities =
public Map<String, Map<String, Capabilities.Phy>> latestDeviceCapabilitiesPhy =
new ConcurrentHashMap<>();
}
@@ -267,7 +269,10 @@ public class Modeler implements Runnable {
if (state != null) {
try {
State stateModel = gson.fromJson(state, State.class);
dataModel.latestState.put(device.serialNumber, stateModel);
dataModel.latestStates.computeIfAbsent(
device.serialNumber,
k -> new LinkedList<>()
).add(stateModel);
logger.debug(
"Device {}: added initial state from uCentralGw",
device.serialNumber
@@ -299,8 +304,17 @@ public class Modeler implements Runnable {
if (state != null) {
try {
State stateModel = gson.fromJson(state, State.class);
dataModel.latestState
.put(record.serialNumber, stateModel);
List<State> latestStatesList = dataModel.latestStates
.computeIfAbsent(
record.serialNumber,
k -> new LinkedList<>()
);
while (
latestStatesList.size() >= params.stateBufferSize
) {
latestStatesList.remove(0);
}
latestStatesList.add(stateModel);
stateUpdates.add(record.serialNumber);
} catch (JsonSyntaxException e) {
logger.error(
@@ -363,9 +377,9 @@ public class Modeler implements Runnable {
String serialNumber,
DeviceCapabilities capabilities
) {
dataModel.latestDeviceCapabilities.put(
dataModel.latestDeviceCapabilitiesPhy.put(
serialNumber,
capabilities.capabilities.getAsJsonObject("wifi")
capabilities.capabilities.wifi
);
}
@@ -377,10 +391,11 @@ public class Modeler implements Runnable {
UCentralApConfiguration config
) {
// Get old vs new radios info and store the new radios info
JsonArray newRadioList = config.getRadioConfigList();
List<UCentralSchema.Radio> newRadioList = config.getRadioConfigList();
Set<String> newRadioBandsSet = config.getRadioBandsSet(newRadioList);
JsonArray oldRadioList = dataModel.latestDeviceStatus
.put(serialNumber, newRadioList);
List<UCentralSchema.Radio> oldRadioList =
dataModel.latestDeviceStatusRadios
.put(serialNumber, newRadioList);
Set<String> oldRadioBandsSet = config.getRadioBandsSet(oldRadioList);
// Print info only when there are any updates
@@ -423,19 +438,19 @@ public class Modeler implements Runnable {
logger.debug("Removed some wifi scan entries from data model");
}
if (
dataModel.latestState.entrySet()
dataModel.latestStates.entrySet()
.removeIf(e -> !isRRMEnabled(e.getKey()))
) {
logger.debug("Removed some state entries from data model");
}
if (
dataModel.latestDeviceStatus.entrySet()
dataModel.latestDeviceStatusRadios.entrySet()
.removeIf(e -> !isRRMEnabled(e.getKey()))
) {
logger.debug("Removed some status entries from data model");
}
if (
dataModel.latestDeviceCapabilities.entrySet()
dataModel.latestDeviceCapabilitiesPhy.entrySet()
.removeIf(e -> !isRRMEnabled(e.getKey()))
) {
logger.debug("Removed some capabilities entries from data model");

View File

@@ -6,24 +6,31 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.modules;
package com.facebook.openwifi.rrm.modules;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.aggregators.Aggregator;
import com.facebook.openwifirrm.aggregators.MeanAggregator;
import com.facebook.openwifirrm.modules.Modeler.DataModel;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.operationelement.HTOperationElement;
import com.facebook.openwifirrm.ucentral.operationelement.VHTOperationElement;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.AggregatedState;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.ies.HTOperation;
import com.facebook.openwifi.cloudsdk.ies.VHTOperation;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.cloudsdk.models.ap.State.Interface;
import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID;
import com.facebook.openwifi.cloudsdk.models.ap.State.Interface.SSID.Association;
import com.facebook.openwifi.rrm.aggregators.Aggregator;
import com.facebook.openwifi.rrm.aggregators.MeanAggregator;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
/**
* Modeler utilities.
@@ -239,9 +246,9 @@ public class ModelerUtils {
return Objects.equals(entry1.bssid, entry2.bssid) &&
entry1.frequency == entry2.frequency &&
entry1.channel == entry2.channel &&
HTOperationElement
HTOperation
.matchesHtForAggregation(entry1.ht_oper, entry2.ht_oper) &&
VHTOperationElement
VHTOperation
.matchesVhtForAggregation(entry1.vht_oper, entry2.vht_oper);
}
@@ -291,7 +298,7 @@ public class ModelerUtils {
/**
* Compute aggregated wifiscans using a given reference time.
*
* @see #getAggregatedWifiScans(com.facebook.openwifirrm.modules.Modeler.DataModel,
* @see #getAggregatedWifiScans(com.facebook.openwifi.rrm.modules.Modeler.DataModel,
* long, Aggregator)
*/
public static Map<String, Map<String, WifiScanEntry>> getAggregatedWifiScans(
@@ -381,4 +388,191 @@ public class ModelerUtils {
}
return aggregatedWifiScans;
}
/**
* This method converts the input State info to an AggregatedState
* and adds it to the bssidToAggregatedStates map. If the bssid/station
* of the input State does not exist in the map, create a new
* AggregatedState list. If the bssid/station of the input State exists,
* then convert State to AggregatedState and check if there exits an
* AggregatedState of the same radio. If there does, append the value
* of aggregation field to the existing AggregatedState, if not, create
* a new AggregatedState and add it to the list.
*
* @param bssidToAggregatedStates map from bssid/station to a list of AggregatedState
* @param state the state that is to be added
*/
static void addStateToAggregation(
Map<String, List<AggregatedState>> bssidToAggregatedStates,
State state
) {
for (Interface stateInterface : state.interfaces) {
if (stateInterface.ssids == null) {
continue;
}
for (SSID ssid : stateInterface.ssids) {
Map<String, Integer> radioInfo = new HashMap<>();
radioInfo.put("channel", ssid.radio.get("channel").getAsInt());
radioInfo.put(
"channel_width",
ssid.radio.get("channel_width").getAsInt()
);
radioInfo
.put("tx_power", ssid.radio.get("tx_power").getAsInt());
for (Association association : ssid.associations) {
if (association == null) {
continue;
}
String key = getBssidStationKeyPair(
association.bssid,
association.station
);
List<AggregatedState> aggregatedStates =
bssidToAggregatedStates
.computeIfAbsent(key, k -> new ArrayList<>());
AggregatedState aggState =
new AggregatedState(association, radioInfo);
/**
* Indicate if the aggState can be merged into some old AggregatedState.
* If true, it will be merged by appending its mcs/rssi field to the old one.
* If false, it will be added to the list aggregatedStates.
*/
boolean canBeMergedToOldAggregatedState = false;
for (
AggregatedState oldAggregatedState : aggregatedStates
) {
if (oldAggregatedState.add(aggState)) {
canBeMergedToOldAggregatedState = true;
break;
}
}
if (!canBeMergedToOldAggregatedState) {
aggregatedStates.add(aggState);
}
bssidToAggregatedStates.put(key, aggregatedStates);
}
}
}
}
/**
* This method aggregates States by bssid/station key pair and radio info.
* if two States of the same bssid/station match in channel, channel width and tx_power
* need to be aggregated to one {@code AggregatedState}. Currently only mcs and
* rssi fields are being aggregated. They are of {@code List<Integer>} type in AggregatedState,
* which list all the values over the time.
*
* @param dataModel the data model which includes the latest recorded States
* @param obsoletionPeriodMs the maximum amount of time (in milliseconds) it
* is worth aggregating over, starting from the
* most recent States and working backwards in time.
* A State exactly {@code obsoletionPeriodMs} ms earlier
* than the most recent State is considered non-obsolete
* (i.e., the "non-obsolete" window is inclusive).
* Must be non-negative.
* @param refTimeMs the reference time were passed to make testing easier
* @return map from serial number to a map from bssid_station String pair to a list of AggregatedState
*/
public static Map<String, Map<String, List<AggregatedState>>> getAggregatedStates(
Modeler.DataModel dataModel,
long obsoletionPeriodMs,
long refTimeMs
) {
if (obsoletionPeriodMs < 0) {
throw new IllegalArgumentException(
"obsoletionPeriodMs must be non-negative."
);
}
Map<String, Map<String, List<AggregatedState>>> aggregatedStates =
new HashMap<>();
for (
Map.Entry<String, List<State>> deviceToStateList : dataModel.latestStates
.entrySet()
) {
String serialNumber = deviceToStateList.getKey();
List<State> states = deviceToStateList.getValue();
if (states.isEmpty()) {
continue;
}
/**
* Sort in reverse chronological order. Sorting is done just in case the
* States in the original list are not chronological already - although
* they are inserted chronologically, perhaps latency, synchronization, etc.
*/
states.sort(
(state1, state2) -> -Long.compare(state1.unit.localtime, state2.unit.localtime)
);
Map<String, List<AggregatedState>> bssidToAggregatedStates =
aggregatedStates
.computeIfAbsent(serialNumber, k -> new HashMap<>());
for (State state : states) {
if (refTimeMs - state.unit.localtime > obsoletionPeriodMs) {
// discard obsolete entries
break;
}
addStateToAggregation(bssidToAggregatedStates, state);
}
}
return aggregatedStates;
}
/**
* This method gets the most recent State from latestStates per device.
*
* @param latestStates list of latest States per device
* @return map from device String to latest State
*/
public static Map<String, State> getLatestState(
Map<String, List<State>> latestStates
) {
Map<String, State> latestState = new ConcurrentHashMap<>();
for (
Map.Entry<String, List<State>> stateEntry : latestStates.entrySet()
) {
String key = stateEntry.getKey();
List<State> value = stateEntry.getValue();
if (value.isEmpty()) {
latestState.put(key, null);
} else {
latestState.put(key, value.get(value.size() - 1));
}
}
return latestState;
}
/** Create a key pair consisted of bssid and station string */
public static String getBssidStationKeyPair(String bssid, String station) {
return String.format(
"bssid: %s, station: %s",
bssid,
station
);
}
/** Return the radio's band, or null if band cannot be found */
public static String getBand(
State.Radio radio,
Map<String, Capabilities.Phy> capabilityPhy
) {
if (radio.phy == null) {
return null;
}
Capabilities.Phy phy = capabilityPhy.get(radio.phy);
if (phy == null) {
return null;
}
String[] bands = phy.band;
if (bands == null || bands.length == 0) {
return null;
}
return bands[0];
}
}

View File

@@ -6,8 +6,9 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.modules;
package com.facebook.openwifi.rrm.modules;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -17,19 +18,19 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.DeviceTopology;
import com.facebook.openwifirrm.RRMAlgorithm;
import com.facebook.openwifirrm.RRMConfig.ModuleConfig.ProvMonitorParams;
import com.facebook.openwifirrm.RRMSchedule;
import com.facebook.openwifirrm.ucentral.UCentralClient;
import com.facebook.openwifirrm.ucentral.prov.models.InventoryTag;
import com.facebook.openwifirrm.ucentral.prov.models.InventoryTagList;
import com.facebook.openwifirrm.ucentral.prov.models.RRMDetails;
import com.facebook.openwifirrm.ucentral.prov.models.SerialNumberList;
import com.facebook.openwifirrm.ucentral.prov.models.Venue;
import com.facebook.openwifirrm.ucentral.prov.models.VenueList;
import com.facebook.openwifi.cloudsdk.UCentralClient;
import com.facebook.openwifi.cloudsdk.models.prov.InventoryTag;
import com.facebook.openwifi.cloudsdk.models.prov.InventoryTagList;
import com.facebook.openwifi.cloudsdk.models.prov.RRMDetails;
import com.facebook.openwifi.cloudsdk.models.prov.SerialNumberList;
import com.facebook.openwifi.cloudsdk.models.prov.Venue;
import com.facebook.openwifi.cloudsdk.models.prov.VenueList;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.DeviceTopology;
import com.facebook.openwifi.rrm.RRMAlgorithm;
import com.facebook.openwifi.rrm.RRMSchedule;
import com.facebook.openwifi.rrm.RRMConfig.ModuleConfig.ProvMonitorParams;
/**
* owprov monitor module.
@@ -159,12 +160,21 @@ public class ProvMonitor implements Runnable {
return null;
}
RRMSchedule schedule = new RRMSchedule();
schedule.cron = RRMScheduler
String[] crons = RRMScheduler
.parseIntoQuartzCron(details.rrm.schedule);
if (schedule.cron == null || schedule.cron.isEmpty()) {
if (crons == null || crons.length == 0) {
return null;
}
// if ANY crons are invalid throw it out since it doesn't make sense to
// schedule partial jobs
for (String cron : crons) {
if (cron == null || cron.isEmpty()) {
return null;
}
}
RRMSchedule schedule = new RRMSchedule();
schedule.crons = Arrays.asList(crons);
if (details.rrm.algorithms != null) {
schedule.algorithms =
@@ -175,6 +185,7 @@ public class ProvMonitor implements Runnable {
)
.collect(Collectors.toList());
}
return schedule;
}

View File

@@ -6,17 +6,17 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.modules;
package com.facebook.openwifi.rrm.modules;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.text.ParseException;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronExpression;
import org.quartz.CronScheduleBuilder;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
@@ -32,10 +32,11 @@ import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.RRMAlgorithm;
import com.facebook.openwifirrm.RRMConfig.ModuleConfig.RRMSchedulerParams;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.RRMAlgorithm;
import com.facebook.openwifi.rrm.RRMSchedule;
import com.facebook.openwifi.rrm.RRMConfig.ModuleConfig.RRMSchedulerParams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -74,15 +75,21 @@ public class RRMScheduler {
/** The scheduler instance. */
private Scheduler scheduler;
/** The zones with active triggers scheduled. */
private Set<String> scheduledZones;
/**
* The job keys with active triggers scheduled. Job keys take the format of
* {@code <zone>:<index>}
*
* @see #parseIntoQuartzCron(String)
* */
private Set<String> scheduledJobKeys;
/** RRM job. */
public static class RRMJob implements Job {
@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
String zone = context.getTrigger().getKey().getName();
String jobKey = context.getTrigger().getKey().getName();
String zone = jobKey.split(":")[0];
logger.debug("Executing job for zone: {}", zone);
try {
SchedulerContext schedulerContext =
@@ -107,13 +114,14 @@ public class RRMScheduler {
* @param linuxCron Linux cron with seconds
* (seconds minutes hours day_of_month month day_of_week [year])
*
* @throws IllegalArgumentException when a linux cron cannot be parsed
* into a valid Quartz spec
* @return String a Quartz supported cron
* @throws IllegalArgumentException when a linux cron cannot be parsed into a
* valid Quartz spec
* @return String[] an array of length 1 or 2 of Quartz supported cron that's
* equivalent to the original linux cron
*/
public static String parseIntoQuartzCron(String linuxCron) {
public static String[] parseIntoQuartzCron(String linuxCron) {
if (CronExpression.isValidExpression(linuxCron)) {
return linuxCron;
return new String[] { linuxCron };
}
String[] split = linuxCron.split(" ");
@@ -144,15 +152,36 @@ public class RRMScheduler {
// if first case failed and only day of week is *, set to ?
split[DAY_OF_WEEK_INDEX] = "?";
} else {
// Quartz does not support both values being set, so return null
return null;
// Quartz does not support both values being set but the standard says that
// if both are specified then it becomes OR of the two fields. Which means
// that we can split it into two separate crons and have it work the same way
split[DAY_OF_MONTH_INDEX] = "?";
String dayOfWeekCron = String.join(" ", split);
split[DAY_OF_MONTH_INDEX] = dayOfMonth;
split[DAY_OF_WEEK_INDEX] = "?";
String dayOfMonthCron = String.join(" ", split);
if (
!CronExpression.isValidExpression(dayOfWeekCron) ||
!CronExpression.isValidExpression(dayOfMonthCron)
) {
logger.error(
"Unable to parse cron {} into valid crons",
linuxCron
);
return null;
}
return new String[] { dayOfWeekCron, dayOfMonthCron };
}
String quartzCron = String.join(" ", split);
if (!CronExpression.isValidExpression(quartzCron)) {
return null;
}
return quartzCron;
return new String[] { quartzCron };
}
/** Constructor. */
@@ -194,7 +223,7 @@ public class RRMScheduler {
// Schedule job and triggers
scheduler.addJob(job, false);
syncTriggers();
logger.info("Scheduled {} RRM trigger(s)", scheduledZones.size());
logger.info("Scheduled {} RRM trigger(s)", scheduledJobKeys.size());
// Start scheduler
scheduler.start();
@@ -218,85 +247,98 @@ public class RRMScheduler {
/**
* Synchronize triggers to the current topology, adding/updating/deleting
* them as necessary. This updates {@link #scheduledZones}.
* them as necessary. This updates {@link #scheduledJobKeys}.
*/
public void syncTriggers() {
Set<String> scheduled = ConcurrentHashMap.newKeySet();
Set<String> prevScheduled = new HashSet<>();
if (scheduledZones != null) {
prevScheduled.addAll(scheduledZones);
if (scheduledJobKeys != null) {
prevScheduled.addAll(scheduledJobKeys);
}
// Add new triggers
for (String zone : deviceDataManager.getZones()) {
DeviceConfig config = deviceDataManager.getZoneConfig(zone);
RRMSchedule schedule = config.schedule;
if (
config.schedule == null ||
config.schedule.cron == null ||
config.schedule.cron.isEmpty()
schedule == null || schedule.crons == null ||
schedule.crons.isEmpty()
) {
continue; // RRM not scheduled
}
try {
CronExpression.validateExpression(config.schedule.cron);
} catch (ParseException e) {
logger.error(
String.format(
"Invalid cron expression (%s) for zone %s",
config.schedule.cron,
zone
),
e
for (int i = 0; i < schedule.crons.size(); i++) {
String cron = schedule.crons.get(i);
// if even one schedule has invalid cron, the whole thing is probably wrong
if (cron == null || cron.isEmpty()) {
logger.error("There was an invalid cron in the schedule");
break;
}
try {
CronExpression.validateExpression(cron);
} catch (ParseException e) {
logger.error(
String.format(
"Invalid cron expression (%s) for zone %s",
cron,
zone
),
e
);
continue;
}
// Create trigger
String jobKey = String.format("%s:%d", zone, i);
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobKey)
.forJob(job)
.withSchedule(
CronScheduleBuilder.cronSchedule(cron)
)
.build();
try {
if (!prevScheduled.contains(jobKey)) {
scheduler.scheduleJob(trigger);
} else {
scheduler.rescheduleJob(trigger.getKey(), trigger);
}
} catch (SchedulerException e) {
logger.error(
"Failed to schedule RRM trigger for job key: " + jobKey,
e
);
continue;
}
scheduled.add(jobKey);
logger.debug(
"Scheduled/updated RRM for job key '{}' at: < {} >",
jobKey,
cron
);
continue;
}
// Create trigger
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(zone)
.forJob(job)
.withSchedule(
CronScheduleBuilder.cronSchedule(config.schedule.cron)
)
.build();
try {
if (!prevScheduled.contains(zone)) {
scheduler.scheduleJob(trigger);
} else {
scheduler.rescheduleJob(trigger.getKey(), trigger);
}
} catch (SchedulerException e) {
logger.error(
"Failed to schedule RRM trigger for zone: " + zone,
e
);
continue;
}
scheduled.add(zone);
logger.debug(
"Scheduled/updated RRM for zone '{}' at: < {} >",
zone,
config.schedule.cron
);
}
// Remove old triggers
prevScheduled.removeAll(scheduled);
for (String zone : prevScheduled) {
for (String jobKey : prevScheduled) {
try {
scheduler.unscheduleJob(TriggerKey.triggerKey(zone));
scheduler.unscheduleJob(TriggerKey.triggerKey(jobKey));
} catch (SchedulerException e) {
logger.error(
"Failed to remove RRM trigger for zone: " + zone,
"Failed to remove RRM trigger for jobKey: " + jobKey,
e
);
continue;
}
logger.debug("Removed RRM trigger for zone '{}'", zone);
logger.debug("Removed RRM trigger for jobKey '{}'", jobKey);
}
this.scheduledZones = scheduled;
this.scheduledJobKeys = scheduled;
}
/** Run RRM algorithms for the given zone. */
@@ -305,16 +347,19 @@ public class RRMScheduler {
// Get algorithms from zone config
DeviceConfig config = deviceDataManager.getZoneConfig(zone);
if (config.schedule == null) {
RRMSchedule schedule = config.schedule;
if (schedule == null) {
logger.error("RRM schedule missing for zone '{}', aborting!", zone);
return;
}
if (
config.schedule.algorithms == null ||
config.schedule.algorithms.isEmpty()
schedule.algorithms == null ||
schedule.algorithms.isEmpty()
) {
logger.debug("Using default RRM algorithms for zone '{}'", zone);
config.schedule.algorithms = Arrays.asList(
logger
.debug("Using default RRM algorithms for zone '{}'", zone);
schedule.algorithms = Arrays.asList(
new RRMAlgorithm(
RRMAlgorithm.AlgorithmType.OptimizeChannel.name()
),
@@ -325,14 +370,15 @@ public class RRMScheduler {
}
// Execute algorithms
for (RRMAlgorithm algo : config.schedule.algorithms) {
for (RRMAlgorithm algo : schedule.algorithms) {
RRMAlgorithm.AlgorithmResult result = algo.run(
deviceDataManager,
configManager,
modeler,
zone,
params.dryRun,
true /* allowDefaultMode */
true, /* allowDefaultMode */
false /* updateImmediately */
);
logger.info(
"'{}' result for zone '{}': {}",
@@ -341,5 +387,6 @@ public class RRMScheduler {
gson.toJson(result)
);
}
configManager.queueZoneAndWakeUp(zone);
}
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.mysql;
package com.facebook.openwifi.rrm.mysql;
import java.sql.Connection;
import java.sql.DriverManager;
@@ -26,9 +26,9 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.Utils;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.models.State;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.rrm.Utils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
@@ -360,7 +360,7 @@ public class DatabaseManager {
/** Convert a list of state records to a State object. */
private State toState(List<StateRecord> records, long ts) {
State state = new State();
state.unit = state.new Unit();
state.unit = new State.Unit();
state.unit.localtime = ts;
// Parse each record
@@ -454,9 +454,10 @@ public class DatabaseManager {
.map(o -> gson.fromJson(o, State.Interface.class))
.collect(Collectors.toList())
.toArray(new State.Interface[0]);
state.radios = new JsonObject[radios.lastKey() + 1];
state.radios = new State.Radio[radios.lastKey() + 1];
for (Map.Entry<Integer, JsonObject> entry : radios.entrySet()) {
state.radios[entry.getKey()] = entry.getValue();
State.Radio radio = new State.Radio();
state.radios[entry.getKey()] = radio;
}
return state;
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.mysql;
package com.facebook.openwifi.rrm.mysql;
/**
* Representation of a record in the "state" table.

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.optimizers.channel;
package com.facebook.openwifi.rrm.optimizers.channel;
import java.util.ArrayList;
import java.util.Arrays;
@@ -18,16 +18,18 @@ import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceConfig;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.modules.ConfigManager;
import com.facebook.openwifirrm.modules.Modeler.DataModel;
import com.facebook.openwifirrm.ucentral.UCentralConstants;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.models.State;
import com.facebook.openwifirrm.ucentral.operationelement.HTOperationElement;
import com.facebook.openwifirrm.ucentral.operationelement.VHTOperationElement;
import com.facebook.openwifi.cloudsdk.models.ap.Capabilities;
import com.facebook.openwifi.cloudsdk.UCentralConstants;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.ies.HTOperation;
import com.facebook.openwifi.cloudsdk.ies.VHTOperation;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.rrm.DeviceConfig;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.modules.ConfigManager;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.facebook.openwifi.rrm.modules.ModelerUtils;
/**
* Channel optimizer base class.
@@ -39,24 +41,6 @@ public abstract class ChannelOptimizer {
/** Minimum supported channel width (MHz), inclusive. */
public static final int MIN_CHANNEL_WIDTH = 20;
/** List of available channels per band for use. */
public static final Map<String, List<Integer>> AVAILABLE_CHANNELS_BAND =
new HashMap<>();
static {
AVAILABLE_CHANNELS_BAND.put(
UCentralConstants.BAND_5G,
Collections.unmodifiableList(
Arrays.asList(36, 40, 44, 48, 149, 153, 157, 161, 165)
)
);
AVAILABLE_CHANNELS_BAND.put(
UCentralConstants.BAND_2G,
Collections.unmodifiableList(
Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
)
);
}
/** Map of channel width (MHz) to available (primary) channels */
protected static final Map<Integer, List<Integer>> AVAILABLE_CHANNELS_WIDTH =
new HashMap<>();
@@ -154,11 +138,11 @@ public abstract class ChannelOptimizer {
// Remove model entries not in the given zone
this.model.latestWifiScans.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestState.keySet()
this.model.latestStates.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestDeviceStatus.keySet()
this.model.latestDeviceStatusRadios.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
this.model.latestDeviceCapabilities.keySet()
this.model.latestDeviceCapabilitiesPhy.keySet()
.removeIf(serialNumber -> !deviceConfigs.containsKey(serialNumber));
}
@@ -197,7 +181,7 @@ public abstract class ChannelOptimizer {
String vhtOper
) {
if (
AVAILABLE_CHANNELS_BAND.get(UCentralConstants.BAND_2G)
UCentralUtils.AVAILABLE_CHANNELS_BAND.get(UCentralConstants.BAND_2G)
.contains(channel)
) {
// 2.4G, it only supports 20 MHz
@@ -208,13 +192,13 @@ public abstract class ChannelOptimizer {
return MIN_CHANNEL_WIDTH;
}
HTOperationElement htOperObj = new HTOperationElement(htOper);
HTOperation htOperObj = new HTOperation(htOper);
if (vhtOper == null) {
// HT mode only supports 20/40 MHz
return htOperObj.staChannelWidth ? 40 : 20;
} else {
// VHT/HE mode supports 20/40/160/80+80 MHz
VHTOperationElement vhtOperObj = new VHTOperationElement(vhtOper);
VHTOperation vhtOperObj = new VHTOperation(vhtOper);
if (!htOperObj.staChannelWidth && vhtOperObj.channelWidth == 0) {
return 20;
} else if (
@@ -234,8 +218,9 @@ public abstract class ChannelOptimizer {
// the difference of 8 means it is consecutive
int channelDiff =
Math.abs(vhtOperObj.channel1 - vhtOperObj.channel2);
// the "8080" below does not mean 8080 MHz wide, it refers to 80+80 MHz channel
return channelDiff == 8 ? 160 : 8080;
// TODO it will currently return just 80 for 80p80 - it should be dealt
// with properly.
return channelDiff == 8 ? 160 : 80;
} else {
return MIN_CHANNEL_WIDTH;
}
@@ -315,25 +300,28 @@ public abstract class ChannelOptimizer {
List<WifiScanEntry> scanRespsFiltered =
new ArrayList<WifiScanEntry>();
for (WifiScanEntry entry : scanResps) {
if (UCentralUtils.isChannelInBand(entry.channel, band)) {
int channelWidth = getChannelWidthFromWiFiScan(
final String entryBand = UCentralUtils
.freqToBand(entry.frequency);
if (entryBand == null || !entryBand.equals(band)) {
continue;
}
int channelWidth = getChannelWidthFromWiFiScan(
entry.channel,
entry.ht_oper,
entry.vht_oper
);
int primaryChannel =
getPrimaryChannel(entry.channel, channelWidth);
List<Integer> coveredChannels =
getCoveredChannels(
entry.channel,
entry.ht_oper,
entry.vht_oper
primaryChannel,
channelWidth
);
int primaryChannel =
getPrimaryChannel(entry.channel, channelWidth);
List<Integer> coveredChannels =
getCoveredChannels(
entry.channel,
primaryChannel,
channelWidth
);
for (Integer newChannel : coveredChannels) {
WifiScanEntry newEntry = new WifiScanEntry(entry);
newEntry.channel = newChannel;
scanRespsFiltered.add(newEntry);
}
for (Integer newChannel : coveredChannels) {
WifiScanEntry newEntry = new WifiScanEntry(entry);
newEntry.channel = newChannel;
scanRespsFiltered.add(newEntry);
}
}
@@ -361,6 +349,7 @@ public abstract class ChannelOptimizer {
* @param band the operational band (e.g., "2G")
* @param serialNumber the device's serial number
* @param state the latest state of all the devices
* @param latestDeviceCapabilitiesPhy latest device phy from capabilities
* @return the current channel and channel width (MHz) of the device in the
* given band; returns a current channel of 0 if no channel in the given
* band is found.
@@ -368,7 +357,8 @@ public abstract class ChannelOptimizer {
protected static int[] getCurrentChannel(
String band,
String serialNumber,
State state
State state,
Map<String, Map<String, Capabilities.Phy>> latestDeviceCapabilitiesPhy
) {
int currentChannel = 0;
int currentChannelWidth = MIN_CHANNEL_WIDTH;
@@ -378,16 +368,40 @@ public abstract class ChannelOptimizer {
radioIndex < state.radios.length;
radioIndex++
) {
int tempChannel = state.radios[radioIndex]
.get("channel")
.getAsInt();
if (UCentralUtils.isChannelInBand(tempChannel, band)) {
currentChannel = tempChannel;
currentChannelWidth = state.radios[radioIndex]
.get("channel_width")
.getAsInt();
State.Radio radio = state.radios[radioIndex];
// check if radio is in band of interest
Map<String, Capabilities.Phy> capabilitiesPhy =
latestDeviceCapabilitiesPhy.get(serialNumber);
if (capabilitiesPhy == null) {
continue;
}
final String radioBand = ModelerUtils.getBand(
radio,
capabilitiesPhy
);
if (radioBand == null || !radioBand.equals(band)) {
continue;
}
int tempChannel = radio.channel;
currentChannel = tempChannel;
// treat as two separate 80MHz channel and only assign to one
// TODO: support 80p80 properly
Integer parsedChannelWidth = UCentralUtils
.parseChannelWidth(
radio.channel_width,
true
);
if (parsedChannelWidth != null) {
currentChannelWidth = parsedChannelWidth;
break;
}
logger.error(
"Invalid channel width {}",
radio.channel_width
);
continue;
}
return new int[] { currentChannel, currentChannelWidth };
}
@@ -627,14 +641,13 @@ public abstract class ChannelOptimizer {
public abstract Map<String, Map<String, Integer>> computeChannelMap();
/**
* Program the given channel map into the AP config and notify the config
* manager.
* Program the given channel map into the AP config.
*
* @param deviceDataManager the DeviceDataManager instance
* @param configManager the ConfigManager instance
* @param channelMap the map of devices (by serial number) to radio to channel
*/
public void applyConfig(
public void updateDeviceApConfig(
DeviceDataManager deviceDataManager,
ConfigManager configManager,
Map<String, Map<String, Integer>> channelMap
@@ -652,8 +665,5 @@ public abstract class ChannelOptimizer {
deviceConfig.autoChannels = entry.getValue();
}
});
// Trigger config update now
configManager.wakeUp();
}
}

View File

@@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.openwifirrm.optimizers.channel;
package com.facebook.openwifi.rrm.optimizers.channel;
import java.util.ArrayList;
import java.util.HashSet;
@@ -20,12 +20,13 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.facebook.openwifirrm.DeviceDataManager;
import com.facebook.openwifirrm.modules.Modeler.DataModel;
import com.facebook.openwifirrm.ucentral.UCentralConstants;
import com.facebook.openwifirrm.ucentral.UCentralUtils;
import com.facebook.openwifirrm.ucentral.WifiScanEntry;
import com.facebook.openwifirrm.ucentral.models.State;
import com.facebook.openwifi.cloudsdk.UCentralConstants;
import com.facebook.openwifi.cloudsdk.UCentralUtils;
import com.facebook.openwifi.cloudsdk.WifiScanEntry;
import com.facebook.openwifi.cloudsdk.models.ap.State;
import com.facebook.openwifi.rrm.DeviceDataManager;
import com.facebook.openwifi.rrm.modules.Modeler.DataModel;
import com.facebook.openwifi.rrm.modules.ModelerUtils;
/**
* Least used channel optimizer.
@@ -89,13 +90,13 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer {
protected static Map<Integer, Integer> getOccupiedOverlapChannels(
Map<Integer, Integer> occupiedChannels
) {
int maxChannel =
UCentralUtils.UPPER_CHANNEL_LIMIT.get(UCentralConstants.BAND_2G);
int minChannel =
UCentralUtils.LOWER_CHANNEL_LIMIT.get(UCentralConstants.BAND_2G);
final int maxChannel =
UCentralUtils.getUpperChannelLimit(UCentralConstants.BAND_2G);
final int minChannel =
UCentralUtils.getLowerChannelLimit(UCentralConstants.BAND_2G);
Map<Integer, Integer> occupiedOverlapChannels = new TreeMap<>();
for (
int overlapChannel : AVAILABLE_CHANNELS_BAND
int overlapChannel : UCentralUtils.AVAILABLE_CHANNELS_BAND
.get(UCentralConstants.BAND_2G)
) {
int occupancy = 0;
@@ -331,17 +332,19 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer {
public Map<String, Map<String, Integer>> computeChannelMap() {
Map<String, Map<String, Integer>> channelMap = new TreeMap<>();
Map<String, List<String>> bandsMap = UCentralUtils
.getBandsMap(model.latestDeviceStatus);
.getBandsMap(model.latestDeviceStatusRadios);
Map<String, Map<String, List<Integer>>> deviceAvailableChannels =
UCentralUtils.getDeviceAvailableChannels(
model.latestDeviceStatus,
model.latestDeviceCapabilities,
AVAILABLE_CHANNELS_BAND
model.latestDeviceStatusRadios,
model.latestDeviceCapabilitiesPhy,
UCentralUtils.AVAILABLE_CHANNELS_BAND
);
Map<String, State> latestState =
ModelerUtils.getLatestState(model.latestStates);
Map<String, String> bssidsMap =
UCentralUtils.getBssidsMap(model.latestState);
UCentralUtils.getBssidsMap(latestState);
for (String band : bandsMap.keySet()) {
// Performance metrics
@@ -369,11 +372,12 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer {
availableChannelsList == null ||
availableChannelsList.isEmpty()
) {
availableChannelsList = AVAILABLE_CHANNELS_BAND.get(band);
availableChannelsList =
UCentralUtils.AVAILABLE_CHANNELS_BAND.get(band);
}
// Get current channel of the device
State state = model.latestState.get(serialNumber);
State state = latestState.get(serialNumber);
if (state == null) {
logger.debug(
"Device {}: No state found, skipping...",
@@ -389,7 +393,12 @@ public class LeastUsedChannelOptimizer extends ChannelOptimizer {
continue;
}
int[] currentChannelInfo =
getCurrentChannel(band, serialNumber, state);
getCurrentChannel(
band,
serialNumber,
state,
model.latestDeviceCapabilitiesPhy
);
int currentChannel = currentChannelInfo[0];
// Filter out APs if the radios in the state do not contain a
// channel in a band given by the state. This can happen when

Some files were not shown because too many files have changed in this diff Show More