summaryrefslogtreecommitdiff
path: root/angelsharkd/src/routes
diff options
context:
space:
mode:
authorAdam Carpenter <adam.carpenter@adp.com>2021-12-01 09:35:06 -0500
committerGitHub <noreply@github.com>2021-12-01 09:35:06 -0500
commit37c5cdff434e86778977a7d7fd00d80b71fcf363 (patch)
treea582caae4e8c0706041497fb16a2525a14002c2e /angelsharkd/src/routes
parent9a940e64d55c4144fb65c702241eeac99a426bd1 (diff)
parente18d0c1a4189d5278639a9b323ae3794118566bc (diff)
downloadaltruistic-angelshark-37c5cdff434e86778977a7d7fd00d80b71fcf363.tar.xz
altruistic-angelshark-37c5cdff434e86778977a7d7fd00d80b71fcf363.zip
Merge pull request #5 from adpllc/extensions
Extensions and simple extension search
Diffstat (limited to 'angelsharkd/src/routes')
-rw-r--r--angelsharkd/src/routes/extensions/README.md13
-rw-r--r--angelsharkd/src/routes/extensions/mod.rs35
-rw-r--r--angelsharkd/src/routes/extensions/simple_deprov.rs5
-rw-r--r--angelsharkd/src/routes/extensions/simple_search/README.md79
-rw-r--r--angelsharkd/src/routes/extensions/simple_search/mod.rs66
-rw-r--r--angelsharkd/src/routes/extensions/simple_search/types.rs167
-rw-r--r--angelsharkd/src/routes/mod.rs1
7 files changed, 366 insertions, 0 deletions
diff --git a/angelsharkd/src/routes/extensions/README.md b/angelsharkd/src/routes/extensions/README.md
new file mode 100644
index 0000000..1aed1dd
--- /dev/null
+++ b/angelsharkd/src/routes/extensions/README.md
@@ -0,0 +1,13 @@
+# Angelshark Daemon Extensions
+
+This module aims to provide a simple way of extending Angelshark's basic
+functionality (running commands on the ACM) with additional HTTP endpoints that
+run one or more commands to achieve a basic business task.
+
+This functionality may not be desirable for all end users, and therefore is
+completely opt-in with feature flags. For example, at compile time, you can add
+`--features simple_search` to enable a given extension called `simple_search`.
+
+To add additional features, read `mod.rs` and `Cargo.toml` for `angelsharkd` to
+see how to conditionally incorporate your own warp HTTP filters into the
+project.
diff --git a/angelsharkd/src/routes/extensions/mod.rs b/angelsharkd/src/routes/extensions/mod.rs
new file mode 100644
index 0000000..7f217d6
--- /dev/null
+++ b/angelsharkd/src/routes/extensions/mod.rs
@@ -0,0 +1,35 @@
+use crate::config::Config;
+use warp::{path, Filter, Rejection, Reply};
+
+#[cfg(feature = "simple_deprov")]
+mod simple_deprov;
+#[cfg(feature = "simple_search")]
+mod simple_search;
+
+/// The extension filter; consists of all compiled optional Angelshark extension
+/// filters combined under `/extensions`.
+pub fn filter(config: &Config) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
+ // Note: this next line deals with the common edge case of having no
+ // extensions loaded with feature flags. It ensures that the the type
+ // checking is right when the return `.and()` is called below.
+ let filters = default().or(default());
+
+ // Block to enable simple_search extension feature. Instantiates a
+ // searchable haystack and configures filters to handle search requests.
+ #[cfg(feature = "simple_search")]
+ let haystack = simple_search::Haystack::new(config.runner.clone());
+ #[cfg(feature = "simple_search")]
+ let filters = filters
+ .or(simple_search::search_filter(haystack.clone()))
+ .or(simple_search::refresh_filter(haystack));
+
+ #[cfg(feature = "simple_deprov")]
+ let filters = filters.or(simple_deprov::filter());
+
+ path("extensions").and(filters)
+}
+
+/// The default, informational extension route.
+fn default() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
+ warp::path::end().map(|| "Angelshark extension route index. Enable extensions with feature switches and access them at `/extensions/<feature>`.")
+}
diff --git a/angelsharkd/src/routes/extensions/simple_deprov.rs b/angelsharkd/src/routes/extensions/simple_deprov.rs
new file mode 100644
index 0000000..2b5ad40
--- /dev/null
+++ b/angelsharkd/src/routes/extensions/simple_deprov.rs
@@ -0,0 +1,5 @@
+use warp::{Filter, Rejection, Reply};
+
+pub fn filter() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
+ warp::path("deprov").map(|| -> &str { todo!() }) // TODO:
+}
diff --git a/angelsharkd/src/routes/extensions/simple_search/README.md b/angelsharkd/src/routes/extensions/simple_search/README.md
new file mode 100644
index 0000000..c0df17a
--- /dev/null
+++ b/angelsharkd/src/routes/extensions/simple_search/README.md
@@ -0,0 +1,79 @@
+# Daemon Extension `simple_search`
+
+This extension implements fast and simple extension searching for clients. Data
+to be searched is downloaded from configured ACMs as a "haystack." Clients pass
+a list of one or more "needles" to search for. Haystack entries matching all
+provided needles are returned.
+
+The haystack starts out empty. To trigger a download, use the `refresh`
+endpoint. Refreshes happen in the background, so clients always receive an
+immediate response. This means that clients will be searching on "stale" data
+(or on the first run, no data) until the refresh completes.
+
+There is no built-in scheduler for periodic haystack refreshes. It is
+recommended to use an external scheduler such as `cron(8)`.
+
+## Getting Started
+
+To enable this feature, compile `angelsharkd` with the `simple_search` flag:
+
+```sh
+cargo build --bin angelsharkd --features simple_search ...
+```
+
+This extension expects a single environment variable,
+`ANGELSHARKD_EXT_SEARCH_ACMS`, to be set at runtime. This var should be a list
+of ACM names from your `asa.cfg`. For example, if your `asa.cfg` is configured
+for ACMs named 01, 02, and 03, and you want to search over 01 and 03, run
+`angelsharkd` like this:
+
+```
+ANGELSHARKD_ADDR='127.0.0.1:3000' ANGELSHARKD_EXT_SEARCH_ACMS='01 03' angelsharkd
+```
+
+## `GET /extensions/search/refresh` Refresh Haystack
+
+`GET /extensions/search/refresh`
+
+```
+200 OK text/plain
+Refresh scheduled.
+```
+
+## `POST /extensions/search` Search Haystack
+
+The return type is the entire list of fields from the `list extension-type`
+command, the ROOM field from the `list station` command, and the configured name
+of the ACM the entry was found on.
+
+```json
+POST /extensions/search
+[
+ "carpenat"
+]
+```
+
+```json
+200 OK
+[
+ [
+ "17571230000",
+ "station-user",
+ "5",
+ "1",
+ "1005",
+ "Carpenter, Adam",
+ "2",
+ "",
+ "CM01",
+ "carpenat"
+ ],
+ ...
+]
+```
+
+## Logging
+
+The `refresh` endpoint always returns successfully. Any errors encountered
+during the refresh are logged as `ERROR`. Successful completed refreshes are
+logged as `INFO`.
diff --git a/angelsharkd/src/routes/extensions/simple_search/mod.rs b/angelsharkd/src/routes/extensions/simple_search/mod.rs
new file mode 100644
index 0000000..5b5dc8e
--- /dev/null
+++ b/angelsharkd/src/routes/extensions/simple_search/mod.rs
@@ -0,0 +1,66 @@
+use log::{error, info};
+use std::convert::Infallible;
+pub use types::Haystack;
+use types::*;
+use warp::{
+ body::{content_length_limit, json},
+ get,
+ hyper::{header, StatusCode},
+ path, post,
+ reply::{self, with},
+ Filter, Rejection, Reply,
+};
+
+mod types;
+
+/// Returns a warp filter to handle HTTP POSTs for searching the haystack.
+pub fn search_filter(
+ haystack: Haystack,
+) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
+ path("search")
+ .and(post())
+ .and(content_length_limit(1024 * 16))
+ .and(json())
+ .and_then(move |terms: Needles| search(haystack.to_owned(), terms))
+ .with(with::header(header::PRAGMA, "no-cache"))
+ .with(with::header(header::CACHE_CONTROL, "no-store, max-age=0"))
+ .with(with::header(header::X_FRAME_OPTIONS, "DENY"))
+}
+
+/// Returns a warp filter to handle HTTP GETs for refreshing the haystack.
+pub fn refresh_filter(
+ haystack: Haystack,
+) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
+ path!("search" / "refresh")
+ .and(get())
+ .and_then(move || refresh(haystack.to_owned()))
+}
+
+/// Runs the search request to find all needles in the haystack and converts the
+/// results into a reply.
+async fn search(haystack: Haystack, needles: Needles) -> Result<impl Reply, Infallible> {
+ // Ok(haystack.search(Vec::new())?)
+ // if let Ok(matches = haystack.search(needle);
+ match haystack.search(needles) {
+ Ok(matches) => Ok(reply::with_status(reply::json(&matches), StatusCode::OK)),
+ Err(e) => Ok(reply::with_status(
+ reply::json(&e.to_string()),
+ StatusCode::INTERNAL_SERVER_ERROR,
+ )),
+ }
+}
+
+/// Immediately returns. Spawns an asynchronous task to complete the haystack
+/// refresh in the background.
+async fn refresh(haystack: Haystack) -> Result<impl Reply, Infallible> {
+ // Run refresh as a background task and immediately return.
+ tokio::spawn(async move {
+ if let Err(e) = haystack.refresh() {
+ error!("{}", e.to_string()); // TODO: use logger
+ } else {
+ info!("Search haystack refreshed.");
+ }
+ });
+
+ Ok("Refresh scheduled")
+}
diff --git a/angelsharkd/src/routes/extensions/simple_search/types.rs b/angelsharkd/src/routes/extensions/simple_search/types.rs
new file mode 100644
index 0000000..72156e5
--- /dev/null
+++ b/angelsharkd/src/routes/extensions/simple_search/types.rs
@@ -0,0 +1,167 @@
+use anyhow::{anyhow, Context, Error};
+use libangelshark::{AcmRunner, Message, ParallelIterator};
+use log::error;
+use std::{
+ collections::HashMap,
+ env,
+ sync::{Arc, Mutex},
+};
+
+const ANGELSHARKD_EXT_SEARCH_ACMS: &str = "ANGELSHARKD_EXT_SEARCH_ACMS";
+const OSSI_STAT_NUMBER_FIELD: &str = "8005ff00";
+const OSSI_STAT_ROOM_FIELD: &str = "0031ff00";
+const OSSI_LIST_STAT_CMD: &str = "list station";
+const OSSI_LIST_EXT_CMD: &str = "list extension-type";
+
+/// Collection of search terms
+pub type Needles = Vec<String>;
+
+/// Collection of ACM extension types with ROOMs (if applicable) and ACM names
+type HaystackEntries = Vec<Vec<String>>;
+
+/// Represents a searchable, refreshable collection of ACM extension data.
+#[derive(Clone)]
+pub struct Haystack {
+ entries: Arc<Mutex<HaystackEntries>>,
+ runner: AcmRunner,
+}
+
+impl Haystack {
+ pub fn new(runner: AcmRunner) -> Self {
+ Self {
+ entries: Arc::new(Mutex::new(Vec::new())),
+ runner,
+ }
+ }
+
+ /// Searches for haystack entries that contain all given needles and returns them.
+ pub fn search(&self, needles: Needles) -> Result<HaystackEntries, Error> {
+ let needles: Vec<_> = needles.iter().map(|n| n.to_lowercase()).collect();
+
+ let entries = self
+ .entries
+ .lock()
+ .map_err(|e| anyhow!(e.to_string()))
+ .with_context(|| "Failed to get haystack inner data lock.")?;
+
+ let matches = entries
+ .iter()
+ .filter(|entry| {
+ let entry_str = entry.join("").to_lowercase();
+ needles
+ .iter()
+ .all(|needle| entry_str.contains(needle.as_str()))
+ })
+ .cloned()
+ .collect();
+ Ok(matches)
+ }
+
+ /// Refreshes the haystack data by running relevant commands on a runner,
+ /// parsing the results, and updating the entries field with the fresh data.
+ /// TODO: Do we want simultaneous refreshes to be possible?
+ /// TODO: The entry generation could probably be simplified and the number of clones reduced.
+ pub fn refresh(&self) -> Result<(), Error> {
+ let mut runner = self.runner.to_owned();
+
+ // Queue jobs in ACM runner
+ let configured_acms = env::var(ANGELSHARKD_EXT_SEARCH_ACMS).with_context(|| {
+ format!(
+ "{} var missing. Cannot refresh haystack.",
+ ANGELSHARKD_EXT_SEARCH_ACMS
+ )
+ })?;
+
+ // Generate jobs and queue on runner.
+ for acm in configured_acms.split_whitespace() {
+ runner
+ .queue_input(acm, &Message::new(OSSI_LIST_EXT_CMD))
+ .queue_input(
+ acm,
+ &Message {
+ command: String::from(OSSI_LIST_STAT_CMD),
+ fields: Some(vec![
+ String::from(OSSI_STAT_NUMBER_FIELD),
+ String::from(OSSI_STAT_ROOM_FIELD),
+ ]),
+ datas: None,
+ error: None,
+ },
+ );
+ }
+
+ // Run jobs and collect output. Filter out uneeded commands, combine errors.
+ let output: Result<Vec<(String, Vec<Message>)>, Error> = runner
+ .run()
+ .map(|(name, output)| {
+ let output: Vec<Message> = output?
+ .into_iter()
+ .filter(|m| m.command != "logoff")
+ .collect();
+ Ok((name, output))
+ })
+ .collect();
+ let output = output.with_context(|| "Failed to run refresh commands on ACM(s).")?;
+
+ // Log any ACM errors encountered
+ for error in output
+ .iter()
+ .map(|(_, messages)| messages)
+ .flatten()
+ .filter_map(|m| m.error.as_ref())
+ {
+ error!("ACM error: {}", error);
+ }
+
+ // Build a map of station number-to-rooms
+ let rooms: HashMap<String, String> = output
+ .iter()
+ .map(|(_, messages)| {
+ messages
+ .iter()
+ .filter(|message| message.command == OSSI_LIST_STAT_CMD)
+ .filter_map(|message| message.datas.to_owned())
+ .flatten()
+ .filter_map(|stat| Some((stat.get(0)?.to_owned(), stat.get(1)?.to_owned())))
+ })
+ .flatten()
+ .collect();
+
+ // Build the haystack from the room map and extension-type output
+ let haystack: HaystackEntries = output
+ .into_iter()
+ .map(|(acm_name, messages)| {
+ let rooms = &rooms;
+ let acm_name = format!("CM{}", acm_name);
+
+ messages
+ .into_iter()
+ .filter(|message| message.command == OSSI_LIST_EXT_CMD)
+ .filter_map(|message| message.datas)
+ .flatten()
+ .map(move |mut extension| {
+ let room = extension
+ .get(0)
+ .map(|num| rooms.get(num))
+ .flatten()
+ .map(|room| room.to_owned())
+ .unwrap_or_else(String::new);
+
+ extension.push(acm_name.to_owned());
+ extension.push(room);
+ extension
+ })
+ })
+ .flatten()
+ .collect();
+
+ // Overwrite shared haystack entries with new data.
+ let mut lock = self
+ .entries
+ .lock()
+ .map_err(|e| anyhow!(e.to_string()))
+ .with_context(|| "Failed to get haystack inner data lock.")?;
+ *lock = haystack;
+ Ok(())
+ }
+}
diff --git a/angelsharkd/src/routes/mod.rs b/angelsharkd/src/routes/mod.rs
index 40d024d..cee0657 100644
--- a/angelsharkd/src/routes/mod.rs
+++ b/angelsharkd/src/routes/mod.rs
@@ -13,6 +13,7 @@ use warp::{
};
mod dtos;
+pub mod extensions;
/// GET / -> Name and version # of app.
pub fn index() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {