diff --git a/Cargo.lock b/Cargo.lock index 58a8223..00cc915 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,20 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -168,6 +182,12 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -213,8 +233,8 @@ dependencies = [ "bytes", "futures-util", "http 0.2.12", - "http-body", - "hyper", + "http-body 0.4.6", + "hyper 0.14.29", "itoa", "matchit", "memchr", @@ -223,7 +243,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower", "tower-layer", "tower-service", @@ -239,7 +259,7 @@ dependencies = [ "bytes", "futures-util", "http 0.2.12", - "http-body", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", @@ -273,6 +293,22 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcder" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627747a6774aab38beb35990d88309481378558875a41da1a4b2e373c906ef0" +dependencies = [ + "bytes", + "smallvec", +] + [[package]] name = "bindgen" version = "0.69.4" @@ -296,6 +332,21 @@ dependencies = [ "which", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -323,6 +374,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + [[package]] name = "byteorder" version = "1.5.0" @@ -386,6 +443,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "cmake" version = "0.1.50" @@ -401,12 +498,42 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -447,6 +574,16 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der-parser" version = "9.0.0" @@ -582,6 +719,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.1.0" @@ -610,6 +758,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -619,6 +782,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -731,8 +904,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -766,6 +941,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -871,6 +1065,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.9.3" @@ -899,9 +1116,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", - "http-body", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -914,168 +1131,121 @@ dependencies = [ ] [[package]] -name = "hyper-timeout" -version = "0.4.1" +name = "hyper" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ - "hyper", + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "itoa", "pin-project-lite", + "smallvec", "tokio", - "tokio-io-timeout", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", + "want", ] [[package]] -name = "icu_collections" -version = "1.5.0" +name = "hyper-rustls" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "futures-util", + "http 1.1.0", + "hyper 1.4.1", + "hyper-util", + "rustls 0.23.10", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", ] [[package]] -name = "icu_locid_transform" -version = "1.5.0" +name = "hyper-timeout" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", + "hyper 0.14.29", + "pin-project-lite", + "tokio", + "tokio-io-timeout", ] [[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" +name = "hyper-tls" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", + "bytes", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" +name = "hyper-util" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.4.1", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" +name = "iana-time-zone" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" +name = "iana-time-zone-haiku" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", + "cc", ] [[package]] name = "idna" -version = "1.0.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -1098,12 +1268,27 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is_terminal_polyfill" version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1137,6 +1322,36 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0afd06142c9bcb03f4a8787c77897a87b6be9c4918f1946c33caa714c27578" +dependencies = [ + "ahash", + "anyhow", + "base64 0.22.1", + "bytecount", + "clap", + "fancy-regex", + "fraction", + "getrandom", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time", + "url", + "uuid", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1183,12 +1398,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.12" @@ -1299,6 +1508,23 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -1309,6 +1535,20 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.5" @@ -1319,6 +1559,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1326,11 +1581,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] -name = "num-integer" -version = "0.1.46" +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ + "num-bigint", + "num-integer", "num-traits", ] @@ -1398,6 +1675,50 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1427,6 +1748,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1739,20 +2070,29 @@ dependencies = [ ] [[package]] -name = "redis-async" -version = "0.17.2" +name = "redis" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e44d2172ccb44736798c4cdc8690fb0a3776a74daa502bfe124708ffc144909c" +checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" dependencies = [ + "async-trait", "bytes", - "futures-channel", - "futures-sink", + "combine", "futures-util", - "log", - "pin-project", + "itoa", + "percent-encoding", + "pin-project-lite", + "rustls 0.22.4", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "ryu", + "sha1_smol", "socket2", "tokio", + "tokio-rustls 0.25.0", "tokio-util", + "url", ] [[package]] @@ -1802,6 +2142,50 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.17.8" @@ -1875,6 +2259,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls" version = "0.23.10" @@ -1890,6 +2288,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -1930,6 +2341,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1942,6 +2362,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags 2.5.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.23" @@ -2002,6 +2445,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.8" @@ -2028,6 +2477,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2066,10 +2524,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] [[package]] name = "stringprep" @@ -2082,6 +2544,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.5.0" @@ -2116,6 +2584,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "synstructure" version = "0.13.1" @@ -2127,6 +2601,27 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -2190,16 +2685,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -2255,6 +2740,16 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-pg-mapper" version = "0.2.0" @@ -2290,13 +2785,38 @@ dependencies = [ "whoami", ] +[[package]] +name = "tokio-postgres-rustls" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" +dependencies = [ + "ring", + "rustls 0.23.10", + "tokio", + "tokio-postgres", + "tokio-rustls 0.26.0", + "x509-certificate", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls", + "rustls 0.23.10", "rustls-pki-types", "tokio", ] @@ -2378,10 +2898,10 @@ dependencies = [ "axum", "base64 0.21.7", "bytes", - "h2", + "h2 0.3.26", "http 0.2.12", - "http-body", - "hyper", + "http-body 0.4.6", + "hyper 0.14.29", "hyper-timeout", "percent-encoding", "pin-project", @@ -2534,6 +3054,7 @@ dependencies = [ "futures-channel", "futures-executor", "futures-util", + "jsonschema", "lazy_static", "log", "petgraph", @@ -2541,7 +3062,8 @@ dependencies = [ "prost", "prost-build", "rdkafka", - "redis-async", + "redis", + "reqwest", "rlimit", "rustls-pemfile", "rustls-pki-types", @@ -2550,12 +3072,14 @@ dependencies = [ "tokio", "tokio-pg-mapper", "tokio-postgres", - "tokio-rustls", + "tokio-postgres-rustls", + "tokio-rustls 0.26.0", "tokio-stream", "tokio-tungstenite 0.23.0", "tonic", "tonic-build", "tungstenite 0.23.0", + "url", "uuid", "warp", "x509-parser", @@ -2605,9 +3129,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -2620,18 +3144,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -2679,7 +3191,7 @@ dependencies = [ "futures-util", "headers", "http 0.2.12", - "hyper", + "hyper 0.14.29", "log", "mime", "mime_guess", @@ -2734,6 +3246,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.92" @@ -2954,16 +3478,33 @@ dependencies = [ ] [[package]] -name = "write16" -version = "1.0.0" +name = "winreg" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] [[package]] -name = "writeable" -version = "0.5.5" +name = "x509-certificate" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "66534846dec7a11d7c50a74b7cdb208b9a581cad890b7866430d438455847c85" +dependencies = [ + "bcder", + "bytes", + "chrono", + "der", + "hex", + "pem", + "ring", + "signature", + "spki", + "thiserror", + "zeroize", +] [[package]] name = "x509-parser" @@ -2983,48 +3524,23 @@ dependencies = [ ] [[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.4" +name = "zerocopy" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerofrom-derive", + "zerocopy-derive", ] [[package]] -name = "zerofrom-derive" -version = "0.1.4" +name = "zerocopy-derive" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn 2.0.66", - "synstructure", ] [[package]] @@ -3046,25 +3562,3 @@ dependencies = [ "quote", "syn 2.0.66", ] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] diff --git a/Cargo.toml b/Cargo.toml index 7a20d60..0f6094d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,38 +5,45 @@ edition = "2021" [dependencies] serde = { version = "1.0.144", features = ["derive"] } -serde_json = "1.0.85" -env_logger = "0.11.3" -log = "0.4.20" +serde_json = { version = "1.0.85" } +env_logger = { version = "0.11.3" } +log = { version = "0.4.20" } tokio = { version = "1.34.0", features = ["full"] } tokio-stream = { version = "0.1.15", features = ["full"] } tokio-tungstenite = { version = "0.23.0" } -tokio-rustls = "0.26.0" -tokio-postgres = { version = "0.7.10", features = ["with-eui48-1"]} -tokio-pg-mapper = "0.2.0" -tungstenite = { version = "0.23.0"} +tokio-rustls = { version = "0.26.0" } +tokio-postgres = { version = "0.7.10", features = ["with-eui48-1"] } +tokio-postgres-rustls = { version = "0.12.0" } +tokio-pg-mapper = { version = "0.2.0" } +tungstenite = { version = "0.23.0" } futures-util = { version = "0.3.0", default-features = false } -futures-channel = "0.3.0" +futures-channel = { version = "0.3.0" } futures-executor = { version = "0.3.0", optional = true } -futures = "0.3.0" -rlimit = "0.10.1" -tonic = "0.11.0" -prost = "0.12" -rdkafka = "0.36.2" -eui48 = { version = "1.1.0", features = ["serde"]} +futures = { version = "0.3.0" } +rlimit = { version = "0.10.1" } +tonic = { version = "0.11.0" } +prost = { version = "0.12" } +rdkafka = { version = "0.36.2" } +eui48 = { version = "1.1.0", features = ["serde"] } uuid = { version = "1.6.1", features = ["serde"] } -redis-async = "0.17.2" -warp = "0.3.7" +redis = { version = "0.25.3", features = [ + "tokio-rustls-comp", + "tls-rustls-insecure", +] } +warp = { version = "0.3.7" } prometheus = { version = "0.13.4", features = ["process"] } -lazy_static = "1.4.0" +lazy_static = { version = "1.4.0" } petgraph = { version = "0.6.4", features = ["stable_graph"] } -flate2 = "1.0.28" -base64 = "0.22.0" -rustls-pemfile = "2.1.2" -rustls-pki-types = "1.7.0" -x509-parser = "0.16.0" -chrono = "0.4.38" -derive_more = "0.99.17" +flate2 = { version = "1.0.28" } +base64 = { version = "0.22.0" } +rustls-pemfile = { version = "2.1.2" } +rustls-pki-types = { version = "1.7.0" } +x509-parser = { version = "0.16.0" } +chrono = { version = "0.4.38" } +derive_more = { version = "0.99.17" } +reqwest = { version = "0.12.5", features = ["json"] } +jsonschema = { version = "0.18.0" } +url = { version = "2.5.2" } [build-dependencies] tonic-build = "0.11.0" diff --git a/README.md b/README.md index 4df275c..26874e5 100644 --- a/README.md +++ b/README.md @@ -55,59 +55,89 @@ Running application with default arguments might not be desired behavior. And thus the run script utilizes the following list of *enviroment* variables that you can define before running it to alternate behavior of the app. The following list is a list of enviroment variables you can define to configure cgw-app behavior in certain way: ``` -CGW_ID - Shard ID -CGW_GRPC_LISTENING_IP - IP to bind gRPC server to (listens for gRPC requests from remote CGWs) -CGW_GRPC_LISTENING_PORT - Port to bind gRPC server to (listens for gRPC requests from remote CGWs) -CGW_GRPC_PUBLIC_HOST - IP or hostname for Redis record (remote CGWs will connect to this particular shard through provided host record; - it's up to deployment config whether remote CGW#1 will be able to access this CGW#0, for example, through provided hostname/IP) -CGW_GRPC_PUBLIC_PORT - PORT for Redis record -CGW_WSS_IP - IP to bind websocket server to (listens for incoming WSS connections from underlying devices - infrastructures) -CGW_WSS_PORT - PORT to bind WSS server to -CGW_WSS_CAS - Web socket CAS certificate file name -CGW_WSS_CERT - Web socket server certificate file name -CGW_WSS_KEY - Web socket server private key file name -CGW_KAFKA_HOST - IP or hostname of remote KAFKA server to connect to (NB API) -CGW_KAFKA_PORT - PORT of remote KAFKA server to connect to -CGW_DB_HOST - IP or hostname of remote database server to connect to -CGW_DB_PORT - PORT of remote database server to connect to -CGW_DB_USER - PSQL DB username (credentials) to use upon connect to DB -CGW_DB_PASS - PSQL DB password (credentials) to use upon connect to DB -CGW_REDIS_HOST - IP or hostname of remote redis-db server to connect to -CGW_REDIS_PORT - PORT of remote redis-db server to connect to -CGW_LOG_LEVEL - Log level to start CGW application with (debug, info) -CGW_METRICS_PORT - PORT of metrics to connect to -CGW_CERTS_PATH - Path to certificates located on host machine -CGW_ALLOW_CERT_MISMATCH - Allow client certificate CN and device MAC address mismatch (used for OWLS) +CGW_ID - Shard ID +CGW_GROUPS_CAPACITY - The CGW instance groups capacity +CGW_GROUPS_THRESHOLD - The CGW instance groups threshold +CGW_GROUP_INFRAS_CAPACITY - The devices capacity for group +CGW_GRPC_LISTENING_IP - IP to bind gRPC server to (listens for gRPC requests from remote CGWs) +CGW_GRPC_LISTENING_PORT - Port to bind gRPC server to (listens for gRPC requests from remote CGWs) +CGW_GRPC_PUBLIC_HOST - IP or hostname for Redis record (remote CGWs will connect to this particular shard through provided host record; + it's up to deployment config whether remote CGW#1 will be able to access this CGW#0, for example, through provided hostname/IP) +CGW_GRPC_PUBLIC_PORT - PORT for Redis record +CGW_WSS_IP - IP to bind websocket server to (listens for incoming WSS connections from underlying devices - infrastructures) +CGW_WSS_PORT - PORT to bind WSS server to +CGW_WSS_CAS - Web socket CAS certificate file name +CGW_WSS_CERT - Web socket server certificate file name +CGW_WSS_KEY - Web socket server private key file name +CGW_KAFKA_HOST - IP or hostname of remote KAFKA server to connect to (NB API) +CGW_KAFKA_PORT - PORT of remote KAFKA server to connect to +CGW_DB_HOST - IP or hostname of remote database server to connect to +CGW_DB_PORT - PORT of remote database server to connect to +CGW_DB_USER - PSQL DB username (credentials) to use upon connect to DB +CGW_DB_PASS - PSQL DB password (credentials) to use upon connect to DB +CGW_DB_TLS - Utilize TLS connection with DB server +CGW_REDIS_HOST - IP or hostname of remote redis-db server to connect to +CGW_REDIS_PORT - PORT of remote redis-db server to connect to +CGW_REDIS_USERNAME - REDIS username (credentials) to use upon connect to +CGW_REDIS_PASSWORD - REDIS password (credentials) to use upon connect to +CGW_REDIS_TLS - Utilize TLS connection with REDIS server +CGW_LOG_LEVEL - Log level to start CGW application with (debug, info) +CGW_METRICS_PORT - PORT of metrics to connect to +CGW_CERTS_PATH - Path to certificates located on host machine +CGW_ALLOW_CERT_MISMATCH - Allow client certificate CN and device MAC address mismatch (used for OWLS) +CGW_NB_INFRA_CERTS_DIR - Path to NB infrastructure (Redis, PostgreSQL) certificates located on host machine +CGW_NB_INFRA_TLS - Utilize TLS connection with NB infrastructure (Redis, PostgreSQL) + If set enabled - the CGW_DB_TLS and CGW_REDIS_TLS values will be ignored and + the TLS connection will be used for Redis and PostgreSQL connection +CGW_UCENTRAL_AP_DATAMODEL_URI - Path to AP Config message JSON Validation schema: + 1. URI in format: "http[s]://", e.g https://somewhere.com/schema.json + 2. Path to local file: "", e.g /etc/host/schema.json +CGW_UCENTRAL_SWITCH_DATAMODEL_URI - Path to Switch Config message JSON Validation schema ``` Example of properly configured list of env variables to start CGW: ```console $ export | grep CGW -declare -x CGW_DB_HOST="localhost" # PSQL server is located at the local host +declare -x CGW_DB_HOST="localhost" declare -x CGW_DB_PORT="5432" -declare -x CGW_DB_USERNAME="cgw" # PSQL login credentials (username) default 'cgw' will be used -declare -x CGW_DB_PASS="123" # PSQL login credentials (password) default '123' will be used -declare -x CGW_GRPC_LISTENING_IP="127.0.0.1" # Local default subnet is 127.0.0.1/24 +declare -x CGW_DB_USERNAME="cgw" +declare -x CGW_DB_PASS="123" +declare -x CGW_DB_TLS="no" +declare -x CGW_GRPC_LISTENING_IP="127.0.0.1" declare -x CGW_GRPC_LISTENING_PORT="50051" declare -x CGW_GRPC_PUBLIC_HOST="localhost" declare -x CGW_GRPC_PUBLIC_PORT="50051" declare -x CGW_ID="0" -declare -x CGW_KAFKA_HOST="localhost" # Kafka is located at the local host +declare -x CGW_KAFKA_HOST="localhost" declare -x CGW_KAFKA_PORT="9092" declare -x CGW_LOG_LEVEL="debug" -declare -x CGW_REDIS_HOST="localhost" # Redis server can be found at the local host +declare -x CGW_REDIS_HOST="localhost" declare -x CGW_REDIS_PORT="6379" +declare -x CGW_REDIS_USERNAME="cgw" +declare -x CGW_REDIS_PASSWORD="123" +declare -x CGW_REDIS_TLS="no" declare -x CGW_METRICS_PORT="8080" -declare -x CGW_WSS_IP="0.0.0.0" # Accept WSS connections at all interfaces / subnets +declare -x CGW_WSS_IP="0.0.0.0" declare -x CGW_WSS_PORT="15002" declare -x CGW_WSS_CAS="cas.pem" declare -x CGW_WSS_CERT="cert.pem" declare -x CGW_WSS_KEY="key.pem" -declare -x CGW_CERTS_PATH="/etc/ssl/certs" # Path to certificates located on host machine -declare -x CGW_ALLOW_CERT_MISMATCH="no" # Allow client certificate CN and device MAC address mismatch +declare -x CGW_CERTS_PATH="/etc/ssl/certs" +declare -x CGW_ALLOW_CERT_MISMATCH="no" +declare -x CGW_NB_INFRA_CERTS_PATH="/etc/nb_infra_certs" +declare -x CGW_NB_INFRA_TLS="no" +declare -x CGW_UCENTRAL_AP_DATAMODEL_URI="https://raw.githubusercontent.com/Telecominfraproject/wlan-ucentral-schema/main/ucentral.schema.json" +declare -x CGW_UCENTRAL_SWITCH_DATAMODEL_URI="https://raw.githubusercontent.com/Telecominfraproject/ols-ucentral-schema/main/ucentral.schema.json" +declare -x CGW_GROUPS_CAPACITY=1000 +declare -x CGW_GROUPS_THRESHOLD=50 +declare -x CGW_GROUP_INFRAS_CAPACITY=2000 ``` # Certificates -The CGW uses a number of certificates to provide security. +The CGW uses two different sets of certificate configuration: +1. AP/Switch connectivity (southbound) +2. Infrastructure connectivity (northbound) + +The AP/Switch connectivity uses a number of certificates to provide security (mTLS). There are 2 types of certificates required for a normal deployment: 1. Server certificates 2. Client certificates @@ -119,3 +149,7 @@ There are several environment variable to configure certificates path and names 2. CGW_WSS_KEY - CGW WSS Private Key 3. CGW_WSS_CAS - Chain certificates to validate client (root/issuer) 4. CGW_CERTS_PATH - path to certificates located on host machine + +The infrastructure connectivity use root certs store - the directory with trusted certificates +The environemt variable to configure certificates path: +1. CGW_NB_INFRA_CERTS_PATH - path to certificates located on host machine diff --git a/run_cgw.sh b/run_cgw.sh index 15f3d75..ac5ac30 100755 --- a/run_cgw.sh +++ b/run_cgw.sh @@ -2,6 +2,9 @@ DEFAULT_ID=0 DEFAULT_LOG_LEVEL="info" +DEFAULT_GROUPS_CAPACITY=1000 +DEFAULT_GROUPS_THRESHOLD=50 +DEFAULT_GROUP_INFRAS_CAPACITY=2000 # By default - use default subnet's SRC ip to listen to gRPC requests DEFAULT_GRPC_LISTENING_IP="0.0.0.0" @@ -29,18 +32,28 @@ DEFAULT_DB_PORT=5432 DEFAULT_DB_NAME="cgw" DEFAULT_DB_USER="cgw" DEFAULT_DB_PASW="123" +DEFAULT_DB_TLS="no" DEFAULT_REDIS_HOST="localhost" DEFAULT_REDIS_PORT=6379 +DEFAULT_REDIS_TLS="no" DEFAULT_METRICS_PORT=8080 CONTAINTER_CERTS_VOLUME="/etc/cgw/certs" +CONTAINTER_NB_INFRA_CERTS_VOLUME="/etc/cgw/nb_infra/certs" +DEFAULT_NB_INFRA_TLS="no" DEFAULT_ALLOW_CERT_MISMATCH="no" +DEFAULT_UCENTRAL_AP_DATAMODEL_URI="https://raw.githubusercontent.com/Telecominfraproject/wlan-ucentral-schema/main/ucentral.schema.json" +DEFAULT_UCENTRAL_SWITCH_DATAMODEL_URI="https://raw.githubusercontent.com/Telecominfraproject/ols-ucentral-schema/main/ucentral.schema.json" + export CGW_LOG_LEVEL="${CGW_LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" export CGW_ID="${CGW_ID:-$DEFAULT_ID}" +export CGW_GROUPS_CAPACITY="${CGW_GROUPS_CAPACITY:-$DEFAULT_GROUPS_CAPACITY}" +export CGW_GROUPS_THRESHOLD="${CGW_GROUPS_THRESHOLD:-$DEFAULT_GROUPS_THRESHOLD}" +export CGW_GROUP_INFRAS_CAPACITY="${CGW_GROUP_INFRAS_CAPACITY:-$DEFAULT_GROUP_INFRAS_CAPACITY}" export CGW_WSS_IP="${CGW_WSS_IP:-$DEFAULT_WSS_IP}" export CGW_WSS_PORT="${CGW_WSS_PORT:-$DEFAULT_WSS_PORT}" export DEFAULT_WSS_THREAD_NUM="${DEFAULT_WSS_THREAD_NUM:-$DEFAULT_WSS_T_NUM}" @@ -60,58 +73,92 @@ export CGW_DB_PORT="${CGW_DB_PORT:-$DEFAULT_DB_PORT}" export CGW_DB_NAME="${CGW_DB_NAME:-$DEFAULT_DB_NAME}" export CGW_DB_USERNAME="${CGW_DB_USER:-$DEFAULT_DB_USER}" export CGW_DB_PASSWORD="${CGW_DB_PASS:-$DEFAULT_DB_PASW}" +export CGW_DB_TLS="${CGW_DB_TLS:-$DEFAULT_DB_TLS}" export CGW_REDIS_HOST="${CGW_REDIS_HOST:-$DEFAULT_REDIS_HOST}" export CGW_REDIS_PORT="${CGW_REDIS_PORT:-$DEFAULT_REDIS_PORT}" +export CGW_REDIS_TLS="${CGW_REDIS_TLS:-$DEFAULT_REDIS_TLS}" export CGW_METRICS_PORT="${CGW_METRICS_PORT:-$DEFAULT_METRICS_PORT}" export CGW_CERTS_PATH="${CGW_CERTS_PATH:-$DEFAULT_CERTS_PATH}" export CGW_ALLOW_CERT_MISMATCH="${CGW_ALLOW_CERT_MISMATCH:-$DEFAULT_ALLOW_CERT_MISMATCH}" +export CGW_NB_INFRA_CERTS_PATH="${CGW_NB_INFRA_CERTS_PATH:-$DEFAULT_CERTS_PATH}" +export CGW_NB_INFRA_TLS="${CGW_NB_INFRA_TLS:-$DEFAULT_NB_INFRA_TLS}" +export CGW_UCENTRAL_AP_DATAMODEL_URI="${CGW_UCENTRAL_AP_DATAMODEL_URI:-$DEFAULT_UCENTRAL_AP_DATAMODEL_URI}" +export CGW_UCENTRAL_SWITCH_DATAMODEL_URI="${CGW_UCENTRAL_SWITCH_DATAMODEL_URI:-$DEFAULT_UCENTRAL_SWITCH_DATAMODEL_URI}" +export RUST_BACKTRACE=1 + +if [ -z "${!CGW_REDIS_USERNAME}" ]; then + export CGW_REDIS_USERNAME="${CGW_REDIS_USERNAME}" +fi + +if [ -z "${!CGW_REDIS_PASSWORD}" ]; then + export CGW_REDIS_PASSWORD="${CGW_REDIS_PASSWORD}" +fi echo "Starting CGW..." -echo "CGW LOG LEVEL : $CGW_LOG_LEVEL" -echo "CGW ID : $CGW_ID" -echo "CGW WSS THREAD NUM : $DEFAULT_WSS_THREAD_NUM" -echo "CGW WSS IP/PORT : $CGW_WSS_IP:$CGW_WSS_PORT" -echo "CGW WSS CAS : $CGW_WSS_CAS" -echo "CGW WSS CERT : $CGW_WSS_CERT" -echo "CGW WSS KEY : $CGW_WSS_KEY" -echo "CGW GRPC PUBLIC HOST/PORT : $CGW_GRPC_PUBLIC_HOST:$CGW_GRPC_PUBLIC_PORT" -echo "CGW GRPC LISTENING IP/PORT : $CGW_GRPC_LISTENING_IP:$CGW_GRPC_LISTENING_PORT" -echo "CGW KAFKA HOST/PORT : $CGW_KAFKA_HOST:$CGW_KAFKA_PORT" -echo "CGW KAFKA TOPIC : $CGW_KAFKA_CONSUME_TOPIC:$CGW_KAFKA_PRODUCE_TOPIC" -echo "CGW DB NAME : $CGW_DB_NAME" -echo "CGW DB HOST/PORT : $CGW_DB_HOST:$CGW_DB_PORT" -echo "CGW REDIS HOST/PORT : $CGW_REDIS_HOST:$CGW_REDIS_PORT" -echo "CGW METRICS PORT : $CGW_METRICS_PORT" -echo "CGW CERTS PATH : $CGW_CERTS_PATH" -echo "CGW ALLOW CERT MISMATCH : $CGW_ALLOW_CERT_MISMATCH" +echo "CGW LOG LEVEL : $CGW_LOG_LEVEL" +echo "CGW ID : $CGW_ID" +echo "CGW GROUPS CAPACITY/THRESHOLD : $CGW_GROUPS_CAPACITY:$CGW_GROUPS_THRESHOLD" +echo "CGW GROUP INFRAS CAPACITY : $CGW_GROUP_INFRAS_CAPACITY" +echo "CGW WSS THREAD NUM : $DEFAULT_WSS_THREAD_NUM" +echo "CGW WSS IP/PORT : $CGW_WSS_IP:$CGW_WSS_PORT" +echo "CGW WSS CAS : $CGW_WSS_CAS" +echo "CGW WSS CERT : $CGW_WSS_CERT" +echo "CGW WSS KEY : $CGW_WSS_KEY" +echo "CGW GRPC PUBLIC HOST/PORT : $CGW_GRPC_PUBLIC_HOST:$CGW_GRPC_PUBLIC_PORT" +echo "CGW GRPC LISTENING IP/PORT : $CGW_GRPC_LISTENING_IP:$CGW_GRPC_LISTENING_PORT" +echo "CGW KAFKA HOST/PORT : $CGW_KAFKA_HOST:$CGW_KAFKA_PORT" +echo "CGW KAFKA TOPIC : $CGW_KAFKA_CONSUME_TOPIC:$CGW_KAFKA_PRODUCE_TOPIC" +echo "CGW DB NAME : $CGW_DB_NAME" +echo "CGW DB HOST/PORT : $CGW_DB_HOST:$CGW_DB_PORT" +echo "CGW DB TLS : $CGW_DB_TLS" +echo "CGW REDIS HOST/PORT : $CGW_REDIS_HOST:$CGW_REDIS_PORT" +echo "CGW REDIS TLS : $CGW_REDIS_TLS" +echo "CGW METRICS PORT : $CGW_METRICS_PORT" +echo "CGW CERTS PATH : $CGW_CERTS_PATH" +echo "CGW ALLOW CERT MISMATCH : $CGW_ALLOW_CERT_MISMATCH" +echo "CGW NB INFRA CERTS PATH : $CGW_NB_INFRA_CERTS_PATH" +echo "CGW NB INFRA TLS : $CGW_NB_INFRA_TLS" +echo "CGW UCENTRAL AP DATAMODEL URI : $CGW_UCENTRAL_AP_DATAMODEL_URI" +echo "CGW UCENTRAL SWITCH DATAMODEL URI : $CGW_UCENTRAL_SWITCH_DATAMODEL_URI" docker run \ - --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ - -v $CGW_CERTS_PATH:$CONTAINTER_CERTS_VOLUME \ - -e CGW_LOG_LEVEL \ - -e CGW_ID \ - -e CGW_WSS_IP \ - -e CGW_WSS_PORT \ - -e DEFAULT_WSS_THREAD_NUM \ - -e CGW_WSS_CAS \ - -e CGW_WSS_CERT \ - -e CGW_WSS_KEY \ - -e CGW_GRPC_LISTENING_IP \ - -e CGW_GRPC_LISTENING_PORT \ - -e CGW_GRPC_PUBLIC_HOST \ - -e CGW_GRPC_PUBLIC_PORT \ - -e CGW_KAFKA_HOST \ - -e CGW_KAFKA_PORT \ - -e CGW_KAFKA_CONSUME_TOPIC \ - -e CGW_KAFKA_PRODUCE_TOPIC \ - -e CGW_DB_NAME \ - -e CGW_DB_HOST \ - -e CGW_DB_PORT \ - -e CGW_DB_USERNAME \ - -e CGW_DB_PASSWORD \ - -e CGW_REDIS_HOST \ - -e CGW_REDIS_PORT \ - -e CGW_FEATURE_TOPOMAP_ENABLE \ - -e CGW_METRICS_PORT \ - -e CGW_ALLOW_CERT_MISMATCH \ + --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ + -v $CGW_CERTS_PATH:$CONTAINTER_CERTS_VOLUME \ + -v $CGW_NB_INFRA_CERTS_PATH:$CONTAINTER_NB_INFRA_CERTS_VOLUME \ + -e CGW_LOG_LEVEL \ + -e CGW_ID \ + -e CGW_GROUPS_CAPACITY \ + -e CGW_GROUPS_THRESHOLD \ + -e CGW_GROUP_INFRAS_CAPACITY \ + -e CGW_WSS_IP \ + -e CGW_WSS_PORT \ + -e DEFAULT_WSS_THREAD_NUM \ + -e CGW_WSS_CAS \ + -e CGW_WSS_CERT \ + -e CGW_WSS_KEY \ + -e CGW_GRPC_LISTENING_IP \ + -e CGW_GRPC_LISTENING_PORT \ + -e CGW_GRPC_PUBLIC_HOST \ + -e CGW_GRPC_PUBLIC_PORT \ + -e CGW_KAFKA_HOST \ + -e CGW_KAFKA_PORT \ + -e CGW_KAFKA_CONSUME_TOPIC \ + -e CGW_KAFKA_PRODUCE_TOPIC \ + -e CGW_DB_NAME \ + -e CGW_DB_HOST \ + -e CGW_DB_PORT \ + -e CGW_DB_USERNAME \ + -e CGW_DB_PASSWORD \ + -e CGW_DB_TLS \ + -e CGW_REDIS_HOST \ + -e CGW_REDIS_PORT \ + -e CGW_REDIS_USERNAME \ + -e CGW_REDIS_PASSWORD \ + -e CGW_REDIS_TLS \ + -e CGW_FEATURE_TOPOMAP_ENABLE \ + -e CGW_METRICS_PORT \ + -e CGW_ALLOW_CERT_MISMATCH \ + -e CGW_NB_INFRA_TLS \ + -e CGW_UCENTRAL_AP_DATAMODEL_URI \ + -e CGW_UCENTRAL_SWITCH_DATAMODEL_URI \ -d -t --network=host --name $2 $1 ucentral-cgw diff --git a/src/cgw_app_args.rs b/src/cgw_app_args.rs new file mode 100644 index 0000000..c5359bd --- /dev/null +++ b/src/cgw_app_args.rs @@ -0,0 +1,669 @@ +use std::{ + env, + net::Ipv4Addr, + path::{Path, PathBuf}, + str::FromStr, +}; + +use url::Url; + +use crate::{ + cgw_errors::{Error, Result}, + AppCoreLogLevel, +}; + +const CGW_DEFAULT_ID: i32 = 0; +const CGW_DEFAULT_GROUPS_CAPACITY: i32 = 1000; +const CGW_DEFAULT_GROUPS_THRESHOLD: i32 = 50; +const CGW_DEFAULT_GROUP_INFRAS_CAPACITY: i32 = 2000; +const CGW_DEFAULT_WSS_T_NUM: usize = 4; +const CGW_DEFAULT_LOG_LEVEL: AppCoreLogLevel = AppCoreLogLevel::Debug; +const CGW_DEFAULT_WSS_IP: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); +const CGW_DEFAULT_WSS_PORT: u16 = 15002; +const CGW_DEFAULT_WSS_CAS: &str = "cas.pem"; +const CGW_DEFAULT_WSS_CERT: &str = "cert.pem"; +const CGW_DEFAULT_WSS_KEY: &str = "key.pem"; +const CGW_DEFAULT_GRPC_LISTENING_IP: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); +const CGW_DEFAULT_GRPC_LISTENING_PORT: u16 = 50051; +const CGW_DEFAULT_GRPC_PUBLIC_HOST: &str = "localhost"; +const CGW_DEFAULT_GRPC_PUBLIC_PORT: u16 = 50051; +const CGW_DEFAULT_KAFKA_HOST: &str = "localhost"; +const CGW_DEFAULT_KAFKA_PORT: u16 = 9092; +const CGW_DEFAULT_KAFKA_CONSUME_TOPIC: &str = "CnC"; +const CGW_DEFAULT_KAFKA_PRODUCE_TOPIC: &str = "CnC_Res"; +const CGW_DEFAULT_DB_HOST: &str = "localhost"; +const CGW_DEFAULT_DB_PORT: u16 = 6379; +const CGW_DEFAULT_DB_NAME: &str = "cgw"; +const CGW_DEFAULT_DB_USERNAME: &str = "cgw"; +const CGW_DEFAULT_DB_PASSWORD: &str = "123"; +const CGW_DEFAULT_DB_TLS: &str = "no"; +const CGW_DEFAULT_REDIS_HOST: &str = "localhost"; +const CGW_DEFAULT_REDIS_PORT: u16 = 6379; +const CGW_DEFAULT_REDIS_TLS: &str = "no"; +const CGW_DEFAULT_ALLOW_CERT_MISMATCH: &str = "no"; +const CGW_DEFAULT_METRICS_PORT: u16 = 8080; +const CGW_DEFAULT_TOPOMAP_STATE: bool = false; +const CGW_DEFAULT_NB_INFRA_TLS: &str = "no"; +const CGW_DEFAULT_UCENTRAL_AP_DATAMODEL_URI: &str = "https://raw.githubusercontent.com/Telecominfraproject/wlan-ucentral-schema/main/ucentral.schema.json"; +const CGW_DEFAULT_UCENTRAL_SWITCH_DATAMODEL_URI: &str = "https://raw.githubusercontent.com/Telecominfraproject/ols-ucentral-schema/main/ucentral.schema.json"; + +pub struct CGWWSSArgs { + /// Number of thread in a threadpool dedicated for handling secure websocket connections + pub wss_t_num: usize, + /// IP to listen for incoming WSS connection + pub wss_ip: Ipv4Addr, + /// PORT to listen for incoming WSS connection + pub wss_port: u16, + /// WSS CAS certificate (contains root and issuer certificates) + pub wss_cas: String, + /// WSS certificate + pub wss_cert: String, + /// WSS private key + pub wss_key: String, + /// Allow Missmatch + pub allow_mismatch: bool, +} + +impl CGWWSSArgs { + fn parse() -> Result { + let wss_t_num: usize = match env::var("DEFAULT_WSS_THREAD_NUM") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse DEFAULT_WSS_THREAD_NUM! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_WSS_T_NUM, + }; + + let wss_ip: Ipv4Addr = match env::var("CGW_WSS_IP") { + Ok(val) => match Ipv4Addr::from_str(val.as_str()) { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_WSS_IP! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_WSS_IP, + }; + + let wss_port: u16 = match env::var("CGW_WSS_PORT") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_WSS_PORT! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_WSS_PORT, + }; + + let wss_cas: String = env::var("CGW_WSS_CAS").unwrap_or(CGW_DEFAULT_WSS_CAS.to_string()); + let wss_cert: String = env::var("CGW_WSS_CERT").unwrap_or(CGW_DEFAULT_WSS_CERT.to_string()); + let wss_key: String = env::var("CGW_WSS_KEY").unwrap_or(CGW_DEFAULT_WSS_KEY.to_string()); + + let mismatch: String = env::var("CGW_ALLOW_CERT_MISMATCH") + .unwrap_or(CGW_DEFAULT_ALLOW_CERT_MISMATCH.to_string()); + let allow_mismatch = mismatch == "yes"; + + Ok(CGWWSSArgs { + wss_t_num, + wss_ip, + wss_port, + wss_cas, + wss_cert, + wss_key, + allow_mismatch, + }) + } +} + +pub struct CGWGRPCArgs { + /// IP to listen for incoming GRPC connection + pub grpc_listening_ip: Ipv4Addr, + /// PORT to listen for incoming GRPC connection + pub grpc_listening_port: u16, + /// IP or hostname for Redis Record + pub grpc_public_host: String, + /// PORT for Redis record + pub grpc_public_port: u16, +} + +impl CGWGRPCArgs { + fn parse() -> Result { + let grpc_listening_ip: Ipv4Addr = match env::var("CGW_GRPC_LISTENING_IP") { + Ok(val) => match Ipv4Addr::from_str(val.as_str()) { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_GRPC_LISTENING_IP! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_GRPC_LISTENING_IP, + }; + + let grpc_listening_port: u16 = match env::var("CGW_GRPC_LISTENING_PORT") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_GRPC_LISTENING_PORT! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_GRPC_LISTENING_PORT, + }; + + let grpc_public_host: String = match env::var("CGW_GRPC_PUBLIC_HOST") { + Ok(val) => { + // 1. Try to parse variable into IpAddress + match Ipv4Addr::from_str(val.as_str()) { + // 2. If parsed - return IpAddress as String value + Ok(ip) => ip.to_string(), + // 3. If parse failed - probably hostname specified + Err(_e) => val, + } + } + // Env. variable is not setup - use default value + Err(_) => CGW_DEFAULT_GRPC_PUBLIC_HOST.to_string(), + }; + + let grpc_public_port: u16 = match env::var("CGW_GRPC_PUBLIC_PORT") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_GRPC_PUBLIC_PORT! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_GRPC_PUBLIC_PORT, + }; + + Ok(CGWGRPCArgs { + grpc_listening_ip, + grpc_listening_port, + grpc_public_host, + grpc_public_port, + }) + } +} + +pub struct CGWKafkaArgs { + /// IP or hostname to connect to KAFKA broker + pub kafka_host: String, + /// PORT to connect to KAFKA broker + pub kafka_port: u16, + /// KAFKA topic from where to consume messages + #[allow(unused)] + pub kafka_consume_topic: String, + /// KAFKA topic where to produce messages + #[allow(unused)] + pub kafka_produce_topic: String, +} + +impl CGWKafkaArgs { + fn parse() -> Result { + let kafka_host: String = match env::var("CGW_KAFKA_HOST") { + Ok(val) => { + // 1. Try to parse variable into IpAddress + match Ipv4Addr::from_str(val.as_str()) { + // 2. If parsed - return IpAddress as String value + Ok(ip) => ip.to_string(), + // 3. If parse failed - probably hostname specified + Err(_e) => val, + } + } + // Env. variable is not setup - use default value + Err(_) => CGW_DEFAULT_KAFKA_HOST.to_string(), + }; + + let kafka_port: u16 = match env::var("CGW_KAFKA_PORT") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_KAFKA_PORT! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_KAFKA_PORT, + }; + + let kafka_consume_topic: String = env::var("CGW_KAFKA_CONSUMER_TOPIC") + .unwrap_or(CGW_DEFAULT_KAFKA_CONSUME_TOPIC.to_string()); + let kafka_produce_topic: String = env::var("CGW_KAFKA_PRODUCER_TOPIC") + .unwrap_or(CGW_DEFAULT_KAFKA_PRODUCE_TOPIC.to_string()); + + Ok(CGWKafkaArgs { + kafka_host, + kafka_port, + kafka_consume_topic, + kafka_produce_topic, + }) + } +} + +pub struct CGWDBArgs { + /// IP or hostname to connect to DB (PSQL) + pub db_host: String, + /// PORT to connect to DB (PSQL) + pub db_port: u16, + /// DB name to connect to in DB (PSQL) + pub db_name: String, + /// DB user name use with connection to in DB (PSQL) + pub db_username: String, + /// DB user password use with connection to in DB (PSQL) + pub db_password: String, + /// Utilize TLS connection with DB server + pub db_tls: bool, +} + +impl CGWDBArgs { + fn parse() -> Result { + let db_host: String = match env::var("CGW_DB_HOST") { + Ok(val) => { + // 1. Try to parse variable into IpAddress + match Ipv4Addr::from_str(val.as_str()) { + // 2. If parsed - return IpAddress as String value + Ok(ip) => ip.to_string(), + // 3. If parse failed - probably hostname specified + Err(_e) => val, + } + } + // Env. variable is not setup - use default value + Err(_) => CGW_DEFAULT_DB_HOST.to_string(), + }; + + let db_port: u16 = match env::var("CGW_DB_PORT") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_DB_PORT! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_DB_PORT, + }; + + let db_name: String = env::var("CGW_DB_NAME").unwrap_or(CGW_DEFAULT_DB_NAME.to_string()); + let db_username: String = + env::var("CGW_DB_USERNAME").unwrap_or(CGW_DEFAULT_DB_USERNAME.to_string()); + let db_password: String = + env::var("CGW_DB_PASSWORD").unwrap_or(CGW_DEFAULT_DB_PASSWORD.to_string()); + + let db_tls_var: String = env::var("CGW_DB_TLS").unwrap_or(CGW_DEFAULT_DB_TLS.to_string()); + let db_tls = db_tls_var == "yes"; + + Ok(CGWDBArgs { + db_host, + db_port, + db_name, + db_username, + db_password, + db_tls, + }) + } +} + +pub struct CGWRedisArgs { + /// IP or hostname to connect to REDIS + pub redis_host: String, + /// PORT to connect to REDIS + pub redis_port: u16, + /// REDIS username + pub redis_username: Option, + /// REDIS password + pub redis_password: Option, + /// Utilize TLS connection with DB server + pub redis_tls: bool, +} + +impl CGWRedisArgs { + fn parse() -> Result { + let redis_host: String = match env::var("CGW_REDIS_HOST") { + Ok(val) => { + // 1. Try to parse variable into IpAddress + match Ipv4Addr::from_str(val.as_str()) { + // 2. If parsed - return IpAddress as String value + Ok(ip) => ip.to_string(), + // 3. If parse failed - probably hostname specified + Err(_e) => val, + } + } + // Env. variable is not setup - use default value + Err(_) => CGW_DEFAULT_REDIS_HOST.to_string(), + }; + + let redis_port: u16 = match env::var("CGW_REDIS_PORT") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_REDIS_PORT! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_REDIS_PORT, + }; + + let redis_username: Option = match env::var("CGW_REDIS_USERNAME") { + Ok(username) => { + if username.is_empty() { + None + } else { + Some(username) + } + } + Err(_) => None, + }; + + let redis_password: Option = match env::var("CGW_REDIS_PASSWORD") { + Ok(password) => { + if password.is_empty() { + None + } else { + Some(password) + } + } + Err(_) => None, + }; + + let redis_tls_var: String = + env::var("CGW_REDIS_TLS").unwrap_or(CGW_DEFAULT_REDIS_TLS.to_string()); + let redis_tls = redis_tls_var == "yes"; + + Ok(CGWRedisArgs { + redis_host, + redis_port, + redis_username, + redis_password, + redis_tls, + }) + } +} + +pub struct CGWMetricsArgs { + // PORT to connect to Metrics + pub metrics_port: u16, +} + +impl CGWMetricsArgs { + fn parse() -> Result { + let metrics_port: u16 = match env::var("CGW_METRICS_PORT") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_METRICS_PORT! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_METRICS_PORT, + }; + + Ok(CGWMetricsArgs { metrics_port }) + } +} + +#[derive(Clone, Debug)] +pub enum CGWValionSchemaRef { + SchemaUri(Url), + SchemaPath(PathBuf), +} + +#[derive(Clone)] +pub struct CGWValidationSchemaArgs { + // URI to AP data model schema + pub ap_schema_uri: CGWValionSchemaRef, + // URI to Switch data model schema + pub switch_schema_uri: CGWValionSchemaRef, +} + +impl CGWValidationSchemaArgs { + fn parse() -> Result { + let ap_schema_uri: CGWValionSchemaRef = match env::var("CGW_UCENTRAL_AP_DATAMODEL_URI") { + Ok(uri) => { + // CGW_UCENTRAL_AP_DATAMODEL_URI is set + if Path::new(&uri).exists() { + // CGW_UCENTRAL_AP_DATAMODEL_URI - is path to local file and file exist + match PathBuf::from_str(&uri) { + Ok(path) => CGWValionSchemaRef::SchemaPath(path), + Err(e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_UCENTRAL_AP_DATAMODEL_URI! Invalid URI: {}, err: {}", + uri, e + ))); + } + } + } else { + match Url::parse(&uri) { + Ok(url) => CGWValionSchemaRef::SchemaUri(url), + Err(e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_UCENTRAL_AP_DATAMODEL_URI! Invalid URI: {}, err: {}", + uri, e + ))); + } + } + } + } + // Environment variable was not set - use default + Err(_e) => match Url::parse(CGW_DEFAULT_UCENTRAL_AP_DATAMODEL_URI) { + // CGW_UCENTRAL_AP_DATAMODEL_URI was not set - try to use default + Ok(uri) => CGWValionSchemaRef::SchemaUri(uri), + Err(e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse default CGW_UCENTRAL_AP_DATAMODEL_URI! Invalid URI: {}, err: {}", + CGW_DEFAULT_UCENTRAL_AP_DATAMODEL_URI, e + ))); + } + }, + }; + + let switch_schema_uri: CGWValionSchemaRef = match env::var( + "CGW_UCENTRAL_SWITCH_DATAMODEL_URI", + ) { + Ok(uri) => { + // CGW_UCENTRAL_SWITCH_DATAMODEL_URI is set + if Path::new(&uri).exists() { + match PathBuf::from_str(&uri) { + Ok(path) => CGWValionSchemaRef::SchemaPath(path), + Err(e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_UCENTRAL_SWITCH_DATAMODEL_URI! Invalid URI: {}, err: {}", + uri, e + ))); + } + } + } else { + match Url::parse(&uri) { + Ok(url) => CGWValionSchemaRef::SchemaUri(url), + Err(e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_UCENTRAL_SWITCH_DATAMODEL_URI! Invalid URI: {}, err: {}", + uri, e + ))); + } + } + } + } + // Environment variable was not set - use default + Err(_e) => match Url::from_str(CGW_DEFAULT_UCENTRAL_SWITCH_DATAMODEL_URI) { + Ok(url) => CGWValionSchemaRef::SchemaUri(url), + Err(e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse default CGW_UCENTRAL_SWITCH_DATAMODEL_URI! Invalid value: {}, err: {}", + CGW_DEFAULT_UCENTRAL_SWITCH_DATAMODEL_URI, e + ))); + } + }, + }; + + Ok(CGWValidationSchemaArgs { + ap_schema_uri, + switch_schema_uri, + }) + } +} + +pub struct AppArgs { + /// Loglevel of application + pub log_level: AppCoreLogLevel, + + /// CGW unique identifier (i32) + pub cgw_id: i32, + + /// CGW groups capacity (i32) + pub cgw_groups_capacity: i32, + + /// CGW groups threshold (i32) + pub cgw_groups_threshold: i32, + + /// CGW group infras capacity (i32) + pub cgw_group_infras_capacity: i32, + + /// Topomap featue status (enabled/disabled) + pub feature_topomap_enabled: bool, + + /// Utilize TLS connection with NB infrastructure (Redis, PostgreSQL) + pub nb_infra_tls: bool, + + pub wss_args: CGWWSSArgs, + + pub grpc_args: CGWGRPCArgs, + + pub kafka_args: CGWKafkaArgs, + + pub db_args: CGWDBArgs, + + pub redis_args: CGWRedisArgs, + + pub metrics_args: CGWMetricsArgs, + + pub validation_schema: CGWValidationSchemaArgs, +} + +impl AppArgs { + pub fn parse() -> Result { + let log_level: AppCoreLogLevel = match env::var("CGW_LOG_LEVEL") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_LOG_LEVEL! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_LOG_LEVEL, + }; + + let cgw_id: i32 = match env::var("CGW_ID") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_ID! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_ID, + }; + + let cgw_groups_capacity: i32 = match env::var("CGW_GROUPS_CAPACITY") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_GROUPS_CAPACITY! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_GROUPS_CAPACITY, + }; + + let cgw_groups_threshold: i32 = match env::var("CGW_GROUPS_THRESHOLD") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_GROUPS_CAPACITY! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_GROUPS_THRESHOLD, + }; + + let cgw_group_infras_capacity: i32 = match env::var("CGW_GROUP_INFRAS_CAPACITY") { + Ok(val) => match val.parse() { + Ok(v) => v, + Err(_e) => { + return Err(Error::AppArgsParser(format!( + "Failed to parse CGW_GROUP_INFRAS_CAPACITY! Invalid value: {}", + val + ))); + } + }, + Err(_) => CGW_DEFAULT_GROUP_INFRAS_CAPACITY, + }; + + let feature_topomap_enabled: bool = match env::var("CGW_FEATURE_TOPOMAP_ENABLE") { + Ok(_) => true, + Err(_) => CGW_DEFAULT_TOPOMAP_STATE, + }; + + let nb_infra_tls_var: String = + env::var("CGW_NB_INFRA_TLS").unwrap_or(CGW_DEFAULT_NB_INFRA_TLS.to_string()); + let nb_infra_tls = nb_infra_tls_var == "yes"; + + let wss_args = CGWWSSArgs::parse()?; + let grpc_args = CGWGRPCArgs::parse()?; + let kafka_args = CGWKafkaArgs::parse()?; + let mut db_args = CGWDBArgs::parse()?; + let mut redis_args = CGWRedisArgs::parse()?; + let metrics_args = CGWMetricsArgs::parse()?; + let validation_schema = CGWValidationSchemaArgs::parse()?; + + if nb_infra_tls { + redis_args.redis_tls = nb_infra_tls; + db_args.db_tls = nb_infra_tls; + } + + Ok(AppArgs { + log_level, + cgw_id, + feature_topomap_enabled, + nb_infra_tls, + wss_args, + grpc_args, + kafka_args, + db_args, + redis_args, + metrics_args, + validation_schema, + cgw_groups_capacity, + cgw_groups_threshold, + cgw_group_infras_capacity, + }) + } +} diff --git a/src/cgw_connection_processor.rs b/src/cgw_connection_processor.rs index efc4837..70633ac 100644 --- a/src/cgw_connection_processor.rs +++ b/src/cgw_connection_processor.rs @@ -19,6 +19,7 @@ use futures_util::{ stream::{SplitSink, SplitStream}, FutureExt, SinkExt, StreamExt, }; + use std::{net::SocketAddr, str::FromStr, sync::Arc}; use tokio::{ net::TcpStream, @@ -70,6 +71,7 @@ pub struct CGWConnectionProcessor { pub idx: i64, pub group_id: i32, pub feature_topomap_enabled: bool, + pub device_type: CGWDeviceType, } impl CGWConnectionProcessor { @@ -81,6 +83,8 @@ impl CGWConnectionProcessor { idx: conn_idx, group_id: 0, feature_topomap_enabled: server.feature_topomap_enabled, + // Default to AP, it's safe, as later-on it will be changed + device_type: CGWDeviceType::CGWDeviceAP, }; conn_processor @@ -186,7 +190,8 @@ impl CGWConnectionProcessor { } self.serial = evt.serial; - let device_type = CGWDeviceType::from_str(caps.platform.as_str())?; + + self.device_type = CGWDeviceType::from_str(caps.platform.as_str())?; // TODO: we accepted tls stream and split the WS into RX TX part, // now we have to ASK cgw_connection_server's permission whether @@ -252,8 +257,7 @@ impl CGWConnectionProcessor { } } - self.process_connection(stream, sink, mbox_rx, device_type) - .await; + self.process_connection(stream, sink, mbox_rx).await; Ok(()) } @@ -261,13 +265,13 @@ impl CGWConnectionProcessor { async fn process_wss_rx_msg( &self, msg: std::result::Result, - device_type: CGWDeviceType, fsm_state: &mut CGWUCentralMessageProcessorState, pending_req_id: u64, ) -> Result { // Make sure we always track the as accurate as possible the time // of receiving of the event (where needed). let timestamp = Local::now(); + let mut kafaka_msg: String = String::new(); match msg { Ok(msg) => match msg { @@ -275,14 +279,32 @@ impl CGWConnectionProcessor { return Ok(CGWConnectionState::ClosedGracefully); } Text(payload) => { - if let Ok(evt) = - cgw_ucentral_event_parse(&device_type, &payload, timestamp.timestamp()) - { + if let Ok(evt) = cgw_ucentral_event_parse( + &self.device_type, + self.feature_topomap_enabled, + &payload, + timestamp.timestamp(), + ) { + kafaka_msg.clone_from(&payload); if let CGWUCentralEventType::State(_) = evt.evt_type { + if let Some(decompressed) = evt.decompressed.clone() { + kafaka_msg = decompressed; + } if self.feature_topomap_enabled { let topo_map = CGWUCentralTopologyMap::get_ref(); - topo_map.process_state_message(&device_type, evt).await; - topo_map.debug_dump_map().await; + + // TODO: remove this Arc clone: + // Dirty hack for now: pass Arc ref of srv to topo map; + // Future rework and refactoring would require to separate + // NB api from being an internal obj of conn_server to be a + // standalone (singleton?) object. + topo_map.enqueue_event( + evt, + self.device_type, + self.serial, + self.group_id, + self.cgw_server.clone(), + ); } } else if let CGWUCentralEventType::Reply(content) = evt.evt_type { if *fsm_state != CGWUCentralMessageProcessorState::ResultPending { @@ -304,16 +326,24 @@ impl CGWConnectionProcessor { } else if let CGWUCentralEventType::RealtimeEvent(_) = evt.evt_type { if self.feature_topomap_enabled { let topo_map = CGWUCentralTopologyMap::get_ref(); - topo_map - .process_device_topology_event(&device_type, evt) - .await; - topo_map.debug_dump_map().await; + // TODO: remove this Arc clone: + // Dirty hack for now: pass Arc ref of srv to topo map; + // Future rework and refactoring would require to separate + // NB api from being an internal obj of conn_server to be a + // standalone (singleton?) object. + topo_map.enqueue_event( + evt, + self.device_type, + self.serial, + self.group_id, + self.cgw_server.clone(), + ); } } } self.cgw_server - .enqueue_mbox_message_from_device_to_nb_api_c(self.group_id, payload)?; + .enqueue_mbox_message_from_device_to_nb_api_c(self.group_id, kafaka_msg)?; return Ok(CGWConnectionState::IsActive); } Ping(_t) => { @@ -394,7 +424,6 @@ impl CGWConnectionProcessor { mut stream: SStream, mut sink: SSink, mut mbox_rx: UnboundedReceiver, - device_type: CGWDeviceType, ) { #[derive(Debug)] enum WakeupReason { @@ -545,7 +574,7 @@ impl CGWConnectionProcessor { let rc = match wakeup_reason { WakeupReason::WSSRxMsg(res) => { last_contact = Instant::now(); - self.process_wss_rx_msg(res, device_type, &mut fsm_state, pending_req_id) + self.process_wss_rx_msg(res, &mut fsm_state, pending_req_id) .await } WakeupReason::MboxRx(mbox_message) => { diff --git a/src/cgw_connection_server.rs b/src/cgw_connection_server.rs index 288ebf6..44f0255 100644 --- a/src/cgw_connection_server.rs +++ b/src/cgw_connection_server.rs @@ -1,5 +1,5 @@ use crate::cgw_device::{ - cgw_detect_device_chages, CGWDevice, CGWDeviceCapabilities, CGWDeviceState, + cgw_detect_device_chages, CGWDevice, CGWDeviceCapabilities, CGWDeviceState, CGWDeviceType, }; use crate::cgw_nb_api_listener::{ cgw_construct_device_capabilities_changed_msg, cgw_construct_device_enqueue_response, @@ -8,11 +8,14 @@ use crate::cgw_nb_api_listener::{ cgw_construct_infra_group_device_del_response, cgw_construct_rebalance_group_response, cgw_construct_unassigned_infra_connection_msg, }; +use crate::cgw_runtime::{cgw_get_runtime, CGWRuntimeType}; use crate::cgw_tls::cgw_tls_get_cn_from_stream; use crate::cgw_ucentral_messages_queue_manager::{ CGWUCentralMessagesQueueItem, CGW_MESSAGES_QUEUE, }; -use crate::cgw_ucentral_parser::cgw_ucentral_parse_command_message; +use crate::cgw_ucentral_parser::{ + cgw_ucentral_parse_command_message, CGWUCentralCommandType, CGWUCentralConfigValidators, +}; use crate::cgw_ucentral_topology_map::CGWUCentralTopologyMap; use crate::AppArgs; @@ -30,10 +33,11 @@ use crate::{ use crate::cgw_errors::{Error, Result}; +use std::str::FromStr; use std::{collections::HashMap, net::SocketAddr, sync::Arc}; use tokio::{ net::TcpStream, - runtime::{Builder, Runtime}, + runtime::Runtime, sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, RwLock, @@ -41,8 +45,6 @@ use tokio::{ time::{sleep, Duration}, }; -use std::sync::atomic::{AtomicUsize, Ordering}; - use serde_json::{Map, Value}; use serde::{Deserialize, Serialize}; @@ -153,6 +155,10 @@ pub struct CGWConnectionServer { // Key: device MAC, Value: Device devices_cache: Arc>, + // UCentral command messages validators + // for access points and switches + config_validator: CGWUCentralConfigValidators, + // User-supplied arguments can disable state/realtime events // processing by underlying connections processors. pub feature_topomap_enabled: bool, @@ -160,6 +166,7 @@ pub struct CGWConnectionServer { enum CGWNBApiParsedMsgType { InfrastructureGroupCreate, + InfrastructureGroupCreateToShard(i32), InfrastructureGroupDelete, InfrastructureGroupInfraAdd(Vec), InfrastructureGroupInfraDel(Vec), @@ -185,65 +192,127 @@ impl CGWNBApiParsedMsg { impl CGWConnectionServer { pub async fn new(app_args: &AppArgs) -> Result> { - let wss_runtime_handle = Arc::new( - Builder::new_multi_thread() - .worker_threads(app_args.wss_t_num) - .thread_name_fn(|| { - static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0); - let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst); - format!("cgw-wss-t-{}", id) - }) - .thread_stack_size(3 * 1024 * 1024) - .enable_all() - .build()?, - ); - let internal_mbox_runtime_handle = Arc::new( - Builder::new_multi_thread() - .worker_threads(1) - .thread_name("cgw-mbox") - .thread_stack_size(1024 * 1024) - .enable_all() - .build()?, - ); - let nb_api_mbox_runtime_handle = Arc::new( - Builder::new_multi_thread() - .worker_threads(1) - .thread_name("cgw-mbox-nbapi") - .thread_stack_size(1024 * 1024) - .enable_all() - .build()?, - ); - let relay_msg_mbox_runtime_handle = Arc::new( - Builder::new_multi_thread() - .worker_threads(1) - .thread_name("cgw-relay-mbox-nbapi") - .thread_stack_size(1024 * 1024) - .enable_all() - .build()?, - ); - let nb_api_mbox_tx_runtime_handle = Arc::new( - Builder::new_multi_thread() - .worker_threads(1) - .thread_name("cgw-mbox-nbapi-tx") - .thread_stack_size(1024 * 1024) - .enable_all() - .build()?, - ); - let queue_timeout_handle = Arc::new( - Builder::new_multi_thread() - .worker_threads(1) - .thread_name("cgw-queue-timeout") - .thread_stack_size(1024 * 1024) - .enable_all() - .build()?, - ); + let wss_runtime_handle = match cgw_get_runtime(CGWRuntimeType::WssRxTx) { + Ok(ret_runtime) => match ret_runtime { + Some(runtime) => runtime, + None => { + return Err(Error::ConnectionServer(format!( + "Failed to find runtime type {:?}", + CGWRuntimeType::WssRxTx + ))); + } + }, + Err(e) => { + return Err(Error::ConnectionServer(format!( + "Failed to get runtime type {:?}, err: {}", + CGWRuntimeType::WssRxTx, + e + ))); + } + }; + + let internal_mbox_runtime_handle = match cgw_get_runtime(CGWRuntimeType::MboxInternal) { + Ok(ret_runtime) => match ret_runtime { + Some(runtime) => runtime, + None => { + return Err(Error::ConnectionServer(format!( + "Failed to find runtime type {:?}", + CGWRuntimeType::WssRxTx + ))); + } + }, + Err(e) => { + return Err(Error::ConnectionServer(format!( + "Failed to get runtime type {:?}, err: {}", + CGWRuntimeType::WssRxTx, + e + ))); + } + }; + + let nb_api_mbox_runtime_handle = match cgw_get_runtime(CGWRuntimeType::MboxNbApiRx) { + Ok(ret_runtime) => match ret_runtime { + Some(runtime) => runtime, + None => { + return Err(Error::ConnectionServer(format!( + "Failed to find runtime type {:?}", + CGWRuntimeType::WssRxTx + ))); + } + }, + Err(e) => { + return Err(Error::ConnectionServer(format!( + "Failed to get runtime type {:?}, err: {}", + CGWRuntimeType::WssRxTx, + e + ))); + } + }; + + let nb_api_mbox_tx_runtime_handle = match cgw_get_runtime(CGWRuntimeType::MboxNbApiTx) { + Ok(ret_runtime) => match ret_runtime { + Some(runtime) => runtime, + None => { + return Err(Error::ConnectionServer(format!( + "Failed to find runtime type {:?}", + CGWRuntimeType::WssRxTx + ))); + } + }, + Err(e) => { + return Err(Error::ConnectionServer(format!( + "Failed to get runtime type {:?}, err: {}", + CGWRuntimeType::WssRxTx, + e + ))); + } + }; + + let relay_msg_mbox_runtime_handle = match cgw_get_runtime(CGWRuntimeType::MboxRelay) { + Ok(ret_runtime) => match ret_runtime { + Some(runtime) => runtime, + None => { + return Err(Error::ConnectionServer(format!( + "Failed to find runtime type {:?}", + CGWRuntimeType::WssRxTx + ))); + } + }, + Err(e) => { + return Err(Error::ConnectionServer(format!( + "Failed to get runtime type {:?}, err: {}", + CGWRuntimeType::WssRxTx, + e + ))); + } + }; + + let queue_timeout_handle = match cgw_get_runtime(CGWRuntimeType::QueueTimeout) { + Ok(ret_runtime) => match ret_runtime { + Some(runtime) => runtime, + None => { + return Err(Error::ConnectionServer(format!( + "Failed to find runtime type {:?}", + CGWRuntimeType::WssRxTx + ))); + } + }, + Err(e) => { + return Err(Error::ConnectionServer(format!( + "Failed to get runtime type {:?}, err: {}", + CGWRuntimeType::WssRxTx, + e + ))); + } + }; let (internal_tx, internal_rx) = unbounded_channel::(); let (nb_api_tx, nb_api_rx) = unbounded_channel::(); // Give NB API client a handle where it can do a TX (CLIENT -> CGW_SERVER) // RX is handled in internal_mbox of CGW_Server - let nb_api_c = match CGWNBApiClient::new(app_args, &nb_api_tx) { + let nb_api_c = match CGWNBApiClient::new(app_args.cgw_id, &app_args.kafka_args, &nb_api_tx) + { Ok(c) => c, Err(e) => { error!( @@ -271,8 +340,34 @@ impl CGWConnectionServer { } }; + // TODO: proper fix. + // Ugly W/A for now; + // The reason behind this change (W/A), is that underlying validator + // uses sync call, which panics (due to it being called in async + // context). + // The proper fix would to be refactor all constructors to be sync, + // but use spawn_blocking where needed in contextes that rely on the + // underlying async calls. + let app_args_clone = app_args.validation_schema.clone(); + let get_config_validator_fut = tokio::task::spawn_blocking(move || { + CGWUCentralConfigValidators::new(app_args_clone).unwrap() + }); + let config_validator = match get_config_validator_fut.await { + Ok(res) => res, + Err(e) => { + error!( + "Failed to retrieve json config validators: {}", + e.to_string() + ); + return Err(Error::ConnectionServer(format!( + "Failed to retrieve json config validators: {}", + e + ))); + } + }; + let server = Arc::new(CGWConnectionServer { - allow_mismatch: app_args.allow_mismatch, + allow_mismatch: app_args.wss_args.allow_mismatch, local_cgw_id: app_args.cgw_id, connmap: CGWConnMap::new(), wss_rx_tx_runtime: wss_runtime_handle, @@ -287,6 +382,7 @@ impl CGWConnectionServer { mbox_relay_msg_runtime_handle: relay_msg_mbox_runtime_handle, devices_cache: Arc::new(RwLock::new(CGWDevicesCache::new())), feature_topomap_enabled: app_args.feature_topomap_enabled, + config_validator, }); let server_clone = server.clone(); @@ -296,8 +392,15 @@ impl CGWConnectionServer { }); let server_clone = server.clone(); + let ifras_capacity = app_args.cgw_group_infras_capacity; + CGWMetrics::get_ref().change_counter( + CGWMetricsCounterType::GroupInfrasCapacity, + CGWMetricsCounterOpType::Set(ifras_capacity.into()), + ); server.mbox_nb_api_runtime_handle.spawn(async move { - server_clone.process_internal_nb_api_mbox(nb_api_rx).await; + server_clone + .process_internal_nb_api_mbox(nb_api_rx, ifras_capacity) + .await; }); server.queue_timeout_handle.spawn(async move { @@ -320,6 +423,12 @@ impl CGWConnectionServer { .await; }); + if server.feature_topomap_enabled { + info!("Topomap enabled, starting queue processor..."); + let topo_map = CGWUCentralTopologyMap::get_ref(); + topo_map.start(&server.wss_rx_tx_runtime).await; + } + Ok(server) } @@ -364,6 +473,13 @@ impl CGWConnectionServer { fn parse_nbapi_msg(&self, pload: &str) -> Option { #[derive(Debug, Serialize, Deserialize)] struct InfraGroupCreate { + r#type: String, + infra_group_id: String, + infra_name: String, + uuid: Uuid, + } + #[derive(Debug, Serialize, Deserialize)] + struct InfraGroupCreateToShard { r#type: String, infra_group_id: String, infra_name: String, @@ -419,6 +535,16 @@ impl CGWConnectionServer { CGWNBApiParsedMsgType::InfrastructureGroupCreate, )); } + "infrastructure_group_create_to_shard" => { + let json_msg: InfraGroupCreateToShard = serde_json::from_str(pload).ok()?; + return Some(CGWNBApiParsedMsg::new( + json_msg.uuid, + group_id, + CGWNBApiParsedMsgType::InfrastructureGroupCreateToShard( + json_msg.infra_shard_id, + ), + )); + } "infrastructure_group_delete" => { let json_msg: InfraGroupDelete = serde_json::from_str(pload).ok()?; return Some(CGWNBApiParsedMsg::new( @@ -489,14 +615,9 @@ impl CGWConnectionServer { CGWConnectionProcessorReqMsg::GroupIdChanged(new_gid); for mac in mac_list.iter() { - match connmap_r_lock.get(&mac) { - Some(c) => { - let _ = c.send(msg.clone()); - debug!("Notified {mac} about GID change (->{new_gid})"); - } - None => { - warn!("Wanted to notify {mac} about GID change (->{new_gid}), but device doesn't exist in map (Not connected still?)"); - } + if let Some(c) = connmap_r_lock.get(mac) { + let _ = c.send(msg.clone()); + debug!("Notified {mac} about GID change (->{new_gid})"); } } }); @@ -505,6 +626,7 @@ impl CGWConnectionServer { async fn process_internal_nb_api_mbox( self: Arc, mut rx_mbox: CGWConnectionServerNBAPIMboxRx, + infras_capacity: i32, ) { debug!("process_nb_api_mbox entry"); @@ -612,10 +734,14 @@ impl CGWConnectionServer { // DB stuff - create group for remote shards to be aware of change let group = CGWDBInfrastructureGroup { id: gid, - reserved_size: 1000i32, + reserved_size: infras_capacity, actual_size: 0i32, }; - match self.cgw_remote_discovery.create_infra_group(&group).await { + match self + .cgw_remote_discovery + .create_infra_group(&group, None) + .await + { Ok(_dst_cgw_id) => { if let Ok(resp) = cgw_construct_infra_group_create_response( gid, @@ -646,8 +772,61 @@ impl CGWConnectionServer { } else { error!("Failed to construct infra_group_create message"); } + } + } + // This type of msg is handled in place, not added to buf + // for later processing. + continue; + } else if let CGWNBApiParsedMsg { + uuid, + gid, + msg_type: CGWNBApiParsedMsgType::InfrastructureGroupCreateToShard(shard_id), + } = parsed_msg + { + // DB stuff - create group for remote shards to be aware of change + let group = CGWDBInfrastructureGroup { + id: gid, + reserved_size: 1000i32, + actual_size: 0i32, + }; + match self + .cgw_remote_discovery + .create_infra_group(&group, Some(shard_id)) + .await + { + Ok(_dst_cgw_id) => { + if let Ok(resp) = cgw_construct_infra_group_create_response( + gid, + String::default(), + uuid, + true, + None, + ) { + self.enqueue_mbox_message_from_cgw_to_nb_api(gid, resp); + } else { + error!("Failed to construct infra_group_create message"); + } + } + Err(e) => { + warn!( + "Create group gid {gid}, uuid {uuid} request failed, reason: {:?}", + e + ); - warn!("Create group gid {gid} received, but it already exists, uuid {uuid}"); + if let Ok(resp) = cgw_construct_infra_group_create_response( + gid, + String::default(), + uuid, + false, + Some(format!( + "Failed to create new group to shard id {}: {:?}", + shard_id, e + )), + ) { + self.enqueue_mbox_message_from_cgw_to_nb_api(gid, resp); + } else { + error!("Failed to construct infra_group_create message"); + } } } // This type of msg is handled in place, not added to buf @@ -666,6 +845,32 @@ impl CGWConnectionServer { .await { Ok(()) => { + // We try to help free topomap memory usage + // by notifying it whenever GID get's destroyed. + // Howover, for allocation we let topo map + // handle it's mem alloc whenever necessary + // on it's own, when data from specific gid + // arrives - we rely on topo map to + // allocate necessary structures on it's own. + // + // In this way, we make sure that we handle + // properly the GID resoration scenario: + // if CGW restarts and loads GID info from + // DB, there would be no notification about + // create/del, and there's no need to + // oflload this responsibility to + // remote_discovery module for example, + // due to the fact that CGW is not designed + // for management of redis without CGW knowledge: + // if something disrupts the redis state / sql + // state without CGW's prior knowledge, + // it's not a responsibility of CGW to be aware + // of such changes and handle it correspondingly. + if self.feature_topomap_enabled { + let topo_map = CGWUCentralTopologyMap::get_ref(); + topo_map.remove_gid(gid).await; + } + if let Ok(resp) = cgw_construct_infra_group_delete_response(gid, uuid, true, None) { @@ -1043,16 +1248,64 @@ impl CGWConnectionServer { // 1. Parse message from NB if let Ok(parsed_cmd) = cgw_ucentral_parse_command_message(&msg.clone()) { - let queue_msg: CGWUCentralMessagesQueueItem = - CGWUCentralMessagesQueueItem::new(parsed_cmd, msg); - - // 2. Add message to queue - { - let queue_lock = CGW_MESSAGES_QUEUE.read().await; - let _ = - queue_lock.push_device_message(device_mac, queue_msg).await; + if parsed_cmd.cmd_type == CGWUCentralCommandType::Configure { + // 2. Get device type + let devices_cache = self.devices_cache.read().await; + match devices_cache.get_device(&device_mac) { + Some(dev) => { + let device_type = dev.get_device_type(); + match self + .config_validator + .validate_config_message(&msg, device_type) + { + Ok(()) => { + let queue_msg: CGWUCentralMessagesQueueItem = + CGWUCentralMessagesQueueItem::new( + parsed_cmd, msg, + ); + + // 3. Add message to queue + { + let queue_lock = + CGW_MESSAGES_QUEUE.read().await; + let _ = queue_lock + .push_device_message( + device_mac, queue_msg, + ) + .await; + } + } + Err(e) => { + error!("Failed to validate config message! Invalid configure message for device: {device_mac}"); + if let Ok(resp) = cgw_construct_device_enqueue_response( + uuid, + false, + Some(format!("Failed to validate config message! Invalid configure message for device: {device_mac}, uuid {uuid}\n{}", e)), + ) { + self.enqueue_mbox_message_from_cgw_to_nb_api(gid, resp); + } else { + error!("Failed to construct device_enqueue message"); + } + continue; + } + } + } + None => { + error!("Failed to validate config message! Device {device_mac} does not exist in cache!"); + continue; + } + } } } else { + if let Ok(resp) = cgw_construct_device_enqueue_response( + uuid, + false, + Some(format!("Failed to parse command message to device: {device_mac}, uuid {uuid}")), + ) { + self.enqueue_mbox_message_from_cgw_to_nb_api(gid, resp); + } else { + error!("Failed to construct device_enqueue message"); + } error!("Failed to parse UCentral command"); } } @@ -1167,6 +1420,8 @@ impl CGWConnectionServer { conn_processor_mbox_tx, ) = msg { + let device_platform: String = caps.platform.clone(); + // if connection is unique: simply insert new conn // // if duplicate exists: notify server about such incident. @@ -1198,14 +1453,23 @@ impl CGWConnectionServer { connmap_w_lock.len() + 1 ); + let device_type = match CGWDeviceType::from_str(caps.platform.as_str()) { + Ok(dev_type) => dev_type, + Err(_e) => { + error!("Failed to parse device {device_mac} type!"); + CGWDeviceType::CGWDeviceUnknown + } + }; + // Received new connection - check if infra exist in cache // If exists - it already should have assigned group // If not - simply add to cache - set gid == 0, devices should't remain in DB let mut devices_cache = self.devices_cache.write().await; let mut device_group_id: i32 = 0; - if let Some(device) = devices_cache.get_device(&device_mac) { + if let Some(device) = devices_cache.get_device_mut(&device_mac) { device_group_id = device.get_device_group_id(); device.set_device_state(CGWDeviceState::CGWDeviceConnected); + device.set_device_type(device_type); let group_id = device.get_device_group_id(); if let Some(group_owner_id) = self @@ -1295,7 +1559,13 @@ impl CGWConnectionServer { devices_cache.add_device( &device_mac, - &CGWDevice::new(CGWDeviceState::CGWDeviceConnected, 0, false, caps), + &CGWDevice::new( + device_type, + CGWDeviceState::CGWDeviceConnected, + 0, + false, + caps, + ), ); if let Ok(resp) = cgw_construct_unassigned_infra_connection_msg( @@ -1315,7 +1585,9 @@ impl CGWConnectionServer { if self.feature_topomap_enabled { let topo_map = CGWUCentralTopologyMap::get_ref(); - topo_map.insert_device(&device_mac).await; + topo_map + .insert_device(&device_mac, device_platform.as_str(), device_group_id) + .await; } connmap_w_lock.insert(device_mac, conn_processor_mbox_tx); @@ -1326,6 +1598,7 @@ impl CGWConnectionServer { let _ = conn_processor_mbox_tx_clone.send(msg); }); } else if let CGWConnectionServerReqMsg::ConnectionClosed(device_mac) = msg { + let mut device_group_id: i32 = 0; // Insert device to disconnected device list { let queue_lock = CGW_MESSAGES_QUEUE.read().await; @@ -1339,7 +1612,8 @@ impl CGWConnectionServer { connmap_w_lock.remove(&device_mac); let mut devices_cache = self.devices_cache.write().await; - if let Some(device) = devices_cache.get_device(&device_mac) { + if let Some(device) = devices_cache.get_device_mut(&device_mac) { + device_group_id = device.get_device_group_id(); if device.get_device_remains_in_db() { device.set_device_state(CGWDeviceState::CGWDeviceDisconnected); } else { @@ -1349,7 +1623,9 @@ impl CGWConnectionServer { if self.feature_topomap_enabled { let topo_map = CGWUCentralTopologyMap::get_ref(); - topo_map.remove_device(&device_mac).await; + topo_map + .remove_device(&device_mac, device_group_id, self.clone()) + .await; } CGWMetrics::get_ref().change_counter( @@ -1448,7 +1724,7 @@ mod tests { #[test] fn can_parse_connect_event() -> Result<()> { let msg = get_connect_json_msg(); - let event: CGWUCentralEvent = cgw_ucentral_ap_parse_message(msg, 0)?; + let event: CGWUCentralEvent = cgw_ucentral_ap_parse_message(false, msg, 0)?; match event.evt_type { CGWUCentralEventType::Connect(_) => { @@ -1466,7 +1742,7 @@ mod tests { fn can_parse_log_event() -> Result<()> { let msg = get_log_json_msg(); - let event: CGWUCentralEvent = cgw_ucentral_ap_parse_message(msg, 0)?; + let event: CGWUCentralEvent = cgw_ucentral_ap_parse_message(false, msg, 0)?; match event.evt_type { CGWUCentralEventType::Log(_) => { diff --git a/src/cgw_db_accessor.rs b/src/cgw_db_accessor.rs index f888b30..91ced45 100644 --- a/src/cgw_db_accessor.rs +++ b/src/cgw_db_accessor.rs @@ -1,5 +1,6 @@ -use crate::AppArgs; +use crate::cgw_app_args::CGWDBArgs; +use crate::cgw_tls::cgw_tls_create_db_connect; use crate::{ cgw_errors::{Error, Result}, cgw_metrics::{CGWMetrics, CGWMetricsHealthComponent, CGWMetricsHealthComponentStatus}, @@ -7,7 +8,8 @@ use crate::{ use eui48::MacAddress; -use tokio_postgres::{row::Row, Client, NoTls}; +use tokio_postgres::NoTls; +use tokio_postgres::{row::Row, Client}; #[derive(Clone)] pub struct CGWDBInfra { @@ -51,40 +53,85 @@ pub struct CGWDBAccessor { } impl CGWDBAccessor { - pub async fn new(app_args: &AppArgs) -> Result { + pub async fn new(db_args: &CGWDBArgs) -> Result { let conn_str = format!( - "host={host} port={port} user={user} dbname={db} password={pass} connect_timeout=10", - host = app_args.db_host, - port = app_args.db_port, - user = app_args.db_username, - db = app_args.db_name, - pass = app_args.db_password + "sslmode={sslmode} host={host} port={port} user={user} dbname={db} password={pass} connect_timeout=10", + host = db_args.db_host, + port = db_args.db_port, + user = db_args.db_username, + db = db_args.db_name, + pass = db_args.db_password, + sslmode = match db_args.db_tls { + true => "require", + false => "disable", + } ); debug!( - "Trying to connect to DB ({}:{})...\nConn args {}", - app_args.db_host, app_args.db_port, conn_str + "Trying to connect to remote db ({}:{})...\nConn args {}", + db_args.db_host, db_args.db_port, conn_str ); - let (client, connection) = match tokio_postgres::connect(&conn_str, NoTls).await { - Ok((cl, conn)) => (cl, conn), - Err(e) => { - error!("Failed to establish connection with DB, reason: {:?}", e); - return Err(Error::DbAccessor("Failed to establish connection with DB")); - } - }; + let client: Client; + if db_args.db_tls { + let tls = match cgw_tls_create_db_connect().await { + Ok(tls_connect) => tls_connect, + Err(e) => { + error!( + "Failed to build TLS connection with remote DB, reason: {}", + e.to_string() + ); + return Err(Error::DbAccessor( + "Failed to build TLS connection with remote DB", + )); + } + }; - tokio::spawn(async move { - if let Err(e) = connection.await { - let err_msg = format!("Connection to DB broken: {}", e); - error!("{}", err_msg); - CGWMetrics::get_ref() - .change_component_health_status( - CGWMetricsHealthComponent::DBConnection, - CGWMetricsHealthComponentStatus::NotReady(err_msg), - ) - .await; - } - }); + let (db_client, connection) = match tokio_postgres::connect(&conn_str, tls).await { + Ok((cl, conn)) => (cl, conn), + Err(e) => { + error!("Failed to establish connection with DB, reason: {:?}", e); + return Err(Error::DbAccessor("Failed to establish connection with DB")); + } + }; + + tokio::spawn(async move { + if let Err(e) = connection.await { + let err_msg = format!("Connection to DB broken: {}", e); + error!("{}", err_msg); + CGWMetrics::get_ref() + .change_component_health_status( + CGWMetricsHealthComponent::DBConnection, + CGWMetricsHealthComponentStatus::NotReady(err_msg), + ) + .await; + } + }); + + client = db_client; + } else { + let (db_client, connection) = match tokio_postgres::connect(&conn_str, NoTls).await { + Ok((cl, conn)) => (cl, conn), + Err(e) => { + error!("Failed to establish connection with DB, reason: {:?}", e); + return Err(Error::DbAccessor("Failed to establish connection with DB")); + } + }; + + tokio::spawn(async move { + if let Err(e) = connection.await { + let err_msg = format!("Connection to DB broken: {}", e); + error!("{}", err_msg); + CGWMetrics::get_ref() + .change_component_health_status( + CGWMetricsHealthComponent::DBConnection, + CGWMetricsHealthComponentStatus::NotReady(err_msg), + ) + .await; + } + }); + + client = db_client; + } tokio::spawn(async move { CGWMetrics::get_ref() diff --git a/src/cgw_device.rs b/src/cgw_device.rs index 5906364..fd2ce15 100644 --- a/src/cgw_device.rs +++ b/src/cgw_device.rs @@ -3,10 +3,12 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)] pub enum CGWDeviceType { CGWDeviceAP, CGWDeviceSwitch, + #[default] + CGWDeviceUnknown, } impl FromStr for CGWDeviceType { @@ -16,6 +18,7 @@ impl FromStr for CGWDeviceType { match s { "ap" => Ok(CGWDeviceType::CGWDeviceAP), "switch" => Ok(CGWDeviceType::CGWDeviceSwitch), + "unknown" => Ok(CGWDeviceType::CGWDeviceUnknown), _ => Err(()), } } @@ -45,17 +48,19 @@ pub struct CGWDeviceCapabilities { impl CGWDeviceCapabilities { pub fn update_device_capabilities(&mut self, new_capabilities: &CGWDeviceCapabilities) { - self.firmware = new_capabilities.firmware.clone(); + self.firmware.clone_from(&new_capabilities.firmware); self.uuid = new_capabilities.uuid; - self.compatible = new_capabilities.compatible.clone(); - self.model = new_capabilities.model.clone(); - self.platform = new_capabilities.platform.clone(); - self.label_macaddr = new_capabilities.label_macaddr.clone(); + self.compatible.clone_from(&new_capabilities.compatible); + self.model.clone_from(&new_capabilities.model); + self.platform.clone_from(&new_capabilities.platform); + self.label_macaddr + .clone_from(&new_capabilities.label_macaddr); } } #[derive(Clone, Default, Deserialize, Serialize)] pub struct CGWDevice { + dev_type: CGWDeviceType, state: CGWDeviceState, group_id: i32, remains_in_db: bool, @@ -64,12 +69,14 @@ pub struct CGWDevice { impl CGWDevice { pub fn new( + dev_type: CGWDeviceType, state: CGWDeviceState, group_id: i32, remains_in_db: bool, capabilities: CGWDeviceCapabilities, ) -> CGWDevice { CGWDevice { + dev_type, state, group_id, remains_in_db, @@ -77,6 +84,14 @@ impl CGWDevice { } } + pub fn set_device_type(&mut self, dev_type: CGWDeviceType) { + self.dev_type = dev_type; + } + + pub fn get_device_type(&self) -> CGWDeviceType { + self.dev_type + } + pub fn set_device_state(&mut self, new_state: CGWDeviceState) { self.state = new_state; } diff --git a/src/cgw_devices_cache.rs b/src/cgw_devices_cache.rs index 103fd71..dc18be9 100644 --- a/src/cgw_devices_cache.rs +++ b/src/cgw_devices_cache.rs @@ -61,10 +61,10 @@ impl CGWDevicesCache { } pub fn check_device_exists(&self, key: &MacAddress) -> bool { - self.cache.get(key).is_some() + self.cache.contains_key(key) } - pub fn get_device(&mut self, key: &MacAddress) -> Option<&mut CGWDevice> { + pub fn get_device_mut(&mut self, key: &MacAddress) -> Option<&mut CGWDevice> { if let Some(value) = self.cache.get_mut(key) { Some(value) } else { @@ -72,6 +72,14 @@ impl CGWDevicesCache { } } + pub fn get_device(&self, key: &MacAddress) -> Option<&CGWDevice> { + if let Some(value) = self.cache.get(key) { + Some(value) + } else { + None + } + } + #[allow(dead_code)] pub fn get_device_id(&self, key: &MacAddress) -> Option { self.cache.get(key).map(|value| value.get_device_group_id()) diff --git a/src/cgw_errors.rs b/src/cgw_errors.rs index b93f50d..c8ee229 100644 --- a/src/cgw_errors.rs +++ b/src/cgw_errors.rs @@ -17,12 +17,18 @@ pub enum Error { Tls(String), + Redis(String), + UCentralParser(&'static str), + UCentralValidator(String), + UCentralMessagesQueue(&'static str), AppArgsParser(String), + Runtime(String), + // -- Externals #[from] Io(std::io::Error), @@ -39,6 +45,9 @@ pub enum Error { #[from] TokioSync(tokio::sync::TryLockError), + #[from] + Tokiofs(tokio::fs::ReadDir), + #[from] IpAddressParse(std::net::AddrParseError), @@ -63,9 +72,6 @@ pub enum Error { #[from] InvalidUri(warp::http::uri::InvalidUri), - #[from] - RedisAsync(redis_async::error::Error), - #[from] StaticStr(&'static str), @@ -79,12 +85,11 @@ pub enum Error { Empty(()), } -impl ToString for Error { - fn to_string(&self) -> String { +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Error::AppArgsParser(message) => message.clone(), - Error::Tls(message) => message.clone(), - _ => format!("{:?}", self), + Error::AppArgsParser(message) | Error::Tls(message) => write!(f, "{}", message), + _ => write!(f, "{:?}", self), } } } diff --git a/src/cgw_metrics.rs b/src/cgw_metrics.rs index 405a4b1..af4b0d1 100644 --- a/src/cgw_metrics.rs +++ b/src/cgw_metrics.rs @@ -27,6 +27,13 @@ lazy_static! { "Max threshold (extra capacity) of groups this shard can handle" ) .expect("metric can be created"); + pub static ref GROUP_INFRAS_CAPACITY: IntGauge = IntGauge::new( + "cgw_group_ifras_capacity", + "Max limit (capacity) of infras the group can handle" + ) + .expect("metric can be created"); + pub static ref GROUP_INFRAS_ASSIGNED_NUM: Arc>> = + Arc::new(RwLock::new(HashMap::new())); pub static ref CONNECTIONS_NUM: IntGauge = IntGauge::new( "cgw_connections_num", "Number of successfully established WSS connections (underlying Infra connections)" @@ -88,10 +95,10 @@ impl fmt::Display for CGWMetricsHealthComponentStatus { pub enum CGWMetricsCounterType { ActiveCGWNum, GroupsAssignedNum, - #[allow(dead_code)] GroupsCapacity, - #[allow(dead_code)] GroupsThreshold, + GroupInfrasCapacity, + GroupInfrasAssignedNum, ConnectionsNum, } @@ -146,11 +153,6 @@ impl CGWMetrics { CGWMetricsHealthComponentStatus::NotReady("Application is starting".to_string()), ); - // TODO: remove: W/A for now, as currently capacity / threshold - // is non-configurable - GROUPS_CAPACITY.set(1000i64); - GROUPS_THRESHOLD.set(50i64); - tokio::spawn(async move { if let Err(err) = register_custom_metrics() { warn!("Failed to register CGW Metrics: {:?}", err); @@ -199,12 +201,17 @@ impl CGWMetrics { }, CGWMetricsCounterType::GroupsCapacity => { if let CGWMetricsCounterOpType::Set(v) = op { - ACTIVE_CGW_NUM.set(v); + GROUPS_CAPACITY.set(v); } } CGWMetricsCounterType::GroupsThreshold => { if let CGWMetricsCounterOpType::Set(v) = op { - ACTIVE_CGW_NUM.set(v); + GROUPS_THRESHOLD.set(v); + } + } + CGWMetricsCounterType::GroupInfrasCapacity => { + if let CGWMetricsCounterOpType::Set(v) = op { + GROUP_INFRAS_CAPACITY.set(v); } } CGWMetricsCounterType::ConnectionsNum => match op { @@ -216,6 +223,51 @@ impl CGWMetrics { } _ => {} }, + _ => {} + } + } + + pub async fn change_group_counter( + &self, + group_id: i32, + counter: CGWMetricsCounterType, + op: CGWMetricsCounterOpType, + ) { + if let CGWMetricsCounterType::GroupInfrasAssignedNum = counter { + let mut lock = GROUP_INFRAS_ASSIGNED_NUM.write().await; + + if let Some(counter) = lock.get(&group_id) { + match op { + CGWMetricsCounterOpType::Inc => { + counter.inc(); + } + CGWMetricsCounterOpType::Dec => { + counter.dec(); + } + _ => {} + } + } else if let Ok(counter) = IntGauge::new( + format!("cgw_group_{group_id}_infras_assigned_num"), + "Number of infras assigned to this particular group", + ) { + if REGISTRY.register(Box::new(counter.clone())).is_ok() { + counter.set(1); + lock.insert(group_id, counter); + } else { + error!("Failed to register GroupInfrasAssignedNum metric for GID {group_id}"); + } + } else { + error!("Failed to create GroupInfrasAssignedNum metric for GID {group_id}"); + } + } + } + + pub async fn delete_group_counter(&self, group_id: i32) { + let mut lock = GROUP_INFRAS_ASSIGNED_NUM.write().await; + if let Some(counter) = lock.remove(&group_id) { + if let Err(e) = REGISTRY.unregister(Box::new(counter)) { + error!("Failed to deregister GroupInfrasAssignedNum metric for GID {group_id}. Err: {e}"); + } } } } @@ -229,6 +281,8 @@ fn register_custom_metrics() -> Result<()> { REGISTRY.register(Box::new(GROUPS_THRESHOLD.clone()))?; + REGISTRY.register(Box::new(GROUP_INFRAS_CAPACITY.clone()))?; + REGISTRY.register(Box::new(CONNECTIONS_NUM.clone()))?; Ok(()) diff --git a/src/cgw_nb_api_listener.rs b/src/cgw_nb_api_listener.rs index 84147fc..311de42 100644 --- a/src/cgw_nb_api_listener.rs +++ b/src/cgw_nb_api_listener.rs @@ -1,6 +1,6 @@ +use crate::cgw_app_args::CGWKafkaArgs; use crate::cgw_device::OldNew; use crate::cgw_ucentral_parser::CGWDeviceChange; -use crate::AppArgs; use crate::cgw_connection_server::{CGWConnectionNBAPIReqMsg, CGWConnectionNBAPIReqMsgOrigin}; use crate::cgw_errors::{Error, Result}; @@ -17,7 +17,7 @@ use rdkafka::{ consumer::{stream_consumer::StreamConsumer, Consumer, ConsumerContext, Rebalance}, producer::{FutureProducer, FutureRecord}, }; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::{ @@ -111,6 +111,35 @@ pub struct ForeignInfraConnection { pub group_owner_shard_id: i32, } +#[derive(Debug, Serialize, Deserialize)] +pub struct APClientJoinMessage { + pub r#type: &'static str, + pub infra_group_id: i32, + pub client: MacAddress, + pub infra_group_infra_device: MacAddress, + pub ssid: String, + pub band: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct APClientLeaveMessage { + pub r#type: &'static str, + pub infra_group_id: i32, + pub client: MacAddress, + pub infra_group_infra_device: MacAddress, + pub band: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct APClientMigrateMessage { + pub r#type: &'static str, + pub infra_group_id: i32, + pub client: MacAddress, + pub to_infra_group_infra_device: MacAddress, + pub to_ssid: String, + pub to_band: String, +} + pub fn cgw_construct_infra_group_create_response( infra_group_id: i32, infra_name: String, @@ -119,7 +148,7 @@ pub fn cgw_construct_infra_group_create_response( error_message: Option, ) -> Result { let group_create = InfraGroupCreateResponse { - r#type: "infrastructure_group_create", + r#type: "infrastructure_group_create_response", infra_group_id, infra_name, uuid, @@ -137,7 +166,7 @@ pub fn cgw_construct_infra_group_delete_response( error_message: Option, ) -> Result { let group_delete = InfraGroupDeleteResponse { - r#type: "infrastructure_group_delete", + r#type: "infrastructure_group_delete_response", infra_group_id, uuid, success, @@ -272,6 +301,61 @@ pub fn cgw_construct_foreign_infra_connection_msg( Ok(serde_json::to_string(&foreign_infra_msg)?) } +pub fn cgw_construct_client_join_msg( + infra_group_id: i32, + client: MacAddress, + infra_group_infra_device: MacAddress, + ssid: String, + band: String, +) -> Result { + let client_join_msg = APClientJoinMessage { + r#type: "ap_client_join", + infra_group_id, + client, + infra_group_infra_device, + ssid, + band, + }; + + Ok(serde_json::to_string(&client_join_msg)?) +} + +pub fn cgw_construct_client_leave_msg( + infra_group_id: i32, + client: MacAddress, + infra_group_infra_device: MacAddress, + band: String, +) -> Result { + let client_join_msg = APClientLeaveMessage { + r#type: "ap_client_leave", + infra_group_id, + client, + infra_group_infra_device, + band, + }; + + Ok(serde_json::to_string(&client_join_msg)?) +} + +pub fn cgw_construct_client_migrate_msg( + infra_group_id: i32, + client: MacAddress, + to_infra_group_infra_device: MacAddress, + to_ssid: String, + to_band: String, +) -> Result { + let client_migrate_msg = APClientMigrateMessage { + r#type: "ap_client_migrate", + infra_group_id, + client, + to_infra_group_infra_device, + to_ssid, + to_band, + }; + + Ok(serde_json::to_string(&client_migrate_msg)?) +} + struct CustomContext; impl ClientContext for CustomContext {} @@ -347,24 +431,21 @@ struct CGWCNCConsumer { } impl CGWCNCConsumer { - pub fn new(app_args: &AppArgs) -> Result { - let consum: CGWCNCConsumerType = Self::create_consumer(app_args)?; + pub fn new(cgw_id: i32, kafka_args: &CGWKafkaArgs) -> Result { + let consum: CGWCNCConsumerType = Self::create_consumer(cgw_id, kafka_args)?; Ok(CGWCNCConsumer { c: consum }) } - fn create_consumer(app_args: &AppArgs) -> Result { + fn create_consumer(cgw_id: i32, kafka_args: &CGWKafkaArgs) -> Result { let context = CustomContext; let consumer: CGWCNCConsumerType = match ClientConfig::new() .set("group.id", GROUP_ID) - .set( - "client.id", - GROUP_ID.to_string() + &app_args.cgw_id.to_string(), - ) - .set("group.instance.id", app_args.cgw_id.to_string()) + .set("client.id", GROUP_ID.to_string() + &cgw_id.to_string()) + .set("group.instance.id", cgw_id.to_string()) .set( "bootstrap.servers", - app_args.kafka_host.clone() + ":" + &app_args.kafka_port.to_string(), + kafka_args.kafka_host.clone() + ":" + &kafka_args.kafka_port.to_string(), ) .set("enable.partition.eof", "false") .set("session.timeout.ms", "6000") @@ -383,7 +464,7 @@ impl CGWCNCConsumer { debug!( "(consumer) (producer) Created lazy connection to kafka broker ({}:{})...", - app_args.kafka_host, app_args.kafka_port, + kafka_args.kafka_host, kafka_args.kafka_port, ); if let Err(e) = consumer.subscribe(&CONSUMER_TOPICS) { @@ -399,16 +480,16 @@ impl CGWCNCConsumer { } impl CGWCNCProducer { - pub fn new(app_args: &AppArgs) -> Result { - let prod: CGWCNCProducerType = Self::create_producer(app_args)?; + pub fn new(kafka_args: &CGWKafkaArgs) -> Result { + let prod: CGWCNCProducerType = Self::create_producer(kafka_args)?; Ok(CGWCNCProducer { p: prod }) } - fn create_producer(app_args: &AppArgs) -> Result { + fn create_producer(kafka_args: &CGWKafkaArgs) -> Result { let producer: FutureProducer = match ClientConfig::new() .set( "bootstrap.servers", - app_args.kafka_host.clone() + ":" + &app_args.kafka_port.to_string(), + kafka_args.kafka_host.clone() + ":" + &kafka_args.kafka_port.to_string(), ) .set("message.timeout.ms", "5000") .create() @@ -422,7 +503,7 @@ impl CGWCNCProducer { debug!( "(producer) Created lazy connection to kafka broker ({}:{})...", - app_args.kafka_host, app_args.kafka_port, + kafka_args.kafka_host, kafka_args.kafka_port, ); Ok(producer) @@ -438,7 +519,11 @@ pub struct CGWNBApiClient { } impl CGWNBApiClient { - pub fn new(app_args: &AppArgs, cgw_tx: &CGWConnectionServerMboxTx) -> Result> { + pub fn new( + cgw_id: i32, + kafka_args: &CGWKafkaArgs, + cgw_tx: &CGWConnectionServerMboxTx, + ) -> Result> { let working_runtime_h = Builder::new_multi_thread() .worker_threads(1) .thread_name("cgw-nb-api-l") @@ -449,11 +534,11 @@ impl CGWNBApiClient { let cl = Arc::new(CGWNBApiClient { working_runtime_handle: working_runtime_h, cgw_server_tx_mbox: cgw_tx.clone(), - prod: CGWCNCProducer::new(app_args)?, + prod: CGWCNCProducer::new(kafka_args)?, }); let cl_clone = cl.clone(); - let consumer: CGWCNCConsumer = CGWCNCConsumer::new(app_args)?; + let consumer: CGWCNCConsumer = CGWCNCConsumer::new(cgw_id, kafka_args)?; cl.working_runtime_handle.spawn(async move { loop { let cl_clone = cl_clone.clone(); diff --git a/src/cgw_remote_discovery.rs b/src/cgw_remote_discovery.rs index e9d06a3..5a911ee 100644 --- a/src/cgw_remote_discovery.rs +++ b/src/cgw_remote_discovery.rs @@ -1,6 +1,7 @@ use crate::{ + cgw_app_args::CGWRedisArgs, cgw_db_accessor::{CGWDBAccessor, CGWDBInfra, CGWDBInfrastructureGroup}, - cgw_device::{CGWDevice, CGWDeviceState}, + cgw_device::{CGWDevice, CGWDeviceState, CGWDeviceType}, cgw_devices_cache::CGWDevicesCache, cgw_errors::{Error, Result}, cgw_metrics::{ @@ -8,6 +9,7 @@ use crate::{ CGWMetricsHealthComponentStatus, }, cgw_remote_client::CGWRemoteClient, + cgw_tls::cgw_read_root_certs_dir, AppArgs, }; @@ -15,9 +17,13 @@ use std::{ collections::HashMap, net::{Ipv4Addr, SocketAddr}, sync::Arc, + time::Duration, }; -use redis_async::resp_array; +use redis::{ + aio::MultiplexedConnection, Client, ConnectionInfo, RedisConnectionInfo, RedisResult, + TlsCertificates, ToRedisArgs, +}; use eui48::MacAddress; @@ -32,6 +38,8 @@ static REDIS_KEY_SHARD_VALUE_ASSIGNED_G_NUM: &str = "assigned_groups_num"; static REDIS_KEY_GID: &str = "group_id_"; static REDIS_KEY_GID_VALUE_GID: &str = "gid"; static REDIS_KEY_GID_VALUE_SHARD_ID: &str = "shard_id"; +static REDIS_KEY_GID_VALUE_INFRAS_CAPACITY: &str = "infras_capacity"; +static REDIS_KEY_GID_VALUE_INFRAS_ASSIGNED: &str = "infras_assigned"; #[derive(Clone, Debug, Default, PartialEq)] pub struct CGWREDISDBShard { @@ -137,24 +145,62 @@ pub struct CGWRemoteIface { #[derive(Clone)] pub struct CGWRemoteDiscovery { db_accessor: Arc, - redis_client: redis_async::client::paired::PairedConnection, + redis_client: MultiplexedConnection, gid_to_cgw_cache: Arc>>, remote_cgws_map: Arc>>, local_shard_id: i32, } +async fn cgw_create_redis_client(redis_args: &CGWRedisArgs) -> Result { + let redis_client_info = ConnectionInfo { + addr: match redis_args.redis_tls { + true => redis::ConnectionAddr::TcpTls { + host: redis_args.redis_host.clone(), + port: redis_args.redis_port, + insecure: true, + tls_params: None, + }, + false => { + redis::ConnectionAddr::Tcp(redis_args.redis_host.clone(), redis_args.redis_port) + } + }, + + redis: RedisConnectionInfo { + username: redis_args.redis_username.clone(), + password: redis_args.redis_password.clone(), + ..Default::default() + }, + }; + + match redis_args.redis_tls { + true => { + let root_cert = cgw_read_root_certs_dir().await.ok(); + + let tls_certs: TlsCertificates = TlsCertificates { + client_tls: None, + root_cert, + }; + + match redis::Client::build_with_tls(redis_client_info, tls_certs) { + Ok(client) => Ok(client), + Err(e) => Err(Error::Redis(format!("Failed to start Redis Client: {}", e))), + } + } + false => match redis::Client::open(redis_client_info) { + Ok(client) => Ok(client), + Err(e) => Err(Error::Redis(format!("Failed to start Redis Client: {}", e))), + }, + } +} + impl CGWRemoteDiscovery { pub async fn new(app_args: &AppArgs) -> Result { debug!( "Trying to create redis db connection ({}:{})", - app_args.redis_host, app_args.redis_port + app_args.redis_args.redis_host, app_args.redis_args.redis_port ); - let redis_client = match redis_async::client::paired::paired_connect( - app_args.redis_host.clone(), - app_args.redis_port, - ) - .await - { + + let redis_client = match cgw_create_redis_client(&app_args.redis_args).await { Ok(c) => c, Err(e) => { error!( @@ -165,7 +211,24 @@ impl CGWRemoteDiscovery { } }; - let db_accessor = match CGWDBAccessor::new(app_args).await { + let redis_client = match redis_client + .get_multiplexed_tokio_connection_with_response_timeouts( + Duration::from_secs(1), + Duration::from_secs(5), + ) + .await + { + Ok(conn) => conn, + Err(e) => { + error!( + "Can't create CGW Remote Discovery client: Get Redis async connection failed ({})", + e + ); + return Err(Error::RemoteDiscovery("Redis client create failed")); + } + }; + + let db_accessor = match CGWDBAccessor::new(&app_args.db_args).await { Ok(c) => c, Err(e) => { error!( @@ -206,41 +269,44 @@ impl CGWRemoteDiscovery { { let redisdb_shard_info = CGWREDISDBShard { id: app_args.cgw_id, - server_host: app_args.grpc_public_host.clone(), - server_port: app_args.grpc_public_port, + server_host: app_args.grpc_args.grpc_public_host.clone(), + server_port: app_args.grpc_args.grpc_public_port, assigned_groups_num: 0i32, - capacity: 1000i32, - threshold: 50i32, + capacity: app_args.cgw_groups_capacity, + threshold: app_args.cgw_groups_threshold, }; + CGWMetrics::get_ref().change_counter( + CGWMetricsCounterType::GroupsCapacity, + CGWMetricsCounterOpType::Set(app_args.cgw_groups_capacity.into()), + ); + + CGWMetrics::get_ref().change_counter( + CGWMetricsCounterType::GroupsThreshold, + CGWMetricsCounterOpType::Set(app_args.cgw_groups_capacity.into()), + ); + let redis_req_data: Vec = redisdb_shard_info.into(); + let mut con = rc.redis_client.clone(); - if let Err(e) = rc - .redis_client - .send::(resp_array![ - "DEL", - format!("{REDIS_KEY_SHARD_ID_PREFIX}{}", app_args.cgw_id) - ]) - .await - { + let res: RedisResult<()> = redis::cmd("DEL") + .arg(format!("{REDIS_KEY_SHARD_ID_PREFIX}{}", app_args.cgw_id)) + .query_async(&mut con) + .await; + if res.is_err() { warn!( - "Failed to destroy record about shard in REDIS, first launch? ({:?})", - e + "Failed to destroy record about shard in REDIS, first launch? ({})", + res.err().unwrap() ); } - if let Err(e) = rc - .redis_client - .send::( - resp_array![ - "HSET", - format!("{REDIS_KEY_SHARD_ID_PREFIX}{}", app_args.cgw_id) - ] - .append(redis_req_data), - ) - .await - { - error!("Can't create CGW Remote Discovery client: Failed to create record about shard in REDIS: {:?}", e); + let res: RedisResult<()> = redis::cmd("HSET") + .arg(format!("{REDIS_KEY_SHARD_ID_PREFIX}{}", app_args.cgw_id)) + .arg(redis_req_data.to_redis_args()) + .query_async(&mut con) + .await; + if res.is_err() { + error!("Can't create CGW Remote Discovery client: Failed to create record about shard in REDIS: {}", res.err().unwrap()); return Err(Error::RemoteDiscovery( "Failed to create record about shard in REDIS", )); @@ -300,50 +366,41 @@ impl CGWRemoteDiscovery { // Clear hashmap lock.clear(); + let mut con = self.redis_client.clone(); - let redis_keys: Vec = match self - .redis_client - .send::>(resp_array!["KEYS", format!("{}*", REDIS_KEY_GID)]) + let redis_keys: Vec = match redis::cmd("KEYS") + .arg(format!("{REDIS_KEY_GID}*")) + .query_async(&mut con) .await { - Err(_) => { + Err(e) => { + error!("Failed to sync gid to cgw map:\n{}", e); return Err(Error::RemoteDiscovery("Failed to get KEYS list from REDIS")); } - Ok(r) => r, + Ok(keys) => keys, }; for key in redis_keys { - let gid: i32 = match self - .redis_client - .send::(resp_array!["HGET", &key, REDIS_KEY_GID_VALUE_GID]) + let gid: i32 = match redis::cmd("HGET") + .arg(&key) + .arg(REDIS_KEY_GID_VALUE_GID) + .query_async(&mut con) .await { - Ok(res) => { - match res.parse::() { - Ok(res) => res, - Err(e) => { - warn!("Found proper key '{key}' entry, but failed to parse GID from it:\n{e}"); - continue; - } - } - } + Ok(gid) => gid, Err(e) => { warn!("Found proper key '{key}' entry, but failed to fetch GID from it:\n{e}"); continue; } }; - let shard_id: i32 = match self - .redis_client - .send::(resp_array!["HGET", &key, REDIS_KEY_GID_VALUE_SHARD_ID]) + + let shard_id: i32 = match redis::cmd("HGET") + .arg(&key) + .arg(REDIS_KEY_GID_VALUE_SHARD_ID) + .query_async(&mut con) .await { - Ok(res) => match res.parse::() { - Ok(res) => res, - Err(e) => { - warn!("Found proper key '{key}' entry, but failed to parse SHARD_ID from it:\n{e}"); - continue; - } - }, + Ok(shard_id) => shard_id, Err(e) => { warn!("Found proper key '{key}' entry, but failed to fetch SHARD_ID from it:\n{e}"); continue; @@ -382,12 +439,20 @@ impl CGWRemoteDiscovery { devices_cache.add_device( &item.mac, &CGWDevice::new( + CGWDeviceType::default(), CGWDeviceState::CGWDeviceDisconnected, item.infra_group_id, true, Default::default(), ), ); + CGWMetrics::get_ref() + .change_group_counter( + item.infra_group_id, + CGWMetricsCounterType::GroupInfrasAssignedNum, + CGWMetricsCounterOpType::Inc, + ) + .await; } } } @@ -398,20 +463,27 @@ impl CGWRemoteDiscovery { // Clear hashmap lock.clear(); - let redis_keys: Vec = self - .redis_client - .send::>(resp_array![ - "KEYS", - format!("{}*", REDIS_KEY_SHARD_ID_PREFIX) - ]) - .await?; + let mut con = self.redis_client.clone(); + let redis_keys: Vec = match redis::cmd("KEYS") + .arg(format!("{REDIS_KEY_SHARD_ID_PREFIX}*")) + .query_async(&mut con) + .await + { + Ok(keys) => keys, + Err(e) => { + error!( + "Can't sync remote CGW map: Failed to get shard record in REDIS: {}", + e + ); + return Err(Error::RemoteDiscovery("Failed to get KEYS list from REDIS")); + } + }; for key in redis_keys { - match self - .redis_client - .send::>(resp_array!["HGETALL", &key]) - .await - { + let res: RedisResult> = + redis::cmd("HGETALL").arg(&key).query_async(&mut con).await; + + match res { Ok(res) => { let shrd: CGWREDISDBShard = CGWREDISDBShard::from(res); if shrd == CGWREDISDBShard::default() { @@ -463,14 +535,22 @@ impl CGWRemoteDiscovery { async fn increment_cgw_assigned_groups_num(&self, cgw_id: i32) -> Result<()> { debug!("Incrementing assigned groups num cgw_id_{cgw_id}"); - self.redis_client - .send::(resp_array![ - "HINCRBY", - format!("{}{cgw_id}", REDIS_KEY_SHARD_ID_PREFIX), - REDIS_KEY_SHARD_VALUE_ASSIGNED_G_NUM, - "1" - ]) - .await?; + let mut con = self.redis_client.clone(); + let res: RedisResult<()> = redis::cmd("HINCRBY") + .arg(format!("{}{cgw_id}", REDIS_KEY_SHARD_ID_PREFIX)) + .arg(REDIS_KEY_SHARD_VALUE_ASSIGNED_G_NUM) + .arg("1") + .query_async(&mut con) + .await; + if res.is_err() { + error!( + "Failed to increment assigned groups number:\n{}", + res.err().unwrap() + ); + return Err(Error::RemoteDiscovery( + "Failed to increment assigned groups number", + )); + } if cgw_id == self.local_shard_id { CGWMetrics::get_ref().change_counter( @@ -484,14 +564,22 @@ impl CGWRemoteDiscovery { async fn decrement_cgw_assigned_groups_num(&self, cgw_id: i32) -> Result<()> { debug!("Decrementing assigned groups num cgw_id_{cgw_id}"); - self.redis_client - .send::(resp_array![ - "HINCRBY", - format!("{}{cgw_id}", REDIS_KEY_SHARD_ID_PREFIX), - REDIS_KEY_SHARD_VALUE_ASSIGNED_G_NUM, - "-1" - ]) - .await?; + let mut con = self.redis_client.clone(); + let res: RedisResult<()> = redis::cmd("HINCRBY") + .arg(format!("{}{cgw_id}", REDIS_KEY_SHARD_ID_PREFIX)) + .arg(REDIS_KEY_SHARD_VALUE_ASSIGNED_G_NUM) + .arg("-1") + .query_async(&mut con) + .await; + if res.is_err() { + error!( + "Failed to decrement assigned groups number:\n{}", + res.err().unwrap() + ); + return Err(Error::RemoteDiscovery( + "Failed to decrement assigned groups number", + )); + } if cgw_id == self.local_shard_id { CGWMetrics::get_ref().change_counter( @@ -503,6 +591,68 @@ impl CGWRemoteDiscovery { Ok(()) } + async fn increment_group_assigned_infras_num(&self, gid: i32) -> Result<()> { + debug!("Incrementing assigned infras num group_id_{gid}"); + + let mut con = self.redis_client.clone(); + let res: RedisResult<()> = redis::cmd("HINCRBY") + .arg(format!("{}{gid}", REDIS_KEY_GID)) + .arg(REDIS_KEY_GID_VALUE_INFRAS_ASSIGNED) + .arg("1") + .query_async(&mut con) + .await; + if res.is_err() { + error!( + "Failed to increment assigned infras number:\n{}", + res.err().unwrap() + ); + return Err(Error::RemoteDiscovery( + "Failed to increment assigned infras number", + )); + } + + CGWMetrics::get_ref() + .change_group_counter( + gid, + CGWMetricsCounterType::GroupInfrasAssignedNum, + CGWMetricsCounterOpType::Inc, + ) + .await; + + Ok(()) + } + + async fn decrement_group_assigned_infras_num(&self, gid: i32) -> Result<()> { + debug!("Decrementing assigned infras num group_id_{gid}"); + + let mut con = self.redis_client.clone(); + let res: RedisResult<()> = redis::cmd("HINCRBY") + .arg(format!("{}{gid}", REDIS_KEY_GID)) + .arg(REDIS_KEY_GID_VALUE_INFRAS_ASSIGNED) + .arg("-1") + .query_async(&mut con) + .await; + if res.is_err() { + error!( + "Failed to decrement assigned infras number:\n{}", + res.err().unwrap() + ); + return Err(Error::RemoteDiscovery( + "Failed to decrement assigned infras number", + )); + } + + CGWMetrics::get_ref() + .change_group_counter( + gid, + CGWMetricsCounterType::GroupInfrasAssignedNum, + CGWMetricsCounterOpType::Dec, + ) + .await; + + Ok(()) + } + async fn get_infra_group_cgw_assignee(&self) -> Result { let lock = self.remote_cgws_map.read().await; let mut hash_vec: Vec<(&i32, &CGWRemoteIface)> = lock.iter().collect(); @@ -521,43 +671,83 @@ impl CGWRemoteDiscovery { } } - warn!( - "Every available CGW is exceeding capacity+threshold limit, using least loaded one..." - ); - if let Some(least_loaded_cgw) = lock - .iter() - .min_by(|a, b| { - a.1.shard - .assigned_groups_num - .cmp(&b.1.shard.assigned_groups_num) - }) - .map(|(_k, _v)| _v) - { - warn!("Found least loaded CGW id: {}", least_loaded_cgw.shard.id); - return Ok(least_loaded_cgw.shard.id); - } - Err(Error::RemoteDiscovery( "Unexpected: Failed to find the least loaded CGW shard", )) } - async fn assign_infra_group_to_cgw(&self, gid: i32) -> Result { + async fn validate_infra_group_cgw_assignee(&self, shard_id: i32) -> Result { + let lock = self.remote_cgws_map.read().await; + + match lock.get(&shard_id) { + Some(instance) => { + let max_capacity: i32 = instance.shard.capacity + instance.shard.threshold; + if instance.shard.assigned_groups_num < max_capacity { + debug!("Found CGW shard to assign group to (id {})", shard_id); + Ok(shard_id) + } else { + Err(Error::RemoteDiscovery( + "Unexpected: Failed to find the least loaded CGW shard", + )) + } + } + None => Err(Error::RemoteDiscovery( + "Unexpected: Failed to find CGW shard", + )), + } + } + + async fn assign_infra_group_to_cgw( + &self, + gid: i32, + shard_id: Option, + infras_capacity: i32, + infras_assigned: i32, + ) -> Result { // Delete key (if exists), recreate with new owner let _ = self.deassign_infra_group_to_cgw(gid).await; - let dst_cgw_id: i32 = self.get_infra_group_cgw_assignee().await?; + // Sync CGWs to get lates data + if let Err(e) = self.sync_remote_cgw_map().await { + error!("Can't create CGW Remote Discovery client: Can't pull records data from REDIS (wrong redis host/port?) ({:?})", e); + return Err(Error::RemoteDiscovery( + "Failed to sync remote CGW info from REDIS", + )); + } + + let dst_cgw_id: i32 = match shard_id { + Some(dest_shard_id) => { + self.validate_infra_group_cgw_assignee(dest_shard_id) + .await? + } + None => self.get_infra_group_cgw_assignee().await?, + }; + + let mut con = self.redis_client.clone(); + let res: RedisResult<()> = redis::cmd("HSET") + .arg(format!("{REDIS_KEY_GID}{gid}")) + .arg(REDIS_KEY_GID_VALUE_GID) + .arg(gid.to_string()) + .arg(REDIS_KEY_GID_VALUE_SHARD_ID) + .arg(dst_cgw_id.to_string()) + .arg(REDIS_KEY_GID_VALUE_INFRAS_CAPACITY) + .arg(infras_capacity.to_string()) + .arg(REDIS_KEY_GID_VALUE_INFRAS_ASSIGNED) + .arg(infras_assigned.to_string()) + .query_async(&mut con) + .await; - self.redis_client - .send::(resp_array![ - "HSET", - format!("{REDIS_KEY_GID}{gid}"), - REDIS_KEY_GID_VALUE_GID, - gid.to_string(), - REDIS_KEY_GID_VALUE_SHARD_ID, - dst_cgw_id.to_string() - ]) - .await?; + if res.is_err() { + error!( + "Failed to assign infra group {} to cgw {}:\n{}", + gid, + dst_cgw_id, + res.err().unwrap() + ); + return Err(Error::RemoteDiscovery( + "Failed to assign infra group to cgw", + )); + } self.gid_to_cgw_cache.write().await.insert(gid, dst_cgw_id); @@ -567,26 +757,47 @@ impl CGWRemoteDiscovery { } pub async fn deassign_infra_group_to_cgw(&self, gid: i32) -> Result<()> { - self.redis_client - .send::(resp_array!["DEL", format!("{REDIS_KEY_GID}{gid}")]) - .await?; + let mut con = self.redis_client.clone(); + let res: RedisResult<()> = redis::cmd("DEL") + .arg(format!("{REDIS_KEY_GID}{gid}")) + .query_async(&mut con) + .await; - debug!("REDIS: deassigned gid{gid} from controlled CGW"); + if res.is_err() { + error!( + "Failed to deassign infra group {}:\n{}", + gid, + res.err().unwrap() + ); + return Err(Error::RemoteDiscovery( + "Failed to deassign infra group to cgw", + )); + } + + debug!("REDIS: deassigned gid {gid} from controlled CGW"); self.gid_to_cgw_cache.write().await.remove(&gid); Ok(()) } - pub async fn create_infra_group(&self, g: &CGWDBInfrastructureGroup) -> Result { + pub async fn create_infra_group( + &self, + g: &CGWDBInfrastructureGroup, + dest_shard_id: Option, + ) -> Result { //TODO: transaction-based insert/assigned_group_num update (DB) self.db_accessor.insert_new_infra_group(g).await?; - let shard_id: i32 = match self.assign_infra_group_to_cgw(g.id).await { + let shard_id: i32 = match self + .assign_infra_group_to_cgw(g.id, dest_shard_id, g.reserved_size, g.actual_size) + .await + { Ok(v) => v, - Err(_e) => { + Err(e) => { + error!("Assign group to CGW shard failed! Err: {}", e.to_string()); let _ = self.db_accessor.delete_infra_group(g.id).await; - return Err(Error::RemoteDiscovery("Assign group to CGW shard failed")); + return Err(e); } }; @@ -627,6 +838,8 @@ impl CGWRemoteDiscovery { device_cache.del_device(key); } + CGWMetrics::get_ref().delete_group_counter(gid).await; + Ok(()) } @@ -639,6 +852,28 @@ impl CGWRemoteDiscovery { // TODO: assign list to shards; currently - only created bulk, no assignment let mut futures = Vec::with_capacity(infras.len()); // Results store vec of MACs we failed to add + + let infras_capacity = match self.get_group_infras_capacity(gid).await { + Ok(capacity) => capacity, + Err(e) => { + error!("Failed to create infreas list: {}", e.to_string()); + return Err(Error::RemoteDiscoveryFailedInfras(infras)); + } + }; + + let infras_assigned = match self.get_group_infras_assigned_num(gid).await { + Ok(assigned) => assigned, + Err(e) => { + error!("Failed to create infreas list: {}", e.to_string()); + return Err(Error::RemoteDiscoveryFailedInfras(infras)); + } + }; + + if infras.len() as i32 + infras_assigned > infras_capacity { + error!("Failed to create infras list - GID {gid} has no enough capacity"); + return Err(Error::RemoteDiscoveryFailedInfras(infras)); + } + let mut failed_infras: Vec = Vec::with_capacity(futures.len()); for x in infras.iter() { let db_accessor_clone = self.db_accessor.clone(); @@ -665,13 +900,14 @@ impl CGWRemoteDiscovery { let mut devices_cache = cache.write().await; let device_mac = infras[i]; - if let Some(device) = devices_cache.get_device(&device_mac) { + if let Some(device) = devices_cache.get_device_mut(&device_mac) { device.set_device_group_id(gid); device.set_device_remains_in_db(true); } else { devices_cache.add_device( &device_mac, &CGWDevice::new( + CGWDeviceType::default(), CGWDeviceState::CGWDeviceDisconnected, gid, true, @@ -679,6 +915,9 @@ impl CGWRemoteDiscovery { ), ); } + + // Update assigned infras num + let _ = self.increment_group_assigned_infras_num(gid).await; } } Err(_) => { @@ -696,7 +935,7 @@ impl CGWRemoteDiscovery { pub async fn destroy_ifras_list( &self, - _gid: i32, + gid: i32, infras: Vec, cache: Arc>, ) -> Result<()> { @@ -724,7 +963,7 @@ impl CGWRemoteDiscovery { } else { let mut devices_cache = cache.write().await; let device_mac = infras[i]; - if let Some(device) = devices_cache.get_device(&device_mac) { + if let Some(device) = devices_cache.get_device_mut(&device_mac) { if device.get_device_state() == CGWDeviceState::CGWDeviceConnected { device.set_device_remains_in_db(false); device.set_device_group_id(0); @@ -732,6 +971,8 @@ impl CGWRemoteDiscovery { devices_cache.del_device(&device_mac); } } + // Update assigned infras num + let _ = self.decrement_group_assigned_infras_num(gid).await; } } Err(_) => { @@ -798,25 +1039,40 @@ impl CGWRemoteDiscovery { // Clear local cache self.gid_to_cgw_cache.write().await.clear(); + let mut con = self.redis_client.clone(); for (cgw_id, _val) in self.remote_cgws_map.read().await.iter() { - if let Err(e) = self - .redis_client - .send::(resp_array![ - "HSET", - format!("{}{cgw_id}", REDIS_KEY_SHARD_ID_PREFIX), - REDIS_KEY_SHARD_VALUE_ASSIGNED_G_NUM, - "0" - ]) - .await - { - warn!("Failed to reset CGW{cgw_id} assigned group num count, e:{e}"); + let res: RedisResult<()> = redis::cmd("HSET") + .arg(format!("{}{cgw_id}", REDIS_KEY_SHARD_ID_PREFIX)) + .arg(REDIS_KEY_SHARD_VALUE_ASSIGNED_G_NUM) + .arg("0") + .query_async(&mut con) + .await; + if res.is_err() { + warn!( + "Failed to reset CGW{cgw_id} assigned group num count, e:{}", + res.err().unwrap() + ); } } for i in groups.iter() { let _ = self.sync_remote_cgw_map().await; let _ = self.sync_gid_to_cgw_map().await; - match self.assign_infra_group_to_cgw(i.id).await { + + let infras_assigned: i32 = match self.get_group_infras_assigned_num(i.id).await { + Ok(infras_num) => infras_num, + Err(e) => { + warn!("Cannot execute rebalancing: {}", e.to_string()); + return Err(Error::RemoteDiscovery( + "Cannot do rebalancing due to absence of any groups created in DB", + )); + } + }; + + match self + .assign_infra_group_to_cgw(i.id, None, i.reserved_size, infras_assigned) + .await + { Ok(shard_id) => { debug!("Rebalancing: assigned {} to shard {}", i.id, shard_id); let _ = self.increment_cgw_assigned_groups_num(shard_id).await; @@ -834,12 +1090,53 @@ impl CGWRemoteDiscovery { pub async fn cleanup_redis(&self) { debug!("Remove from Redis shard id {}", self.local_shard_id); // We are on de-init stage - ignore any errors on Redis clean-up - let _ = self - .redis_client - .send::(resp_array![ - "DEL", - format!("{REDIS_KEY_SHARD_ID_PREFIX}{}", self.local_shard_id) - ]) + let mut con = self.redis_client.clone(); + let _res: RedisResult<()> = redis::cmd("DEL") + .arg(format!( + "{REDIS_KEY_SHARD_ID_PREFIX}{}", + self.local_shard_id + )) + .query_async(&mut con) .await; } + + pub async fn get_group_infras_capacity(&self, gid: i32) -> Result { + let mut con = self.redis_client.clone(); + + let capacity: i32 = match redis::cmd("HGET") + .arg(format!("{}{gid}", REDIS_KEY_GID)) + .arg(REDIS_KEY_GID_VALUE_INFRAS_CAPACITY) + .query_async(&mut con) + .await + { + Ok(cap) => cap, + Err(e) => { + warn!("Failed to get infras capacity for GID {gid}:\n{e}"); + return Err(Error::RemoteDiscovery("Failed to get infras capacity")); + } + }; + + Ok(capacity) + } + + pub async fn get_group_infras_assigned_num(&self, gid: i32) -> Result { + let mut con = self.redis_client.clone(); + + let infras_assigned: i32 = match redis::cmd("HGET") + .arg(format!("{}{gid}", REDIS_KEY_GID)) + .arg(REDIS_KEY_GID_VALUE_INFRAS_ASSIGNED) + .query_async(&mut con) + .await + { + Ok(cap) => cap, + Err(e) => { + warn!("Failed to get infras assigned number for GID {gid}:\n{e}"); + return Err(Error::RemoteDiscovery( + "Failed to get group infras assigned number", + )); + } + }; + + Ok(infras_assigned) + } } diff --git a/src/cgw_remote_server.rs b/src/cgw_remote_server.rs index b559484..b064389 100644 --- a/src/cgw_remote_server.rs +++ b/src/cgw_remote_server.rs @@ -1,4 +1,4 @@ -use crate::AppArgs; +use crate::cgw_app_args::CGWGRPCArgs; pub mod cgw_remote { tonic::include_proto!("cgw.remote"); @@ -52,11 +52,11 @@ pub struct CGWRemoteServer { } impl CGWRemoteServer { - pub fn new(app_args: &AppArgs) -> Self { + pub fn new(cgw_id: i32, grpc_args: &CGWGRPCArgs) -> Self { let remote_cfg = CGWRemoteConfig::new( - app_args.cgw_id, - app_args.grpc_listening_ip, - app_args.grpc_listening_port, + cgw_id, + grpc_args.grpc_listening_ip, + grpc_args.grpc_listening_port, ); CGWRemoteServer { cfg: remote_cfg } } diff --git a/src/cgw_runtime.rs b/src/cgw_runtime.rs new file mode 100644 index 0000000..8349fbc --- /dev/null +++ b/src/cgw_runtime.rs @@ -0,0 +1,115 @@ +use crate::cgw_errors::{Error, Result}; + +use lazy_static::lazy_static; + +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, +}; + +use tokio::runtime::{Builder, Runtime}; + +#[derive(Hash, Eq, PartialEq, Debug)] +pub enum CGWRuntimeType { + WssRxTx, + MboxInternal, + MboxNbApiRx, + MboxNbApiTx, + MboxRelay, + QueueTimeout, +} + +lazy_static! { + static ref RUNTIMES: Mutex>> = Mutex::new(HashMap::new()); +} + +pub fn cgw_initialize_runtimes(wss_t_num: usize) -> Result<()> { + let wss_runtime_handle = Arc::new( + Builder::new_multi_thread() + .worker_threads(wss_t_num) + .thread_name_fn(|| { + static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0); + let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst); + format!("cgw-wss-t-{}", id) + }) + .thread_stack_size(3 * 1024 * 1024) + .enable_all() + .build()?, + ); + let internal_mbox_runtime_handle = Arc::new( + Builder::new_multi_thread() + .worker_threads(1) + .thread_name("cgw-mbox") + .thread_stack_size(1024 * 1024) + .enable_all() + .build()?, + ); + let nb_api_mbox_rx_runtime_handle = Arc::new( + Builder::new_multi_thread() + .worker_threads(1) + .thread_name("cgw-mbox-nbapi") + .thread_stack_size(1024 * 1024) + .enable_all() + .build()?, + ); + let nb_api_mbox_tx_runtime_handle = Arc::new( + Builder::new_multi_thread() + .worker_threads(1) + .thread_name("cgw-mbox-nbapi-tx") + .thread_stack_size(1024 * 1024) + .enable_all() + .build()?, + ); + let relay_msg_mbox_runtime_handle = Arc::new( + Builder::new_multi_thread() + .worker_threads(1) + .thread_name("cgw-relay-mbox-nbapi") + .thread_stack_size(1024 * 1024) + .enable_all() + .build()?, + ); + let queue_timeout_handle = Arc::new( + Builder::new_multi_thread() + .worker_threads(1) + .thread_name("cgw-queue-timeout") + .thread_stack_size(1024 * 1024) + .enable_all() + .build()?, + ); + + let mut runtimes = match RUNTIMES.lock() { + Ok(runtimes_lock) => runtimes_lock, + Err(e) => { + return Err(Error::Runtime(format!( + "Failed to get runtimes lock: {}", + e + ))); + } + }; + + runtimes.insert(CGWRuntimeType::WssRxTx, wss_runtime_handle); + runtimes.insert(CGWRuntimeType::MboxInternal, internal_mbox_runtime_handle); + runtimes.insert(CGWRuntimeType::MboxNbApiRx, nb_api_mbox_rx_runtime_handle); + runtimes.insert(CGWRuntimeType::MboxNbApiTx, nb_api_mbox_tx_runtime_handle); + runtimes.insert(CGWRuntimeType::MboxRelay, relay_msg_mbox_runtime_handle); + runtimes.insert(CGWRuntimeType::QueueTimeout, queue_timeout_handle); + + Ok(()) +} + +pub fn cgw_get_runtime(runtime_type: CGWRuntimeType) -> Result>> { + let runtimes = match RUNTIMES.lock() { + Ok(runtimes_lock) => runtimes_lock, + Err(e) => { + return Err(Error::Runtime(format!( + "Failed to get runtimes lock: {}", + e + ))); + } + }; + + Ok(runtimes.get(&runtime_type).cloned()) +} diff --git a/src/cgw_tls.rs b/src/cgw_tls.rs index 76715c7..7e011c3 100644 --- a/src/cgw_tls.rs +++ b/src/cgw_tls.rs @@ -1,10 +1,15 @@ +use crate::cgw_app_args::CGWWSSArgs; use crate::cgw_errors::{collect_results, Error, Result}; -use crate::AppArgs; use eui48::MacAddress; use rustls_pki_types::{CertificateDer, PrivateKeyDer}; +use std::fs; +use std::io::BufRead; +use std::path::Path; use std::{fs::File, io::BufReader, str::FromStr, sync::Arc}; use tokio::net::TcpStream; +use tokio_postgres_rustls::MakeRustlsConnect; +use tokio_rustls::rustls; use tokio_rustls::{ rustls::{server::WebPkiClientVerifier, RootCertStore, ServerConfig}, server::TlsStream, @@ -13,6 +18,7 @@ use tokio_rustls::{ use x509_parser::parse_x509_certificate; const CGW_TLS_CERTIFICATES_PATH: &str = "/etc/cgw/certs"; +const CGW_TLS_NB_INFRA_CERTS_PATH: &str = "/etc/cgw/nb_infra/certs"; pub async fn cgw_tls_read_certs(cert_file: &str) -> Result>> { let file = match File::open(cert_file) { @@ -103,9 +109,10 @@ pub async fn cgw_tls_get_cn_from_stream(stream: &TlsStream) -> Result Err(Error::Tls("Failed to read peer comman name!".to_string())) } -pub async fn cgw_tls_create_acceptor(args: &AppArgs) -> Result { + +pub async fn cgw_tls_create_acceptor(wss_args: &CGWWSSArgs) -> Result { // Read root/issuer certs. - let cas_path = format!("{}/{}", CGW_TLS_CERTIFICATES_PATH, args.wss_cas); + let cas_path = format!("{}/{}", CGW_TLS_CERTIFICATES_PATH, wss_args.wss_cas); let cas = match cgw_tls_read_certs(cas_path.as_str()).await { Ok(cas_pem) => cas_pem, Err(e) => { @@ -115,7 +122,7 @@ pub async fn cgw_tls_create_acceptor(args: &AppArgs) -> Result { }; // Read cert. - let cert_path = format!("{}/{}", CGW_TLS_CERTIFICATES_PATH, args.wss_cert); + let cert_path = format!("{}/{}", CGW_TLS_CERTIFICATES_PATH, wss_args.wss_cert); let mut cert = match cgw_tls_read_certs(cert_path.as_str()).await { Ok(cert_pem) => cert_pem, Err(e) => { @@ -126,7 +133,7 @@ pub async fn cgw_tls_create_acceptor(args: &AppArgs) -> Result { cert.extend(cas.clone()); // Read private key. - let key_path = format!("{}/{}", CGW_TLS_CERTIFICATES_PATH, args.wss_key); + let key_path = format!("{}/{}", CGW_TLS_CERTIFICATES_PATH, wss_args.wss_key); let key = match cgw_tls_read_private_key(key_path.as_str()).await { Ok(pkey) => pkey, Err(e) => { @@ -162,3 +169,53 @@ pub async fn cgw_tls_create_acceptor(args: &AppArgs) -> Result { // Create the TLS acceptor. Ok(TlsAcceptor::from(Arc::new(config))) } + +pub async fn cgw_read_root_certs_dir() -> Result> { + let mut certs_vec = Vec::new(); + + // Read the directory entries + for entry in fs::read_dir(Path::new(CGW_TLS_NB_INFRA_CERTS_PATH))? { + let entry = entry?; + let path = entry.path(); + + // Check if the entry is a file and has a .crt extension (or other extensions if needed) + if path.is_file() { + let extension = path.extension().and_then(|ext| ext.to_str()); + if extension == Some("crt") || extension == Some("pem") { + let cert_contents = fs::read(path)?; + certs_vec.extend(cert_contents); + } + } + } + + Ok(certs_vec) +} + +pub async fn cgw_get_root_certs_store() -> Result { + let certs = cgw_read_root_certs_dir().await?; + + let buf = &mut certs.as_slice() as &mut dyn BufRead; + let certs = rustls_pemfile::certs(buf); + let mut root_cert_store = rustls::RootCertStore::empty(); + for cert in certs.flatten() { + let _r = root_cert_store.add(cert); + } + + Ok(root_cert_store) +} + +pub async fn cgw_tls_create_db_connect() -> Result { + let root_store = match cgw_get_root_certs_store().await { + Ok(certs) => certs, + Err(e) => { + error!("{}", e.to_string()); + return Err(e); + } + }; + + let config = rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + + Ok(tokio_postgres_rustls::MakeRustlsConnect::new(config)) +} diff --git a/src/cgw_ucentral_ap_parser.rs b/src/cgw_ucentral_ap_parser.rs index 7b91a98..4d64d8a 100644 --- a/src/cgw_ucentral_ap_parser.rs +++ b/src/cgw_ucentral_ap_parser.rs @@ -13,13 +13,13 @@ use crate::cgw_ucentral_parser::{ CGWUCentralEventRealtimeEventWClientJoin, CGWUCentralEventRealtimeEventWClientLeave, CGWUCentralEventReply, CGWUCentralEventState, CGWUCentralEventStateClients, CGWUCentralEventStateClientsData, CGWUCentralEventStateClientsType, - CGWUCentralEventStateLLDPData, CGWUCentralEventStateLinks, CGWUCentralEventType, - CGWUCentralJRPCMessage, + CGWUCentralEventStateLLDPData, CGWUCentralEventStateLinks, CGWUCentralEventStatePort, + CGWUCentralEventType, CGWUCentralJRPCMessage, }; fn parse_lldp_data( lldp_peers: &Map, - links: &mut Vec, + links: &mut HashMap>, ) -> Result<()> { let directions = [ ( @@ -54,12 +54,15 @@ fn parse_lldp_data( .ok_or_else(|| Error::UCentralParser("Failed to prase port"))? .to_string(); - links.push(CGWUCentralEventStateLinks { - local_port, + let local_port = CGWUCentralEventStatePort::PhysicalWiredPort(local_port); + + let clients_data = CGWUCentralEventStateLinks { remote_serial, remote_port, is_downstream, - }); + }; + + links.insert(local_port, vec![clients_data]); } } @@ -108,7 +111,7 @@ fn parse_wireless_ssids_info( fn parse_wireless_clients_data( ssids: &Vec, - links: &mut Vec, + links: &mut HashMap>, upstream_ifaces: &[String], ssids_map: &HashMap, timestamp: i64, @@ -172,23 +175,28 @@ fn parse_wireless_clients_data( })?; } - links.push(CGWUCentralEventStateClients { + let local_port = CGWUCentralEventStatePort::WirelessPort(ssid, band); + + let clients_data = CGWUCentralEventStateClients { client_type: CGWUCentralEventStateClientsType::Wireless( // Track timestamp of initial connection: // if we receive state evt , substract // connected since from it, to get // original connection timestamp. timestamp - ts, - ssid, - band, ), - local_port: local_port.clone(), remote_serial, // TODO: rework remote_port to have Band, RSSI, chan etc // for an edge. remote_port: "".to_string(), is_downstream: true, - }); + }; + + if let Some(ref mut existing_vec) = links.get_mut(&local_port) { + existing_vec.push(clients_data); + } else { + links.insert(local_port, vec![clients_data]); + } } } } @@ -200,7 +208,7 @@ fn parse_wireless_clients_data( fn parse_wired_clients_data( clients: &Vec, - links: &mut Vec, + links: &mut HashMap>, upstream_ifaces: &[String], timestamp: i64, ) -> Result<()> { @@ -241,23 +249,30 @@ fn parse_wired_clients_data( continue; } + let local_port = CGWUCentralEventStatePort::PhysicalWiredPort(local_port); + let remote_serial = MacAddress::from_str( client["mac"] .as_str() .ok_or_else(|| Error::UCentralParser("Failed to parse mac address"))?, )?; - links.push(CGWUCentralEventStateClients { + let clients_data: CGWUCentralEventStateClients = CGWUCentralEventStateClients { // Wired clients don't have data. // Treat as latest connected ts. client_type: CGWUCentralEventStateClientsType::Wired(timestamp), - local_port, remote_serial, // TODO: rework remote_port to have speed / duplex characteristics // for an edge. remote_port: "".to_string(), is_downstream: true, - }); + }; + + if let Some(ref mut existing_vec) = links.get_mut(&local_port) { + existing_vec.push(clients_data); + } else { + links.insert(local_port, vec![clients_data]); + } } Ok(()) @@ -265,7 +280,7 @@ fn parse_wired_clients_data( fn parse_interface_data( interface: &Map, - links: &mut Vec, + links: &mut HashMap>, upstream_ifaces: &[String], timestamp: i64, ) -> Result<()> { @@ -304,7 +319,11 @@ fn parse_link_state_data( } } -fn parse_state_event_data(map: CGWUCentralJRPCMessage, timestamp: i64) -> Result { +fn parse_state_event_data( + feature_topomap_enabled: bool, + map: CGWUCentralJRPCMessage, + timestamp: i64, +) -> Result { if !map.contains_key("params") { return Err(Error::UCentralParser( "Invalid state event received: params is missing", @@ -347,37 +366,68 @@ fn parse_state_event_data(map: CGWUCentralJRPCMessage, timestamp: i64) -> Result )?; if let Value::Object(state_map) = &state_map["state"] { - let mut lldp_links: Vec = Vec::new(); - let mut clients_links: Vec = Vec::new(); - - if state_map.contains_key("lldp-peers") { - if let Value::Object(v) = &state_map["lldp-peers"] { - parse_lldp_data(v, &mut lldp_links)?; + let mut lldp_links: HashMap< + CGWUCentralEventStatePort, + Vec, + > = HashMap::new(); + let mut clients_links: HashMap< + CGWUCentralEventStatePort, + Vec, + > = HashMap::new(); + + if feature_topomap_enabled { + if state_map.contains_key("lldp-peers") { + if let Value::Object(v) = &state_map["lldp-peers"] { + parse_lldp_data(v, &mut lldp_links)?; + } } - } - let mut upstream_ifaces: Vec = Vec::new(); - let mut downstream_ifaces: Vec = Vec::new(); + let mut upstream_ifaces: Vec = Vec::new(); + let mut downstream_ifaces: Vec = Vec::new(); - if state_map.contains_key("link-state") { - if let Value::Object(obj) = &state_map["link-state"] { - parse_link_state_data(obj, &mut upstream_ifaces, &mut downstream_ifaces); + if state_map.contains_key("link-state") { + if let Value::Object(obj) = &state_map["link-state"] { + parse_link_state_data(obj, &mut upstream_ifaces, &mut downstream_ifaces); + } } - } - if let Value::Array(arr) = &state_map["interfaces"] { - for interface in arr { - if let Value::Object(iface) = interface { - parse_interface_data( - iface, - &mut clients_links, - &upstream_ifaces, - timestamp, - )?; + if let Value::Array(arr) = &state_map["interfaces"] { + for interface in arr { + if let Value::Object(iface) = interface { + parse_interface_data( + iface, + &mut clients_links, + &upstream_ifaces, + timestamp, + )?; + } } } } + // Replace compressed data + let mut origin_msg = map.clone(); + let params_value = match Value::from_str(unzipped_data.as_str()) { + Ok(val) => val, + Err(_e) => { + return Err(Error::ConnectionProcessor( + "Failed to cast decompressed message to JSON Value", + )); + } + }; + if let Some(value) = origin_msg.get_mut("params") { + *value = params_value; + } + + let kafka_msg = match serde_json::to_string(&origin_msg) { + Ok(msg) => msg, + Err(_e) => { + return Err(Error::ConnectionProcessor( + "Failed to create decompressed Event message", + )); + } + }; + let state_event = CGWUCentralEvent { serial, evt_type: CGWUCentralEventType::State(CGWUCentralEventState { @@ -388,6 +438,7 @@ fn parse_state_event_data(map: CGWUCentralJRPCMessage, timestamp: i64) -> Result links: clients_links, }, }), + decompressed: Some(kafka_msg), }; return Ok(state_event); @@ -402,8 +453,12 @@ fn parse_state_event_data(map: CGWUCentralJRPCMessage, timestamp: i64) -> Result .as_str() .ok_or_else(|| Error::UCentralParser("Failed to parse mac address"))?, )?; - let mut lldp_links: Vec = Vec::new(); - let mut clients_links: Vec = Vec::new(); + let mut lldp_links: HashMap> = + HashMap::new(); + let mut clients_links: HashMap< + CGWUCentralEventStatePort, + Vec, + > = HashMap::new(); if state_map.contains_key("lldp-peers") { if let Value::Object(v) = &state_map["lldp-peers"] { @@ -438,6 +493,7 @@ fn parse_state_event_data(map: CGWUCentralJRPCMessage, timestamp: i64) -> Result links: clients_links, }, }), + decompressed: None, }; return Ok(state_event); @@ -655,6 +711,7 @@ fn parse_realtime_event_data( }, ), }), + decompressed: None, }) } "client.leave" => { @@ -734,6 +791,7 @@ fn parse_realtime_event_data( }, ), }), + decompressed: None, }) } _ => { @@ -743,7 +801,11 @@ fn parse_realtime_event_data( } } -pub fn cgw_ucentral_ap_parse_message(message: &str, timestamp: i64) -> Result { +pub fn cgw_ucentral_ap_parse_message( + feature_topomap_enabled: bool, + message: &str, + timestamp: i64, +) -> Result { let map: CGWUCentralJRPCMessage = match serde_json::from_str(message) { Ok(m) => m, Err(e) => { @@ -775,6 +837,7 @@ pub fn cgw_ucentral_ap_parse_message(message: &str, timestamp: i64) -> Result Result Result; +pub struct CGWUCentralConfigValidators { + ap_schema: JSONSchema, + switch_schema: JSONSchema, +} + +impl CGWUCentralConfigValidators { + pub fn new(uris: CGWValidationSchemaArgs) -> Result { + let ap_schema = cgw_initialize_json_validator(uris.ap_schema_uri)?; + let switch_schema = cgw_initialize_json_validator(uris.switch_schema_uri)?; + + Ok(CGWUCentralConfigValidators { + ap_schema, + switch_schema, + }) + } + + pub fn validate_config_message(&self, message: &str, device_type: CGWDeviceType) -> Result<()> { + let msg: CGWUCentralJRPCMessage = match serde_json::from_str(message) { + Ok(m) => m, + Err(e) => { + error!("Failed to parse input json {e}"); + return Err(Error::UCentralParser("Failed to parse input json")); + } + }; + + let config = match msg.get("params") { + Some(cfg) => cfg, + None => { + error!("Failed to get configs, invalid config message recevied"); + return Err(Error::UCentralParser( + "Failed to get configs, invalid config message recevied", + )); + } + }; + + let config = match config.get("config") { + Some(cfg) => cfg, + None => { + error!("Failed to get config params, invalid config message recevied"); + return Err(Error::UCentralParser( + "Failed to get config params, invalid config message recevied", + )); + } + }; + + let result = match device_type { + CGWDeviceType::CGWDeviceAP => self.ap_schema.validate(config), + CGWDeviceType::CGWDeviceSwitch => self.switch_schema.validate(config), + CGWDeviceType::CGWDeviceUnknown => { + error!("Failed to validate configure message for device type unknown"); + return Err(Error::UCentralParser( + "Failed to validate configure message for device type unknown", + )); + } + }; + + let mut json_errors: String = String::new(); + if let Err(errors) = result { + for error in errors { + json_errors += &format!("JSON: Validation error: {}\n", error); + json_errors += &format!("JSON: Instance path: {}\n", error.instance_path); + } + error!("{json_errors}"); + return Err(Error::UCentralValidator(json_errors)); + } + + Ok(()) + } +} + #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] pub struct CGWUCentralEventLog { pub serial: MacAddress, @@ -40,7 +117,6 @@ pub struct CGWUCentralEventConnect { #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] pub struct CGWUCentralEventStateLinks { - pub local_port: String, #[serde(skip)] pub remote_serial: MacAddress, pub remote_port: String, @@ -51,8 +127,8 @@ pub struct CGWUCentralEventStateLinks { pub enum CGWUCentralEventStateClientsType { // Timestamp Wired(i64), - // Timestamp, Ssid, Band - Wireless(i64, String, String), + // Timestamp + Wireless(i64), // VID FDBClient(u16), } @@ -60,7 +136,6 @@ pub enum CGWUCentralEventStateClientsType { #[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct CGWUCentralEventStateClients { pub client_type: CGWUCentralEventStateClientsType, - pub local_port: String, #[serde(skip)] pub remote_serial: MacAddress, pub remote_port: String, @@ -70,13 +145,42 @@ pub struct CGWUCentralEventStateClients { #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] pub struct CGWUCentralEventStateLLDPData { // links reported by the device: - pub links: Vec, + // local port (key), vector of links (value) + pub links: HashMap>, } #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] pub struct CGWUCentralEventStateClientsData { // links reported by the device (wired and wireless): - pub links: Vec, + // Composed into hashmap of Port(key), and vector of links + // seen on this particular port. + pub links: HashMap>, +} + +// One 'slice' / part of edge (Mac + port); +// To make a proper complete edge two parts needed: +// SRC -> DST +#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] +pub enum CGWUCentralEventStatePort { + // Physical port description (port name) + #[serde(skip)] + PhysicalWiredPort(String), + // Wirelss port description (ssid, band) + #[serde(skip)] + WirelessPort(String, String), +} + +impl fmt::Display for CGWUCentralEventStatePort { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CGWUCentralEventStatePort::PhysicalWiredPort(port) => { + write!(f, "{port}") + } + CGWUCentralEventStatePort::WirelessPort(ssid, band) => { + write!(f, "WirelessClient({ssid},{band})") + } + } + } } #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] @@ -146,6 +250,7 @@ pub enum CGWUCentralEventType { pub struct CGWUCentralEvent { pub serial: MacAddress, pub evt_type: CGWUCentralEventType, + pub decompressed: Option, } #[derive(Deserialize, Debug, Serialize)] @@ -262,6 +367,7 @@ pub fn cgw_ucentral_parse_connect_event(message: Message) -> Result Result Result { match device_type { - CGWDeviceType::CGWDeviceAP => cgw_ucentral_ap_parse_message(message, timestamp), - CGWDeviceType::CGWDeviceSwitch => cgw_ucentral_switch_parse_message(message, timestamp), + CGWDeviceType::CGWDeviceAP => { + cgw_ucentral_ap_parse_message(feature_topomap_enabled, message, timestamp) + } + CGWDeviceType::CGWDeviceSwitch => { + cgw_ucentral_switch_parse_message(feature_topomap_enabled, message, timestamp) + } + CGWDeviceType::CGWDeviceUnknown => Err(Error::UCentralParser( + "Failed to parse event message for device type unknown", + )), + } +} + +fn cgw_get_json_validation_schema(schema_ref: CGWValionSchemaRef) -> Result { + match schema_ref { + CGWValionSchemaRef::SchemaUri(url) => cgw_download_json_validation_schemas(url), + CGWValionSchemaRef::SchemaPath(path) => cgw_load_json_validation_schemas(path.as_path()), + } +} + +fn cgw_download_json_validation_schemas(url: Url) -> Result { + let client = reqwest::blocking::Client::new(); + let response = match client.get(url.clone()).send() { + Ok(r) => match r.text() { + Ok(t) => t, + Err(e) => { + return Err(Error::UCentralValidator(format!( + "Failed to convert response from target URI {url} to text fromat: {e}" + ))); + } + }, + Err(e) => { + return Err(Error::UCentralValidator(format!( + "Failed to receive response from target URI {url}: {e}" + ))); + } + }; + + match serde_json::from_str(&response) { + Ok(json_schema) => Ok(json_schema), + Err(e) => Err(Error::UCentralValidator(format!( + "Failed to deserialize text response from target URI {url}: {e}" + ))), + } +} + +fn cgw_load_json_validation_schemas(path: &Path) -> Result { + let file = match File::open(path) { + Ok(f) => f, + Err(e) => { + return Err(Error::UCentralValidator(format!( + "Failed to open TLS certificate file: {}. Error: {}", + path.display(), + e + ))); + } + }; + + let reader = BufReader::new(file); + match serde_json::from_reader(reader) { + Ok(json_schema) => Ok(json_schema), + Err(e) => Err(Error::UCentralValidator(format!( + "Failed to read JSON schema from file {}: {e}", + path.display() + ))), + } +} + +pub fn cgw_initialize_json_validator(schema_ref: CGWValionSchemaRef) -> Result { + let schema = match cgw_get_json_validation_schema(schema_ref) { + Ok(sch) => sch, + Err(e) => { + return Err(Error::UCentralValidator(e.to_string())); + } + }; + + match JSONSchema::compile(&schema) { + Ok(json_schema) => Ok(json_schema), + Err(e) => Err(Error::UCentralValidator(format!( + "Failed to compile input schema to validation tree: {e}", + ))), } } diff --git a/src/cgw_ucentral_switch_parser.rs b/src/cgw_ucentral_switch_parser.rs index d4df325..7b63cf4 100644 --- a/src/cgw_ucentral_switch_parser.rs +++ b/src/cgw_ucentral_switch_parser.rs @@ -1,19 +1,19 @@ use eui48::MacAddress; use serde_json::Value; -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use crate::cgw_errors::{Error, Result}; use crate::cgw_ucentral_parser::{ CGWUCentralEvent, CGWUCentralEventLog, CGWUCentralEventState, CGWUCentralEventStateClients, CGWUCentralEventStateClientsData, CGWUCentralEventStateClientsType, - CGWUCentralEventStateLLDPData, CGWUCentralEventStateLinks, CGWUCentralEventType, - CGWUCentralJRPCMessage, + CGWUCentralEventStateLLDPData, CGWUCentralEventStateLinks, CGWUCentralEventStatePort, + CGWUCentralEventType, CGWUCentralJRPCMessage, }; fn parse_lldp_data( data: &Value, - links: &mut Vec, + links: &mut HashMap>, upstream_port: &Option, ) -> Result<()> { if let Value::Object(map) = data { @@ -52,18 +52,22 @@ fn parse_lldp_data( } }; - links.push(CGWUCentralEventStateLinks { - local_port, + let local_port = CGWUCentralEventStatePort::PhysicalWiredPort(local_port); + + let clients_data = CGWUCentralEventStateLinks { remote_serial, remote_port, is_downstream, - }); + }; + + links.insert(local_port, vec![clients_data]); } } } Ok(()) } + /* Example of "mac-forwarding-table" in json format: ... @@ -74,10 +78,9 @@ Example of "mac-forwarding-table" in json format: }, ... */ - fn parse_fdb_data( data: &Value, - links: &mut Vec, + links: &mut HashMap>, upstream_port: &Option, ) -> Result<()> { if let Value::Object(map) = data { @@ -90,6 +93,15 @@ fn parse_fdb_data( } } + let local_port = CGWUCentralEventStatePort::PhysicalWiredPort(local_port); + + // We iterate on a per-port basis, means this is our first + // iteration on this particular port, safe to create empty + // vec and populate it later on. + links.insert(local_port.clone(), Vec::new()); + + let mut existing_vec = links.get_mut(&local_port); + for (k, v) in port.iter() { let vid = { match u16::from_str(k.as_str()) { @@ -100,19 +112,27 @@ fn parse_fdb_data( } } }; + if let Value::Array(macs) = v { for mac in macs.iter() { let remote_serial = MacAddress::from_str(mac.as_str().ok_or_else(|| { Error::UCentralParser("Failed to parse mac address") })?)?; - links.push(CGWUCentralEventStateClients { + + let clients_data = CGWUCentralEventStateClients { client_type: CGWUCentralEventStateClientsType::FDBClient(vid), - local_port: local_port.clone(), remote_serial, remote_port: format!("", vid), is_downstream: true, - }); + }; + + if let Some(ref mut existing_vec) = existing_vec { + existing_vec.push(clients_data); + } else { + warn!("Unexpected: tried to push clients_data {:?}:{}, while hashmap entry (key) for it does not exist", + local_port, clients_data.remote_port); + } } } } @@ -124,6 +144,7 @@ fn parse_fdb_data( } pub fn cgw_ucentral_switch_parse_message( + feature_topomap_enabled: bool, message: &str, timestamp: i64, ) -> Result { @@ -162,6 +183,7 @@ pub fn cgw_ucentral_switch_parse_message( log: params["log"].to_string(), severity: serde_json::from_value(params["severity"].clone())?, }), + decompressed: None, }; return Ok(log_event); @@ -176,34 +198,42 @@ pub fn cgw_ucentral_switch_parse_message( Error::UCentralParser("Failed to parse serial from params") })?)?; let mut upstream_port: Option = None; - let mut lldp_links: Vec = Vec::new(); + let mut lldp_links: HashMap< + CGWUCentralEventStatePort, + Vec, + > = HashMap::new(); // We can reuse logic as used in AP, it's safe and OK // since under the hood FDB macs are basically // switch's , where underlying client type // (FDBClient) will have all additional met info, like VID - let mut clients_links: Vec = Vec::new(); + let mut clients_links: HashMap< + CGWUCentralEventStatePort, + Vec, + > = HashMap::new(); - if state_map.contains_key("default-gateway") { - if let Value::Array(default_gw) = &state_map["default-gateway"] { - if let Some(gw) = default_gw.first() { - if let Value::String(port) = &gw["out-port"] { - upstream_port = Some(port.as_str().to_string()); + if feature_topomap_enabled { + if state_map.contains_key("default-gateway") { + if let Value::Array(default_gw) = &state_map["default-gateway"] { + if let Some(gw) = default_gw.first() { + if let Value::String(port) = &gw["out-port"] { + upstream_port = Some(port.as_str().to_string()); + } } } } - } - if state_map.contains_key("lldp-peers") { - parse_lldp_data(&state_map["lldp-peers"], &mut lldp_links, &upstream_port)?; - } + if state_map.contains_key("lldp-peers") { + parse_lldp_data(&state_map["lldp-peers"], &mut lldp_links, &upstream_port)?; + } - if state_map.contains_key("mac-forwarding-table") { - parse_fdb_data( - &state_map["mac-forwarding-table"], - &mut clients_links, - &upstream_port, - )?; + if state_map.contains_key("mac-forwarding-table") { + parse_fdb_data( + &state_map["mac-forwarding-table"], + &mut clients_links, + &upstream_port, + )?; + } } let state_event = CGWUCentralEvent { @@ -216,6 +246,7 @@ pub fn cgw_ucentral_switch_parse_message( links: clients_links, }, }), + decompressed: None, }; return Ok(state_event); diff --git a/src/cgw_ucentral_topology_map.rs b/src/cgw_ucentral_topology_map.rs index 525f2e9..9fd661a 100644 --- a/src/cgw_ucentral_topology_map.rs +++ b/src/cgw_ucentral_topology_map.rs @@ -1,126 +1,195 @@ use crate::{ + cgw_connection_server::CGWConnectionServer, cgw_device::CGWDeviceType, + cgw_nb_api_listener::{ + cgw_construct_client_join_msg, cgw_construct_client_leave_msg, + cgw_construct_client_migrate_msg, + }, cgw_ucentral_parser::{ CGWUCentralEvent, CGWUCentralEventRealtimeEventType, CGWUCentralEventStateClientsType, - CGWUCentralEventType, + CGWUCentralEventStatePort, CGWUCentralEventType, }, }; -use petgraph::dot::{Config, Dot}; -use petgraph::{ - graph::{EdgeIndex, NodeIndex}, - stable_graph::StableGraph, - visit::NodeRef, - Direction, + +use tokio::{ + runtime::Runtime, + sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex, + }, + time::{sleep, Duration}, }; -use std::{collections::HashMap, fmt, sync::Arc}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; use tokio::sync::RwLock; use eui48::MacAddress; -type WirelessClientBand = String; -type WirelessClientSsid = String; - -// One 'slice' / part of edge (Mac + port); -// To make a proper complete edge two parts needed: -// SRC -> DST -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub enum CGWUCentralTopologySubEdgePort { - // Used in subedge - PhysicalWiredPort(String), - WirelessPort, - - // Used in subedge - // Wired client reported by AP (no dst port info available) - // TODO: Duplex speed? - WiredClient, - // Wieless client reported by AP: SSID + Band - WirelessClient(WirelessClientSsid, WirelessClientBand), - - WiredFDBClient(u16), -} +type ClientLastSeenTimestamp = i64; -impl fmt::Display for CGWUCentralTopologySubEdgePort { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CGWUCentralTopologySubEdgePort::PhysicalWiredPort(port) => { - write!(f, "{port}") - } - CGWUCentralTopologySubEdgePort::WirelessPort => { - write!(f, "WirelessPort") - } - CGWUCentralTopologySubEdgePort::WiredClient => { - write!(f, "WiredClient") - } - CGWUCentralTopologySubEdgePort::WirelessClient(ssid, band) => { - write!(f, "WirelessClient({ssid},{band})") - } - CGWUCentralTopologySubEdgePort::WiredFDBClient(vid) => { - write!(f, "VLAN_{vid}") - } - } - } -} +// Client mac, ssid, band +type ClientsJoinList = Vec<(MacAddress, String, String)>; -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub struct CGWUCentralTopologySubEdge { - pub serial: MacAddress, - pub port: CGWUCentralTopologySubEdgePort, -} +// Client mac, band +type ClientsLeaveList = Vec<(MacAddress, String)>; + +// Client mac, new AP mac, band, ssid +type ClientsMigrateList = Vec<(MacAddress, MacAddress, String, String)>; -// Complete edge consisting of SRC -> DST 'sub-edges' -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub struct CGWUCentralTopologyEdge(CGWUCentralTopologySubEdge, CGWUCentralTopologySubEdge); +// Last seen, ssid, band +type ClientsConnectedList = (ClientLastSeenTimestamp, String, String); + +struct CGWTopologyMapQueueMessage { + evt: CGWUCentralEvent, + dev_type: CGWDeviceType, + node_mac: MacAddress, + gid: i32, + conn_server: Arc, +} -type EdgeCreationTimestamp = i64; +type CGWTopologyMapQueueRxHandle = UnboundedReceiver; +type CGWTopologyMapQueueTxHandle = UnboundedSender; // We have to track the 'origin' of any node we add to topo map, // because deletion decision should be made on the following basis: // - direct WSS connection should be always kept in the topo map, // and only erased when disconnect happens; -// - any nodes, that are added to topo map as 'clients' (lldp peers, -// wired and wireless clients, fdb info) should be deleted when the node -// that reported them gets deleted; -// however if 'client' node also exists in topo map, and is currently connected -// to CGW (WSS), then it should be left untouched; +// - any nodes, that are added to topo map as lldp peers +// should be deleted when the node that reported them gets deleted; #[derive(Debug, Clone)] enum CGWUCentralTopologyMapNodeOrigin { UCentralDevice, StateLLDPPeer, - StateWiredWireless, } -// We have to track the 'origin' of any edge we add to topo map, -// because deletion decision should be made on the following basis: -// - 'client.leave' should remove edge (only if join timestamp < leave timestamp); -// - 'client.join' should remove edge and create (potentially with new SRC node) -// a new edge with device/node that reports this event. -// Only, if join timestamp > join timestamp (state evt, realtime join evt) -#[derive(Debug, Clone)] -enum CGWUCentralTopologyMapEdgeOrigin { - StateLLDPPeer, - StateWiredWireless(EdgeCreationTimestamp), +#[derive(Debug)] +struct CGWUCentralTopologyMapConnectionParams { + // TODO: actually use for graph building; + // Currently, unused. + #[allow(unused)] + mac: MacAddress, + #[allow(unused)] + last_seen: ClientLastSeenTimestamp, +} + +#[derive(Debug)] +struct CGWUCentralTopologyMapConnectionsData { + // Infra node list = list of mac addresses (clients, fdb entires, + // arp neighbors etc etc) reported on single port + infra_nodes_list: Vec, + + // (Optional) node parent that we know for sure we're directly + // connected to (LLDP data, for example); Could be either + // UCentral connected device, or lldp-peer that AP reports + // (it's also possible that this peer will reconnect later-on + // as a UCentral connect, but for some reason still haven't, and + // AP already _sees_ it). + // This is used whenever child node gets disconnected / removed: + // we have to make sure we remove _connection_ between UCentral + // node A (parent) with node B (child) + // In case if parent get's removed though, there's no need to notify parent, + // however. This is because whenever _child_ node sends a new state meessage, + // connection to _parent_ node will be deduced from that data (basically, + // update upon receiving new state message from AP for example). + // + // The _connection_ (edge) creation is parent-driven, however: + // In the following example AP1 is connected to SW1: + // SW1 < - > AP1 + // In case if AP1 sends state data before SW1 doest, the internal data about + // wifi clients and so on will be populated, but the connection with SW1 + // and AP1 will only 'appear' in the topo map once the switch sends a + // state message with explicitly stating, that it _sees_ the AP1 directly. + // + // The only exception is when AP reports uplink lldp peer and it's not + // a UCentral device / still not connected: + // then AP is responsible to firsly create lldp-peer-node, and report + // it as AP's parent (and also clear / remove it upon AP disconnect event, + // as the _knowledge_ about this lldp-peer-node comes _only_ from the AP + // itself) + parent_topology_node_mac: Option, + + // In case if AP/Switch detects non-ucentral downstream LLDP peers, + // it should track the list of them for later-on removal (in case if + // this ucentral device get's disconnected). + // Hashmap is used for O(1) access. + child_lldp_nodes: HashMap, +} + +#[derive(Debug)] +struct CGWUCentralTopologyMapConnections { + links_list: HashMap, } #[derive(Debug)] struct CGWUCentralTopologyMapData { - node_idx_map: HashMap, - edge_idx_map: HashMap, - graph: StableGraph, + // Device nodes are only created upon receiving a new UCentral connection, + // or LLDP peer info. + topology_nodes: HashMap< + MacAddress, + ( + CGWUCentralTopologyMapNodeOrigin, + CGWUCentralTopologyMapConnections, + // This hashmap is needed to keep track of _all_ topomap nodes + // connected (directly reported) by this device, to detect + // _connect_ / _disconnect_ events: + // we need to keep track whenever we see newly connected WiFi client, + // or an event of such device getting disconnected. + // This need to be found out fast, hence sacrifice memory in the + // name of faster lookup; + // LIMITATION: + // * Works only on a per-group basis (if wifi-client migrates to + // another GID, this event would be missed) + // (as per current implementation). + // Track key:client mac, values:last seen timestamp, ssid and band + HashMap, + ), + >, } #[derive(Debug)] pub struct CGWUCentralTopologyMap { - data: Arc>, + // Stored on a per-gid basis + data: Arc< + RwLock< + HashMap< + i32, + ( + CGWUCentralTopologyMapData, + // This hashmap is needed to keep track of _all_ topomap nodes + // connected (directly reported) by this device, to detect _migration_ + // process: + // we need to keep track whenever WiFi client of AP_1, for example, + // 'silently' migrates to AP_2. + // + // We should also track the last time seen value of this + // client / node, to make appropriate decision + // whenever leave/join/migrate happens. + // + // LIMITATION: + // * Works only on a per-group basis (if wifi-client migrates to + // another GID, this event would be missed) + // (as per current implementation). + // Track key:client mac, values:parent AP mac, last seen timestamp, ssid and band + HashMap, + ), + >, + >, + >, + queue: ( + Arc, + Arc>, + ), + started: Mutex, } lazy_static! { pub static ref CGW_UCENTRAL_TOPOLOGY_MAP: CGWUCentralTopologyMap = CGWUCentralTopologyMap { - data: Arc::new(RwLock::new(CGWUCentralTopologyMapData { - node_idx_map: HashMap::new(), - edge_idx_map: HashMap::new(), - graph: StableGraph::new(), - })) + data: Arc::new(RwLock::new(HashMap::new())), + queue: { + let (tx, rx) = unbounded_channel::(); + (Arc::new(tx), Arc::new(Mutex::new(rx))) + }, + started: Mutex::new(false), }; } @@ -129,195 +198,750 @@ impl CGWUCentralTopologyMap { &CGW_UCENTRAL_TOPOLOGY_MAP } - pub async fn insert_device(&self, serial: &MacAddress) { + pub async fn start(&self, rt: &Runtime) { + let mut started = self.started.lock().await; + + if !*started { + *started = true; + rt.spawn(async move { + CGWUCentralTopologyMap::process_queue().await; + }); + } + } + + async fn process_queue() { + info!("TopoMap: queue processor started"); + let topo_map = CGWUCentralTopologyMap::get_ref(); + + let buf_capacity = 2000; + let mut buf: Vec = Vec::with_capacity(buf_capacity); + let mut num_of_msg_read = 0; + + loop { + let mut rx_mbox = topo_map.queue.1.lock().await; + + if num_of_msg_read < buf_capacity { + // Try to recv_many, but don't sleep too much + // in case if no messaged pending and we have + // TODO: rework to pull model (pull on demand), + // compared to curr impl: push model (nb api listener forcefully + // pushed all fetched data from kafka). + // Currently recv_many may sleep if previous read >= 1, + // but no new messages pending + // + // It's also possible that this logic staggers the processing, + // in case when every new message is received <=9 ms for example: + // Single message received, waiting for new up to 10 ms. + // New received on 9th ms. Repeat. + // And this could repeat up untill buffer is full, or no new messages + // appear on the 10ms scale. + // Highly unlikly scenario, but still possible. + let rd_num = tokio::select! { + v = rx_mbox.recv_many(&mut buf, buf_capacity - num_of_msg_read) => { + v + } + _v = sleep(Duration::from_millis(100)) => { + 0 + } + }; + num_of_msg_read += rd_num; + + // We read some messages, try to continue and read more + // If none read - break from recv, process all buffers that've + // been filled-up so far (both local and remote). + // Upon done - repeat. + if rd_num >= 1 || num_of_msg_read == 0 { + continue; + } + } + + debug!("Received {num_of_msg_read} events from devices, processing..."); + + while !buf.is_empty() { + let m = buf.remove(0); + match m.evt.evt_type { + CGWUCentralEventType::State(_) => { + topo_map + .process_state_message( + &m.dev_type, + &m.node_mac, + m.evt, + m.gid, + m.conn_server, + ) + .await; + } + CGWUCentralEventType::RealtimeEvent(_) => { + topo_map + .process_device_topology_event( + &m.dev_type, + &m.node_mac, + m.evt, + m.gid, + m.conn_server, + ) + .await; + } + _ => {} + } + } + + debug!("Done processing {num_of_msg_read} events from devices"); + + buf.clear(); + num_of_msg_read = 0; + } + } + + pub fn enqueue_event( + &self, + evt: CGWUCentralEvent, + dev_type: CGWDeviceType, + node_mac: MacAddress, + gid: i32, + conn_server: Arc, + ) { + let _ = self.queue.0.send(CGWTopologyMapQueueMessage { + evt, + dev_type, + node_mac, + gid, + conn_server, + }); + } + + pub async fn remove_gid(&self, gid: i32) { let mut lock = self.data.write().await; - Self::add_node( - &mut lock, - serial, - CGWUCentralTopologyMapNodeOrigin::UCentralDevice, - ); + lock.remove(&gid); } - pub async fn remove_device(&self, serial: &MacAddress) { + pub async fn insert_device(&self, topology_node_mac: &MacAddress, platform: &str, gid: i32) { + // TODO: rework to use device / accept deivce, rather then trying to + // parse string once again. + if CGWDeviceType::from_str(platform).is_err() { + warn!( + "Tried to insert {} into tomo map, but failed to parse it's platform string", + topology_node_mac + ); + return; + } + + let map_connections = CGWUCentralTopologyMapConnections { + links_list: HashMap::new(), + }; + let mut lock = self.data.write().await; - Self::remove_node(&mut lock, serial); + + // Clear occurance of this mac from ANY of the groups. + // This can only happen whenever device get's GID assigned from + // 0 (unassigned) to some specific GID, for example: + // was gid 0 - we created node initially - then NB's assigned device + // to a specific GID. + for (_gid, (v, _)) in lock.iter_mut() { + let _ = v.topology_nodes.remove(topology_node_mac); + } + + // Try to insert new topo-map node, however it's possible that it's the + // first isert: + // - if first time GID is being manipulated - we also have to create + // a hashmap that controls this GID; + // - if exists - simply insert new topomap node into existing GID map. + if let Some((ref mut topology_map_data, _)) = lock.get_mut(&gid) { + topology_map_data.topology_nodes.insert( + *topology_node_mac, + ( + CGWUCentralTopologyMapNodeOrigin::UCentralDevice, + map_connections, + HashMap::new(), + ), + ); + } else { + let mut topology_map_data: CGWUCentralTopologyMapData = CGWUCentralTopologyMapData { + topology_nodes: HashMap::new(), + }; + topology_map_data.topology_nodes.insert( + *topology_node_mac, + ( + CGWUCentralTopologyMapNodeOrigin::UCentralDevice, + map_connections, + HashMap::new(), + ), + ); + lock.insert(gid, (topology_map_data, HashMap::new())); + } } - pub async fn process_state_message(&self, _device_type: &CGWDeviceType, evt: CGWUCentralEvent) { + pub async fn remove_device( + &self, + topology_node_mac: &MacAddress, + gid: i32, + // TODO: remove this Arc: + // Dirty hack for now: pass Arc ref of srv to topo map; + // Future rework and refactoring would require to separate + // NB api from being an internal obj of conn_server to be a + // standalone (singleton?) object. + conn_server: Arc, + ) { let mut lock = self.data.write().await; + // Disconnected clients (seen before, don't see now) client mac from -> AP mac + let mut clients_leave_list: ClientsLeaveList = Vec::new(); - if let CGWUCentralEventType::State(s) = evt.evt_type { - // To make sure any leftovers are handled, node that reports - // state message is getting purged and recreated: - // since state message hold necessary information, - // we can safely purge all edge info and recreate it from - // the state message. Any missed / deleted by mistake - // edges will appear on the next iteration of state / realtime event - // processing. - Self::remove_node(&mut lock, &s.local_mac); - - // Re-create node with origin being UCentralDevice, as this - // device is directly connected to the CGW. - Self::add_node( - &mut lock, - &s.local_mac, - CGWUCentralTopologyMapNodeOrigin::UCentralDevice, + if let Some((ref mut topology_map_data, ref mut existing_nodes_map)) = lock.get_mut(&gid) { + Self::clear_related_nodes(topology_map_data, topology_node_mac); + if let Some((_, _, removed_clients_list)) = + topology_map_data.topology_nodes.remove(topology_node_mac) + { + // We have to clear Per-device connected clients from global + // map. + for client_mac in removed_clients_list.keys() { + if let Some(( + _existing_client_parent_node_mac, + _existing_client_node_last_seen_ts, + _existing_client_ssid, + existing_client_band, + )) = existing_nodes_map.remove(client_mac) + { + clients_leave_list.push((*client_mac, existing_client_band)); + } + } + } + } + + if !clients_leave_list.is_empty() { + Self::handle_clients_leave( + *topology_node_mac, + clients_leave_list, + gid, + conn_server.clone(), ); + } + } - // Start with LLDP info processing - for link in s.lldp_data.links { - let subedge_src = CGWUCentralTopologySubEdge { - serial: s.local_mac, - port: CGWUCentralTopologySubEdgePort::PhysicalWiredPort(link.local_port), - }; - let subedge_dst = CGWUCentralTopologySubEdge { - serial: link.remote_serial, - port: CGWUCentralTopologySubEdgePort::PhysicalWiredPort(link.remote_port), - }; + // We still want to have an easy access for node mac that reported this event, + // hence it's easier to just pass it as an argument, rather then fetching + // it from the array itself. + fn handle_clients_join( + node_mac: MacAddress, + clients_list: ClientsJoinList, + gid: i32, + + // TODO: remove this Arc: + // Dirty hack for now: pass Arc ref of srv to topo map; + // Future rework and refactoring would require to separate + // NB api from being an internal obj of conn_server to be a + // standalone (singleton?) object. + conn_server: Arc, + ) { + if clients_list.is_empty() { + return; + } - // No duplicates can exists, since it's LLDP data - // (both uCentral and underlying LLDP agents do not - // support multiple LLDP clients over single link, - // so it's not expect to have duplicates here). - // Hence we have to try and remove any duplicates - // we can find (either based on SRC or DST device's - // subedge info. - Self::remove_edge(&mut lock, &subedge_src); - Self::remove_edge(&mut lock, &subedge_dst); - - // Any neighbour seen in LLDP is added to the graph. - // Whenever parent (entity reporting the LLDP data) - // get's removed - neighbour nodes and connected - // edges will be purged. - Self::add_node( - &mut lock, - &link.remote_serial, - CGWUCentralTopologyMapNodeOrigin::StateLLDPPeer, - ); - - if link.is_downstream { - Self::add_edge( - &mut lock, - CGWUCentralTopologyEdge(subedge_src, subedge_dst), - CGWUCentralTopologyMapEdgeOrigin::StateLLDPPeer, - ); + // We have AP mac, iterate only over keys - client macs + for (client_mac, new_ssid, new_band) in clients_list { + let msg = cgw_construct_client_join_msg(gid, client_mac, node_mac, new_ssid, new_band); + if let Ok(r) = msg { + let _ = conn_server.enqueue_mbox_message_from_device_to_nb_api_c(gid, r); + } else { + warn!("Failed to convert client.leave event to string!"); + } + } + } + + // We still want to have an easy access for node mac that reported this event, + // hence it's easier to just pass it as an argument, rather then fetching + // it from the array itself. + fn handle_clients_leave( + node_mac: MacAddress, + clients_list: ClientsLeaveList, + gid: i32, + + // TODO: remove this Arc: + // Dirty hack for now: pass Arc ref of srv to topo map; + // Future rework and refactoring would require to separate + // NB api from being an internal obj of conn_server to be a + // standalone (singleton?) object. + conn_server: Arc, + ) { + if clients_list.is_empty() { + return; + } + + // We have AP mac, iterate only over keys - client macs + for (client_mac, band) in clients_list { + let msg = cgw_construct_client_leave_msg(gid, client_mac, node_mac, band); + if let Ok(r) = msg { + let _ = conn_server.enqueue_mbox_message_from_device_to_nb_api_c(gid, r); + } else { + warn!("Failed to convert client.leave event to string!"); + } + } + } + + // We still want to have an easy access for node mac that reported this event, + // hence it's easier to just pass it as an argument, rather then fetching + // it from the array itself. + fn handle_clients_migrate( + clients_list: ClientsMigrateList, + gid: i32, + + // TODO: remove this Arc: + // Dirty hack for now: pass Arc ref of srv to topo map; + // Future rework and refactoring would require to separate + // NB api from being an internal obj of conn_server to be a + // standalone (singleton?) object. + conn_server: Arc, + ) { + if clients_list.is_empty() { + return; + } + + // We have AP mac, iterate only over keys - client macs + for (client_mac, new_parent_ap_mac, new_band, new_ssid) in clients_list { + let msg = cgw_construct_client_migrate_msg( + gid, + client_mac, + new_parent_ap_mac, + new_ssid, + new_band, + ); + if let Ok(r) = msg { + let _ = conn_server.enqueue_mbox_message_from_device_to_nb_api_c(gid, r); + } else { + warn!("Failed to convert client.leave event to string!"); + } + } + } + + // Process state message in an ublocking-manner as long as possible: + // * Function does alot of unnecessary (on the first glance) cloning + // and allocations, but it's needed to make sure we block the topomap + // for as short period of time as possible, to not clog/starve any + // other topo map users (other connections/devices). + // * All the allocations and copying is done to make sure at the end + // of the function we only do alterations to the topomap itself, + // and other calculations and tree-traverals should be kept at minimum. + // Overall design is part of software optimizations / approach to ublock + // other threads accessing the topomap. + pub async fn process_state_message( + &self, + device_type: &CGWDeviceType, + topology_node_mac: &MacAddress, + evt: CGWUCentralEvent, + gid: i32, + + // TODO: remove this Arc: + // Dirty hack for now: pass Arc ref of srv to topo map; + // Future rework and refactoring would require to separate + // NB api from being an internal obj of conn_server to be a + // standalone (singleton?) object. + conn_server: Arc, + ) { + if let CGWUCentralEventType::State(s) = evt.evt_type { + // Clear any related (child, or parent nodes we explicitly + // created). + // The child/parent node can be explicitly created, + // if information about the node is only deduced from + // lldp peer information, and the underlying node is not + // a ucentral device. + { + let mut lock = self.data.write().await; + if let Some((ref mut topology_map_data, _)) = lock.get_mut(&gid) { + Self::clear_related_nodes(topology_map_data, topology_node_mac); } else { - Self::add_edge( - &mut lock, - CGWUCentralTopologyEdge(subedge_dst, subedge_src), - CGWUCentralTopologyMapEdgeOrigin::StateLLDPPeer, - ); + error!("Unexpected: GID {gid} doesn't exists (should've been created prior to state processing)"); + return; } } - // Clients data processing: - // add all nodes seen in clients; - // add new edges; - for link in &s.clients_data.links { - // Treat state timestamp as edge-creation timestamp only for - // events that do not report explicit connection timestamp - // (no association establishment timestamp for wired clients, - // however present for wireless for example). - let mut link_timestamp = s.timestamp; - let (subedge_src, subedge_dst) = { - if let CGWUCentralEventStateClientsType::Wired(_) = link.client_type { - // We can safely skip MAC from adding if - // it already exists (could be due to previous LLDP - // info processed). - if lock.node_idx_map.contains_key(&link.remote_serial) { - continue; - } - ( - CGWUCentralTopologySubEdge { - serial: s.local_mac, - port: CGWUCentralTopologySubEdgePort::PhysicalWiredPort( - link.local_port.clone(), - ), - }, - CGWUCentralTopologySubEdge { - serial: link.remote_serial, - // TODO: Duplex speed? - port: CGWUCentralTopologySubEdgePort::WiredClient, - }, - ) - } else if let CGWUCentralEventStateClientsType::Wireless(ts, ssid, band) = - &link.client_type - { - // Since wireless association explicitly reports the - // timestamp for when link's been established, we can - // use this value reported from AP. - // For any other case (LLDP, wired), we use - // the event's base timestamp value; - link_timestamp = *ts; + // Mac address of upstream node with local port we see the peer on + let mut upstream_lldp_node: Option<(MacAddress, CGWUCentralEventStatePort)> = None; + let mut downstream_lldp_nodes: HashMap = + HashMap::new(); + let mut nodes_to_create: Vec<( + MacAddress, + ( + CGWUCentralTopologyMapNodeOrigin, + CGWUCentralTopologyMapConnections, + HashMap, + ), + )> = Vec::new(); + + // Map connections that will be populated on behalf of device + // that sent the state data itself. + let mut map_connections = CGWUCentralTopologyMapConnections { + links_list: HashMap::new(), + }; + + // Start with LLDP processing, as it's the backbone core + // of deducing whether we have some umanaged (non-ucentral) + // devices / nodes. + for (local_port, links) in s.lldp_data.links { + for link in links { + let lldp_peer_map_connections = CGWUCentralTopologyMapConnections { + links_list: HashMap::new(), + }; + let mut lldp_peer_map_conn_data = CGWUCentralTopologyMapConnectionsData { + infra_nodes_list: Vec::new(), + parent_topology_node_mac: None, + child_lldp_nodes: HashMap::new(), + }; + + if link.is_downstream { + // We create this downstream node, which means we say that + // we're the node for this lldp-downstream-node + // to-be-created. + lldp_peer_map_conn_data.parent_topology_node_mac = Some(*topology_node_mac); + downstream_lldp_nodes.insert(local_port.clone(), link.remote_serial); + } else { + // Use only single upstream lldp peer (only 1 supported) + if upstream_lldp_node.is_none() { + upstream_lldp_node = Some((link.remote_serial, local_port.clone())); - ( - CGWUCentralTopologySubEdge { - serial: s.local_mac, - port: CGWUCentralTopologySubEdgePort::WirelessPort, - }, - CGWUCentralTopologySubEdge { - serial: link.remote_serial, - port: CGWUCentralTopologySubEdgePort::WirelessClient( - ssid.clone(), - band.clone(), + // We create this upstream node, which means we say that + // we're the node for this lldp-upstream-node + // to-be-created. We also populate links of this node + // with our - and only our single - mac address. + let mut child_lldp_nodes: HashMap = HashMap::new(); + + child_lldp_nodes.insert(*topology_node_mac, ()); + + lldp_peer_map_conn_data.child_lldp_nodes = child_lldp_nodes; + + map_connections.links_list.insert( + CGWUCentralEventStatePort::PhysicalWiredPort( + link.remote_port.clone(), ), - }, - ) - } else if let CGWUCentralEventStateClientsType::FDBClient(vid) = - &link.client_type - { - // We can safely skip MAC from adding if - // it already exists (could be due to previous LLDP - // info processed). - if lock.node_idx_map.contains_key(&link.remote_serial) { + lldp_peer_map_conn_data, + ); + } else { + // Already found one upstream peer, skip this one; continue; } - ( - CGWUCentralTopologySubEdge { - serial: s.local_mac, - port: CGWUCentralTopologySubEdgePort::PhysicalWiredPort( - link.local_port.clone(), - ), - }, - CGWUCentralTopologySubEdge { - serial: link.remote_serial, - port: CGWUCentralTopologySubEdgePort::WiredFDBClient(*vid), - }, - ) - } else { - continue; } + + nodes_to_create.push(( + link.remote_serial, + ( + CGWUCentralTopologyMapNodeOrigin::StateLLDPPeer, + lldp_peer_map_connections, + // Don't care about _child_ nodes tracking for LLDP + // peers - create empty map. + HashMap::new(), + ), + )); + } + } + + // List (map) of child nodes that are directly connected to the + // parent node that reports state event message. + // + // We need to catch any connected/disconnected/migrated events + // based on this data. + let mut new_connected_child_clients_map: HashMap = + HashMap::new(); + + for (local_port, links) in s.clients_data.links.into_iter() { + // Filled on a per-port basis. + let mut local_map_conn_data = CGWUCentralTopologyMapConnectionsData { + infra_nodes_list: Vec::new(), + parent_topology_node_mac: None, + child_lldp_nodes: HashMap::new(), }; - // In case when client silently migrates from AP1 to AP2, - // we have to explicitly remove that from AP1, - // and 'migrate' it to AP2. - // Do this only using , to make sure - // we clear only unique destination (wifi client on band X, - // for example) edge counterparts. - // NOTE: deleting subedge will remove both SRC and DST - // from map, as map stores them separately. - Self::remove_edge(&mut lock, &subedge_dst); - - Self::add_node( - &mut lock, - &link.remote_serial, - CGWUCentralTopologyMapNodeOrigin::StateWiredWireless, - ); - - if link.is_downstream { - Self::add_edge( - &mut lock, - CGWUCentralTopologyEdge(subedge_src, subedge_dst), - CGWUCentralTopologyMapEdgeOrigin::StateWiredWireless(link_timestamp), - ); - } else { - Self::add_edge( - &mut lock, - CGWUCentralTopologyEdge(subedge_dst, subedge_src), - CGWUCentralTopologyMapEdgeOrigin::StateWiredWireless(link_timestamp), + // We're processing the link reports for the port that is + // also a port that to upstream LLDP peer + if let Some((lldp_peer_mac, ref lldp_peer_local_port)) = upstream_lldp_node { + if *lldp_peer_local_port == local_port { + local_map_conn_data.parent_topology_node_mac = Some(lldp_peer_mac); + } + } + + // Will be skipped, in case if upstream processing took + // place: it's not allowed by design to have the same + // mac be an upstream as well as downstream peer; + if let Some(lldp_peer_mac) = downstream_lldp_nodes.get(&local_port) { + local_map_conn_data + .child_lldp_nodes + .insert(*lldp_peer_mac, ()); + } + + for link_seen_on_port in links { + // Treat state timestamp as edge-creation timestamp only for + // events that do not report explicit connection timestamp + // (no association establishment timestamp for wired clients, + // however present for wireless for example). + let link_timestamp = { + if let CGWUCentralEventStateClientsType::Wireless(ts) = + link_seen_on_port.client_type + { + // We need to track on a port-agnostic level macs + // of wireles clients to easily track down the + // migrated/disconnected/connected clients fast. + if let CGWDeviceType::CGWDeviceAP = device_type { + if let CGWUCentralEventStatePort::WirelessPort(ref ssid, ref band) = + local_port + { + new_connected_child_clients_map.insert( + link_seen_on_port.remote_serial, + (ts, ssid.clone(), band.clone()), + ); + } + } + ts + } else if let CGWUCentralEventStateClientsType::Wired(ts) = + link_seen_on_port.client_type + { + ts + } else { + s.timestamp + } + }; + + local_map_conn_data.infra_nodes_list.push( + CGWUCentralTopologyMapConnectionParams { + mac: link_seen_on_port.remote_serial, + last_seen: link_timestamp, + }, ); } + + map_connections + .links_list + .insert(local_port, local_map_conn_data); + } + + // Also add _this_ node that reported state to the list of added nodes; + nodes_to_create.push(( + *topology_node_mac, + ( + CGWUCentralTopologyMapNodeOrigin::UCentralDevice, + map_connections, + new_connected_child_clients_map, + ), + )); + + let mut lock = self.data.write().await; + if let Some((ref mut topology_map_data, ref mut existing_nodes_map)) = + lock.get_mut(&gid) + { + for (node_mac, (node_origin, node_connections, connected_child_clients_map)) in + nodes_to_create.into_iter() + { + // Unconditionally insert/replace our AP / switch node; + if node_mac == *topology_node_mac { + // For APs we have a special handling of tracking wifi devices: + // track newly connected, disconnected or migrates + // events. + if let CGWDeviceType::CGWDeviceAP = device_type { + // New connected clients (first time seen) client mac -> on AP mac, + // ssid, band + let mut clients_join_list: ClientsJoinList = Vec::new(); + // Disconnected clients (seen before, don't see now) client mac from -> AP mac, band + let mut clients_leave_list: ClientsLeaveList = Vec::new(); + // Migrated client mac -> to (AP mac, ssid, band) + let mut clients_migrate_list: ClientsMigrateList = Vec::new(); + + // We also have to iterate through wireless clients + // to detect client connect/disconnect/migrate events. + for (client_mac, (last_seen_ts, ssid, band)) in + connected_child_clients_map.iter() + { + if let Some(( + existing_client_parent_node_mac, + existing_client_node_last_seen_ts, + existing_client_ssid, + existing_client_band, + )) = existing_nodes_map.remove(client_mac) + { + // We know that existing node for some reason has not _our_ parent mac, + // means it's either we're late in seeing this MAC (it's long go + // migrated), or this is an actual migration event detected: + // * compare current TS with existing, if current is higher + if existing_client_parent_node_mac != node_mac { + if *last_seen_ts >= existing_client_node_last_seen_ts { + // Update TS, update mac of AP - owner, generate + // migrate event + existing_nodes_map.insert( + *client_mac, + ( + node_mac, + *last_seen_ts, + ssid.clone(), + band.clone(), + ), + ); + clients_migrate_list.push(( + *client_mac, + node_mac, + ssid.clone(), + band.clone(), + )); + + // Since it's a migrate event, also __remove__ client + // from client list of a node which client's migrated. + if let Some((_, _, ref mut old_clients_data)) = + topology_map_data + .topology_nodes + .get_mut(&existing_client_parent_node_mac) + { + let _ = old_clients_data.remove(client_mac); + } + } else { + // Turns out, latest evt timestamp is + // more recent than this one; + // Ignore this one (skip), + // and make sure we insert removed + // data back as it was. + existing_nodes_map.insert( + *client_mac, + ( + existing_client_parent_node_mac, + existing_client_node_last_seen_ts, + existing_client_ssid, + existing_client_band, + ), + ); + continue; + } + } else { + // Just update the TS + existing_nodes_map.insert( + *client_mac, + ( + node_mac, + *last_seen_ts, + existing_client_ssid, + existing_client_band, + ), + ); + } + } else { + // Create new entry, generate connected event + existing_nodes_map.insert( + *client_mac, + (node_mac, *last_seen_ts, ssid.clone(), band.clone()), + ); + clients_join_list.push(( + *client_mac, + ssid.clone(), + band.clone(), + )); + } + } + + // Lastly, we have to detect disconnected client: + // a disconnected client is present in old cache data + // of a node, but is missing from current report. + // We also have to make sure we don't report + // migrate twice - if it's missing from current + // state report, we still have to make sure + // it's not connected to another AP. + if let Some((_, _, old_clients_data)) = + topology_map_data.topology_nodes.get(&node_mac) + { + for old_client_data_mac in old_clients_data.keys() { + // Tricky way to skip found macs; + // We don't want to have double-borrow; + if connected_child_clients_map.contains_key(old_client_data_mac) + { + continue; + } + + // We're past check, which means there's a mac present in old data, + // but missing in new report: either disconnected + // or migrated - check this here. + + // It seems like mac is present in missing from current report, + // but was present in previous one: + // - check if by any chance the client's migrated + // to some other AP, or just plain disconnected + if let Some(( + existing_client_parent_node_mac, + existing_client_node_last_seen_ts, + existing_client_ssid, + existing_client_band, + )) = existing_nodes_map.remove(old_client_data_mac) + { + if existing_client_parent_node_mac == node_mac { + // Parent mac is the same == disconnected event + // And we keep track only for disconnected events, + // the migration was reported by another AP. + clients_leave_list + .push((*old_client_data_mac, existing_client_band)); + } else { + existing_nodes_map.insert( + *old_client_data_mac, + ( + existing_client_parent_node_mac, + existing_client_node_last_seen_ts, + existing_client_ssid, + existing_client_band, + ), + ); + } + } + } + } + + if !clients_join_list.is_empty() { + Self::handle_clients_join( + *topology_node_mac, + clients_join_list, + gid, + conn_server.clone(), + ); + } + + if !clients_leave_list.is_empty() { + Self::handle_clients_leave( + *topology_node_mac, + clients_leave_list, + gid, + conn_server.clone(), + ); + } + + if !clients_migrate_list.is_empty() { + Self::handle_clients_migrate( + clients_migrate_list, + gid, + conn_server.clone(), + ); + } + } + + Self::add_node( + topology_map_data, + node_mac, + node_origin, + node_connections, + connected_child_clients_map, + ); + } else { + // Skip UCentral-device (not this device/node) controlled + // topomap entries. + // We only add nodes that we explicitly created. + // On the next iteration of state data our lldp-peer-partners + // will update topo map on their own, if we didn't here. + if let Some((CGWUCentralTopologyMapNodeOrigin::UCentralDevice, _, _)) = + topology_map_data.topology_nodes.get(&node_mac) + { + continue; + } + + // It's clear that this node is created by us in this iteration of + // lldp parsing, so it's safe to add it. + Self::add_node( + topology_map_data, + node_mac, + node_origin, + node_connections, + connected_child_clients_map, + ); + } + } + } else { + error!("Unexpected: GID {gid} doesn't exists (should've been created prior to state processing)"); } } } @@ -325,433 +949,451 @@ impl CGWUCentralTopologyMap { pub async fn process_device_topology_event( &self, _device_type: &CGWDeviceType, + topology_node_mac: &MacAddress, evt: CGWUCentralEvent, + gid: i32, + + // TODO: remove this Arc: + // Dirty hack for now: pass Arc ref of srv to topo map; + // Future rework and refactoring would require to separate + // NB api from being an internal obj of conn_server to be a + // standalone (singleton?) object. + conn_server: Arc, ) { - struct ExistingEdge { - idx: EdgeIndex, - timestamp: EdgeCreationTimestamp, - key: CGWUCentralTopologyEdge, - } + // With realtime events, we want to make them absolutely synchronous: + // Since we could possibly handle event for a MAC that was + // previously present, but never received for it, + // we want to traverse through the whole topo map (including + // infra node list) and find _which_ exactly node we should + // remove from which device's links list. + // + // Same applies for event: it's possible that it's a + // late-leave message (client already joined another AP, + // and we successfully handled that event), we might want + // to check the timestamp of this message, with addition-timestamp + // of existing mac inside the infra node list and then decide upon. + + // New connected clients (first time seen) client mac -> on AP mac, + // ssid, band + let mut clients_join_list: ClientsJoinList = Vec::new(); + // Disconnected clients (seen before, don't see now) client mac from -> AP mac, band + let mut clients_leave_list: ClientsLeaveList = Vec::new(); + // Migrated client mac -> to (AP mac, ssid, band) + let mut clients_migrate_list: ClientsMigrateList = Vec::new(); + let mut lock = self.data.write().await; - let mut existing_edge: Option = None; - - if let CGWUCentralEventType::RealtimeEvent(rt) = evt.evt_type { - if let CGWUCentralEventRealtimeEventType::WirelessClientJoin(rt_j) = &rt.evt_type { - for key in lock.edge_idx_map.keys() { - // Try to find edge: - // we're looking for an edge with wireless client - // () with specific () properties. - // However, the check is global: - // We do not care AP reported the client initially: - // since the new client can appear on any given AP that - // is connected to us, we have to make sure that if - // AP2 receives client.join, and client serial is already - // associated with AP1, the connection edge between - // AP1 and should be purged, - // and then assigned to AP2. - // - // This only applies, however, to the join message. - // Late-leave events should be ignored, in case if - // client appears on new/other AP. - if let CGWUCentralTopologySubEdgePort::WirelessClient(_, dst_band) = &key.1.port + if let Some((ref mut topology_map_data, ref mut existing_nodes_map)) = lock.get_mut(&gid) { + if let CGWUCentralEventType::RealtimeEvent(rt) = evt.evt_type { + if let CGWUCentralEventRealtimeEventType::WirelessClientJoin(rt_j) = &rt.evt_type { + if let Some(( + existing_client_parent_node_mac, + existing_client_node_last_seen_ts, + existing_client_ssid, + existing_client_band, + )) = existing_nodes_map.remove(&rt_j.client) { - if key.1.serial == rt_j.client && *dst_band == *rt_j.band { - if let Some(( - edge_idx, - CGWUCentralTopologyMapEdgeOrigin::StateWiredWireless( - edge_timestamp, - ), - )) = lock.edge_idx_map.get(key) - { - existing_edge = Some(ExistingEdge { - idx: *edge_idx, - timestamp: *edge_timestamp, - key: key.to_owned(), - }); - break; + // We know that existing node for some reason has not _our_ parent mac, + // means it's either we're late in seeing this MAC (it's long go + // migrated), or this is an actual migration event detected: + // * compare current TS with existing, if current is higher + if existing_client_parent_node_mac != evt.serial { + if rt.timestamp >= existing_client_node_last_seen_ts { + // Update TS, update mac of AP - owner, generate + // migrate event + existing_nodes_map.insert( + rt_j.client, + ( + evt.serial, + rt.timestamp, + rt_j.ssid.clone(), + rt_j.band.clone(), + ), + ); + clients_migrate_list.push(( + rt_j.client, + evt.serial, + rt_j.ssid.clone(), + rt_j.band.clone(), + )); + + // Since it's a migrate event, also __remove__ client + // from client list of a node which client's migrated. + if let Some((_, _, ref mut old_clients_data)) = topology_map_data + .topology_nodes + .get_mut(&existing_client_parent_node_mac) + { + let _ = old_clients_data.remove(&rt_j.client); + } + } else { + existing_nodes_map.insert( + rt_j.client, + ( + existing_client_parent_node_mac, + existing_client_node_last_seen_ts, + existing_client_ssid, + existing_client_band, + ), + ); } + } else { + // Just update the TS + existing_nodes_map.insert( + rt_j.client, + ( + evt.serial, + rt.timestamp, + rt_j.ssid.clone(), + rt_j.band.clone(), + ), + ); } - } - } - if let Some(e) = existing_edge { - // New client joined, and new event timestamp is bigger (newer): - // - delete existing edge from map; - // - update graph; - if rt.timestamp > e.timestamp { - let _ = lock.graph.remove_edge(e.idx); - - // Remove SRC (tuple idx 0 == src) -> DST (idx 1 == dst) edge - let mut edge = CGWUCentralTopologyEdge(e.key.0, e.key.1); - let _ = lock.edge_idx_map.remove(&edge); - - // We do not delete the leaf-disconnected bode, - // as we will try to recreate it later on anyways. - - // Remove DST (tuple idx 1 == dst) -> SRC (idx 0 == src) edge - edge = CGWUCentralTopologyEdge(edge.1, edge.0); - let _ = lock.edge_idx_map.remove(&edge); } else { - warn!( - "Received late join event: event ts {:?} vs existing edge ts {:?}", - rt.timestamp, e.timestamp - ); - // New event is a late-reported / processed event; - // We can safely skip it; - return; - } - } - - // Now simply update internall state: - // - create node (if doesnt exist already) - // - create edge; - // - update graph; - Self::add_node( - &mut lock, - &rt_j.client, - CGWUCentralTopologyMapNodeOrigin::StateWiredWireless, - ); - - let (subedge_src, subedge_dst) = { - ( - CGWUCentralTopologySubEdge { - serial: evt.serial, - port: CGWUCentralTopologySubEdgePort::WirelessPort, - }, - CGWUCentralTopologySubEdge { - serial: rt_j.client, - port: CGWUCentralTopologySubEdgePort::WirelessClient( + // Create new entry, generate connected event + existing_nodes_map.insert( + rt_j.client, + ( + evt.serial, + rt.timestamp, rt_j.ssid.clone(), rt_j.band.clone(), ), - }, - ) - }; - Self::add_edge( - &mut lock, - CGWUCentralTopologyEdge(subedge_src, subedge_dst), - CGWUCentralTopologyMapEdgeOrigin::StateWiredWireless(rt.timestamp), - ); - } else if let CGWUCentralEventRealtimeEventType::WirelessClientLeave(rt_l) = rt.evt_type - { - for key in lock.edge_idx_map.keys() { - // Try to find edge: - // we're looking for an edge with wireless client - // () with specific () properties, which is also - // reported by the AP, as it's a leave event - // (AP1 can't expect us to delete existing edge, if AP2 - // is already associated with this client) - if let CGWUCentralTopologySubEdgePort::WirelessClient(_, dst_band) = &key.1.port + ); + clients_join_list.push((rt_j.client, rt_j.ssid.clone(), rt_j.band.clone())); + } + } else if let CGWUCentralEventRealtimeEventType::WirelessClientLeave(rt_l) = + rt.evt_type + { + // Unconditionally remove this client from our clients list. + if let Some((_, _, ref mut old_clients_data)) = + topology_map_data.topology_nodes.get_mut(&evt.serial) { - if key.1.serial == rt_l.client && *dst_band == *rt_l.band && - // Part that checks if AP that reports also - // is associated with this client. - // If not - it's a 'late' leave event that can be ignored. - key.0.serial == evt.serial - { - if let Some(( - edge_idx, - CGWUCentralTopologyMapEdgeOrigin::StateWiredWireless( - edge_timestamp, + let _ = old_clients_data.remove(&rt_l.client); + } + + if let Some(( + existing_client_parent_node_mac, + existing_client_node_last_seen_ts, + existing_client_ssid, + existing_client_band, + )) = existing_nodes_map.remove(&rt_l.client) + { + if existing_client_parent_node_mac == evt.serial { + clients_leave_list.push((rt_l.client, existing_client_band)); + } else { + existing_nodes_map.insert( + rt_l.client, + ( + existing_client_parent_node_mac, + existing_client_node_last_seen_ts, + existing_client_ssid, + existing_client_band, ), - )) = lock.edge_idx_map.get(key) - { - existing_edge = Some(ExistingEdge { - idx: *edge_idx, - timestamp: *edge_timestamp, - key: key.to_owned(), - }); - break; - } + ); } } } - if let Some(e) = existing_edge { - // We still have to check whether this leave message - // is newer than the existing timestamp: - // It's possible that state + leave events were shuffled, - // in a way that leave gets processed only after state's - // processing's been completed. - // This results in a discardtion of the late leave event. - if rt.timestamp > e.timestamp { - let _ = lock.graph.remove_edge(e.idx); - - // Remove SRC (tuple idx 0 == src) -> DST (idx 1 == dst) edge - let mut edge = CGWUCentralTopologyEdge(e.key.0, e.key.1); - let _ = lock.edge_idx_map.remove(&edge); - - // Also remove dst node if it's a leaf-disconnected node - Self::remove_disconnected_leaf_node(&mut lock, &edge.1.serial); - - // Remove DST (tuple idx 1 == dst) -> SRC (idx 0 == src) edge - edge = CGWUCentralTopologyEdge(edge.1, edge.0); - let _ = lock.edge_idx_map.remove(&edge); - } else { - warn!( - "Received late leave event: event ts {:?} vs existing edge ts {:?}", - rt.timestamp, e.timestamp - ); - // New event is a late-reported / processed event; - // We can safely skip it; - return; - } - } } } - } - - fn add_node( - data: &mut CGWUCentralTopologyMapData, - node_mac: &MacAddress, - origin: CGWUCentralTopologyMapNodeOrigin, - ) -> NodeIndex { - match data.node_idx_map.get_mut(node_mac) { - None => { - let idx = data.graph.add_node(*node_mac); - let _ = data.node_idx_map.insert(*node_mac, (idx, origin)); - idx - } - Some((idx, existing_origin)) => { - if let CGWUCentralTopologyMapNodeOrigin::UCentralDevice = existing_origin { - *idx - } else { - *existing_origin = origin; - *idx - } - } + if !clients_join_list.is_empty() { + Self::handle_clients_join( + *topology_node_mac, + clients_join_list, + gid, + conn_server.clone(), + ); } - // TODO: handle case: - // this either means that we detected this node at some new - // position, or this is a "silent" reconnect / re-appearence; - // e.g. delete all connected edges, child nodes etc etc; - } - - // Checks before removal, safe to call - fn remove_disconnected_leaf_node(data: &mut CGWUCentralTopologyMapData, node_mac: &MacAddress) { - let mut node_idx_to_remove: Option = None; - if let Some((node_idx, origin)) = data.node_idx_map.get(node_mac) { - // Skip this node, as it's origin is known from - // uCentral connection, not state data. - if let CGWUCentralTopologyMapNodeOrigin::UCentralDevice = origin { - debug!("Not removing disconnected leaf {:?} - reason: uCentral device (direct connection to CGW)", node_mac); - return; - } + if !clients_leave_list.is_empty() { + Self::handle_clients_leave( + *topology_node_mac, + clients_leave_list, + gid, + conn_server.clone(), + ); + } - let mut edges = data - .graph - // We're interested only if there are edges for - // this (potentially) disconnected leaf-node - .neighbors_directed(*node_idx, Direction::Incoming) - .detach(); + if !clients_migrate_list.is_empty() { + Self::handle_clients_migrate(clients_migrate_list, gid, conn_server.clone()); + } + } - if edges.next_edge(&data.graph).is_none() { - node_idx_to_remove = Some(*node_idx); + fn add_node( + map_data: &mut CGWUCentralTopologyMapData, + new_node_mac: MacAddress, + new_origin: CGWUCentralTopologyMapNodeOrigin, + mut new_connections: CGWUCentralTopologyMapConnections, + connected_child_clients_map: HashMap, + ) { + // This operation only covers non-ucentral-controlled devices, + // so it shouldn't affect UCentral-controlled node's add perf; + // + // Special case check / handling: + // lldp lldp + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // + // We are inserting the . + // It's possible, that either UC_DEVICE_1 or UC_DEVICE_2 reported it, + // but with current design - with direct node replace fast approach - it + // means that we either lose parent or child relation. + // + // Try to restore it (if any) - basically, a merge operation. + // + // It could be potentially a case when we have to + // restore multiple relations: both parent and few child, consider + // the following example: + // + // -- 1.UC_DEVICE_1 reports , topomap state: + // (parent) lldp (child) + // UC_DEVICE_1 ------> + // + // -- 2.Some UC_DEVICE_2 connects to CGW, and it's also connected + // to . Whenever UC_DEVICE_2 reports + // it's state message, we have to make sure the topomap state + // would be the following: + // + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // + // -- 3.We do this, by making sure we the data about + // UC_DEVICE_1 connection in the + // + // -- 4.Same appliest for child links; + // Consider some UC_DEVICE_3 connects, and it's also connected + // to the , the perfect topo map state + // should be: + // + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // | + // | lldp (child) + // -------------------> UC_DEVICE_3 + // + // However, all this should be done fast, as we're interested in + // only in restoring missing links on a per-port basis. + if let Some((CGWUCentralTopologyMapNodeOrigin::StateLLDPPeer, old_node_connections, _)) = + map_data.topology_nodes.remove(&new_node_mac) + { + for (old_port, old_conn_data) in old_node_connections.links_list.into_iter() { + // We want to fill missing port links on old node: + // * check if _new_ links have old port entry; + // * if not - most likely information about this link + // originates from some other UCentral device - + // means we have to _restore_ it here + new_connections + .links_list + .entry(old_port) + .or_insert(old_conn_data); } } - if let Some(node_idx) = node_idx_to_remove { - debug!("MAC {:?} is a disconnected leaf node, removing", node_mac); - data.node_idx_map.remove(node_mac); - data.graph.remove_node(node_idx); - } + map_data.topology_nodes.insert( + new_node_mac, + (new_origin, new_connections, connected_child_clients_map), + ); } - fn remove_node(data: &mut CGWUCentralTopologyMapData, node_mac: &MacAddress) { - if let Some((node, _)) = data.node_idx_map.remove(node_mac) { - // 'Potential' list of nodes we can safely remove. - // Duplicates may exist, because multiple edges can originate - // from src node (AP, switch) to a single other node - // (for example client's connected both through - // the WiFi and the cable, or client's connected - // to the AP on multiple bands etc). - // - // Not every node from this list gets removed, as - // once again: node (client) can be connected to - // multiple APs at once on different bands, - // or client's seen for example both on WiFi - // and cable. - let mut nodes_to_remove: Vec = Vec::new(); - - let mut edges_to_remove: Vec = Vec::new(); - let mut map_edge_keys_to_remove: Vec = Vec::new(); - let mut edges = [ - data.graph - .neighbors_directed(node, Direction::Outgoing) - .detach(), - data.graph - .neighbors_directed(node, Direction::Incoming) - .detach(), - ]; - - while let Some(edge) = edges[0].next_edge(&data.graph) { - // We iterate over edges that are connected with this SRC - // node, and collect all the destination Node indexes, - // to check them afterwards whether they still have - // some edges connected to them. - // If not - we remove the nodes out off the internal map. - // NOTE: It's possible that two Websocket devices are - // connected and we'll try to remove DST node even though - // knowledge about this device's presence in our map - // originates from WSS connection, not state message. - // However, internal map also has meta information - // about the origin of appearence in map, hence - // it solves the issue. - // (if node.origin == WSS then ) - // - // NOTE: we do this only for neighbors - // From treeview-graph perspective, we're clearing - // nodes that originate from this device. - if let Some((_, node_dst)) = data.graph.edge_endpoints(edge) { - nodes_to_remove.push(node_dst); + fn clear_related_nodes( + map_data: &mut CGWUCentralTopologyMapData, + topology_node_mac: &MacAddress, + ) { + let mut nodes_to_remove: Vec = Vec::new(); + + // Stored to later-on find grandparent macs of this particular + // child. Used to deduce whether should be removed alongside + // this that is being processed. + let mut parent_node_macs: Vec = Vec::new(); + + // Stored to later-on find grandchild macs of this particular + // child. Used to deduce whether should be removed alongside + // this that is being processed. + let mut child_node_macs: Vec = Vec::new(); + + // Stored grandchild and grandparent related nodes to current + // that is being cleared up. + // + // Grandchild hashmap: Key = parent, value = vec of child macs. + let mut grandchild_node_macs: HashMap> = HashMap::new(); + // Grandparent hashmap: Key = parent, value = vec of child macs. + let mut grandparent_node_macs: HashMap> = HashMap::new(); + + // We found this node in our topo map: + // - clear child nodes (if this node them directly) + // - clear parent node (if this node it directly) + // + // First, try to fill parent / child macs into a vec for later + // traversal / checks. + if let Some((_origin, connections, _connected_child_nodes_map)) = + map_data.topology_nodes.get(topology_node_mac) + { + for link in connections.links_list.values() { + for child_mac in link.child_lldp_nodes.keys() { + child_node_macs.push(*child_mac); } - data.graph.remove_edge(edge); - edges_to_remove.push(edge); - } - - while let Some(edge) = edges[1].next_edge(&data.graph) { - data.graph.remove_edge(edge); - edges_to_remove.push(edge); + if let Some(parent_mac) = link.parent_topology_node_mac { + parent_node_macs.push(parent_mac); + } } + }; - for node_idx in nodes_to_remove { - let mut node_edges = data - .graph - .neighbors_directed(node_idx, Direction::Incoming) - .detach(); - - // Check if at least one edge is connecting this - // Node; If not - purge it, but only if this node - // has been added through the means of State message - // or realtime events. - // - // If it's an active WSS connection we have established, - // we should skip this node, as it's not our responsibility - // here to destroy it. - if node_edges.next_edge(&data.graph).is_none() { - let mut node_to_remove: Option<&MacAddress> = None; - if let Some(node_weight) = data.graph.node_weight(node_idx) { - if let Some((_, origin)) = data.node_idx_map.get(node_weight) { - // Skip this node, as it's origin is known from - // uCentral connection, not state data. - if let CGWUCentralTopologyMapNodeOrigin::UCentralDevice = origin { - debug!("Not removing disconnected leaf {:?} - reason: uCentral device (direct connection to CGW)", node_weight); - continue; - } - - node_to_remove = Some(node_weight); + // Traverse through child nodes, find grandchild nodes (if any). + for child_mac in child_node_macs { + // Special case check / handling: + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // + // For the UC_DEVICE_1, the child is , + // and grandchildren of UC_DEVICE_1 is also a UCentral device, + // (UC_DEVICE_2), + // which means if we're clearing related nodes for + // UC_DEVICE_1, we have to make sure we won't be deleting + // child nodes (including and + // grandchildren UC_DEVICE_2). + + if let Some((_, child_connections, _)) = map_data.topology_nodes.get_mut(&child_mac) { + for child_links in child_connections.links_list.values_mut() { + if let Some(child_parent_mac) = child_links.parent_topology_node_mac { + if child_parent_mac == *topology_node_mac { + child_links.parent_topology_node_mac = None; } } + } - if let Some(node_mac) = node_to_remove { - data.node_idx_map.remove(node_mac); - data.graph.remove_node(node_idx); + for child_links in child_connections.links_list.values() { + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // ^^^^^^^^^^^^^^^^^^^^^ + // is pointing to + + for grandchild_mac in child_links.child_lldp_nodes.keys() { + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // ^^^^^^^^^^^ + // is pointing to UC_DEVICE_2 + + if let Some(v) = grandchild_node_macs.get_mut(&child_mac) { + v.push(*grandchild_mac); + } else { + grandchild_node_macs.insert(child_mac, vec![*grandchild_mac]); + } } } } + } - for (k, e) in &data.edge_idx_map { - for x in &edges_to_remove { - if *x == e.0 { - map_edge_keys_to_remove.push(k.to_owned()); + // Traverse through parent nodes, find grandparent nodes (if any). + for parent_mac in parent_node_macs { + // Special case check / handling: + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // + // For the UC_DEVICE_2, the parent is + // and grandparent in UC_DEVICE_1 which is also a UCentral + // device which means, if we're clearing related nodes for + // UC_DEVICE_2, we have to make sure we won't be deleting + // parent nodes (including and + // grandparent UC_DEVICE_1). + + if let Some((_, parent_connections, _)) = map_data.topology_nodes.get(&parent_mac) { + for parent_links in parent_connections.links_list.values() { + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // ^^^^^^^^^^^^^^^^^^^^^ + // is pointing to + + if let Some(grandparent_mac) = parent_links.parent_topology_node_mac { + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // ^^^^^^^^^^^ + // is pointing to UC_DEVICE_1 + if let Some(v) = grandparent_node_macs.get_mut(&parent_mac) { + v.push(grandparent_mac); + } else { + grandparent_node_macs.insert(parent_mac, vec![grandparent_mac]); + } } } } + } - for key in map_edge_keys_to_remove { - data.edge_idx_map.remove(&key); + for (child_mac, grandchild_macs) in grandchild_node_macs.iter() { + let mut child_node_should_be_removed = true; + + for grandchild_mac in grandchild_macs.iter() { + if let Some((CGWUCentralTopologyMapNodeOrigin::UCentralDevice, _, _)) = + map_data.topology_nodes.get(grandchild_mac) + { + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // ^^^^^^^^^^^ + // is pointing to UC_DEVICE_2 origin + // and since it's a UCentral device, we can't delete + // the UC_DEVICE_1 child (), + // because the information about + // can be received from either UC_DEVICE_1 or UC_DEVICE_2. + child_node_should_be_removed = false; + break; + } } - data.graph.remove_node(node); - } - } - fn add_edge( - data: &mut CGWUCentralTopologyMapData, - edge: CGWUCentralTopologyEdge, - origin: CGWUCentralTopologyMapEdgeOrigin, - ) { - let node_src_subedge: CGWUCentralTopologySubEdge = edge.0; - let node_dst_subedge: CGWUCentralTopologySubEdge = edge.1; - let (node_src_idx, node_dst_idx) = { - ( - match data.node_idx_map.get(&node_src_subedge.serial) { - Some((idx, _)) => *idx, - None => { - warn!( - "Tried to add edge for non-existing node {:?}", - node_src_subedge.serial - ); - return; - } - }, - match data.node_idx_map.get(&node_dst_subedge.serial) { - Some((idx, _)) => *idx, - None => { - warn!( - "Tried to add edge for non-existing node {:?}", - node_dst_subedge.serial - ); - return; - } - }, - ) - }; + // (parent) lldp (child) + // UC_DEVICE_1 ------> + // ^^^^^^^^^^^^^^^^^^^^^ + // is going to be removed, as it has + // only single direct UCentral parent (this) device. - let edge_idx = data.graph.add_edge( - node_src_idx, - node_dst_idx, - format!("{}<->{}", node_src_subedge.port, node_dst_subedge.port), - ); + if child_node_should_be_removed { + nodes_to_remove.push(*child_mac); + } + } - data.edge_idx_map.insert( - CGWUCentralTopologyEdge(node_src_subedge.clone(), node_dst_subedge.clone()), - (edge_idx, origin.clone()), - ); - data.edge_idx_map.insert( - CGWUCentralTopologyEdge(node_dst_subedge, node_src_subedge), - (edge_idx, origin.clone()), - ); - } + for (parent_mac, grandparent_macs) in grandchild_node_macs.iter() { + let mut parent_node_should_be_removed = true; + + for grandparent_mac in grandparent_macs.iter() { + if let Some((CGWUCentralTopologyMapNodeOrigin::UCentralDevice, _, _)) = + map_data.topology_nodes.get(grandparent_mac) + { + // (parent) lldp (child, parent) lldp (child) + // UC_DEVICE_1 ------> ------> UC_DEVICE_2 + // ^^^^^^^^^^^ + // is pointing to UC_DEVICE_1 origin + // and since it's a UCentral device, we can't delete + // the UC_DEVICE_2 parent (), + // because the information about + // can be received from either UC_DEVICE_1 or UC_DEVICE_2. + parent_node_should_be_removed = false; + + break; + } + } - fn remove_edge(data: &mut CGWUCentralTopologyMapData, subedge: &CGWUCentralTopologySubEdge) { - let mut keys_to_remove: Vec = Vec::new(); + // (parent) lldp (child) + // ------> UC_DEVICE_1 + // ^^^^^^^^^^^^^^^^^^^^^ + // + // is going to be removed, as it has + // only single direct UCentral child (this) device. - for key in data.edge_idx_map.keys() { - if key.0 == *subedge || key.1 == *subedge { - let key_to_remove = key.to_owned(); - keys_to_remove.push(key_to_remove); + if parent_node_should_be_removed { + nodes_to_remove.push(*parent_mac); } } - if let Some(key) = keys_to_remove.first() { - if let Some((edge_idx, _)) = data.edge_idx_map.get(key) { - data.graph.remove_edge(*edge_idx); + for node_to_remove in nodes_to_remove { + let mut node_should_be_removed = false; + + if let Some((origin, _, _)) = map_data.topology_nodes.get(&node_to_remove) { + match origin { + CGWUCentralTopologyMapNodeOrigin::UCentralDevice => (), + _ => node_should_be_removed = true, + } } - } - for k in keys_to_remove { - let _ = data.edge_idx_map.remove(&k); + if node_should_be_removed { + let _ = map_data.topology_nodes.remove(&node_to_remove); + } } } - - pub async fn debug_dump_map(&self) { - let lock = self.data.read().await; - let dotfmt = format!( - "{:?}", - Dot::with_attr_getters( - &lock.graph, - &[Config::NodeNoLabel, Config::EdgeNoLabel], - &|_, er| { format!("label = \"{}\"", er.weight()) }, - &|_, nr| { format!("label = \"{}\" shape=\"record\"", nr.weight()) } - ) - ) - .replace("digraph {", "digraph {\n\trankdir=LR;\n"); - debug!( - "graph dump: {} {}\n{}", - lock.node_idx_map.len(), - lock.edge_idx_map.len(), - dotfmt - ); - } } diff --git a/src/main.rs b/src/main.rs index a50a710..87920dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ #![warn(rust_2018_idioms)] +mod cgw_app_args; mod cgw_connection_processor; mod cgw_connection_server; mod cgw_db_accessor; @@ -10,6 +11,7 @@ mod cgw_nb_api_listener; mod cgw_remote_client; mod cgw_remote_discovery; mod cgw_remote_server; +mod cgw_runtime; mod cgw_tls; mod cgw_ucentral_ap_parser; mod cgw_ucentral_messages_queue_manager; @@ -23,6 +25,9 @@ extern crate log; #[macro_use] extern crate lazy_static; +use cgw_app_args::AppArgs; +use cgw_runtime::cgw_initialize_runtimes; + use tokio::{ net::TcpListener, runtime::{Builder, Handle, Runtime}, @@ -31,12 +36,7 @@ use tokio::{ time::{sleep, Duration}, }; -use std::{ - env, - net::{Ipv4Addr, SocketAddr}, - str::FromStr, - sync::Arc, -}; +use std::{env, net::SocketAddr, str::FromStr, sync::Arc}; use rlimit::{setrlimit, Resource}; @@ -70,369 +70,6 @@ impl FromStr for AppCoreLogLevel { } } -const CGW_DEFAULT_ID: i32 = 0; -const CGW_DEFAULT_WSS_T_NUM: usize = 4; -const CGW_DEFAULT_LOG_LEVEL: AppCoreLogLevel = AppCoreLogLevel::Debug; -const CGW_DEFAULT_WSS_IP: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); -const CGW_DEFAULT_WSS_PORT: u16 = 15002; -const CGW_DEFAULT_WSS_CAS: &str = "cas.pem"; -const CGW_DEFAULT_WSS_CERT: &str = "cert.pem"; -const CGW_DEFAULT_WSS_KEY: &str = "key.pem"; -const CGW_DEFAULT_GRPC_LISTENING_IP: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); -const CGW_DEFAULT_GRPC_LISTENING_PORT: u16 = 50051; -const CGW_DEFAULT_GRPC_PUBLIC_HOST: &str = "localhost"; -const CGW_DEFAULT_GRPC_PUBLIC_PORT: u16 = 50051; -const CGW_DEFAULT_KAFKA_HOST: &str = "localhost"; -const CGW_DEFAULT_KAFKA_PORT: u16 = 9092; -const CGW_DEFAULT_KAFKA_CONSUME_TOPIC: &str = "CnC"; -const CGW_DEFAULT_KAFKA_PRODUCE_TOPIC: &str = "CnC_Res"; -const CGW_DEFAULT_DB_HOST: &str = "localhost"; -const CGW_DEFAULT_DB_PORT: u16 = 6379; -const CGW_DEFAULT_DB_NAME: &str = "cgw"; -const CGW_DEFAULT_DB_USERNAME: &str = "cgw"; -const CGW_DEFAULT_DB_PASSWORD: &str = "123"; -const CGW_DEFAULT_REDIS_HOST: &str = "localhost"; -const CGW_DEFAULT_REDIS_PORT: u16 = 6379; -const CGW_DEFAULT_ALLOW_CERT_MISMATCH: &str = "no"; -const CGW_DEFAULT_METRICS_PORT: u16 = 8080; -const CGW_DEFAULT_TOPOMAP_STATE: bool = false; - -/// CGW server -pub struct AppArgs { - /// Loglevel of application - log_level: AppCoreLogLevel, - - /// CGW unique identifier (u64) - cgw_id: i32, - - /// Number of thread in a threadpool dedicated for handling secure websocket connections - wss_t_num: usize, - /// IP to listen for incoming WSS connection - wss_ip: Ipv4Addr, - /// PORT to listen for incoming WSS connection - wss_port: u16, - /// WSS CAS certificate (contains root and issuer certificates) - wss_cas: String, - /// WSS certificate - wss_cert: String, - /// WSS private key - wss_key: String, - - /// IP to listen for incoming GRPC connection - grpc_listening_ip: Ipv4Addr, - /// PORT to listen for incoming GRPC connection - grpc_listening_port: u16, - /// IP or hostname for Redis Record - grpc_public_host: String, - /// PORT for Redis record - grpc_public_port: u16, - - /// IP or hostname to connect to KAFKA broker - kafka_host: String, - /// PORT to connect to KAFKA broker - kafka_port: u16, - /// KAFKA topic from where to consume messages - #[allow(unused)] - kafka_consume_topic: String, - /// KAFKA topic where to produce messages - #[allow(unused)] - kafka_produce_topic: String, - - /// IP or hostname to connect to DB (PSQL) - db_host: String, - /// PORT to connect to DB (PSQL) - db_port: u16, - /// DB name to connect to in DB (PSQL) - db_name: String, - /// DB user name use with connection to in DB (PSQL) - db_username: String, - /// DB user password use with connection to in DB (PSQL) - db_password: String, - - /// IP or hostname to connect to REDIS - redis_host: String, - /// PORT to connect to REDIS - redis_port: u16, - - /// Allow Missmatch - allow_mismatch: bool, - - /// PORT to connect to Metrics - metrics_port: u16, - - /// Topomap featue status (enabled/disabled) - feature_topomap_enabled: bool, -} - -impl AppArgs { - fn parse() -> Result { - let log_level: AppCoreLogLevel = match env::var("CGW_LOG_LEVEL") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_LOG_LEVEL! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_LOG_LEVEL, - }; - - let cgw_id: i32 = match env::var("CGW_ID") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_ID! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_ID, - }; - - let wss_t_num: usize = match env::var("DEFAULT_WSS_THREAD_NUM") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse DEFAULT_WSS_THREAD_NUM! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_WSS_T_NUM, - }; - - let wss_ip: Ipv4Addr = match env::var("CGW_WSS_IP") { - Ok(val) => match Ipv4Addr::from_str(val.as_str()) { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_WSS_IP! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_WSS_IP, - }; - - let wss_port: u16 = match env::var("CGW_WSS_PORT") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_WSS_PORT! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_WSS_PORT, - }; - - let wss_cas: String = env::var("CGW_WSS_CAS").unwrap_or(CGW_DEFAULT_WSS_CAS.to_string()); - let wss_cert: String = env::var("CGW_WSS_CERT").unwrap_or(CGW_DEFAULT_WSS_CERT.to_string()); - let wss_key: String = env::var("CGW_WSS_KEY").unwrap_or(CGW_DEFAULT_WSS_KEY.to_string()); - - let grpc_listening_ip: Ipv4Addr = match env::var("CGW_GRPC_LISTENING_IP") { - Ok(val) => match Ipv4Addr::from_str(val.as_str()) { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_GRPC_LISTENING_IP! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_GRPC_LISTENING_IP, - }; - - let grpc_listening_port: u16 = match env::var("CGW_GRPC_LISTENING_PORT") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_GRPC_LISTENING_PORT! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_GRPC_LISTENING_PORT, - }; - - let grpc_public_host: String = match env::var("CGW_GRPC_PUBLIC_HOST") { - Ok(val) => { - // 1. Try to parse variable into IpAddress - match Ipv4Addr::from_str(val.as_str()) { - // 2. If parsed - return IpAddress as String value - Ok(ip) => ip.to_string(), - // 3. If parse failed - probably hostname specified - Err(_e) => val, - } - } - // Env. variable is not setup - use default value - Err(_) => CGW_DEFAULT_GRPC_PUBLIC_HOST.to_string(), - }; - - let grpc_public_port: u16 = match env::var("CGW_GRPC_PUBLIC_PORT") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_GRPC_PUBLIC_PORT! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_GRPC_PUBLIC_PORT, - }; - - let kafka_host: String = match env::var("CGW_KAFKA_HOST") { - Ok(val) => { - // 1. Try to parse variable into IpAddress - match Ipv4Addr::from_str(val.as_str()) { - // 2. If parsed - return IpAddress as String value - Ok(ip) => ip.to_string(), - // 3. If parse failed - probably hostname specified - Err(_e) => val, - } - } - // Env. variable is not setup - use default value - Err(_) => CGW_DEFAULT_KAFKA_HOST.to_string(), - }; - - let kafka_port: u16 = match env::var("CGW_KAFKA_PORT") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_KAFKA_PORT! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_KAFKA_PORT, - }; - - let kafka_consume_topic: String = env::var("CGW_KAFKA_CONSUMER_TOPIC") - .unwrap_or(CGW_DEFAULT_KAFKA_CONSUME_TOPIC.to_string()); - let kafka_produce_topic: String = env::var("CGW_KAFKA_PRODUCER_TOPIC") - .unwrap_or(CGW_DEFAULT_KAFKA_PRODUCE_TOPIC.to_string()); - - let db_host: String = match env::var("CGW_DB_HOST") { - Ok(val) => { - // 1. Try to parse variable into IpAddress - match Ipv4Addr::from_str(val.as_str()) { - // 2. If parsed - return IpAddress as String value - Ok(ip) => ip.to_string(), - // 3. If parse failed - probably hostname specified - Err(_e) => val, - } - } - // Env. variable is not setup - use default value - Err(_) => CGW_DEFAULT_DB_HOST.to_string(), - }; - - let db_port: u16 = match env::var("CGW_DB_PORT") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_DB_PORT! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_DB_PORT, - }; - - let db_name: String = env::var("CGW_DB_NAME").unwrap_or(CGW_DEFAULT_DB_NAME.to_string()); - let db_username: String = - env::var("CGW_DB_USERNAME").unwrap_or(CGW_DEFAULT_DB_USERNAME.to_string()); - let db_password: String = - env::var("CGW_DB_PASSWORD").unwrap_or(CGW_DEFAULT_DB_PASSWORD.to_string()); - - let redis_host: String = match env::var("CGW_REDIS_HOST") { - Ok(val) => { - // 1. Try to parse variable into IpAddress - match Ipv4Addr::from_str(val.as_str()) { - // 2. If parsed - return IpAddress as String value - Ok(ip) => ip.to_string(), - // 3. If parse failed - probably hostname specified - Err(_e) => val, - } - } - // Env. variable is not setup - use default value - Err(_) => CGW_DEFAULT_REDIS_HOST.to_string(), - }; - - let redis_port: u16 = match env::var("CGW_REDIS_PORT") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_REDIS_PORT! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_REDIS_PORT, - }; - - let mismatch: String = env::var("CGW_ALLOW_CERT_MISMATCH") - .unwrap_or(CGW_DEFAULT_ALLOW_CERT_MISMATCH.to_string()); - let allow_mismatch = mismatch == "yes"; - - let metrics_port: u16 = match env::var("CGW_METRICS_PORT") { - Ok(val) => match val.parse() { - Ok(v) => v, - Err(_e) => { - return Err(Error::AppArgsParser(format!( - "Failed to parse CGW_METRICS_PORT! Invalid value: {}", - val - ))); - } - }, - Err(_) => CGW_DEFAULT_METRICS_PORT, - }; - - let feature_topomap_enabled: bool = match env::var("CGW_FEATURE_TOPOMAP_ENABLE") { - Ok(_) => true, - Err(_) => CGW_DEFAULT_TOPOMAP_STATE, - }; - - Ok(AppArgs { - log_level, - cgw_id, - wss_t_num, - wss_ip, - wss_port, - wss_cas, - wss_cert, - wss_key, - grpc_listening_ip, - grpc_listening_port, - grpc_public_host, - grpc_public_port, - kafka_host, - kafka_port, - kafka_consume_topic, - kafka_produce_topic, - db_host, - db_port, - db_name, - db_username, - db_password, - redis_host, - redis_port, - allow_mismatch, - metrics_port, - feature_topomap_enabled, - }) - } -} - pub struct AppCore { cgw_server: Arc, main_runtime_handle: Arc, @@ -496,7 +133,7 @@ impl AppCore { let main_runtime_handle: Arc = self.main_runtime_handle.clone(); let core_clone = self.clone(); - let cgw_remote_server = CGWRemoteServer::new(&self.args); + let cgw_remote_server = CGWRemoteServer::new(self.args.cgw_id, &self.args.grpc_args); let cgw_srv_clone = self.cgw_server.clone(); let cgw_con_serv = self.cgw_server.clone(); self.grpc_server_runtime_handle.spawn(async move { @@ -527,12 +164,12 @@ async fn server_loop(app_core: Arc) -> Result<()> { debug!( "Starting WSS server, listening at {}:{}", - app_core.args.wss_ip, app_core.args.wss_port + app_core.args.wss_args.wss_ip, app_core.args.wss_args.wss_port ); // Bind the server's socket let sockaddress = SocketAddr::new( - std::net::IpAddr::V4(app_core.args.wss_ip), - app_core.args.wss_port, + std::net::IpAddr::V4(app_core.args.wss_args.wss_ip), + app_core.args.wss_args.wss_port, ); let listener: Arc = match TcpListener::bind(sockaddress).await { Ok(listener) => Arc::new(listener), @@ -548,7 +185,7 @@ async fn server_loop(app_core: Arc) -> Result<()> { } }; - let tls_acceptor = match cgw_tls_create_acceptor(&app_core.args).await { + let tls_acceptor = match cgw_tls_create_acceptor(&app_core.args.wss_args).await { Ok(acceptor) => acceptor, Err(e) => { error!("Failed to create TLS acceptor. Error: {}", e.to_string()); @@ -637,6 +274,12 @@ async fn main() -> Result<()> { // Configure logger setup_logger(args.log_level); + // Initialize runtimes + if let Err(e) = cgw_initialize_runtimes(args.wss_args.wss_t_num) { + error!("Failed to initialize CGW runtimes: {}", e.to_string()); + return Err(e); + } + if args.feature_topomap_enabled { warn!("CGW_FEATURE_TOPOMAP_ENABLE is set, TOPO MAP feature (unstable) will be enabled (realtime events / state processing) - heavy performance drop with high number of devices connected could be observed"); } @@ -659,7 +302,9 @@ async fn main() -> Result<()> { // Make sure metrics are available any of the components // starts up; - CGWMetrics::get_ref().start(args.metrics_port).await?; + CGWMetrics::get_ref() + .start(args.metrics_args.metrics_port) + .await?; let app = Arc::new(AppCore::new(args).await?); app.run(shutdown_notify).await; diff --git a/utils/docker/certs/server.crt b/utils/docker/certs/server.crt new file mode 100644 index 0000000..ffeff23 --- /dev/null +++ b/utils/docker/certs/server.crt @@ -0,0 +1,58 @@ +-----BEGIN CERTIFICATE----- +MIIE+jCCAuKgAwIBAgIUKNl5fnws7cASsTTcKxmACp8L+mMwDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEAwwCQ0EwHhcNMjQwNzE3MTYxODA1WhcNMjUwNzE3MTYxODA1 +WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQC/DfDxcFkyH2tjS5hbEnDPF7cLFd03UsgiqPTgLwGdyYnNYURIGMxs +gl243FkycmYRz1OsEY1+zf71vi0XNulfdl83+1t+hZcEt1HGzXrO92smznHdDBL7 +1sqDWO00n1XSmWHo7J9qIWjuvKoBwE5lS25ghSzuZWP/P5WBvOa/2wUyfOdB33DV +8CiHCBjf1C2tp+sqw5kFKT0v7lSZTLnCMRH0VlzRc/1CHHrVn7VyvpN618QDJ4D2 +AjSxs+uGhlu7ppWYXdB34bVJT0ffT0KkPnOQ6pKJ8uLXxMpSfgATHERZfigXZ5yb +yTMx/Rwclil828icwowizmGs8AXrI7vk/g4t4JJ74yGIc0A+HKsQ2Gu193pmwQNH +pOctN7NMnF+Lvcqf7j3vIWCLVSJDXUom28VAr/egfr3K5xEfipaiePlAUV6PF90k +Ou3p/kHlNS9dkqx4tM1z6surq4znr3+oq4Ldr+1UNRnBLc/yT01IwdbD2NKPTSJI +yO9uybx0yU04zJC6ZhlZb+Z0kOtF1+T2jzbSjFKIlxoDBUlXonGVW4TXh5S1CUFH +FxyJfsPjocayOYikCb6chTj+9aiTaWJI8txLUO7ejvIINZRg7C1hgBobETePcbvZ +gRFQCbcQUh/6yRTVY+3aOPhf/DcKmb/eM9ijsQxsZaAYrOnu8xXkYQIDAQABo0sw +STAJBgNVHRMEAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAsGA1UdDwQEAwIFoDAa +BgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAJrs +4xelo+wV3kNoqztngdgAFnoNmxE7xN1c/XrdiIc/xY/xQz61lAUF5ouyy1ma/VB9 +a9/5exYWiXW8TpZM7CwccYk7ShDpoIZvUTj4YwNo2F9zlYRsgTjk1ekYz0PKsQGz +v8dF9xmUYn86bGITu+MlJ4konXdJ2riaV2Cx6LwErLXZ0mGxmHNQ/r49QsApEtFf +Y3P+2uYkmSV8UVvRTClyWoxVbojAyAXBv8K5/5/Yuq/NEdl36w6P+gVTXcK3eXy9 +2tktdHd1qAowQFAcdn5h+nmrnYMCdD3F3wu0CLDUFVunY6lQg5Gxsft3s95kbucT +qFrl1Fr1xOaU4pCLwMcUZ6sjURH1jAiSWMdtM1imRXuQDDFvD4DQAktg9UR5UTlM +0NFO1GXIMTA9y2MxQeZTUC7zEF7QRjXJ/9xW6VBK+1iFDyzYa+7iFb7Gsqeu5WHv +p5fiZe6JKN93XzzWcCWEg98IIS01yTGiygkyO2yWOeZ4xEWQuT7p4gi3e5oydBgC +kVsd6slWlJtDP7iGYOwJXucw6pvKMGYj7Ol4kxPosyIb9SV42cp52JAIgPLKTaHn +kmrwIfYV+Zn7xoX8FERg8/Oua0mOeeij9JREoYwm5XV52Fhv3p7qDSqe3HIEL6ft +SfHCEEAf2JEwQj9R+QWhPrWAeNPUqTeianr8Ks9e +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE5DCCAsygAwIBAgIUZwOLM/yaHJBJ8XDvAxE/P3xDFqgwDQYJKoZIhvcNAQEL +BQAwDTELMAkGA1UEAwwCQ0EwHhcNMjQwNzE3MTYxNjAxWhcNMjUwNzE3MTYxNjAx +WjANMQswCQYDVQQDDAJDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AKg69y4KkyMzE0WNPSlwHfF90dy+oqQ9OETVEeSkNx3ImDqx07tdv/z8QZRjFTSy +1OI7S3jSpcinTFlCCoa1mNcEe7Fut2u63YPENRj/lLZpPxo0S168A9yUjkN4c36u +zNyLirBxtqmuxp/4jJ4I0nyUN3eVpsdLztR7mMRm0vvaU5DjeevmtXvk4aiAwLov +uffnQhte9hGY/ZWfbG7EZ4+okMP7m6W6XWxaMShknzxWQONmCv8wkEA3yEUhvyzl +8abpj3SO5vaW6eHmr7sGXMDx6DHs3UCH9Bk3kBmylzttSRH3p4fBtYZle11dlC7N +Ks+QFijfTkN5kIHyuaUFieGKkzeHnc+ACJMUultTBdKwOCZMAk4IUkwG8G5LYozH +I2e5FRWlvUJS4WgF8Vy6dfCSjALlGES62JM0hc6hnqZmKR3A2xkxTM3vcZElq/w1 +Ibi6ezqbO5QmqBuoNGIgsphuF8cYtdla30FQumQpN2WuyWFkBUwkjMJDooWdQeJT +pAntBP+Gx+PjY5329uMf4Q0Q7JGBXO9LVeRGgav8sJDG4EadXzgtigP9nyjy1L3b +QHPkMoqPXYLSqSC2hxxbeiHPA2Kn10/Wx90MSbE81CS4LSoHVMSQ7B2pc3zHkcTn +PnQuQdDFzFKXeso6JCI66dPCBGnVHIhhfbjgplXUvlkNAgMBAAGjPDA6MAwGA1Ud +EwQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBS3FvhJKWuL/EX+hMJi6i37 +q9OZCjANBgkqhkiG9w0BAQsFAAOCAgEAU9sjWemPhkPPdDc4Ph280gvQKWfLhfx/ +pzWtKHmsMPgz4XglQRzj5yHFe5RW0dTUk6FGHgxaIXQaRoP1L2T8k2Z4GLjUNyXP +nUEoYgs4OpnZytg+rgHr3cu7qocM9lSi1ZoE4XomijJhe5okREU7tZvj1pHL1Zmn +TEUXIx8ktl4VDgDXiSS7QXQ9W2chs2gYyxxWeNUyHckURNzbCDnJlkuOBdsXV7eV +v7D60JHP2Pem7zrLhV/G8P7JaRVfY2ZTZB2lH7tKBEhYIpa5muG0JjjyoDkXyOfB +s6VX0xjt8ny+E7wklYJb3TQRJRkOFn08HvhV9Jycs2HvpjSsdHBjn+NbF1ea74TI +2brgW1GjR30+H5mJPsbr79Qriu0ibuwor/+u8UvPrEaeXpbUp+QJ0XAtzXNqrsV3 +yCYtIvDstOrxV7M3hYbZyoBiq92dgwVyMAydUMxMn9EAQBqfpen9Hh7pnET9ORNU +QcJumJDf4YUdAgB6qpTW54LhmhwQOm2zTGuMUdARYGmVNfZbd7HSW/JVJcYtFjus +obiozqvXQVuu1g52JmcpWAlxJLXSFABPifaCI7zy1+XZi4ppzSNKG4+N82QkM5xr +KyATGQnS67yAdYzjBz8bsA0vf72oVVOaWLxCmVpj25mf+G+HCYjnwtWlRgH64tBL +vQLbQZQgNHo= +-----END CERTIFICATE----- diff --git a/utils/docker/certs/server.key b/utils/docker/certs/server.key new file mode 100644 index 0000000..6427c71 --- /dev/null +++ b/utils/docker/certs/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC/DfDxcFkyH2tj +S5hbEnDPF7cLFd03UsgiqPTgLwGdyYnNYURIGMxsgl243FkycmYRz1OsEY1+zf71 +vi0XNulfdl83+1t+hZcEt1HGzXrO92smznHdDBL71sqDWO00n1XSmWHo7J9qIWju +vKoBwE5lS25ghSzuZWP/P5WBvOa/2wUyfOdB33DV8CiHCBjf1C2tp+sqw5kFKT0v +7lSZTLnCMRH0VlzRc/1CHHrVn7VyvpN618QDJ4D2AjSxs+uGhlu7ppWYXdB34bVJ +T0ffT0KkPnOQ6pKJ8uLXxMpSfgATHERZfigXZ5ybyTMx/Rwclil828icwowizmGs +8AXrI7vk/g4t4JJ74yGIc0A+HKsQ2Gu193pmwQNHpOctN7NMnF+Lvcqf7j3vIWCL +VSJDXUom28VAr/egfr3K5xEfipaiePlAUV6PF90kOu3p/kHlNS9dkqx4tM1z6sur +q4znr3+oq4Ldr+1UNRnBLc/yT01IwdbD2NKPTSJIyO9uybx0yU04zJC6ZhlZb+Z0 +kOtF1+T2jzbSjFKIlxoDBUlXonGVW4TXh5S1CUFHFxyJfsPjocayOYikCb6chTj+ +9aiTaWJI8txLUO7ejvIINZRg7C1hgBobETePcbvZgRFQCbcQUh/6yRTVY+3aOPhf +/DcKmb/eM9ijsQxsZaAYrOnu8xXkYQIDAQABAoICAQCt7LYURW+dtoafTTk0ZzQ1 +AuTKfav16jFxhBfwYjp5dvgw9MQhUhn/Cirh2A6HYydSPUhxk0UZU9QvyGHqCT4o +fm0uXG+tXVXeoDgc4ABVm117ZWK4lX5OrvmK2xCN4CNT5fgBADAbgLCy7SNjFrsH +ccTYr0P4/moq+qpTAjGaJHu1u4kXKZ7h05BBZPioAtNWjFAEjS2nSiR+ltuC9xsA +EoXCxkAXGR1L6vtTr+GRxLYjlXrGWFcJHXb08tKNk5C84mi63WSjTeEoDLlkLBaB +yKySF0kxteAWBvscX8IXo5sBUVyT+enS1DX74uyNhZHdLYOSWXUTVogK1DK/Hbe0 +qCLg1pRsPycwzhmI39pvnoZOIedwOk2oGMu7yy9wlsXQnQfY92hoHt+/HOnIrRxX +x86XrSdY9SX+r20PN8iVnybODAO7LA6LEBNAcpaudnU8NinqvCfhK/sk76YMIkWo +OCuPx2iQ7sEwDUXfvW3MUs2orbxleCZE25j/vFjMiEhe3MnCnf2RF4TzKD4/WvlW +UYfXzbP9BjyjsyOkWagigXNSMKjcO/VmejCbBGiLjFl6YOtLWFU+x/6J2eU1/b26 +F18zBbDMAc7gtUHYP1JWMMD7Lg5XFfsOlmo3W4RFHKtd0nUyrkMwBCAT+i44gFBL +sNVkYW9GdHsg2ll+BjeBhQKCAQEA+5yyGk7yFxenvmLmveLL6qXdVsEKAFAo8c9A +kmbrdkWZsmSInVZ2tpo93fS1IS4N0em9tZHkThTJuuoQrbFWQ+jzkzVdmErUFl2/ +3x9VxS51zxZ/ZrJbNpkIxQe/2m5Xr02J2pyJQsc4N/28Jmr8sRzs1ZUR+ep7v5Zq +EXIHx5BLskVj9x/pQC+I9tlxyojJ5MtEFWTHM3oLGeDHIGovO+EW3r2v5lqsug0d +s/81tKoLa2mrq6p7j+MpuFc0X5b98QG+ucZWtA3CNYyCXbNdC4qtl8zeqRIh3qXC +9ajWHUgiB9TJSIjcM/Auymd0KTlkElmXFZ+ZUnvKr/OGpsEj6wKCAQEAwmLj5U60 +NukQuqfoHSWvRCzIRjakuavytiY5NXEguEuTqHT+UOt0UWyref9dn5/dEW8BXACs +1K4wT4Pa1PvuNRrQ4dYkWPwOx/FHSaGyCe/b5MVsVMrw3iCRUk23zK8hL2iMXKHc +BeSJ0QJ+Kgb/k8iFdTpX6zqYImuNwdgjT1+0ndIl0HM/jG2U2yBumNH8BS0usYJW +g7We4lYEXqtyxDJiTFcuchmFdtknLRIUOwWZAhgI8eOt+UpGH3hTGeUS0T2g/531 +ABX22XLkHeo7KOTD+pembbHulgvFNpLA9EI0EPMQQAOBgTKaY7EeiDUsQkP/L5d6 +BJoJhkza0uVh4wKCAQEAksQwT5BFPpsZycA+//xPHixqE7S+dLhNad+Ottc5+d9X +a+uglMZesNz/wXyAOz516UAC3Oqg1brigRkPaYHL5Aj6K6AxXCgp1nIQ/cF1cnNL +XOSuo+Tdt9dEekmu62b51tPP2aj9l+pFLMPhADff9h/9NgiiV7kjPforHMn7J7lP +rkkzqm7+y+Xuaq8j5RQtUDwRFrmWSLyjxRCMlqfZrX/6qyrSc/foUQ5diSUQ2rVl +u94DuTrUoHXNXC3h6vBUaESwUAUVhimQY1P2p1l8qMLXx3hFWTGueAUQ1+MIIkR1 +NUQ1tQ3ABLvRT1dRNsq3SMzhiEd0U9zJuiC/Jn36yQKCAQAYtpHauWLYCFGEKYyt +B/l8ZWUg6BmRMXcuCTYEwVkzlQg0xor+prCnGXXDkN/KR3zHlqFJnRxb/blOoqjT +oyPpxHsB+0OrvH/0k4xIpDIKaWA/eYoITbTJyMIxAIh5kVpauKP/suRSK3gKBpMb +rMAZfcjZ2o0K7uwglCP1nREAKl7AIdOE6OIPbG8cXMcyzp+H2PKyxqtRG2oTxHPR +xWJV50HwCrVw9CWvsnP0mvPPfSqyxXN9rUCVDQhVP+rww9rcl0U8ukxHsoMrqhuu +YfUbgdoYpecW0yROFzj/czDs3O3Zqc1LFicE0fYm7oG/N2NlGVf8KPnuU9caJ/M6 +FMeZAoIBAD/ItfYpsh+UrxC0g7QlE7XoTIuC0k1nlfWFsx9fGTCLM7D13bpf5nzR +7JExiplU/HV3HHrEvXqufOedmIE6MSPvZzAj3z2Lqq7w7NtHk5GSImkZqs9onh8i +fhQChAYNg1DMXTVu6c0HX23EGMu0ySe+knOE+KJQeZqmxXR28xaTai4u8HjZ7jWA +7qX3NrlPGU8l1uZKMuT9kLkLhaRDHba4CLiZvt88uRYkSR9DotJud7lmuTdiUuN9 +L9/wVeS30N6g4bfOxv4+wkXR3oEZj+DtmdopeRKAb9kqJa/+yc9/uT7g6ePQnR4H +vLXrRz8CyZat8oyhEGQIsEHQaMxS6YQ= +-----END PRIVATE KEY----- diff --git a/utils/docker/certs/server_redis.key b/utils/docker/certs/server_redis.key new file mode 100644 index 0000000..6427c71 --- /dev/null +++ b/utils/docker/certs/server_redis.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC/DfDxcFkyH2tj +S5hbEnDPF7cLFd03UsgiqPTgLwGdyYnNYURIGMxsgl243FkycmYRz1OsEY1+zf71 +vi0XNulfdl83+1t+hZcEt1HGzXrO92smznHdDBL71sqDWO00n1XSmWHo7J9qIWju +vKoBwE5lS25ghSzuZWP/P5WBvOa/2wUyfOdB33DV8CiHCBjf1C2tp+sqw5kFKT0v +7lSZTLnCMRH0VlzRc/1CHHrVn7VyvpN618QDJ4D2AjSxs+uGhlu7ppWYXdB34bVJ +T0ffT0KkPnOQ6pKJ8uLXxMpSfgATHERZfigXZ5ybyTMx/Rwclil828icwowizmGs +8AXrI7vk/g4t4JJ74yGIc0A+HKsQ2Gu193pmwQNHpOctN7NMnF+Lvcqf7j3vIWCL +VSJDXUom28VAr/egfr3K5xEfipaiePlAUV6PF90kOu3p/kHlNS9dkqx4tM1z6sur +q4znr3+oq4Ldr+1UNRnBLc/yT01IwdbD2NKPTSJIyO9uybx0yU04zJC6ZhlZb+Z0 +kOtF1+T2jzbSjFKIlxoDBUlXonGVW4TXh5S1CUFHFxyJfsPjocayOYikCb6chTj+ +9aiTaWJI8txLUO7ejvIINZRg7C1hgBobETePcbvZgRFQCbcQUh/6yRTVY+3aOPhf +/DcKmb/eM9ijsQxsZaAYrOnu8xXkYQIDAQABAoICAQCt7LYURW+dtoafTTk0ZzQ1 +AuTKfav16jFxhBfwYjp5dvgw9MQhUhn/Cirh2A6HYydSPUhxk0UZU9QvyGHqCT4o +fm0uXG+tXVXeoDgc4ABVm117ZWK4lX5OrvmK2xCN4CNT5fgBADAbgLCy7SNjFrsH +ccTYr0P4/moq+qpTAjGaJHu1u4kXKZ7h05BBZPioAtNWjFAEjS2nSiR+ltuC9xsA +EoXCxkAXGR1L6vtTr+GRxLYjlXrGWFcJHXb08tKNk5C84mi63WSjTeEoDLlkLBaB +yKySF0kxteAWBvscX8IXo5sBUVyT+enS1DX74uyNhZHdLYOSWXUTVogK1DK/Hbe0 +qCLg1pRsPycwzhmI39pvnoZOIedwOk2oGMu7yy9wlsXQnQfY92hoHt+/HOnIrRxX +x86XrSdY9SX+r20PN8iVnybODAO7LA6LEBNAcpaudnU8NinqvCfhK/sk76YMIkWo +OCuPx2iQ7sEwDUXfvW3MUs2orbxleCZE25j/vFjMiEhe3MnCnf2RF4TzKD4/WvlW +UYfXzbP9BjyjsyOkWagigXNSMKjcO/VmejCbBGiLjFl6YOtLWFU+x/6J2eU1/b26 +F18zBbDMAc7gtUHYP1JWMMD7Lg5XFfsOlmo3W4RFHKtd0nUyrkMwBCAT+i44gFBL +sNVkYW9GdHsg2ll+BjeBhQKCAQEA+5yyGk7yFxenvmLmveLL6qXdVsEKAFAo8c9A +kmbrdkWZsmSInVZ2tpo93fS1IS4N0em9tZHkThTJuuoQrbFWQ+jzkzVdmErUFl2/ +3x9VxS51zxZ/ZrJbNpkIxQe/2m5Xr02J2pyJQsc4N/28Jmr8sRzs1ZUR+ep7v5Zq +EXIHx5BLskVj9x/pQC+I9tlxyojJ5MtEFWTHM3oLGeDHIGovO+EW3r2v5lqsug0d +s/81tKoLa2mrq6p7j+MpuFc0X5b98QG+ucZWtA3CNYyCXbNdC4qtl8zeqRIh3qXC +9ajWHUgiB9TJSIjcM/Auymd0KTlkElmXFZ+ZUnvKr/OGpsEj6wKCAQEAwmLj5U60 +NukQuqfoHSWvRCzIRjakuavytiY5NXEguEuTqHT+UOt0UWyref9dn5/dEW8BXACs +1K4wT4Pa1PvuNRrQ4dYkWPwOx/FHSaGyCe/b5MVsVMrw3iCRUk23zK8hL2iMXKHc +BeSJ0QJ+Kgb/k8iFdTpX6zqYImuNwdgjT1+0ndIl0HM/jG2U2yBumNH8BS0usYJW +g7We4lYEXqtyxDJiTFcuchmFdtknLRIUOwWZAhgI8eOt+UpGH3hTGeUS0T2g/531 +ABX22XLkHeo7KOTD+pembbHulgvFNpLA9EI0EPMQQAOBgTKaY7EeiDUsQkP/L5d6 +BJoJhkza0uVh4wKCAQEAksQwT5BFPpsZycA+//xPHixqE7S+dLhNad+Ottc5+d9X +a+uglMZesNz/wXyAOz516UAC3Oqg1brigRkPaYHL5Aj6K6AxXCgp1nIQ/cF1cnNL +XOSuo+Tdt9dEekmu62b51tPP2aj9l+pFLMPhADff9h/9NgiiV7kjPforHMn7J7lP +rkkzqm7+y+Xuaq8j5RQtUDwRFrmWSLyjxRCMlqfZrX/6qyrSc/foUQ5diSUQ2rVl +u94DuTrUoHXNXC3h6vBUaESwUAUVhimQY1P2p1l8qMLXx3hFWTGueAUQ1+MIIkR1 +NUQ1tQ3ABLvRT1dRNsq3SMzhiEd0U9zJuiC/Jn36yQKCAQAYtpHauWLYCFGEKYyt +B/l8ZWUg6BmRMXcuCTYEwVkzlQg0xor+prCnGXXDkN/KR3zHlqFJnRxb/blOoqjT +oyPpxHsB+0OrvH/0k4xIpDIKaWA/eYoITbTJyMIxAIh5kVpauKP/suRSK3gKBpMb +rMAZfcjZ2o0K7uwglCP1nREAKl7AIdOE6OIPbG8cXMcyzp+H2PKyxqtRG2oTxHPR +xWJV50HwCrVw9CWvsnP0mvPPfSqyxXN9rUCVDQhVP+rww9rcl0U8ukxHsoMrqhuu +YfUbgdoYpecW0yROFzj/czDs3O3Zqc1LFicE0fYm7oG/N2NlGVf8KPnuU9caJ/M6 +FMeZAoIBAD/ItfYpsh+UrxC0g7QlE7XoTIuC0k1nlfWFsx9fGTCLM7D13bpf5nzR +7JExiplU/HV3HHrEvXqufOedmIE6MSPvZzAj3z2Lqq7w7NtHk5GSImkZqs9onh8i +fhQChAYNg1DMXTVu6c0HX23EGMu0ySe+knOE+KJQeZqmxXR28xaTai4u8HjZ7jWA +7qX3NrlPGU8l1uZKMuT9kLkLhaRDHba4CLiZvt88uRYkSR9DotJud7lmuTdiUuN9 +L9/wVeS30N6g4bfOxv4+wkXR3oEZj+DtmdopeRKAb9kqJa/+yc9/uT7g6ePQnR4H +vLXrRz8CyZat8oyhEGQIsEHQaMxS6YQ= +-----END PRIVATE KEY----- diff --git a/utils/docker/docker-compose.yml b/utils/docker/docker-compose.yml index eda5b51..38de6ae 100644 --- a/utils/docker/docker-compose.yml +++ b/utils/docker/docker-compose.yml @@ -32,12 +32,13 @@ services: image: "postgres:latest" ports: - "5432:5432" + user: postgres command: - "postgres" - "-c" - "max_connections=400" - "-c" - - "shared_buffers=20MB" + - "shared_buffers=20MB" env_file: - postgresql.env restart: always diff --git a/utils/kafka_producer/data/message_template.json b/utils/kafka_producer/data/message_template.json index bb1f1fd..468541e 100644 --- a/utils/kafka_producer/data/message_template.json +++ b/utils/kafka_producer/data/message_template.json @@ -3,7 +3,6 @@ "type": "infrastructure_group_create", "infra_group_id": "key", "infra_name": "name", - "infra_shard_id": 0, "uuid": "290d06b6-8eba-11ee-8005-aabbccddeeff" }, "del_group": { diff --git a/utils/kafka_producer/fill_groups.sh b/utils/kafka_producer/fill_groups.sh new file mode 100755 index 0000000..166f68d --- /dev/null +++ b/utils/kafka_producer/fill_groups.sh @@ -0,0 +1,125 @@ +#!/bin/bash +################################################################################ + +kafka_host="NULL" +starting_mac_offset="NULL" +mac_max_num=0 +total_macs_generated=0 +num_of_groups_filled=0 +out_fname="/tmp/gid_gen.sh" +gid_offset=0 + +################################################################################ + +function incr_mac() { + in_mac=$1 + in_incr_num=$2 + in_incr_num=$(($in_incr_num + 1)) + mac=$(echo $in_mac | tr -d ':') + macadd=$(( 0x$mac + $in_incr_num )) + macnew=$(printf "%012X" $macadd | sed 's/../&:/g;s/:$//') + echo $macnew +} + +function usage() { + usage="$(basename "$0") [-o ] [-s ip:port] [-c n] [-g n] -- randomly generate and fill KAFKA BUS with groups and infras list create / add messages + + where: + -o Starting mac offset (e.g. 00:00:00:00:00:01) + -s KAFKA server (in a IP:PORT fashion) + -c infras (mac addresses) number to create + -g group ID offset to start from" + echo "$usage" +} + +function check_args() { + if [ $starting_mac_offset = "NULL" ] ; then + echo "ERR: Starting mac offset ('-o' option) is NULL (unset)" + echo + usage + exit 4; + fi + + if [ $mac_max_num -eq 0 ] ; then + echo "ERR: Num of macs to create has to be greater than zero ('-c' option is NULL - unset)" + echo + usage + exit 5; + fi + + if [ $kafka_host = "NULL" ] ; then + echo "ERR: Kafka host has to be set ('-s' option is NULL - unset)" + echo + usage + exit 6; + fi + + if ! [ -e ./main.py ] ; then + echo "Failed to find main.py file of kafka_producer!" + exit 2; + fi + + if ! [ `realpath main.py | grep "kafka_producer"` ] ; then + echo -e \ + "Found main.py but it seems invalid:\n"\ + "expected it to be inside 'kafka_producer' dir.\n" \ + "$0 should be executed from that folder as well."; + exit 2 + fi +} + +################################################################################ + +while getopts ':o:hs:c:g:' option; do + case "$option" in + h) usage + exit + ;; + o) starting_mac_offset=$OPTARG + echo -e "OPT_IN: starting mac offset will be used: '$starting_mac_offset'" + ;; + s) kafka_host=$OPTARG + echo -e "OPT_IN: Kafka host:port will be used: '$kafka_host'" + ;; + c) mac_max_num=$OPTARG + echo -e "OPT_IN: Will create the following random macs num: '$mac_max_num'" + ;; + g) gid_offset=$OPTARG + echo -e "OPT_IN: Will start creating groups starting from the following GID offset: '$gid_offset'" + ;; + :) printf "missing argument for -%s\n" "$OPTARG" >&2 + echo "$usage" >&2 + exit 1 + ;; + \?) printf "illegal option: -%s\n" "$OPTARG" >&2 + echo "$usage" >&2 + exit 1 + ;; + esac +done +check_args +################################################################################ + +echo "#!/bin/bash" > $out_fname +echo >> $out_fname + +mac_offset=$starting_mac_offset +num_of_groups_filled=$gid_offset +while true ; do + echo "Processing mac offset $mac_offset..." + random_mac_num=`seq 40 200 | sort -R | head -1` && echo "Generated mac num for gid $num_of_groups_filled - $random_mac_num" + echo "python3 ./main.py -s $kafka_host -c 1 --new-group $num_of_groups_filled 0 generated_group_$num_of_groups_filled""_infra_num_$random_mac_num" >> $out_fname + echo "python3 ./main.py -s $kafka_host -d $num_of_groups_filled '$mac_offset^$random_mac_num'" >> $out_fname + total_macs_generated=$((total_macs_generated + random_mac_num + 1)) + num_of_groups_filled=$((num_of_groups_filled + 1)) + if [ $total_macs_generated -ge $mac_max_num ]; then + break; + fi + mac_offset=`incr_mac $mac_offset $random_mac_num` +done +echo "Created '$total_macs_generated' infra entries dispersed among '$((num_of_groups_filled - gid_offset))' number of groups" + +chmod +x $out_fname + +echo +echo "Output file generated, and can be launched: $out_fname"